Linux中的进程影像 - 程序的目标文件(一)
所有接触过计算编程的人都知道HelloWorld这个名字,不论学习使用的是C语言、JAVA语言或者其他语言,往往都会从接触HelloWorld开始。但这些HelloWorld背后的逻辑往往要比它所打印的一行字符更加复杂、更难懂。下面来看看在Linux操作系统中,一个C语言程序HelloWorld.c后面的故事。
int main(void)
{
printf("hello world!\n");
return 0;
}
该代码第1行指出编译器需要将stdio.h头文件中的声明包含进来,以便获得printf()函数的原型-实际上就是编译器预处理功能将stdio.h的文本内容替换掉第一行。第2声明了主函数main(),它将作为整个程序的入口(操作系统看到的入口却在该程序的初始化代码上)。该主函数忽略了命令行参数(void),因为这里不需要用到参数,第4行调用C语言库(libc)中的标准输出函数printf()将”hello world!“输出,第5行返回结果,程序终止。这是一个最简单的程序代码,将该程序保存到HelloWold.c中,然后我们在Linux中完成它的编译。
gcc -c HelloWorld.c
此时,将在同一目录下产生HelloWorld.c的目标文件HelloWorld.o,通过file命令查看HelloWorld.o为:
从以上信息可以看出,HelloWorld.o是一个编译成x86-64架构的指令,可重定位的目标文件,该文件按照ELF的布局存放在磁盘中,而且还保留着符号表。
先看看C代码在目标文件中被编译成什么样的汇编指令-用objdump-d命令完成代码段的反汇编工作(-D)可以反汇编全部内容:
总共有0x17个字节(0~0x16)的指令序列,最左边的是序列偏移量,中间是二进制代码,右边是汇编代码。这里只是代码部分的反汇编,总关键的是从第b个字节开始的那个函数调用。如果想得到关于目标文件的更多文件信息,可以使用objdump工具来进一步查看。
使用-hrt参数打印出了该ELF格式的目标文件中各个节(section)的头部摘要信息(节是代码组织的一个单位,即某段相同属性的代码或者数据。)和显示了重定位入口。
编号为0的.txt节是HelloWorld.c编译后的x86-64平台上的可执行的二进制代码,一共有17个字节。经过链接后在运行时由程序装载器把它构造成进程的代码段。
编号为1的.data段是用于保存已初始化的全局变量和局部静态变量,在这里程序中没有带初始化的全局变量和静态变量,所以并不需要占用空间来保存这些信息,故该section的长度为0,否则它的大小为所具有初始数据所占的空间大小。
编号为2的.bss段,保存未初始化的全局变量和局部静态变量,未初始化的全局变量和局部静态变量的值默认为0,本来它们也可以放在.data段,但是由于它们都是0,所以他们在.data段分配空间并且存放数据0是没有必要的。程序运行时这些变量是要占空间的,所以目标文件必须记录所有未初始化的全局变量和局部静态变量的大小,记为.bss段。一句话,.bss只是预留位置而已,他并没有内容(没有CONTENT一项)在磁盘中不占据空间。
编号为3的.rodata是编译器识别出来的只读数据,此处对应字符串”hello world!"的14个字符。但对于不同系统来说,这个section可以包含到进程的代码段(只读)。
编号为4的.comment包含0x1d的字节的注释。
编号5的.note是额外的编译器信息。
SYMBOL TABLE包含所有的符号信息,各列分别为:
这里不做详细介绍,感兴趣可以找相关资料看看。
1.2 源代码与目标文件的对应
从以上汇编代码可以看出,ELF文件的”节“标记是从上面汇编代码产生的,如果考虑的更全面一些,将全局变量、是否有初始化等因素考虑进来,源代码和目标代码之间的对应关系可以如下图所示,所有的代码归到.text节,带有初始值的全局变量或静态变量都归到.data节中,未初始化的全局变量和静态变量归到.bss节中,函数的局部变量是堆栈变量,不需要再目标文件中占用空间。
下面通过objdump -s来查看各个节的二进制内容,如下图所示:
从上面可以看到制只读数据.rodata节的内容就是”hello world!“字符串,注释为.comment节里面是编译器插入的说明性文字。此处仅是二进制编码。
到这里目标文件就介绍完成了,下次将介绍可执行文件与进行影像。