深入理解计算机系统部分章节笔记
深入理解计算机系统部分章节笔记
第七章 链接
链接是将代码和数据片段组合成一个单一文件的过程,这个文件可以被load到内存并执行。
链接使得分离编译成为可能,将大型应用程序分成多个小的模块,当更改其中一个小模块时,只需要重新编译它,再链接,而不必去编译其他文件。
编译器驱动程序包括了预处理器、编译器、汇编器和链接器。它将ASCII码源文件翻译成可执行目标文件。


预处理器cpp将main.c翻译成一个ASCII码的中间文件main.i。

我们打开main.i文件看看
# 1 "main.c.test"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "main.c.test"
......// 略
# 216 "/usr/lib/gcc/x86_64-linux-gnu/7/include/stddef.h" 3 4
typedef long unsigned int size_t;
typedef signed char __int8_t;
......// 略
# 2 "main.c.test" 2
# 2 "main.c.test"
int sum(int* a, int n);
int array[2] = {1, 2};
int main()
{
int val = sum(array, 2);
return val;
}
我在main.c.test里面include了stdio.h,经过cpp预处理器处理,可以看到它给源文件前面加了好多信息,还将stdio.h文件内容也添加了进来,生成了ASCII码的main.i文件。
接下来我们将main.i文件用gcc编译器编译生成汇编语言文件main.s:
manatee@ManateeDesktop:~/csapp$ gcc -S main.i
.file "main.c"
.text
.globl array
.data
.align 8
.type array, @object
.size array, 8
array:
.long 1
.long 2
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $2, %esi
leaq array(%rip), %rdi
call sum@PLT
movl %eax, -4(%rbp)
movl -4(%rbp), %eax
leave
.cfi_def_cfa 7, 8
接下来用as汇编器,将main.s翻译成可重定位目标文件main.o,再用ld连接器将目标文件组合形成可执行目标文件prog:
manatee@ManateeDesktop:~/csapp$ as -o main.o main.s
manatee@ManateeDesktop:~/csapp$ ld -o prog main.o sum.o
manatee@ManateeDesktop:~/csapp$ ./prog
shell调用操作系统中的loader函数,将可执行文件prog中的代码和数据复制到内存,然后执行它。
目标文件
- 可重定位目标文件:包含二进制代码和数据,可在编译时与其他可重定位目标文件合并,形成一个可执行目标文件。
- 可执行目标文件:包含二进制代码和数据,可被加载到内存执行。
- 共享目标文件:特殊可重定位目标文件,可在加载时或运行时被动态加载到内存并链接。
各个系统目标文件格式不同,x86-64Linux和Unix使用的格式为ELF。

- .text:代码
- .rodata:只读数据
- .data:初始化的全局和静态变量。局部变量在运行时被保存在栈中,并不在.data和.bss中
- .bss:未初始化的全局和静态变量、被初始化为0的全局和静态变量。这在目标文件中不占用空间,在运行时才会给变量分配内存,初始值为0
- .symtab:符号表
每个可重定位目标文件包含一个符号表,其中符号表包含了3中不同符号:
- 被该文件定义和引用的全局符号:该文件中定义的全局函数和非静态全局变量
- 引用其他目标文件的全局符号:其他文件中定义的全局函数和非静态全局变量
- 该文件定义和引用的局部符号:static修饰的变量和函数,这些可被该目标文件使用,但不能在其他目标文件中被引用。
链接器解决多重定义的全局符号
如果多个目标文件定义了同名的全局符号,链接器如何区分?根据以下规则:
函数和初始化的全局变量是强符号,未初始化的全局变量是弱符号。
- 不能定义多个同名强符号
- 一个强符号和多个弱符号同名,就选择强符号
- 多个弱符号同名,就任选一个。
举例来说:
/* foo1.c */
int f() {return 0;}
/* foo2.c */
int f() {return 0;}
由于强符号f被定义了2次,编译和链接这两个文件时会出错。
/* foo1.c */
int x = 1119;
/* foo2.c */
int x = 1119;
同理,上面也行不通。再看一个下面的例子:
/* foo1.c */
int x = 1119;
/* foo2.c */
int x;
这次可以通过编译,链接器会选择foo1.c中的强符号x。
链接静态库
将相关的目标文件打包成一个单独的文件就形成了静态库,当链接器链接静态库时,它只复制应用程序引用的静态库中的模块,未引用的不会复制,这减少了可执行文件的占用空间。Linux静态库以.a结尾标识。
下面用addvec.c和multvec.c创建了一个静态库libvector.a,并使用这个库:
manatee@ManateeDesktop:~/csapp$ gcc -c addvec.c multvec.c
manatee@ManateeDesktop:~/csapp$ ar rcs libvector.a addvec.o multvec.o
manatee@ManateeDesktop:~/csapp$ gcc -c main.c
manatee@ManateeDesktop:~/csapp$ gcc -static -o prog main.o ./libvector.a
manatee@ManateeDesktop:~/csapp$ ./prog
z = [4 6]
/* addvec.c */
int addcnt = 0;
void addvec(int* x, int* y, int* z, int n)
{
int i;
addcnt++;
for(i = 0; i < n; i++)
z[i] = x[i] + y[i];
}
/* multvec.c */
int multcnt = 0;
void multvec(int* x, int* y, int* z, int n)
{
int i;
multcnt++;
for(i = 0; i < n; i++)
z[i] = x[i] * y[i];
}
/* vector.h */
void addvec(int* x, int* y, int* z, int n);
void multvec(int* x, int* y, int* z, int n);
/* main.c */
#include <stdio.h>
#include "vector.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
addvec(x, y, z, 2);
printf("z = [%d %d]\n", z[0], z[1]);
return 0;
}
链接器在链接时会从静态库libvector.a中复制引用的addvec.o模块到可执行文件,printf.o模块同理,程序并未使用multvec.o定义的符号,链接器就不会复制它。
重定位
链接器完成符号解析后就把每个模块中每个符号引用与它的定义相关联。
- 重定位节和符号定义。将类型相同的节,如每个模块的.data节合并成为一个新的.data节。将运行时内存地址设置给新的节和每个符号。这样每条指令和全局变量就有唯一的运行时内存地址了。
- 重定位节中的符号引用。修改代码和数据节中对每个符号的引用,使之指向正确的运行时地址。
重定位完后,形成可执行目标文件,它连续的片被映射到连续的内存段。

加载器通过目标文件头部表中的信息将进程虚拟地址空间中的页映射到可执行文件中页大小的片,新的代码段和数据段被初始化为可执行文件的相应段,然后加载器跳转到_start地址,它最终调用main函数,除了头部表的一些信息,加载过程中没有任何从磁盘到内存的数据复制。知道CPU引用一个被映射的虚拟页时才会复制相应页面到内存。
动态链接共享库
静态库的一些代码会被复制到每个引用它的运行的进程中,而动态库不同,它在运行或加载时,可以通过动态链接器加载到进程的任意内存地址,在Linux中以.so结尾的文件表示。所有引用一个.so动态库的可执行目标文件共享这个.so文件中的代码和数据。静态库则是每次链接都需要复制代码和数据。共享库的.text节可以被多个运行的进程共享

浙公网安备 33010602011771号