链接(Linking) 是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译、加载、运行时,在早期,链接是被手动执行的,在现代系统中,链接是由一个叫做链接器的程序自动执行的。
预处理时:处理的过程
以#开头的预编译指令;删除以“#define”并展开所定义的宏;处理所有的条件预编译指令,如#if,#define;插入头文件到#include处,可使用递归的方式进行处理;删除所有的注释,添加行号和文件名标识符;保留所有的#pragma编译指令。经过预处理后,得到的是与处理文件(hello.i),它还是一个可读的文本文件,但是不包含任何宏定义。编译(compile time)时:将源代码翻译成机器代码。编译的过程就是将得到的预处理文件进行词法、语法、语义分析、生成汇编代码文件,经过编译后的代码文件还是可读的文件,但是CPU无法理解执行它。
汇编时:
汇编文件代码由汇编指令构成;汇编器(as)用来将汇编语言程序转换成机器指令序列。汇编指令和机器指令都属于机器指令,所构成的程序称为机器及代码。汇编的结果是一个可重定位目标文件,包含的结果是不可读的二进制代码。链接:将多个可重定位的目标文件合并以生成可执行目标文件。
汇编的语言:用助记符表示操作码,用符号表示位置,用助记符表示寄存器。链接的操作步骤:确定符号引用关系(符号解析);合并相关.o文件;确定每个符号的地址(优点:模块化,分成多个源程序文件;效率高,可分开编译);在指令中填入新地址。在gcc 中的执行方法: 1.预处理:hello.c变成helloc.i(命令gcc -E或cpp) 2.编译:hello.i变成hello.s(命令gcc -0g -S) 3.汇编:hello.s变成hello.o(gcc或者as) 4.链接:hello.o+所需静态库——hello(gcc或ld)
从图中可以看出连接的本质就是合并相同的节,在合并之前还要进行符号解析
知道了这些知识后,那我们来具体看看在Linux中是怎么样的吧
//mismatch-main.c long int x; /* Weak symbol */ #include <stdio.h> int main(int argc, char *argv[]) { printf("%ld\n", x); return 0; } //文件mismatch-variable /* Global strong symbol */ double x = 3.14;第一段代码中定义了一个长整型的弱符号x,并且没有赋予初值,在第二段代码mismatch-variable中定义了一个长整型的强符号x,并赋值为3.14。 在Linux平台下输入gcc -Wall -Og -o mismatch mismatch-main.c mismatch-variable.c 可得到一个重定位目标文件mismatch 这时我们再输入./mismatch可得到一个结果为4614253070214989087 那为什么是这个结果呢? 是因为强符号x=3.14,而浮点数在计算机中的存储、运算、表示等是按照IEEE754标准。 在命令gcc -Wall -Og -o mismatch mismatch-main.c mismatch-variable.c中
-Wall是表示允许发出gcc提供的所有有用的报警信息-Og是表示启用全局优化-o是表示设定输出文件名,不加此选项会默认可执行文件名为a.out此外,这条命令也可以将.o文件链接到可执行目标文件。 //global.h extern int g; int f(); //global-c1.c #include "global.h" int f() { return g+1; } //global-c2.c #include <stdio.h> #include <stdlib.h> #include "global.h" int g = 0; int main(int argc, char *argv[]) { if (argc >= 2) { g = atoi(argv[1]); } printf("g = %d. f() = %d\n", g, f()); return 0; }在Linux下输入gcc -Wall -Og -o global global.h global-c1.c global-c2.c后再输入./global可得到如下结果
在这里是定义了一个int型的全局变量g,并且赋予了初值0,所以执行出来的结果是没有问题的。
所有的编译系统都提供一种机制,将所有相关的目标模块打包成为一个单独的文件,称为静态库(static library),它可以用来做链接器的输入。 库函数模块是许多的函数如printf,scanf,sqrt等函数都无需自己再去写,只要从共享的库函数中调用。 在Linux系统中,静态库以一种存档(archive)的特殊文件格式存放在磁盘中。存档文件时一组连接起来的可重定位目标文件的集合,有一个头不用来描述每个成员目标文件的大小和位置。存档文件名由后缀 .a标识,这样可以增强链接器功能,时期工呢过通过查找一个或者多个库文件中的定义符号来解析符号。
要创建这些函数,我们需要使用AR工具 ar (归档程序)能将制定的.o文件打包生成静态库文件。 接下来看一个例子
/* addvec.c */ /* $begin addvec */ 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]; } /* $end addvec */ /* multvec.c */ /* $begin multvec */ 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]; } /* $end multvec */将这两段代码打包到libvector库函数中 通过使用
gcc -c addvec.c multvec.car rcs libvector.a addvec.o multvec.o /* main2.c */ /* $begin main2 */ #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; } /* $end main2 */在main2.c 代码段中包含了头文件vector.h,定义了libvextor.a中例程的函数原型 接下来输入
gcc -c main2.cgcc -static -o prog2c main2.o ./libvector.a./prog2c 可得到以下结果: 链接的过程如下:链接器完成符号解析后就可以进行重定位了。重定位由两部分组成:
重定位节和符号定义,链接器将所有相同类型的节合并为同一个类型的新的聚合类。重定位节中的符号引用,在这一步中,链接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。执行这一步,链接器主要依赖于可重定位目标模块中称为重定位条目(relocation entry)的数据结构。 可重定位目标文件 .text:已编译程序的机器代码.rodata:只读数据,比如printf语句中的格式串和开关语句的跳转表.data:已初始化的全局和静态C变量,局部C变量保存在栈中.bss:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量.symtab:符号表,存放在程序中定义和引用的函数和全局变量的信息接下来我们看一下代码中的符号
#include <stdio.h> int time; int foo(int a) { int b = a + 1; return b; } int main(int argc, char *argv[]) { printf("%d\n", foo(5)); return 0; }在Linux环境中敲readelf -s symbols.o 可以得到
可以知道的是foo,main为全局变量,并且他们都为函数,所以为强符号。 foo是占4个字节,因为它是int型printf是没有类型的,它是全局变量,UND应该表示的是未定义的time 也是占4个字节,是int型的变量,它也是全局变量,然后它是COM,这个是表示它是未初始化的全局变量。同时代码中还有很多的局部变量。objdump -dx main.o
-d表示将代码段反编译-x显示所有的可用头信息,包括符号表、重定位入口,使用此选项可以将文件内容做标注,方便我们查看理解 这个是可重定位文件而不是可执行目标文件,所以它的起始地址为0。在节部分,大部分节名都有对应,其中File off指明了每个节在ELF中的偏移地址,不是实际地址。Size指明了每个节所占大小,例如.data节的地址为0x00000054加上长度0x00000008就为0x0000005c,即.bss节的地址。 objdump -dx -j .data main.o-j name显示指定名称为nane的section的信息, 这里在main.o的反汇编里仅显示。.data节信息 运用该指令我们只会看到.data节中的信息,最后反汇编.data节里存放的全局变量array,由于它已被初始化,且机器为小端模式,因此,00000000 <array>: 0: 01 00 00 00 02 00 00 00我真的太垃圾了,这个作业拖了一个月才完成。之前刚学的时候是真的不会,也不知道老师给的代码要怎么样在Linux下去运行,这两天看了很多的别的同学写的日志,自己经过琢磨,实践,终于完成了。写完这些东西发现对知识又有了进一步的了解,毕竟这两天都在和它打交道。总之,自己还是太差劲了,还需要很努力很努力!
该文章中的许多信息来源于《深入理解计算机系统》,前两张图片来源于南京大学袁春风老师课件,有一张图片来源于老师所给的PPT,部分内容参考于https://blog.csdn.net/ziyonghong/article/details/101560077 https://blog.csdn.net/angranxueer/article/details/102236976
加一点东西 在hello.c文件中
#include <stdio.h> int main() { printf("Hello World!"); return 0; } 用readelf -S hello.o查看 用readelf -h hello.o查看 用readelf -s hello.o查看