10.程序的链接

可执行文件生成过程

#include <stdio.h>

int main()
{
    printf("hello world\n");
}

程序的功能是在屏幕上输出 hello world。

  • 文本编辑器输入程序,hello.c ,用 ASCII 表示的文本文件

  • 预处理,(cpp) hello.c 处理为 hello.i 用 ASCII 表示的文本文件

  • 编译,(cc1) hello.i 处理为 hello.s 汇编语言程序文件,这还是用 ascii 表示的文本

  • 汇编,(as) hello.c 处理为 hello.o 可重定位目标程序,二进制文件,一条一条机器指令,计算机直接是被执行

  • 源程序调用了 printf ,需要用 ld 链接,把不同模块拼接起来

Linux 下使用的工具

  • 预处理过程,处理预编译指令,

    • gcc -E hello.c -o hello.i

    • cpp hello.c > hello.i

    • 删除 #define 并展开对应的宏

    • 处理条件预编译指令

    • 插入头文件到 #include ,递归方式处理

    • 删除注释

    • 添加行号和文件摸标识,供调试用

    • 保留编译指令,#pragma 后面编译器用

  • 编译过程,词法分析,语法分析,语义分析,生成汇编代码文件。此程序称为编译器,compiler

    • gcc -S hello.i -o hello.s

    • gcc -S hello.c -o hello.s 两步合一步

    • /usr/lib/gcc/***/cc1 hello.c

    • 编译后,得到汇编代码文件(汇编语言源程序),人可读的 ascii 文件,CPU 不可读

  • 汇编过程,汇编器把汇编语言源程序转换为机器指令序列(机器语言程序)。汇编代码源程序和机器语言程序都是机器级代码,一一对应,

    • gcc -c hello.s -o hello.o

    • gcc -c hello.c -o hello.o 三步合一

    • as hello.s -o hello.o

    • 得到的为可重定位目标文件,

  • 链接,前面步骤针对的是一个模块(一个 .c 文件),得到一个目标文件,链接将多个 .o 合并,生成可执行文件

    • gcc -static -o myproc main.o test.o

    • ld -static -o myproc main.o test.o

    • 不指定输出文件名,默认为 a.out

gcc 是上面的 cpp cc1 as 工具的集合。

链接器

最早的时候,穿孔纸袋编程。存在跳转地址,但是只有修改了就比较麻烦。后来出现了汇编,用符号表示跳转位置,而非绝对地址,即汇编程序中做的事,程序确定后,手工汇编。链接时,先确定标号的地址,然后把地址填入最终的二进制文件中。

高级语言出现后,多人开发,子程序的地址,实际上函数名就是符号。函数名、变量名就是符号的定义引用。本质上和汇编中的符号没区别。最终编译,在链接之前,调用时跳转的地址是无法确定的。链接时,才能确定最终的地址

链接操作的步骤

  • 符号解析:确定符号的引用,互相之间的调用关系

  • 合并模块,把代码和数据都合到一起,放到同一个地址空间,虚拟地址空间,

  • 确定符号地址,合并不同模块到一个地址空间以后,代码放一起,数据放一起,相对位置救恩确定了

  • 指令中填入新地址。

链接的好处:

  • 模块化,

    • 一个程序可以分模块

    • 公共库,如数学、C标准库

  • 效率高

    • 时间上,可以分开编译,只编译修改过的模块

    • 空间上,源文件无需包含共享库的源码,

链接的本质

例程

// main.c
int buf[2] = {1,2};

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;
}

每个模块有自己的代码数据。初始化的全局变量,未初始化的全局变量,静态变量,局部变量。

局部变量分配在栈中,不能在过程(函数)外被引用,因此不是符号定义。

  • gcc -O2 -g -o p main.c swap.c

两个源码分别转换生成 main.o 和 swap.o ,生成可重定位的二进制文件,此时这里面的符号还没有值,地址没有值。

链接过程的本质 : 合并相同的节

上面代码里,每个模块有的节 .text .data .bss

可执行文件存储

可执行文件里有一段,程序头表,程序头表描述了如何映射,即链接时,程序不知道是去哪里执行的,代码段会装到内存里去执行,具体装载的位置还不知道。链接的时候把可执行文件合并到了虚拟地址空间里,

在程序运行时的空间,

目标文件

目标文件指包含目标代码(机器语言代码)的文件。早期非标准,几种标准的格式

  • DOS 里 .com ,

  • linux 里 elf 格式

三类目标文件

  • 可重定位目标文件。代码和数据可以和其他可重定位文件合并为可执行文件

    • 每个 .o 由对应的 .c 生成

    • 每个 .o 文件代码和数据地址从 0 开始

  • 可执行目标文件

    • linux 默认为 a.out

    • windows 为 .exe

    • 包含的代码和数据可以直接被复制到内存执行

    • 代码和数据地址为虚拟地址空间中的地址

  • 共享的目标文件 (linux中的 *.so)

    • 特殊的可重定位目标文件,在运行时装入内存并自动链接,共享库文件

    • windows 中为 *.dll

可重定位文件可执行文件都是 ELF 格式的文件,可以使用 file 指令查看。

ELF 文件的两种视图

  • 链接视图(被链接):可重定位目标文件

  • 执行视图(被执行):可执行目标文件

从链接视图来看,ELF 文件由节构成,节是 ELF 中由相同特征的最小处理单位

  • .text

  • .data

  • .rodata

  • .bss

从执行视图来看,ELF 文件由段组成,程序头表反映了不同节和段的关系。节到段的映射。比如 .data 节和 .bss 节映射到一个可读可写段中。

可重定位文件

.c 经过编译汇编生成 .o ,这个文件里有不同的节

  • ELF 头,

    • 包含16byte标识信息,

    • 文件l类型 (.o ,so)

    • 机器类型 (IA-32)

    • 节头表的偏移、节头表表项大小和个数

  • .text 节

    • 编译之后的代码部分

  • .rodata 节

    • 只读数据,如printf的格式串,switch case 的跳转表

  • .data 节

    • 已初始化的全部变量

  • .bss 节

    • 未初始化的全局变量

.bss 节仅是占位符,文件中不占用磁盘空间,仅在节头表里说有这么个东西。节头表指出长度。区分初始化和未初始化是为了空间效率。

最后更新于