CSAPP7

CSAPP7

exdoubled Lv4

静态链接

链接器完成两个任务:

符号解析和重定位

目标文件定义和引l用符号,每个符号对应于一个函数、一个全局变量或一个静态变量(即C语言中任何以static属性声明的变量)。符号解析的目的是将每个符号引用正好和一个符号定义关联起来

编译器和汇编器生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。链接器使用汇编器产生的重定位条目的详细指令,不加甄别地执行这样的重定位。

目标文件

目标文件有三种形式

  • 可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
  • 可执行目标文件。包含二进制代码和数据,其形式可以被直接复制到内存并执行。
  • 共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行时被动态地加载进内存并链接。

编译器和汇编器生成可重定位目标文件

链接器生成可执行目标文件

.c=>.i=>.s=>.o=>prog

.c是源代码

.i 是一个ASCII文件,包含预处理器的输出

.s 是一个ASCII文件,包含汇编器的输出

.o 是一个二进制文件,包含可重定位目标文件

prog 是一个二进制文件,包含可执行目标文件

可重定位目标文件

这是一个 可重定位目标文件的格式:

330X347/43.png

ELF头包含大小,目标文件类型,机器类型,节头部表的文件偏移和节头部表的大小等信息

下面都是节

.text 已编译程序的机器码

.rodata 只读数据,比如printf语句的格式串和 switch语句的跳转表等

.data 已初始化的全局和静态 C 变量

.bss 未初始化的全局和静态 C 变量,以及所有初始化为 0 的全局和静态 C 变量

.symtab 符号表,存放程序中定义和引用的函数和全局变量信息,不包含局部变量的条目

.rel.text 一个.text节中位置的列表,当链接器把这个目标文件和其他文件组合 时,需要修改这些位置

.rel.data 被模块引用或定义的所有全局变量的重定位信息。一般而言,任何已初 始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要 修改

.debug 调试符号表其条目是程序中定义的局部变量和类型定义,程序中定义和引用的全局变量,以及原始的C源文件,只有用 -g 才会生成

.line 源代码行号表,只有用 -g 才会生成

.strtab 字符串表,包含符号表中所有符号的名字,只有用 -g 才会生成

注意,局部变量的条目不出现在符号表中,运行时被保存在栈中

符号和符号表

每个模块 \(m\) 都有一个符号表,有三种符号

  • \(m\) 定义,能被其他模块使用的全局符号 对应非静态函数和全局变量
  • 其他模块定义被 \(m\) 引用的全局符号,称为外部符号,对应其他模块的非静态函数和全局变量
  • 只在 \(m\) 中使用的符号,称为局部符号,对应静态函数和静态变量

定义为带有Cstatic属性的本地过程变量是不在栈中管理的。相反,编译器在.data或.bss中为每个定义分配空间

ABS:不该被重定位的符号

UNDEF:未定义的符号,本目的模块引用,其他地方定义

COMMON:未初始化的全局(extern)变量

.bss:未初始化的静态(stastic)变量,初始化为0的全局(extern)和静态(stastic)变量

.data:初始化的全局(extern)和静态(stastic)变量

练习 7.1

m.c

1
2
3
4
5
6
void swap();
int buf[2] = {1,2}
int main(){
swap();
return 0;
}

swap.c

1
2
3
4
5
6
7
8
9
10
11
12
13
extern int buf[];

int *bufp0 = &buf[0];
int *bufp1;

void swap(){
int temp;

bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
符号.symtab条目?符号类型在哪个模块被定义
buf外部m.c.data
bufp0全局swap.c.data
bufp1全局swap.cCOMMON
swap全局swap.c.text
temp

符号解析

编译器只允许每个模块中每个局部符号有一个定义。静态局部变量也会有本地链接器符号,编译器还要确保它们拥有唯一的名字

当编译器遇到一个不是在当前模块中定义 的符号(变量或函数名)时,会假设该符号是在其他某个模块中定义的,生成一个链接器符号表条目,并把它交给链接器处理

如果链接器在它的任何输入模块中都找不到这个被引 用符号的定义,就输出一条(通常很难阅读的)错误信息并终止

解析多重定义的全局符号

全局符号有强和弱

函数和已初始化的全局变量是强符号

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

根据强弱符号的定义,Linux 链接器使用下面的规则来处理多重定义的符号名:

  • 规则 1:不允许有多个同名的强符号。
  • 规则 2:如果有一个强符号和多个弱符号同名,那么选择强符号。
  • 规则 3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个。

为了更好地理解这三个规则,我们需要先明确在 C 语言中: * 强符号(Strong Symbol):指初始化的全局变量和所有函数。 * 弱符号(Weak Symbol):指未初始化的全局变量。

以下是针对这三个规则的代码示例:

规则 1:不允许有多个同名的强符号

如果两个不同的源文件中都定义了同名的初始化全局变量或同名函数,链接器会报错。

  • file1.c
    1
    2
    int x = 10; // 强符号
    int main() { return 0; }
  • file2.c
    1
    int x = 20; // 强符号,与 file1.c 中的 x 冲突
  • 结果:链接时报错,提示 redefinition of 'x'multiple definition of 'x'

规则 2:如果有一个强符号和多个弱符号同名,那么选择强符号

如果一个变量在一个地方初始化了(强),在其他地方没有初始化(弱),链接器会统一使用那个初始化的值。

  • file1.c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <stdio.h>
    int x = 100; // 强符号

    void print_x();

    int main() {
    print_x(); // 输出 x 的值
    return 0;
    }
  • file2.c
    1
    2
    3
    4
    5
    int x; // 弱符号

    void print_x() {
    printf("%d\n", x);
    }
  • 结果:程序可以成功编译链接,运行后输出 100。链接器选择了强符号 x = 100

规则 3:如果有多个弱符号同名,那么从这些弱符号中任意选择一个

如果所有地方都没有初始化该全局变量,链接器会随机(或根据具体实现,通常选占用空间最大的)选择一个,而不会报错。

  • file1.c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <stdio.h>
    int x; // 弱符号

    void set_x();

    int main() {
    x = 5;
    set_x();
    printf("%d\n", x);
    return 0;
    }
  • file2.c
    1
    2
    3
    4
    5
    int x; // 弱符号

    void set_x() {
    x = 10;
    }
  • 结果:程序可以成功链接。file1.cfile2.c 实际上共享同一个内存地址的 x。在 main 函数中修改 x 之后调用 set_xx 的值会被修改为 10

在实际开发中,规则 2 和规则 3 可能会引入非常隐蔽的 Bug(例如不同文件定义的变量类型大小不一致时),因此建议始终初始化全局变量,或使用 static 关键字将其限制在文件作用域内。

特别是如果一个文件中定义 int,另一个文件定义 double,那么它们的大小不同,链接器选择其中一个后,另一个文件访问该变量时会导致未定义行为。

练习 7.2

REF(x.i)->DEF(x.k) 表示链接器把模块 i 中对符号 x 的任意引用与模块 k 中对符号 x 的定义关联起来

1
2
3
4
5
6
// 1
int main(){}

// 2
int main;
int p2(){}
  1. REF(main.2)->DEF(main.1)

  2. REF(main.1)->DEF(main.1)

1
2
3
4
5
6
// 1
void main(){}

// 2
int main = 1;
int p2(){}
  1. REF(main.2)->DEF(错误)

  2. REF(main.1)->DEF(错误)

1
2
3
4
5
6
7
// 1
int x;
void main(){}

// 2
double x = 1.0;
int p2(){}
  1. REF(x.2)->DEF(x.2)

  2. REF(x.1)->DEF(x.2)

静态库链接

libc.a 是 Linux/Unix 系统中 C 语言标准库(Standard C Library)的静态库版本

632X321/44.png

它是一个所谓的“归档文件”(Archive file),通常使用 ar 工具创建,后缀名为 .a

它包含了 C 标准库中所有函数的二进制目标代码(如 printf.o, scanf.o, malloc.o, strcpy.o 等)

按需提取:当你在编译程序时使用静态链接方式,链接器(ld)会扫描 libc.a。如果你的程序调用了 printf,链接器只会从 libc.a 中提取 printf.o 模块并将其合并到最终的可执行文件中

独立性:链接完成后,生成的可执行文件包含了运行所需的全部代码。这意味着它在运行时不需要依赖系统中的外部库文件,具有很好的移植性

  • 优点
    • 独立运行:可执行文件在没有安装开发环境或特定版本库的机器上也能运行。
    • 运行效率:省去了运行时动态加载和重定位库函数的时间
  • 缺点
    • 体积臃肿:每个静态链接的程序都会包含一份库代码副本,浪费磁盘空间
    • 内存浪费:如果有多个程序同时运行,内存中会存在多份相同的库代码(如多份 printf 代码)
    • 更新困难:如果 libc.a 有 Bug 或更新,所有相关的程序都必须重新编译链接

在现代开发中,动态链接库 (libc.so) 是默认首选,因为它更节省空间且易于维护。libc.a 通常只在某些特殊场景(如嵌入式开发、内核开发或需要构建完全独立的单一二进制文件)时使用

如果想强制使用静态库链接 libc,通常在 GCC 命令中加入 -static 参数:

1
gcc -static main.c -o my_program

这将导致生成一个体积巨大的可执行文件,因为所有的标准库代码都被打包进去了

连接顺序

如果 liby.a 依赖 libx.a,那么在链接时必须把 liby.a 放在 libx.a 之前:

1
gcc main.o liby.a libx.a -o prog

foo.c 调用 libx.a 中函数,又调用 liby.a 中函数,而 liby.a 又调用 libx.a 中函数,那么链接时必须把 libx.a 出现两次

1
gcc foo.o libx.a liby.a libx.a -o prog

练习 7.3

1
gcc p.o libx.a
1
gcc p.o libx.a liby.a
1
gcc p.o libx.a liby.a libx.a

重定位条目

当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置

因此,汇编器在生成目标文件时,会为每个需要重定位的地址生成一个重定位条目(Relocation Entry)。这些条目告诉链接器在链接时需要修改哪些地址,以及如何修改它们

ELF 重定位条目格式如下

1
2
3
4
5
6
typedef struct{
long offset; //需要重定位的地址偏移
long type:32, //重定位类型
symbol:32; //符号表索引
long addend; //常数加数
} Elf64_Rela;

链接器的重定位算法的伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
foreach section s {
foreach relocation entry r {
refptr = s + r.offset; /* 指向需要被重定位的引用的指针 */

/* 重定位一个 PC 相对引用 (PC-relative reference) */
if (r.type == R_X86_64_PC32) {
refaddr = ADDR(s) + r.offset; /* 引用的运行时地址 */
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr);
}

/* 重定位一个绝对引用 (Absolute reference) */
if (r.type == R_X86_64_32)
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
}
}

这段文字详细介绍了重定位 PC 相对引用的计算过程。其 Markdown 格式如下:

1. 重定位 PC 相对引用

函数 main 调用 sum 函数,sum 函数是在模块 sum.o 中定义的。

回想一下,重定位条目和指令实际上存放在目标文件的不同节中。为了方便,OBJDUMP 工具把它们显示在一起。

call 指令开始于字节偏移 0xe 的地方,包括 1 字节的操作码 0xe8,后面跟着的是对目标 sum 的 32 位 PC 相对引用的占位符。

相应的重定位条目 r 由 4 个字段组成: * r.offset = 0xf * r.symbol = sum * r.type = R_X86_64_PC32 * r.addend = -4

这些字段告诉链接器修改开始于偏移量 0xf 处的 32 位 PC 相对引用,这样在运行时它会指向 sum 例程。现在,假设链接器已经确定: ADDR(s) = ADDR(.text) = 0x4004d0

ADDR(r.symbol) = ADDR(sum) = 0x4004e8

使用算法,链接器首先计算出引用的运行时地址:

1
2
3
refaddr = ADDR(s) + r.offset
= 0x4004d0 + 0xf
= 0x4004df

然后,更新该引用,使得它在运行时指向 sum 程序:

1
2
3
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr)
= (unsigned) (0x4004e8 + (-4) - 0x4004df)
= (unsigned) (0x5)

在得到的可执行目标文件中,call 指令有如下的重定位的形式: 4004de: e8 05 00 00 00 callq 4004e8 <sum>

在运行时,call 指令将存放在地址 0x4004de 处。当 CPU 执行 call 指令时,PC 的值为 0x4004e3(即紧随在 call 指令之后的指令地址)。为了执行这条指令,CPU 执行以下的步骤: 1. 将 PC 压入栈中 2. PC ← PC + 0x5 = 0x4004e3 + 0x5 = 0x4004e8

因此,要执行的下一条指令就是 sum 例程的第一条指令

2. 重定位绝对引用

重定位绝对引用相当简单。例如,mov 指令将 array 的地址(一个 32 位立即数值)复制到寄存器 %edi 中。mov 指令开始于字节偏移量 0x9 的位置,包括 1 字节操作码 0xbf,后面跟着对 array 的 32 位绝对引用的占位符。

对应的占位符条目 r 包括 4 个字段: * r.offset = 0xa * r.symbol = array * r.type = R_X86_64_32 * r.addend = 0

这些字段告诉链接器要修改从偏移量 0xa 开始的绝对引用,这样在运行时它将会指向 array 的第一个字节。现在,假设链接器已经确定: ADDR(r.symbol) = ADDR(array) = 0x601018

链接器使用算法的第 13 行修改了引用:

1
2
3
*refptr = (unsigned) (ADDR(r.symbol) + r.addend)
= (unsigned) (0x601018 + 0)
= (unsigned) (0x601018)

在得到的可执行目标文件中,该引用有下面的重定位形式:

4004d9: bf 18 10 60 00 mov $0x601018,%edi %edi = &array

综合到一起,在加载的时候,加载器会把这些节中的字节直接复制到内存,不再进行任何修改地执行这些指令。

家庭作业

7.6

符号swap.o.symtab条目?符号类型定义符号的模块
buf外部m.o.data
bufp0全局swap.o.data
bufp1局部swap.o.bss
swap全局swap.o.text
temp
incr局部swap.o.text
count局部swap.o.bss

7.7

double x; 前添加 stastic

7.8

(a)REF(main.1)->DEF(main.1)

(b)REF(main.2)->DEF(main.2)

(a)REF(main.1)->DEF(未知)

(b)REF(main.2)->DEF(未知)

(a)REF(x.1)->DEF(错误)

(b)REF(x.2)->DEF(错误)

7.9

foo6 中 main 为强符号

bar6 中 main 为弱符号

链接时选择强符号

输出 main 地址

main 地址为 push 或 REX

对应机器码为 push:0x48

7.10

1
gcc p.o libx.a
1
gcc p.o libx.a libx.a liby.a
1
gcc p.o libx.a liby.a libx.a libz.a

7.11

可执行文件中,.bss 不占据实际空间,运行时才分配空间

7.12

r.type= R_X86_64_PC32时的重定位计算: *refptr = Addr(r.symbol) + r.addend - Addr(.text) - r.offset

对A: *refptr = 0x4004f8 - 0x4 - 0x4004e0 - 0xa = 0xa

对B: *refptr = 0x400500 - 0x4 - 0x4004d0 - 0xa = 0x22

  • Title: CSAPP7
  • Author: exdoubled
  • Created at : 2026-01-06 23:00:00
  • Updated at : 2026-01-07 23:21:22
  • Link: https://github.com/exdoubled/exdoubled.github.io.git/CSAPP/CSAPP7/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments