计算机工作原理(二)目标文件

一、简介

编译器编译源代码后生成的文件叫目标文件(Linux下.o或Windows下.obj),从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。

二、目标文件是什么样的

目标文件中的内容至少有编译后的机器指令代码、数据,除了这些数据以外,还包括了链接时所需要的一些信息,比如符号表、调试信息、字符串等。

一般目标文件将这些信息按不同的属性,以“节”的形式存储,有时候也叫“段”,在一般情况下,他们都表示一个一定长度的区域。

程序源代码被编译后的机器指令经常被放在代码段(Code Section)里,代码段常见的名字有.code.text

全局变量和局部静态变量数据经常放在数据段(Data Section),数据段的名字一般都叫.data

File Header
.text section
.data section
.bss section

对照上面的表格来看,一般C语言的编译后执行语句都编译成机器代码,保存在.text段。

已初始化的全局变量和局部静态变量都保存在.data段。

未初始化的全局变量和局部静态变量都保存在.bss段。

我们知道未初始化的全局变量和局部静态变量默认值都为0,本来他们也可以被在.data段的,但是因为它们都是0,示意图为它们在.data段分配空间并且存放数据0是没有必要的。程序运行的时候它们的确是要占内存空间的,并且可执行文件必须记录所有未初始化的全局变量和局部静态变量的大小综合,记为.bss段,所以.bss段只是为未初始化的全局变量和局部静态变量预留位置而已,它并没有内容,所以它在文件中也不占据空间。

总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据,代码段属于程序指令,而数据段和.bss段属于程序数据。

为什么要将数据和指令的存放分开?

1.当程序被装载后,数据和指令分别被映射到两个虚存区域,由于数据区对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读,这样可以防止程序的指令被有意或无意的改写。

2.另外一方面对于现代CPU来说,他们有着极为强大的缓存(Cache)体系,由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。

3.第3个原因是最重要的原因,就是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须要保存一份改程序的指令部分,对于指令这种只读的区域是这样,对于其他的只读数据也一样,比如很多程序里面带有的图标、图片、文本等资源也是属于可以共享的,这样就可以节省大量内存。

3、深入目标文件的具体细节

下面列举一个简单的C语言demo(SimpleSection.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(void){
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    
    func1(static_var + static_var2 + a + b);
    
    return a;
}

使用GCC来编译这个文件:

$ gcc -c SimpleSection.c

使用binutilsodjdump工具查看object内部的结构:

$ objdump -h SimpleSection.o

会得到如下几个段的信息:

0 .text
1 .data
2 .bss
3 .rodata
4 .comment
5 .note.GNU-stack

逐个来看着几个段,看看他们包含了什么内容。

(1)代码段

包含的是func1()main()的指令。

(2)数据段和只读数据段

.data段保存的是那些已经初始化了的全局静态变量和局部静态变量,分别是global_init_varstatic_var

.rodata存放的是只读数据,不光在语义上支持C++const关键字,而且操作系统在加载的时候可以将.rodata段的属性映射成只读,这样对于这个段的任何修改操作都会作为非法操作处理。保证了程序的安全性。

(3)BSS段

.bss段存放的是未初始化的全局变量和局部静态变量,上述代码中只有static_var2被存放在了.bss段,而global_uninit_var却没有被存放在任何段,只是一个未定义的COMMON符号。这其实跟不同的语言与不同的编译器实现有关,有些编译器会将全局的未初始化变量存放在目标文件的.bss段,有些则不存放,只是预留一个未定义的全局变量符号,等到最终链接成可执行文件的时候再在.bss段分配空间。

(4)其他段

常用的段名 说明
.rodata1 存放只读数据,比如字符串常量、全局const变量
.comment 存放的是编译器版本信息
.debug 调试信息
.dynamic 动态链接信息
.hash 符号哈希表
.line 调试时的行号表
.note 额外的编译器信息
.strtab 字符串表,用于存储ELF文件中用到的各种字符串
自定义段

正常情况下,GCC编译出来的目标文件中,代码会被放到.text段中,全局变量和静态变量会被放到.data.bss段,有时候我们希望变量或某些代码能够放到你所指定的段中去,GCC提供了一个扩展机制,使得程序员可以指定变量所处的段:

__attribute__((section("Foo"))) int glbal = 42;
__attribute__((section("BAR"))) void foo()
{
    ...
}

链接的接口—符号

链接过程的本质就是要把多个不同的目标文件之间相互“粘”在一起,在链接中,我们将函数和变量统称为符号,函数名或变量名就是符号名,我们可以将符号看做是链接中的粘合剂,整个链接过程正是基于符号才能正确完成,链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应的符号表,这个表里面记录了目标文件中所用到的所有符号,每个定义的符号有一个对应的值,叫做符号值,对于变量和函数来说,符号值就是他们的地址。

函数签名

函数签名包含了一个函数的信息,包括函数名,它的参数类型,它所在的类和名称空间及其他信息。函数签名用于识别不同的函数。

推荐阅读更多精彩内容