2025-11-09
7.1 编译器驱动程序
编译的过程:
main.c- 预处理器 ->
main.in - 编译器 ->
main.s(汇编语言) - 汇编器 ->
main.o(可重定位目标文件) - 然后链接器将若干个
.o组合起来生成可执行文件
7.2 静态链接
static linker 需要完成 symbol resolution & relocation.
7.3 目标文件
目标文件有三种:可重定位目标文件,可执行目标文件,共享目标文件
7.4 可重定位目标文件
- ELF 头:字的大小&字节顺序 - ELF头大小&文件类型&机器类型&节头部表得到偏移答案小数量,等.
- .text:编译后的机器代码.
- .rodata:只读数据.
- .data:已初始化的全局&static 变量.
- .bss:未初始化的static 变量,初始化为0的全局/static变量,不在ELF占据空间.
- .symtab:符号表,存放定义的函数&全局变量的信息.
- .rel.text:链接器需要修改的 text 部分的位置列表.
- .rel.data:链接器需要修改的 data 部分的位置列表.
- .debug:调试用的符号表.
- .line:调试用的行号/指令的映射.
- .strtab:字符串表,包含 symtab/debug 中的符号表和节头部的节名字.
- 节头部表:不同节的位置和大小.
7.5 symtab
每个符号被分配到目标文件的某个节,有三个特殊的伪节. 这个意思是在符号表中,正常的东西会直接标记其所属节里面;但是伪节的话就只会在这里写上是 ABS/UND/COM.
可执行目标文件没有这样的伪节.
ABS:不该被重定向的符号(代表一个绝对的常量值)
UNDEF:未定义的符号(例如 printf)
COMMMON:未初始化的全局变量(弱符号)
7.6 符号解析
多重定义的全局符号
强符号:函数&已初始化的全局变量,为强符号.
弱符号:未初始化的全局变量.
规则:
- 不允许多个重名的强符号
- 一个强符号 vs 多个弱符号重名,选择弱符号
- 多个若符号重名,任选一个弱符号
其实本质就是 data/text/bss 不能重,并且优先这三者,com 相同直接合并就好了.
与静态库链接
静态库:相关的函数被编译为独立的目标模块,封装成一个静态库文件,然后链接器只需要复制程序引用的目标模块.
使用静态库解析引用
编译的时候从左到右读命令行输入文件,遇到未解析符号就记录下来,然后每个文件查看有没有可以解决的未解析符号.
所以需要保证被引用的放在引用的的右侧.
7.7 重定向
重定位由两步组成:重定位节&符号定义,以及重定位节中的符号引用.
重定位条目
生成一个目标文件的时候,由于不知道最终数据&代码放在什么位置所以需要生成重定位条目(.rel). 格式上,需要包含 offset(那个元素的节偏移),type(重定位类型),symbol(被修改引用指向的符号),addend(对最终得到的值的一个偏移)
书上讲的两种 type:R_X86_64_PC32,32 位 PC
相对引用(跑的时候需要加上 +PC);R_X86_64_32,32
位绝对引用.
重定位符号引用
对于节 \(s\)
中的符号引用,无论如何都需要算出 refptr = s + offset
表示那个需要修改的符号引用的地址
PC 相对引用:
先计算出引用的运行时 PC:
refaddr = ADDR(s) + r.offset然后计算相对地址:
*refptr = ADDR(r.symbol) + r.addend - refaddr
绝对引用:
*refptr = ADDR(r.symbol) + r.addend
7.8 可执行目标文件
可执行目标文件没有 .rel 的节,并且有个段头部表将节映射到运行时内存段,.init 表示初始化代码.
只有 bss 前的那些会被写到内存.
程序头部表将可执行文件的连续的 chunk 映射到连续的内存段,有 read-only segment 和 read-write data segment. 读写段指 .data 和 .bss,read-only 则是之前的内容.
7.9 加载可执行文件
内存 \(2^{48}\) 往上的部分为内核内存,对用户不可见.
往下的部分和之前说的都是一样的. 从上往下是栈,然后中间某个地方往上是共享库的内存映射区域. 然后下面的从下往上就是 init, text, rodata;然后是 read-write 段的 data 和 bss,然后再是运行时的堆(malloc 创建)
7.10 动态链接
共享库可以被不同的正在运行的进程共享. 是在敲下 ./
的时候进行链接的.
run-time linking:需要用 dlopen 这样的东西.
用的时候需要用 dlsym.
动态链接能够节省空间,并且共享库已经在 memory 和 cache 中了. 不过还是需要多存一些表,但是总之还是更节省空间.
7.13 插桩
eg 对malloc检测.
编译时插桩:
写一个
malloc.h,在mymalloc.c中定义一个mymalloc,然后在malloc.h中#define mmalloc mymalloc
链接时插桩:
在
mymalloc.c中定义__wrap_malloc,然后其中真正的malloc直接调用__real_malloc. 使用-Wl,--wrap,malloc就可以直接插桩.
运行时插桩:
创建一个共享库,然后塞到 LD_PRELOAD. 此时这个共享库中
mymalloc.c就需要用dlopen/dlsym来调用真正的malloc.