7.2 静态链接
为了构造可执行文件,链接器必须完成两个主要任务:
- 符号解析。将每个符号引用和一个符号定义连接起来。
- 重定位。编译器和汇编器生成从 0 地址开始的代码和数据节。连接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节。然后修改所有对这些符号的引用,使得他们指向这个内存位置。
7.3 目标文件
- 可重定位目标文件 .o, .a(.a 就是一堆打包的.o)
- 可执行目标文件 elf
- 共享目标文件 .so
7.4 可重定位目标文件
- .text 代码段
- .rodata 只读数据段,保存字符串,const 数据等。
- .data 数据段。全局和静态变量。
- .bss 未初始化和初始化为 0 的全局和静态变量。不占空间。
- .symtab 符号表,存放程序中定义和引用的函数和全局变量信息。
- .rel.text .text 节中需要重定位的代码段。
- .rel.data .data 节中需要重定位的数据段,被模块引用或定义的所有全局变量。
- .debug 调试符号表。包含程序中定义的局部变量和类型定义,定义和引用的全局变量,原始的 C 源文件。
- .line 原始 C 文件中的行号和.text 节中机器指令的映射。
- .strtab 字符串表。
7.5 符号和符号表
- 由当前模块定义并能被其他模块引用的全局符号
- 其他模块定义并被当前模块引用的全局符号,称为外部符号
- 只被当前模块定义和引用的的局部符号
下面两个局部静态变量:
int f()
{
static int x = 1;
return x;
}
int g()
{
static int x = 2;
return x;
}
两个 x 分别在各自的函数中可见,这两个 x 都会保存到.data 段,比如 x.1 表示 f()中的定义,x.2 表示 g()中的定义。
符号表条目:
typedef struct {
int name; // 字符串表中的字节偏移,指向符号的字符串名
char type:4, // 表示函数还是data
binding:4; // 表示全局还是局部
char reserved;
short section; // section index,表示属于哪个节
long value; // 重定位文件:该符号距离节起始位置的偏移地址,可执行文件:绝对地址。
long size; // 该符号的大小
} Elf64_Symbol
比如执行readelf -s xxx
查看符号表:
Value 对应结构体中的 value, Size 对应 size, Type 对应 type, Bind 对应 binding, Ndx 对应 section, Name 对应 name。
可以看到 main 符号,位于.text (Ndx=1) 段,偏移量为 0,大小为 24Bytes。 全局变量 array 符号,位于.data (Ndx=3) 段,偏移量为 0,大小为 8Bytes。
可重定位文件有三个特殊的伪节: ABS(不该被重定位的符号), UNDEF(未定义的符号), COMMON(未被分配位置的未初始化的数据)。
COMMON 和.bss 的区别:
- COMMON 未初始化的全局变量(弱符号)
- .bss 未初始化的静态变量,以及初始化为 0 的全局变量和静态变量(强符号)
因为弱的全局符号无法确认外部模块是否也定义了同名的符号,所以不能确定地分配到.data
或.bss
段,而是先保留在COMMON
段。
7.6 符号解析
7.6.1 连接器如何解析多重定义的全局符号
- 不允许有多个同名的强符号。
- 如果有一个强符号和多个弱符号同名,那么选择强符号。
- 如果有多个弱符号同名,那么从这些弱符号中任意选择一个。
如果在一个模块里全局变量 x 未被初始化,另一个模块里 x 初始化了,那么链接器会安静的选择定义的强符号。
下面这个例子会打印出来 x=15212,与预想打印出来的 15213 冲突了。
// foo3.c
int x = 15213;
int main()
{
f();
printf("x= %d\n", x);
return 0;
}
// foo4.c
int x;
void f()
{
x = 15212;
}
如果这两个 x 是弱定义的话,也会发生相同的事情。会造成一些不易察觉的运行时错误。
通过 GCC, -fno-common
, 在遇到多重定义的全局符号时触发错误。
看下面两个例子:
// 1.c
int x;
int y;
p1(){}
// 2.c
double x;
p2(){}
// 1.c
int x = 7;
int y = 5;
p1(){}
// 2.c
double x;
p2(){}
如果在2.c
中修改 x 的值,那么会 overwrite1.c
中的 y。即使在第二个例子中 x 是强符号,由于类型不同仍然会覆盖。
7.6.2 与静态库链接
7.6.3 链接器如何使用静态库来解析引用
链接时,只会把库.a 中用到的.o 复制到最后的可执行文件。
如果定义一个符号的库出现在引用这个符号的目标文件之前,那么引用就不能被解析,链接会失败。gcc -static ./libvector.a main.c
会出错。
正确的写法是gcc -static -o prog main.o -L. -lvector
其中-static
告诉链接器应该构建一个完全链接的可执行文件,可以加载到内存并运行,无需进一步链接。-L.
在当前目录查找库,-lvector
等价于libvector.a
所以库的一般准则是将他们放在命令行的结尾。如果库之间不是相互独立的,那么必须进行排序,确保定义在引用之后。
7.7 重定位
- 重定位节和符号定义。将所有同类型的节合并。
- 重定位节中的符号引用。修改对每个符号的引用,使得他们指向正确的运行时地址。
7.7.1 重定位条目
遇到对位置未知的目标引用,会生成一个重定位条目,放在.rel.text
和.rel.data
中。
重定位条目的结构为:
typedef struct{
long offset; // 该引用在节中的offset
long type:32, // 重定位类型
symbol:32; // 重定位符号
long addend; // 重定位中需要用到的一个常量
} Elf64_Rela
两种基本类型的重定位:
R_X86_64_PC32
: 使用 32 位的 PC相对地址的引用。R_X86_64_32
: 使用 32 位的绝对地址的引用。
7.7.2 重定位符号引用
重定位算法:
foreach section s { // 每个节section
foreach relocation entry r { // 每个重定位条目
refptr=s+ r.offset; /* ptr to reference to be relocated */
/* Relocate a PC-relative reference */
if (r.type == R_X86_64_PC32) {
refaddr = ADDR(s) + r.offset; /* ref’s run-time address */
*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr);
}
/* Relocate an absolute reference */
if (r.type == R_X86_64_32)
*refptr = (unsigned) (ADDR(r.symbol) + r.addend);
}
}
假设目标文件中的汇编代码如下,会在每个需要重定位的引用后面,产生一个重定位条目。
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
重定位 PC 相对引用
上面对 sum 的相对引用可以得到重定位条目:
r.offset = 0xf r.symbol = sum r.type = R_X86_64_PC32 r.addend = -4
假设链接器已经确定:ADDR(s) = ADDR(.text) = 0x4004d0
, ADDR(r.symbol) = ADDR(sum) = 0x4004e8
。
根据重定位算法,计算出引用的运行时地址:refaddr = ADDR(s) + r.offset = 0x4004df
更新该引用,使得它在运行时指向 sum 程序:*refptr = (unsigned) (ADDR(r.symbol) + r.addend - refaddr) = 0x4004e8 -4 - 0x4004df = 0x5
因此在执行到 call 指令,地址为 0x4004de,pc 跳转到 sum 符号,PC = PC + 0x5 = (0x4004de + 0x4) + 0x5 = 0x4004e8
重定位绝对引用
对 array 的绝对引用可得到重定位条目:
r.offset = 0xa r.symbol = array r.type = R_X86_64_32 r.addend = 0
假设链接器已经确定:ADDR(r.symbol) = ADDR(array) = 0x601018
,那么*refptr = (unsigned) (ADDR(r.symbol) + r.addend) = 0x601018
, 对应的汇编变成9: bf 18 10 60 00 mov $0x601018,%edi
7.8 可执行目标文件
和目标文件相比,多了Segment header table
程序头部表,描述了可执行文件段加载到内存的地址。可以通过objdump -h
查看
size
是总内存大小,VMA
是虚拟地址,LMA
是加载地址,File off
是该段在目标文件中的偏移量,Algn
是该段的对齐要求。
注意 VMA 和 LMA 的区别。
Idx Name Size VMA LMA File off Algn
15 .text 00000107 0000000000001060 0000000000001060 00001060 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE
.init
定义了一个_init
函数,程序初始化代码会调用到。
7.10 动态链接共享库
生成动态链接库:gcc -shared -fpic -o libvector.so addvec.c multvec.c
其中-fpic
用来生成位置无关码,-shared
用来生成动态链接库。
生成 prog 可执行文件,并指定动态链接库:gcc -o prog main.c ./libvector.so
7.11 从应用程序中加载和链接共享库
除了在程序加载和运行时可以加载动态库,还可以通过dlopen(const char *filename, int flag)
函数在程序中显式加载和链接动态库。
dlsym
函数输入dlopen
返回的句柄和符号的字符串,返回该符号的地址。dlclose
卸载共享库。
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
void *handle;
void (*addvec)(int *, int *, int *, int);
char *error;
/* Dynamically load the shared library containing addvec() */
handle = dlopen("./libvector.so", RTLD_LAZY);
/* Get a pointer to the addvec() function we just loaded */
addvec = dlsym(handle, "addvec");
/* Now we can call addvec() just like any other function */
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
/* Unload the shared library */
dlclose(handle);
return 0;
}
7.12 位置无关代码
位置无关代码 (PIC) 是无论其绝对地址如何(即通过使用相对寻址)都可以执行的代码。PIC 用于共享库,允许库代码位于内存中的任何位置。
GCC 使用-fpic
编译选项生成位置无关码,共享库的编译必须总是使用该选项。
PIC 数据引用
PIC 函数调用
7.13 库打桩机制
库打桩机制,允许截获对共享库函数的调用,需要创建一个原型和目标函数完全一样包装函数。
比如我们想要截获 malloc 和 free 函数,int.c 中调用 malloc:
//int.c
#include <stdio.h>
#include <malloc.h>
int main()
{
int *p = malloc(32);
free(p);
return 0;
}
7.13.1 编译时打桩
创建一个本地的 malloc.h 头文件,将 malloc 和 free 函数定义成我们自己的 mymalloc 和 myfree 函数:
// malloc.h
#define malloc(size) mymalloc(size)
#define free(ptr) myfree(ptr)
void *mymalloc(size_t size);
void myfree(void *ptr);
这两个函数的实现为:
// mymalloc.c
#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);
}
编译: gcc -I. -o intc int.c mymalloc.c
,由于有-I.
参数,所以会进行打桩,在搜索通常的系统目录之前会现在当前目录中查找 malloc.h,这样调用 malloc 会执行到我们的 mymalloc()函数中。
7.13.2 链接时打桩
#include <stdio.h>
void *__real_malloc(size_t size);
void __real_free(void *ptr);
void *__wrap_malloc(size_t size)
{
void *ptr = __real_malloc(size); /* Call libc malloc */
printf("malloc(%d) = %p\n", (int)size, ptr);
return ptr;
}
void __wrap_free(void *ptr)
{
__real_free(ptr); /* Call libc free */
printf("free(%p)\n", ptr);
}
利用--wrap <func>
进行编译时打桩,告诉编译器把对 func 的引用解析成wrap_
gcc -Wl,--warp,malloc -Wl,--wrap,free -o intl int.c mymalloc.c
,-Wl,option 可以把 option 传给链接器,这样 malloc 的实现就被我们替换成__wrap_malloc,而实际的 malloc 实现被替换成了__real_malloc。
7.13.3 运行时打桩
可以通过设置LD_PRELOAD
环境变量来实现运行时打桩。
如果将其设置为一个共享库路径名的列表,那么当执行一个程序,需要解析未定义的引用时,会先搜索 LD_PRELOAD 路径的库,然后才会搜索其他任何的库。
执行LD_PRELOAD="./mymalloc.so" ./intr
,就会把 main 中的 malloc 函数替换成我们自己制作的动态库 mymalloc.so 中的实现了。