自己动手学编译器的链接和加载(一)
最近学习链接器的链接和装载过程。
首先说说一个程序从源代码到可执行文件的流程(以linux平台上c程序为例):
第一步预编译过程的命令如下:
gcc -E test.c -o test.i 或 cpp test.c > test.i
由.c文件生成.i预处理文件
第二步:
gcc -S test.i -o test.s
由.i生成.s汇编文件
第三步:
as test.s -o test.o 或
gcc -c test.s -o test.o
生成目标文件
第四步:
ld -static /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i686-linux-gnu/4.6.3/crtbeginT.o -L/usr/lib/gcc/i686-linux-gnu/4.6.3 -L/usr/lib -L/usr/lib -L/lib test.o --start-group -lgcc -lgcc_eh -lc --end-group /usr/lib/gcc/i686-linux-gnu/4.6.3/crtend.o /usr/lib/crtn.o
现在就是来看看这一坨乱七八糟的东西是啥
这里先从最基本的看起吧。
首先看看目标文件的格式,在linux上中间文件的格式和可执行文件的格式很像,差别无非是中间文件中一些引用的函数和变量的地址是相对地址,经过链接后在可执行文件中变成了绝对地址
linux上可执行文件的格式是ELF(Executable Linkable Format)
以下是一个实例小程序test.c 这个东西应该是非常具备典型行的。
1 #include<stdio.h> 2 3 int a = 1; 4 double b; 5 static int e; 6 7 static int c = 2; 8 static double d; 9 10 void func(int x, double y){ 11 printf("%d, %f\n", x, y); 12 } 13 int main(){ 14 15 static int c_m = 4; 16 static double d_m; 17 18 int a_m = 3; 19 double b_m; 20 func(a_m, d_m); 21 return 0; 22 }
以下是test.c代码与生成的test.o文件一些最主要段的对应关系:
这个是通过objdump -h test.o命令得来的:
size表示段的大小,VMA表示段的虚拟内存地址,LMA表示段的加载地址,在链接之前都是0,file off表示段在文件中的偏移量
.text是文件代码段,存放可执行代码,局部变量的声明和定义也是在其中。
.data是数据段,存放已初始化的全局变量,全局静态变量,和局部静态变量。
.bss存放的是未初始化的全局静态变量,局部静态变量,可能还有未初始化的全局变量(这个取决于不同的编译器,我的gcc编译器是不将未初始化的全局变量放在.bss中的,只是声明了一个符号,等到最终链接成可执行文件的时候再在.bss段分配空间)
从上图也可以看到,.bss段的CONTENTS属性不存在,即表明这个段实际上是不存在的,只是标示了符号而已。
还有一点,变量的存放是要字节对齐的,故一个int和两个double实际上占了24个字节。
.rodata是只读数据段,在这里存放的是printf("%d, %f\n", x, y);中的d和f
.comment段.note.GNU-stack和.eh_frame暂时不管了
可以使用objdump -s -d test.o察看各个段中的内容:
先看section .text中的内容:
对照程序的反汇编结果
可以发现.text中存放的正是func和main函数的指令0000-002e存放func的指令。002f-005c存放的是main的指令。
在看section .data:
发现从低地址至高地址12个字节依次存放的是a, c, c_m值
看section .rodata:
从后面的ASII码就知道他存放的确实是%d,%f两个只读的数据
现在已经基本弄清楚了ELF文件的一些布局和结果,之后再继续研究研究。