计算机 · 2021年12月10日 0

Linking

C程序的编译过程

  1. 预处理cpp [other arguments] main.c /tmp/main.i
  2. 编译cc1 /tmp/main.i main.c -O2 [other arguments] -o /tmp/main.s
  3. 汇编as [other arguments] -o /tmp/main.o /tmp/main.s
  4. 链接ld -o p [system object files and args] /tmp/main.o /tmp/swap.o

在用gcc编译程序的时候,可以用-v选项打印出gcc在整个编译过程中所使用的具体命令。

-v Print (on standard error output) the commands executed to run the stages of compilation. Also print the version number of the compiler driver program and of the preprocessor and the compiler proper.

两种链接方式

静态链接(Static Linking)

静态链接需要完成的任务:

  • Symbol resolution 让所有的符号引用指向唯一的符号定义
  • Relocation 编译器和汇编器产生的可重定位目标文件的代码段和数据段的地址都是从0开始的,静态链接通过为符号定义指定内存地址来重新定位这些段(relocate),然后修改所有的符号引用让它们能够指向正确的内存地址。

动态链接(Dynamic Linking)

三种目标文件

  • Relocatable object file可重定位目标文件组成描述ELF header链接器通过ELF header来解析此目标文件。前16个字节描述word size和字节序。剩下部分包含的信息有:ELF header自身的长度、目标文件类型(relocatable/executable/shared)、目标机器类型(e.g.,IA32)、section header table的文件偏移量,section header table中每个条目的长度和条目的数目。.text机器码.rodata只读数据.data初始化了的C全局变量.bss未初始化的C全局变量。由于是未初始化的,所以实际不占用硬盘空间。名字的由来:Block Storage Start,可以理解为Better Save Space来方便记忆.symtab函数和全局变量的符号表(symbol table),此符号表不包含局部变量.rel.text包含一个.text段中在链接时需要修改的地方的列表。一般只要是调用了外部函数或者使用了全局变量的地方都需要在链接时进行修改。在可执行文件中,这个段是不需要的。.rel.data在此目标文件中被引用或定义的全局变量的重定位信息。.debug包含一个用于调试的符号表(局部变量、typedef等等),编译时指定-g选项会生成.debug段.line包含.text段中机器指令与源程序行数的对应关系.strtab一个字符串表(null-terminated character strings),为.symtab段和.debug段中的符号表所引用Section header table包含了描述从.text段到.strtab段每一段的位置和大小的条目,每个条目的大小是固定的
  • Executable object file可执行目标文件格式运行该格式可执行目标文件的系统a.out早期Unix系统Common Object File Format(COFF)早期System V UnixPortable Executable(PE),COFF的变种Windows NTUnix Executable and Linkable Format(ELF)现代Unix系统,如Linux、后期的System V Unix,BSD Unix和Sun Solaris
  • Shared object file

链接发生的时机

符号和符号表

对于链接器,有三种不同的符号:

  • 全局符号(Global symbols) 在模块m定义且可以被其他模块引用的全局符号
  • 外部符号(External symbols) 被模块m引用但是在其他模块定义的全局符号
  • 本地符号(Local symbols): 模块m定义且只能被模块m自己使用的符号 这里的Local不是local variable的local,而是指static。一般的局部变量是和符号表没有关系的,他们由栈处理,但是如果是静态(static)的局部变量,那么该变量就和全局变量一样处理。

.symtab段中的ELF符号表中的每个条目的类型:

typedef struct {
	int name;		/* String table offset */
	int value;		/* Section offset, or VM address */
	int size;		/* Object size in bytes */
	char type:4,		/* Data, func, section, or src file name (4 bits) */
		binding:4;	/* Local or global (4 bits) */
	char reserved;		/* Unused */
	char section;		/* Section header index, ABS, UNDEF, or COMMON */
} Elf_Symbol;

各字段具体含义参考csapp的电子版(第2版)661页。

符号解析

链接器如何解析被定义了多次的全局符号

  • strong symbol 函数和初始化了的全局变量是strong symbol
  • weak symbol 未初始化的全局变量

解析规则:

  1. 不允许存在多个strong symbol
  2. 有一个strong symbol和多个weak symbol,取strong symbol
  3. 有多个weak symbol,任意取一个

csapp举了一个例子:

/* foo5.c */
#include <stdio.h>
void f(void);

int x = 15213;
int y = 15212;

int main() {
    f();
    printf("x = 0x%x y = 0x%x \n", x, y);
    return 0;
}

/* bar5.c */
double x;

void f() {
    x = -0.0;
}

编译执行会产生以下结果:

linux> gcc -o foobar5 foo5.c bar5.c
linux> ./foobar5
x = 0x0 y = 0x80000000

使用静态库

  1. 静态库 将指定的(多个)函数给分别编译成目标文件,然后打包成一个所谓的静态库文件。
    在编译应用程序时,指定要链接的静态库文件,链接器会根据应用程序的需要,只拷贝应用程序所需要的模块到最终的可执行程序中。
  2. 创建静态库
    gcc -c addvec.c multvec.c
    ar rcs libvector.a addvec.o multvec.o
    即先生成可重定位目标文件,然后打包成静态库。
  3. 使用静态库
    • --static选项 加了--static选项时,链接器会将相应的模块拷贝到最终的可执行程序中,在载入时不再需要任何链接操作。不加--static选项,载入时仍然会需要链接操作。
    • 关于静态库链接的顺序 如果一个可重入目标文件或这静态库a引用了另一个静态库里面的某个模块b,那么在链接器的参数列表里,b应该放在a的后面。具体算法参考csapp电子版(第二版)670页。
      同一个静态库或者可重入目标文件可以在链接器的参数列表里出现多次,以满足静态库链接的顺序要求。

重定位(Relocation)

在完成符号解析之后,链接器需要将输入的目标文件给合并为可执行文件且为每个符号指定运行时的内存地址。重定位(Relocation)包括两个步骤:

  1. Relocating sections and symbol definitions 把不同输入目标文件的同一类型的段给合并为可执行目标文件的该类型的段,且指定运行时的内存地址。
  2. Relocating symbol references within sections 重定位符号引用

Relocation Entries

当汇编器生成目标文件时,如果汇编器不知道某个符号究竟会被放在内存的哪个地方,汇编器就会生成一个relocationentry。

typedef struct {
	int offset;		/* Offset of the reference to relocate */
	int symbol:24,		/* Symbol the reference should point to */
	    type:8;		/* Relocation type */
} Elf32_Rel;

ELF文件格式定义了11种重定位类型;其中最基本的两种是:

  • R_386_PC32 使用PC-relative address(PC,即program counter,下一条指令的地址)。
  • R_386_32 使用32位绝对地址

Relocating Symbol References

foreach section s {
	foreach relocation entry r {
		refptr = s + r.offset;	/* ptr to reference to be relocated */

		/* Relocate a PC-relative reference */
		if (r.type == R_386_PC32) {
			refaddr = ADDR(s) + r.offset;	/* ref's runtime address */
			*refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr);
		}

		/* Relocate an absolute reference */
		if (r.type == R_386_32)
			*refptr = (unsigned) (ADDR(r.symbol) + *refptr);
	}
}

可执行目标文件

作用段名称属性
可执行文件的开始,描述整个可执行文件的整体格式以及包含了程序的入口(entry point)ELF header只读(code segment的一部分)
描述如何将此可执行程序载入到内存中运行,可以通过objdump命令查看到此部分的内容Segment header table
定义一个名为*_init*的函数,该函数会被程序的初始化代码执行.init同上
.rodata同上
.data可读、可写(data segment的一部分)
.bss同上
.symtab不载入内存
.debug同上
.line同上
.strtab同上
描述目标文件的各个段(Describes object file sections)Section header table同上

下面便是一个可执行文件的Segment header table示例(经过objdump的):

Read-only code segment
LOAD off	0x00000000 vaddr 0x08048000 paddr 0x08048000 align 2**12
	filesz 0x00000448 memsz 0x00000448 flags r-x

Read/write data segment
LOAD off	0x00000448 vaddr 0x08049448 paddr 0x08049448 align 2**12
	filesz 0x000000e8 memsz 0x00000104 flags rw-

载入可执行目标文件

执行程序的时候(通过execve函数),装载器(loader)会把可执行目标文件的代码和数据拷贝到内存,然后跳转到该程序的开始(entry point)。这个拷贝和跳转的过程就是所谓的装载(loading)。
下图是一个程序在运行时的内存画像:

Linux下用于处理目标文件的程序

程序名功能
ar创建静态库,以及插入、删除、列出和提取成员
strings列出目标文件中可打印的字符串
strip删除目标文件中的符号表
nm列出目标文件中定义的符号
size列出目标文件的各个段的名称和大小
readelf显示目标文件的完整结构
objdump所有此类型的程序的源头
ldd列出可执行文件在运行时需要的动态库