ICS-Chap7-Linker1

2025-11-09

ICS

7.1 编译器驱动程序

编译的过程:

  1. main.c
  2. 预处理器 -> main.in
  3. 编译器 -> main.s (汇编语言)
  4. 汇编器 -> main.o(可重定位目标文件)
  5. 然后链接器将若干个 .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 符号解析

多重定义的全局符号

强符号:函数&已初始化的全局变量,为强符号.

弱符号:未初始化的全局变量.

规则:

  1. 不允许多个重名的强符号
  2. 一个强符号 vs 多个弱符号重名,选择弱符号
  3. 多个若符号重名,任选一个弱符号

其实本质就是 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.