编译器编译源代码后生成的文件叫做目标文件。从结构上讲,它已经是编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其本身是按照可执行文件格式存储的。
现在PC上流行的可执行文件格式,主要是Windows下的PE和Linux的ELF。
动态链接库(Windows的.dll和Linux的.so)和静态链接库(Windows的.lib和Linux的.a)文件也按照可执行文件格式存储。
目标文件格式
/* test.c */
int printf(const char* format, ...);
int global_init_var = 84;
int global_uninit_var;
void func1(int i)
{
printf("%d\n", i);
}
int main()
{
static int static_var = 85;
static int static_var2;
int a = 1;
int b;
func1(static_var + static_var2 + a + b);
return a;
}
一般C语言编译后执行语句都编译成机器代码,保存在.txt
段。
已初始化的全局变量和局部静态变量都保存在.data
段。
未初始化的全局变量和局部静态变量一般放在.bss
段。
比如上面的
global_init_var
和static_var
在.data
段,global_uninit_var
和 static_var2
在bss
段。
总的来说,程序源代码被编译后主要分成两种段:程序指令 和 程序数据,代码段属于程序指令,而数据段和.bss
段属于程序数据。
也许会问:为什么要把程序的指令和数据分开?主要因为:
-
一方面当程序被装载后,数据和指令分别被映射到两个虚存区域,由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个区域的权限可以被分别设置成可读写和只读,这样可以防止程序的指令被有意或无意地改写。
-
另一方面是对于现代CPU来说,它们有着极为强大的缓存体系。指令区和数据区的分离有利于提高程序的局部性,现代CPU的缓存一般都被设计成数据缓存和指令缓存分离。
-
当系统中运行着多个程序的副本时,它们的指令都是一样的,所以内存中只需要保存一份改程序的指令部分。对于程序的指令如此,其他只读数据也类似,比如:
$ gcc -m32 -c test.c -o test.o #
$ objdump -h test.o # -h 把ELF文件的各个段的基本信息打印出来
objdump可用来查看各目标文件的结构和内容。参数-h
表示把ELF文件的各个段的基本信息打印出来。
代码段
$ objdump -d test.o
-d
将包含指令的段反汇编。
数据段和只读数据段
-
.data段.png.data
段
.data
段保存是初始化了得全局变量,静态变量和局部静态变量,前面有2个这样的变量是global_init_var = 84 = 0x00000054
和static_var = 85 = 0x00000055
,一共8字节。(x86 的是小端序,高位存放在高地址中)
-
.rodata段.png.rodata
只读数据段
保存的是只读数据,一般是程序的只读变量(如const修饰的变量)和字符串常量。比如上面我们用到了字符串常量%d\n
,它是一种只读数据。"25640a00"也恰好是这个字符串常量的ASCII字节序。
.bss
段
.bss
段存放的是未初始化的全局变量和局部静态变量。可以看到该段的大小只有4字节,但global_uninit_var
和 static_var2
的大小应该是8字节。这不是矛盾了?
实际上,只有static_var2
被存放到了被存放到了.bss
段,而global_uninit_var
却没有存放在任何段,只是一个未定义的COMMON
符号。这跟编译器实现有关,有些编译器会将全局的未初始化变量存放在目标文件的.bss
段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss
段分配空间。
.comment
段
.comment
段一般存放编译器版本信息
网友评论