CSAPP笔记-链接
链接
将代码和数据片段收集并组合成一个单一文件的过程,这个文件可以被加载到内存并执行
1.编译器驱动程序
// main.c
int sum(int* a, int n);
int array[2] = {1, 2};
int main() {
int val = sum(array, 2);
return val;
}
// sum.c
int sum(int* a, int c) {
int i, s = 0;
for (i = 0; i < c; i++) {
s += a[i];
}
return s;
}
静态编译过程
cpp [other arguments] main.c /tmp/main.i 预处理
ccl /tmp/main.i -Og [other arguments] -o /tmp/main.s 编译器
as [other arguments] -o /tmp/main.o /tmp/main.s 汇编器
ld -o prog [system object files and args] /tmp/main.o /tmp/sum.o 链接
./prog 调用os中一个加载器的函数,将可执行文件prog的代码和数据复制到内存,控制转移到这个程序的开头
2.静态链接
可重定位目标文件和命令行参数作为输入
一个完全链接的、可加载和运行的可执行目标文件作为输出
可重定位目标文件由不同的代码和数据节组成
链接器的任务
符号解析(symbol resolution)将每个符号引用和一个符号定义关联起来,每个符号定义对应一个函数,全局变量或静态变量
重定位(relocation)编译器和汇编器生成从地址0开始的代码和数据节,链接器通过每个符号定义与一个内存位置关联起来,重定位这些节,然后修改所有对这些符号的引用,使他们指向这个内存位置。
3.目标文件
- 可重定位目标文件:二进制代码和数据,编译时可与其他可重定位模板文件合并创建出一个可执行目标文件
- 可执行目标文件:二进制代码和数据,可被直接复制到内存并执行
- 共享目标文件:特殊的可重定位目标文件,可在加载时或运行时被动态的加载到内存并链接
一个目标模块就是一个字节序列,一个目标文件是一个以文件形式存放在磁盘中的目标模块
4.可重定位目标文件(executable and linkable Format,ELF)
ELF
.text 已编译程序的机器代码
.rodata 只读数据,字符串
.data 已初始化的全局和静态C变量
.bss 未初始化的全局和静态C变量,占位符,内存运行时分配
.symtab 符号表,存放程序中定义和引用的函数和全局变量的信息
不包含局部变量的条目
.rel.text .text节中的列表,目标文件和其他文件结合时需要修改
.rel.data 被模块引用和定义的全局变量的重定位信息
.debug 调试符号表,-g
.line 源C程序中的行号和.text节中机器指令之间的映射 -g
.strtab 字符串表,包括.symtab和.debug节中的符号表,以及节头部中的节名字
5.符号和符号表
- 由模块m定义并能被其他模块引用的全局符号。非静态C函数和全局变量
- 其他模块定义并被m引用的全局符号,外部符号
- 只被模块m定义和引用的局部符号,static属性的C函数和全局变量
typedef struct {
int name; /* String table offset */
char type:4, /* Function or data (4 bits) */
binding:4; /* Local or global (4 bits) */
char reserved; /* Unused */
short section; /* Section header index */
long value; /* Section offset or absolute address */
long size; /* Object size in bytes */
} Elf64_Syrnbol;
6.符号解析
-
解析多重定义的全局符号
处理多重定义的符号名规则,函数和已初始化的全局变量为强符号,未初始化为弱符号
不允许多个同名的强符号
如果一个强符号和多个弱符号,选择强符号
多个弱符号同名,弱符号中任选一个
GCC -fno-common 标志这样的选项调用链接器, 这个选项会告诉链接器, 在遇到多重定义的全局符号时, 触发一个错误。 或者使 用 -Werror选项, 它会把所有的警告都变为错误 -
与静态库链接
将所有相关的目标模块打包成一个输出的可执行文件
linux> gcc -c addvec.c multvec.c
linux> ar rcs libvector.a addvec.o multvec.o
linux> gcc -c main2.c
linux> gcc -static -o prog2c main2.o -L. -lvector
- 链接器如何使用静态库来解析引用
链接器维护一个可重定位目标文件E,未解析的符号集合U,前面输入文件中已定义的符号集合D- 对于命令行输入文件f,如果是目标文件,f加到E中,修改U和D来反映f中符号的定义和引用
- f是存档文件,尝试匹配U中未解析的符号和由存档文件定义的符号。如果某个存档文件成员m,定义了一个符号来解析U中的一个引用,就将m加到E中,并修改链接器中的U和D来反映m中的符号定义和引用。
- 如果对输入文件扫描后,U是非空的,链接器输出一个错误
gcc -static ./libvector.a main2.c
处理libvector.a时,U是空的,所以没有libvector.a中的成员加入E中,所以会报错
对于库一般放在命令行结尾,被依赖的库放后方
7.重定位
在重定位步骤中,合并输入模块,为每个符号分配运行时地址
-
由两步组成:1.重定位节和符号定义,将相同类型的节合并成同一类型的新的聚合节,如将所有的.data节全部合并成一个节,然后链接器将运行时内存地址赋给新的聚合节,赋给输入模块定义的每个节,以及赋给输入模块定义的每个符号。这一步完成程序的每条指令和全局变量都有唯一的运行时内存地址了
2.重定位节中的符号引用,链接器修改代码节和数据节中对每个符号的引用,使他们指向正确的运行时地址。这一步链接器依赖于可重定位目标模块中的重定位条目数据结构 -
重定位条目
相对引用和绝对引用
8.可执行目标文件
.init节定义了一个小函数,叫做_init程序的初始化代码会调用它,因为可执行文件是完全链接的,不需要.rel节
可执行文件连续的片很容易被映射到连续的内存段
9.加载可执行目标文件
10.动态链接共享库
11.从应用程序中加载和链接共享库
12.位置无关代码
13.库打桩
- 编译时打桩,使用包装函数替换目标函数
// int.c
#include <stdio.h>
#include <malloc.h>
int main() {
int *p = malloc(32);
free(p);
return 0;
}
// mymalloc.h
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void* ptr);
// mymalloc.c
#ifdef COMPILETIME
#include <stdio.h>
#include <malloc.h>
void* mymalloc(size_t size) {
void* ptr = malloc(size);
printf("malloc(%d) = %p\n",
(int)size, ptr);
return ptr;
}
void myfree(void* ptr) {
free(ptr);
printf("free(%p)\n", ptr);
}
#endif
lfgphicprd21228:/home/link/complier # gcc -DCOMPILETIME -c mymalloc.c
lfgphicprd21228:/home/link/complier # gcc -I. -o intc int.c mymalloc.o
lfgphicprd21228:/home/link/complier # ./intc
malloc(32) = 0x242e010
free(0x242e010)
使用-I.参数,告诉C预处理器在搜索通常的系统目录之前,先在当前目录中查找malloc.h
- 链接时打桩
--wrap f表示对符号f的引用解析成__wrap_f,对符号 __real_f的引用解析成f
#ifdef LINKTIME
#include <stdio.h>
// __real_malloc --> malloc
void* __real_malloc(size_t size);
void __real_free(void* ptr);
// malloc --> __wrap_malloc
void* __wrap_malloc(size_t size) {
void* ptr = __real_malloc(size);
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
void* __wrap_free(void* ptr) {
__real_free(ptr);
printf("free(%p)\n", ptr);
}
#endif
lfgphicprd21228: # gcc -DLINKTIME -c mymalloc.c
lfgphicprd21228: # gcc -c int.c
lfgphicprd21228: # gcc -Wl,--wrap,malloc -Wl,--wrap,free -o intl int.o mymalloc.o
lfgphicprd21228: # ./intl
malloc(32) = 0x248b010
free(0x248b010)
- 运行时打桩
编译时打桩需要访问源代码,链接时打桩需要访问程序的可重定位对象文件,运行时打桩只需要访问可执行目标文件
LD_PRELOAD环境变量被设置为一个共享库路径名的列表,执行一个程序,需要解析未定义引用时,LD-Linux.SO会先搜索LD_PRELOAD库,再搜索其他库
lfgphicprd21228: # gcc -DRUNTIME -shared -fpic -o mymalloc.so mymalloc.c -ldl
lfgphicprd21228: # gcc -o inter int.c
lfgphicprd21228: # LD_PRELOAD="./mymalloc.so" ./inter
malloc(32) = 0x24f3040
free(0x24f3040)
#ifdef RUNTIME
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
void* malloc(size_t size) {
void *(*mallocp)(size_t size);
char* error;
mallocp = dlsym(RTLD_NEXT, "malloc");
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
char* ptr = mallocp(size);
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
void free(void* ptr) {
void (*freep)(void* ) = NULL;
char* error;
if (!ptr) {
return;
}
freep = dlsym(RTLD_NEXT, "free");
if ((error = dlerror()) != NULL) {
fputs(error, stderr);
exit(1);
}
freep(ptr);
printf("free(%p)\n", ptr);
}
#endif