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_,把对real_func 的引用解析为 func。

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 中的实现了。