CSAPP 读书笔记系列chap7
chap7 链接 linking
这一次谈的是链接,也就是把各种代码(和库)和数据片段收集并组合成一个单一文件(可执行文件,例如:ELF)的过程.
链接可以* 执行在不同的阶段 *
- 编译时(complie time): 源代码被翻译成机器代码时
- 加载时(load time ): 程序被加载器加载到内存并执行时
- 运行时(run time): 由应用程序来执行
所以,连接器在软件设计中举足轻重,在设计模式中也有其一番作为,例如工厂模式,可以把软件的编译时依赖关系转为运行时依赖.
例如下列过程:
/* sum.c */
int sum(int *a, int n)
{
int i, s = 0;
for (i = 0; i < n; i++) {
s += a[i];
}
return s;
}
和其main函数
/* main.c */
int sum(int *a, int n);
int array[2] = {1, 2};
int main()
{
int val = sum(array, 2);
return val;
}
当使用命令行
linux> gcc -Og -o prog main.c sum.c #产生prog文件
linux> ./prog #把prog文件加载到内存
其过程如下:
图片出自 http://wdxtub.com/2016/04/16/thin-csapp-4/
更加具体的为
预处理器cpp: cpp [other args] main /tmp/main.i # .i 中间文件
编译器cc1: cc1 /tmp/main.i -Og [other args] -o tmp/main.s # .s 汇编文件
汇编器as: as [other args] -o /tmp/main.o /tmp/main.s # .o 可重定位文件
链接器ld: ld -o prog [other args] /tmp/main.o /tmp/sum.o # prog 可执行目标文件
7.2 静态链接
链接器的两个任务:
符号解析 Symbol resolution: 在汇编的基础上,把每一个符号表中的符号定义和引用* 关联*起来,
重定位 Relocation:把每个内存位置和符号定义关联起来,从而* 重定位 *编译器和汇编器产生的代码和数据节.
这里一个关键的概念是:
* 目标文件纯粹是一个字节块的集合 *,里面有不同的块(例如程序代码,程序数据).下面仔细说
7.3 可执行目标文件
编译器和汇编器产出可重定位目标文件,连接器产生可执行目标文件
可执行目标文件有三种:
可重定位目标文件 Relocatable object file (.o file)
- 每个 .o 文件都是由对应的 .c 文件通过编译器和汇编器生成,包含代码和数据,可以与其他可重定位目标文件合并创建一个可执行或共享的目标文件
可执行目标文件 Executable object file (a.out file)
- 由链接器生成,可以直接通过加载器加载到内存中充当进程执行的文件,包含代码和数据
共享目标文件 Shared object file (.so file)
- 共享库文件, windows 中被称为 Dynamic Link Libraries(DLLs),是类特殊的可重定位目标文件,可以在链接(静态共享库)时加入目标文件或加载时或运行时(动态共享库)被动态的加载到内存并执行
目标文件格式为也一般为(概念相似):
a.out: 第一个Unix系统使用的
PE格式: windows上的
Math-O格式: Mac OS-X的
ELF 格式: 可执行可连接格式,现代Linux和Unix使用的,主要讨论
7.4 可重定位目标文件
例如ELF格式的,长如下样子:
图片出自 http://wdxtub.com/2016/04/16/thin-csapp-4/
每个区的包含文件如下:
- ELF header
- 包含 word size, byte ordering, file type (.o, exec, .so), machine type, etc
- Segment header table
- 包含 page size, virtual addresses memory segments(sections), segment sizes
.text section
- 代码部分
.rodata section
- 只读数据部分,例如跳转表
- .data section
- 初始化的全局变量
- .bss section
- 未初始化的全局变量
- .symtab section
- 包含 symbol table, procudure 和 static variable names 以及 section names 和 location
- .rel.txt section
.text section 的重定位信息 - .rel.data section
- .data section 的重定位信息
- .debug section
- 包含 symbolic debugging (gcc -g) 的信息
- Section header table
- 每个 section 的大小和偏移量
7.5符号和符号表.symbol
C语音中的符号(变量和函数)在符号表 .symbol section 中有三种形式:
- 全局符号 Global symbols
- 在当前模块中定义,且可以被其他代码引用的符号,例如非静态 C 函数和非静态全局变量
- 外部符号 External symbols
- 同样是全局符号,但是是在其他模块(也就是其他的源代码)中定义的,但是可以在当前模块中引用
- 本地符号 Local symbols
- 在当前模块中定义,只能被当前模块引用的符号,带static 属性,例如静态函数和静态全局变量
注意: 符号表.symbols 中不包含对应本地非静态non-static程序的任何符号,也就是区分本地链接器符号Local linker symbol和本地程序符号 local program variables,这里讨论的是前者.
例如下面:
// 文件 main.c
int sum(int *a, int n);
int array[2] = {1, 2}; // 变量 array 在此定义
int main() // 定义了一个全局函数
{
int val = sum(array, 2);
// val 是局部变量,链接器并不知道
// sum 函数是一个全局引用
// array 变量是一个全局引用
return val;
}
// -----------------------------------------
// 文件 sum.c
int sum(int *a, int n) // 定义了一个全局函数
{
int i, s = 0;
// i 和 s 是局部变量,链接器并不知道
for (i = 0; i < n; i++)
s += a[i];
return s;
}
本地非静态non-static程序的任何符号也就是由栈自动管理,有时候也称为自动变量?
这里说的面向对象中的封装也是使用 static 实现的
另外,全局变量的初始化是十分重要的,见effective C++的条例
7.6 符号解析 Symbol resolution
把每一个符号表中的符号定义和引用* 关联*起来,而且是唯一可辨别的
C++中的重载也是这一阶段实现的,通过其唯一的方法名称和参数列表来编码(叫做重整,其反过程称为回复)成对链接器的唯一名字.
而链接器对全局符号也会有一定的规则,利用的是强弱定理:
- 强符号:函数和初始化的全局变量
- 弱符号:未初始化的全局变量
// 文件 p1.c
int foo = 5; // 强符号,已初始化
p1() { ... } // 强符号,函数
// -----------------------------------------
// 文件 p2.c
int foo; // 弱符号,未初始化
p2() { ... } // 强符号,函数
链接器在处理强弱符号的时候遵守以下规则:
- 1.不能出现多个同名的强符号,不然就会出现链接错误
- 2.如果有同名的强符号和弱符号,选择强符号,也就意味着弱符号是『无效』d而
- 3.如果有多个弱符号,随便选择一个
pdf上的几个例子:
- 1.链接错误
// 文件 p1.c
int x;
p1() { ... }
// -----------------------------------------
// 文件 p2.c
p1() { ... }
可以看到上面代码中声明了两个同名的函数,都是强符号,所以会出现链接错误。
- 2.未初始化
// 文件 p1.c
int x;
p1() { ... }
// -----------------------------------------
// 文件 p2.c
int x;
p2() { ... }
上面的两个 x 实际上在执行时会引用同一个未初始化的整型,并不是两个独立的变量。
- 3.变量相互影响
// 文件 p1.c
int x;
int y;
p1() { ... }
// -----------------------------------------
// 文件 p2.c
double x;
p2() { ... }
x 会复写overwrite y
因为* 编译器不会进行类型检测的,Linker does not do type checking.*
对于这类错误,最后使用-Werror,将warning 转为 error
所以这里的一些编程规则是:
对于Global全局变量:
- 尽量使用静态变量替代
- 定义全局变量的时候记得初始化
- 注意使用 extern 关键字
- 将其变为一个弱符号
- 记得在其他文件中定义
7.7 重定位:
把每个内存位置和符号定义关联起来,从而* 重定位 *编译器和汇编器产生的代码和数据节.
都出自 http://wdxtub.com/2016/04/16/thin-csapp-4/
出自 http://wdxtub.com/2016/04/16/thin-csapp-4/
通过 objdump -r -d main.o 反编译对应的可重定位对象文件,可以得到如下的汇编代码:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8, %rsp
4: be 02 00 00 00 mov $0x2, %esi
9: bf 00 00 00 00 mov $0x0, %edi # %edi = &array
a: R_X86_64_32 array # Relocation entry
e: e8 00 00 00 00 callq 13 <main+0x13> # sum()
f: R_X86_64_PC32 sum-0x4 # Relocation entry
13: 48 83 c4 08 add $0x8, %rsp
17: c3 retq
这里我们可以看到,编译器用 relocation entry 来标记不同的调用(注意看对应的代码后面四组数字都是零,就是留出位置让链接器在链接的时候填上对应的实际内存地址)
在完成链接之后我们得到 prog 这个程序,同样反编译 objdump -dx prog 可以看到:
00000000004004d0 <main>:
4004d0: 48 83 ec 08 sub $0x8, %rsp
4004d4: be 02 00 00 00 mov $0x2, %esi
4004d9: bf 18 10 60 00 mov $0x0, %edi # %edi = &array
4004de: e8 05 00 00 00 callq 4004e8 <sum> # sum()
4004e3: 48 83 c4 08 add $0x8, %rsp
4004e7: c3 retq
00000000004004e8 <sum>:
4004e8: b8 00 00 00 00 mov $0x0, %eax
...
...
400501: f3 c3 repz retq
对应的地址已经被填上去了,这里注意用的是相对的位置,比方说 0x4004de 中的 05 00 00 00 的意思实际上是说要在下一句的基础上加上 0x5,也就是 0x4004e8,即 sum 函数的开始位置。
其载入内存时:
静态库和共享库
- 静态库 Static Library
抄自: http://wdxtub.com/2016/04/16/thin-csapp-4/
静态库是一个外部函数与变量的集合体。静态库的文件内容,通常包含一堆程序员自定的变量与函数,其内容不像动态链接库那么复杂,在编译期间由编译器与连接器将它集成至应用程序内,并制作成目标文件以及可以独立运作的可执行文件。而这个可执行文件与编译可执行文件的程序,都是一种程序的静态创建(static build)
具体过程就是把不同文件的 .o 文件通过 Archiver 打包成为一个 .a 文件。Archiver 支持增量更新,如果有函数变动,只需要重新编译改动的部分。
在 C 语言中最常用的是 C 标准库与 C 数学库。C 标准库一般可以通过 libc.a 来进行引用,大小 4.6 MB,包含 1496 个对象文件,主要负责输入输出、内存分配、信号处理、字符串处理、操作数据和实践、生成随机数及整型的数学运算。C 数学库可以通过 libm.a 来引用,大小 2 MB,包含 444 个对象文件,主要是提供浮点数运算的支持(比如三角函数、幂次等等)
对于例子
// 文件 main.c
#include "vector.h"
#include <stdio.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main() {
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
return 0;
}
// -----------------------------------------
// 文件 addvec.c
void addvec(int *x, int *y, int *z, int n) {
int i;
for (i = 0; i < n; i++)
z[i] = x[i] + y[i];
}
// -----------------------------------------
// 文件 multvec.c
void multvec(int *x, int *y, int *z, int n) {
int i;
for (i = 0; i < n; i++)
z[i] = x[i] * y[i];
}
其链接过程如下:
解析外部引用
其步骤为:
- 扫描当前命令中的 .o 和 .a 文件
- 扫描过程中,维护一个当前未解析引用的列表
- 扫描到新的 .o 或 .a 文件时,试图去寻找未解析引用
- 如果扫描结束时仍旧有为解析的引用,则报错
也就是* 按顺序查找 *
例如:
unix> gcc -L. libtest.o -lmine
# 上面这句不会出错,但是下面的会
unix> gcc -L. -lmine libtest.o
libtest.o: In function `main`:
libtest.o(.text+0x4): Undefined reference to `libfun`
所以静态库链接一般准则
- 一般是放在命令行的最后
gcc -L. libtest.o -lmine - 两个库交互引用时
例如库libx.a 和 liby.b
- 可以把两个库合在一起
- 或命令行为:
gcc foo.c libx.a liby.a libx.a
但静态库占用的内存太多,因为库的代码会被复制到每个运行进程的文本段中;
也不支持热升级,所以有了共享库
- 共享库 shared Library
定义: 其为一个目标模块,在运行和加载时,课程加载到任意的内存地址,并和一个内存中的程序链接起来.
对于上述过程,其动态链接为:
而产生一个动态库 libvector.so的命令行为:
gcc -shared -fpic -o libvector.so addvec.c multvec.
其中:
shared 表示创建一个共享库
fpic : File Position-Independent Code ,位置无关的代码,可以加载无需重定位的代码.具体可以看书p489
库打桩
这里说的是一个类似链接技术,可以在不同时刻运行:
- 编译时(complie time): 源代码被翻译成机器代码时
- 加载时(load time ): 程序被加载器加载到内存并执行时
- 运行时(run time): 由应用程序来执行
主要是截取共享库的调用,换为执行自己的代码,所以可以统计函数调用的次数等功能.
其使用为加入-I.参数
例如
gcc -Wall -I. -o intc int.c mymalloc.o
一些处理目标文件的工具
- AR : 创建静态库等
- OBJDUMP: 所有二进制工具之母
- LDD : 列出一个可执行文件在运行时所需要的共享库
etc
总结
之前实习做堡垒机项目的时候,对链接的许多概念特别是共享库很不成熟.现在回到CSAPP这本书这里重拾基础,当时一个观点就是:勿在浮沙上筑高台。感觉基础不牢地动山摇似得,希望可以有个好基础吧。加油!