时间:2025-11-11 16:37
人气:
作者:admin
bin 文件通常用于嵌入式裸机程序的烧录,elf 可执行文件通常运行在操作系统之上。
bin 是扁平的二进制文件,没有任何说明,它假设加载它的环境(如嵌入式引导程序,BootRom)已经预先知道了代码存放的地址,代码的入口,数据段,代码段的地址。大家如果烧录过嵌入式裸机程序应该有所体会。
elf 则是带有详细说明和装配图的文件,因此 elf 可执行程序的运行是需要对其所包含的信息进行解析并建立执行环境的,这就决定了其不可能作为裸机程序去执行。
一个可执行文件加载过程中,需要创建执行所需要的内存空间。对于操作系统而言,一般指的是每个进程的虚拟地址空间。一个进程的内存空间一般存在四个核心区域,代码段(.text),数据段(.data),堆(.heap),栈(.stack)。
需要注意的是,栈的地址是从高地址到低地址,堆的地址是从低地址到高地址。
C语言从源代码到可执行文件一般需要四个过程,即预处理,编译,汇编,链接。
预处理,处理源代码中的预处理指令(以#开头),生成纯 C 代码(.i文件)。
#define定义的宏(如#define PI 3.14替换为实际值)。#include指令指向的头文件(如stdio.h)内容插入当前文件。#if、#ifdef等指令保留或删除部分代码(如调试代码#ifdef DEBUG ... #endif)。//和/* */注释,不影响代码逻辑。编译,将预处理后的纯 C 代码(.i文件)转换为汇编代码(.s文件)。
汇编,将汇编代码(.s文件)转换为机器指令(二进制目标文件,.o或.obj)。
call printf转换为对应的机器码),生成的目标文件包含:目标文件是 “部分编译” 的结果,可能包含未解析的外部符号(如
printf函数的地址尚未确定)。
链接,将多个目标文件(.o)和库文件(如libc.so)合并,生成可执行文件。
printf在 C 标准库中的地址)。链接又分为静态链接和动态链接。Linux 环境下使用 gcc 进行编译,默认使用动态链接。
printf、malloc)直接复制到可执行文件中,形成一个独立的二进制文件。
.lib,Linux 下为.a)。ld.so,Windows 的ntdll.dll) 加载依赖的库文件到内存,并解析引用,后面会展开讲。
.dll,Linux 下为.so,macOS 下为.dylib)。/lib,/usr/lib。gcc -E main.c -o main.i # 预处理
gcc -S main.i -o main.s # 编译
gcc -c main.s -o mian.o # 汇编
gcc -static main.o -o main # 静态链接
gcc main.o -o main # 默认动态链接
将自己的代码编译为动态库:
# -fPIC为位置无关代码;-shared指示gcc生成一个共享库而不是一个可执行文件,共享库可以被多个程序同时使用节省了内存和磁盘空间。
gcc hello.o -fPIC -shared -o libxxx.so
gcc main.o -L ./ -lhello -o main2 # main 中用到 hello.c 的库函数
# -lhello -l + 库的名称。上面的命令只是告诉动态链接库是谁,并没有指定动态库所在的路径,因此需要添加要使用的动态库文件路径,然后执行。
# 或者将库文件所在路径添加进环境变量 LD_LIBRARY_PATH
将自己的代码编译为静态库:
ar crv libhello.a hello.o
gcc main.o -L ./ -lhello -o main3 # 注意:当同一个目录下既有静态库又有动态库,默认链接动态库。
elf 文件的主要包括ELF头部,程序头表,节区,节头表几个部分。
ELF头部
main 函数,而是动态链接器 _start 的入口,后面会讲。程序头表
程序头表相当于一个加载说明书,告诉操作系统如何将 elf 文件的内容映射到内存中,以创建一个进程。程序头表在实现上是一个结构体数组,每个结构体(segment)描述了文件中的一块区域应该如何被映射到内存中。
每个程序头都包含了该段在文件中的偏移,在内存中的虚拟地址,大小,执行权限等。
节区
保存着不同节的具体内容。
.text:存放程序指令(代码)。.data:存放已初始化的全局变量和静态变量。.bss:存放未初始化的全局变量和静态变量(此节在文件中不占空间,只在运行时在内存中分配)。.rodata:存放只读数据(如常量字符串)。.symtab:符号表,记录了函数和变量的名称及其地址。.symtab / .dynsym:符号表,.symtab 包含所有符号(包括本地符号),.dynsym 仅包含动态链接所需的符号(如外部库函数)。.rel.text / .rel.data:重定位信息,用于链接时修正地址。节头表
所有节的索引目录。存放一个数组,数组中的每个元素对应一个节,描述了该节的名称(在.strtab中的索引),类型,在文件中的偏移,大小,链接信息等。
PT_LOAD->p_vaddr 和PT_LOAD->p_memsz 决定。设置权限。p_memsz 的空间。_start 函数开始运行。.dynamic 节区,找到程序依赖的共享库列表,加载这些库。
符号表,字符串表,重定位信息都属于节区。
静态链接情况下不存在动态库,根据上一节所讲的 elf 文件加载过程,需要动态库的可执行文件是运行时加载,然后进行重定位的,因此静态链接的可执行文件在编译完成的时候,重定位就已经完成,节区中的重定位信息被删除或者为空,符号表通常会保留,用于 GDB 调试。
符号表 .symtab 与字符串 strtab 表结合作用,符号表中保存着程序中变量,函数,(文件名,节区名)等的名称(索引 st_name,指向 strtab 中的对应位置),地址(st_value,通常是函数,变量的地址或者偏移量),大小(st_size,一个数组或者函数的字节大小),类型(st_info,如 STT_FUNC 函数, STT_OBJECT 对象,绑定属性:STB_LOCAL:局部符号;STB_GLOBAL:全局符号;STB_WEAK:弱符号;st_shndx:一个索引,指明该符号位于哪个节区。)
为什么不直接把字符串存在
.symtab里呢?因为这样做效率低下且浪费空间。使用索引的方式,多个符号可以共享同一个字符串(例如,多个文件都引用printf),并且符号表条目可以保持固定大小,便于快速查找。
重定位表(rel.text/rel.data)要解决的问题:当存在多个 .c 文件时,一个 c 文件使用到另一个 c 文件的函数,在编译单个 c 文件时,编译器并不知道调用的这个函数的最终地址,也不知道自己定义的函数或者变量最终会被链接到可执行文件的哪个地址。这时汇编器会生成一个重定位条目,并留下一个占位符,表示这个位置的代码需要被修正。重定位条目生成在汇编阶段,最终地址的确定发生在链接阶段。每个重定位条目(通常是一个结构体变量,Elf_Rel或Elf_Rela)包含需要被修正的地址在节点中的偏移量 r_offset,r_info,存放符号索引和重定位类型。
对某个函数和变量进行重定位,首先是要知道这个需要被重定位的函数变量在哪(r_offset),其次是要知道要填充进这个占位符的地址是什么(从符号表中获得r_info)。
两种重定位表的介绍:
.rel.text:包含了对代码节区 (.text) 的重定位信息。例如,call printf 指令中的 printf 函数地址,在编译时是未知的,这里就会生成一个重定位条目。.rel.data:包含了对已初始化数据节区 (.data) 的重定位信息。例如,int *ptr = &global_var; 这行代码,ptr 变量在 .data 节区,但它存储的 global_var 的地址在编译时也是未知的,这里也会生成一个重定位条目。对于一个工程文件的编译流程,预处理,编译,汇编,链接。假设一个简单的工程文件包括 a.c,b.c 包括静态库文件 .a。在汇编阶段,汇编器会对各个 .c 文件进行汇编,由于这时各个文件中的函数变量在可执行文件中的地址并没有被确定,会生成很多重定位条目。在链接阶段,链接器 ld 会读取所有的 .o 文件和静态库文件 .a,把所有同类型的节区合并,读取各个 .o 文件的符号表,创建一个全局符号表,并且在这个过程中进行符号解析。此时整个可执行文件的地址,符号表基本确定,需要根据重定位条目对一些占位符进行重定位处理。
r_info 找到符号表中的对应位置,获取该符号的最终地址。r_info 中的重定位类型,计算出需要写入的值。r_offset 指定的需要重定位的占位符的位置,执行重定位。链接完成之后,.text 和 .data 中的地址引用都是完整的、可以直接运行的。.rel.text 和 .rel.data 节区通常会被丢弃,因为所有重定位工作已经完成,不再需要它们了。.symtab 和 .strtab 可能会被保留(用于调试),或者被 strip 工具移除以减小文件大小。
首先,根据 elf 加载流程,动态链接的地址重定位是在可执行文件执行过程中在内核分配完内存空间和栈空间之后,调用 ld.so 动态连接器,转到用户空间加载依赖的共享库,然后进行运行时地址重定位。最终跳转程序入口,开始执行程序。
链接过程:
.rela.plt(函数)和 .rela.dyn(数据)节区中,对应的动态符号表 .dynsym 是 .symtab 的一个子集,只包含用于动态链接的全局符号。动态字符串表为.dynstr。程序加载过程:
ld.so,并执行。DT_NEEDED 条目,加载所有依赖的共享库文件,此时主程序所用的库文件都有了自己的地址。然后根据 DT_RELA、DT_RELASZ、DT_JMPREL告诉动态链接器 .rela.dyn 和 .rela.plt 的位置。.rela.dyn。.rela.plt 。使用延迟绑定策略,当函数第一次被调用时,控制权会转到 PLT 中,PLT 代码会触发 ld.so 解析真正的函数地址。解析完成后写入 PLT,后续调用查表即可。对整个 elf 可执行文件的加载过程,大多数人其实只需要了解即可,知道它的流程是怎么样的,重定位和链接的关系和设计思想。以及,elf 文件加载和 bin 文件的区别。
总结的未免凌乱,不足之处欢迎讨论!
Steady progress!