Chapter IV
CSAPP NOTE CHAP IV
Chap 4 程序的链接#
一个大的程序往往会分成多个源程序来编写,因而需要对各个不同源程序文件分别进行编译或汇编。
多个源程序会生成多个不同的目标代码文件。这些目标代码文件中包含指令、数据等信息。
此外,在程序中还会调用一些标准库函数,这些函数库也是一些目标代码文件。
因此,编译之后,需要将目标代码文件,包括用到的函数库目标文件,链接生成一个可执行文件。
编译、汇编和链接#
编译和汇编#
将源程序转化为执行程序的过程:

对应的命令:
gcc hello.c -o hello.i -E
gcc hello.i -o hello.s -S
gcc hello.s -o hell.o -c
gcc hello.o -o helloplaintext链接#
将多个可重定位的目标文件合成一个可执行文件。

例子
main.c:
int add(int ,int);
int main()
{
return add(20, 13);
}
test.c:
int add(int i,int j) {
int x=i+j;
return x;
}
/*
gcc -c -o main.o main.c
gcc -c -o test.o test.c
ld -o test main.o test.o
*/c将不同类型的源程序,编译、链接为一个执行程序:

链接的任务:
-
符号解析
将符号的引用与一个确定的符号定义建立关联。
符号:全局变量名、函数名、静态的局部变量名
非静态的局部变量名不是符号。参数名不是符号。
编译器将所有符号存放在目标文件的符号表(symbol table)中。
符号表是一个结构数组,每个表项包含符号名、长度和位置信息等。
-
重定位
在合并生成执行文件时,重新确定每条指令的地址、每个数据的地址。
在指令中更新所引用符号对应的地址。
例子 观查前例中的main.o、test.o和test文件
objdump -d test.o

objdump -d test

目标文件格式#
目标代码(Object Code):机器语言代码
目标文件(Object File):包含目标代码的文件
广义的目标文件:
-
可重定位目标文件:编译或汇编输出的目标文件
-
可执行目标文件:链接输出的目标文件
狭义的目标文件:仅指可重定位目标文件。
可重定位目标文件和可执行目标文件,可以看作是目标文件的两种视图:
-
链接视图(被链接)
-
执行视图(被执行)

可重定位目标文件主要由不同的节(section)组成。不同的节描述了目标文件中不同类型的信息及其特征。
-
text代码节 -
rodata只读数据节 -
data已初始化全局数据节 -
bss未初始化全局数据节 -
symtab节
符号表(symbol table)。符号包括函数名、全局变量名。符号表保存与这些符号相关的信息。每个可重定位目标文件都有一个.symtab节。
-
.rel.text.text节相关的可重定位信息。 -
.rel.data.data节相关的可重定位信息。 -
.debug节 调试用符号表。带-g选项的gcc命令会得到这张表。 -
line节 C源程序中的行号和.text节中机器指令之间的映射。带-g选项的gcc命令会得到这张表。 -
.strtab节字符串表,包括.symtab节和.debug节中的符号以及节头表中的节名字符串。
可执行目标文件由不同的段(segment)组成,描述节如何映射到存储段中。可多个节映射到同一段。如:可合并.data节和.bss节,并映射到一个可读可写数据段中。
可执行文件和共享库文件必须具有程序头表,而可重定位目标文件无需程序头表。
可重定位目标文件必须具有节头表。
符号表和符号解析#
符号和符号表#
每个可重定位目标文件都有一个符号表,它包含了在该模块中定义的符号。有三种符号:
-
Global symbols:全局符号
由该模块定义并能被其他模块引用的符号,包括函数和全局变量
-
External symbols:外部符号
由其他模块定义并被该模块引用的全局符号
注:在由多个模块组成的一个程序中,每个外部符号,都应该有一个对应的全局符号。否在链接时,会报“外部符号未定义”的错误
-
Local symbols:局部符号
由该模块定义,且仅由该模块引用的符号。例如在模块中定义的带static的函数和全局变量,带static的局部变量。
注:程序中的局部变量不是局部符号
// 符号表(symtab)中每个条目的结构如下:
typedef struct {
int name; /*符号对应字符串在strtab节中的偏移量*
int value; /*在对应节中的偏移量,可执行文件中是虚拟地址*/
int size; /*符号对应目标所占字节数*/
char type: 4, /*符号对应目标的类型:数据、函数、源文件、节*/
binding: 4; /*符号类别:全局符号、局部符号、弱符号*/
char reserved;
char section; /*符号对应目标所在的节,或其他情况*/
} Elf_Symbol;
// 例子
# main.c
int buf[2] = {1, 2};
extern void swap();
int main()
{
swap();
return 0;
}
# swap.c
extern int buf[];
int *bufp0 = &buf[0];
static int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
/*
main.c 中的全局变量名buf为全局符号
main.c 中的函数名swap为外部符号
swap.c中的外部变量名buf为外部符号
swap.c中的函数名swap为全局符号
swap.c中的全局变量名bufp0为全局符号
swap.c 中的static变量名bufp1为局部符号
swap.c中,函数swap中的局部变量temp,不是局部符号。
*/c

符号解析#
将每个模块中引用的符号与某个目标模块中的定义符号建立关联。
首先定义三个集合:
-
集合 E:可重定位目标文件集合
-
集合 U:是未解析符号的集合
-
集合 D:定义符号的集合
初始化E、U、D为空;
for 命令行中的文件f {
if f为目标文件 {
f --> E;
f中的未解析符号 --> U;
f中的定义符号 --> D;
if 添加了重复的定义符号
报错退出;
} else if f为库文件{
for f中的模块m {
for U中的未定义符号x {
if x在m中定义 {
将x从U转移到D中;
if 添加了重复的定义符号
报错退出;
}
}
}
}
}
合并E中的目标文件为可执行目标文件;plaintext静态库的生成与使用#
Linux 中,静态库文件采用存档档案(archive)的文件格式,文件后缀为.a。
// myproc1.c
#include <stdio.h>
void myfunc1()
{
printf("%s","This is myfuncl from mylib!\n");
)
// myproc2.c
#include <stdio.h>
void myfunc2()
{
printf("%s","This is myfunc2 from mylib!\n");
)
/*
生成静态库的过程:
gcc -c myprocl.c myproc2.c
ar rcs mylib.a myproc1.o myproc2.o
*/c程序main.c使用了mylib.a中的函数myfunc1
void myfuncl(viod);
int main()
{
myfunc1();
return 0;
}cmain.c的编译链接过程:
gcc -c main.c
gcc -static -o myproc main.o ./my1ib.a
注:命令中使用 -static 选项指示链接器应生成一个静态链接的可执行目标文件。
重定位#
链接器完成符号解析后,进入重定位过程。此过程分两步:
-
重定位节和符号定义
-
重定位节中的符号引用
ELF中重定位条目格式如下:
typedef struct {
int offset; /*节内偏移*/
int symbol:24, /*所绑定符号*/
type: 8; /*重定位类型*/
} Elf32_Rel;
"offset"是需要修改的引用的节偏移
"symbol"标识引用应指向的符号
"type"指示链接器如何修改新引用c重定位类型主要有两种:
lR_386_PC32:重定位使用32位PC相关地址引用
lR_386_32:重定位使用32位绝对地址引用
例子

/*重定位算法*/
for 每个输入节s {
for s中的每个重定位表项r {
s的起始地址 + r.offset --> refptr; // refptr为指向程序中的符号引用的指针
if (r.type == R_386_PC32) {
refaddr = s的起始地址 + r.offfset;// refaddr为符号引用的运行时地址
*refptr = ADDR(r.symbol) + *refptr - refaddr; // 修改符号引用的内容为相对地址
}
if (r.type == R_386_32)
*refptr = ADDR(r.symbol) + *refptr;// 修改符号引用的内容为绝对地址
}
}c可执行文件的加载#
通过调用 execve 系统调用函数来调用加载器
加载器(loader)根据可执行文件的程序(段)头表中的信息,将可执行文件的代码和数据从磁盘“拷贝”到存储器中
加载后,将 PC(EIP)设定指向 Entry point (即符号_start处),最终执行 main 函数,以启动程序执行。

可执行文件的存储器映像:
