目标文件里装了什么东西

来源:互联网 发布:3d模型软件 编辑:程序博客网 时间:2024/04/28 16:44

了解程序中各种变量在编译和运行阶段的状态和属性,对我们开发程序而言是非常有益的,通过对以下几个例子的探讨,搞清楚目标文件中都包含了哪些信息,能让我们明白C语言中各种存储类对程序的影响,也让我们在编写一个程序的时候胸有成竹。就像那个谁说的,真正的程序员,必须对自己程序的每一个字节了如指掌。

我们看看如下这个程序代码:


/* simple_section.c */

 

int printf(const char *format, ...); //懒得包含头文件,声明一下printf就行了

 

int global_init_var = 1; //已初始化的全局变量
int global_uninit_var; //未初始化的全局变量

 

void
func(int i)
{
    printf("%d/n", i);
}

 

int
main(void)
{
    static int static_init_var = 2; //已初始化的静态局部变量
    static int static_uninit_var; //未初始化的静态局部变量

    int a=3; //已初始化的局部变量
    int b; //未初始化的局部变量

 

    func(static_init_var + static_uninit_var+ a + b);
    return a;
}


我们知道,对于已经初始化了的全局变量和静态局部变量而言,它们是在编译阶段被编译器存放在可执行目标文件的.data段中的,而对于未被初始化的全局变量和静态局部变量,编译的时候并未被分配空间,而是仅仅在.bss段中标记它们,当程序运行的时候才为它们在内存中分配空间,并把它们初始化为零。

接下来我们用gcc编译以上代码,用选项 -c 获取一个可重定位目标文件simple_section.o,用objdump工具查看它,如下:



objdump命令中的选项 -h 的意思是把ELF文件中各个段的基本信息打印出来,当然也可以用 -x 选项打印更详细的信息。

从图中我们可以看到,没有经过链接的可重定位文件包含我们熟悉的.text段、.data段等,size字段表明了各个段的大小,file off表示各个段相对于文件头的偏移量,.text段的偏移量是0x000034,即52个字节,这52个字节存放了ELF文件的文件头,也就是通常说的ELF头。从第53个字节开始就是.text段,这个段存放了源程序中编译出来的所有机器指令。

紧跟着的第0x85个字节开始就是.data段,这个段存放了源程序中所有已经初始化了的静态局部变量和初始化了的全局变量。从图中看到这个段此时共有8个字节,分别存放了程序中的全局变量global_init_var 和静态局部变量static_init_var。.rodata段存放了程序中的只读数据,所谓的只读数据包括字符串常量、整数常量、浮点数常量。图中显示.rodata段共4个字节,里面存放的是printf语句中的字符串"%d/n",总共4个字节(包括字符串结束符'/0')。

.bss段的大小是4个字节,但是我们知道.bss段是不占用存储空间的,这里的4个字节大小只是一个说明而已,并没有占用磁盘空间。bss三个字母的意思是 block started by symbol,我们也可以记成 better save space,也就是说它是用来节省空间的,我们可以我们在各个字段的属性信息里也可以看到,除了.bss段之外的每个段都有 CONTENTS 属性,CONTENTS 表示这个段在磁盘文件中是真实内容。而.bss只有 ALLOC 属性,并没有在文件中占有空间,这样做的原因是没有必要为它们(所有未初始化的全局变量和所有未初始化的静态局部变量)分配磁盘空间,由于它们没有初始化,系统在运行程序的时候会为他们全部默认初始化为0,既然都是0,就没有必要在磁盘里放一堆0了,只要记住它们在运行的时候需要多少内存字节就行了。

另外注意到,.bss段中的size是4个字节,而我们的程序中有一个未初始化的全局变量global_uninit_var和一个未初始化的静态局部变量static_uninit_var,按道理需要8个字节才对。少了4个字节到哪儿去了?为了搞清楚这个问题,我们需要更多的信息,用readelf可以查看更多好玩的详细的信息,readelf是一个很好用的工具,可以用来方便地查看ELF文件的内部情况。我们用 -S 选项来预览一下文件的所有段的情况:


 


输出的结果就是ELF文件段表的内容,可以看到,除了前面用objdump工具看到的几个基本的段之外还有其他的辅助字段,其中看到一个叫做符号表的.symtab字段,这个字段保存了ELF文件中各个段的基本属性,比如每个段的段名、长度、在文件中的偏移量等。

这个段表是以一个结构体数组为基础的,这个结构体就在/usr/include/elf.h中定义,叫做Elf32_Shdr,编译程序的时候就是用这个结构体来对应目标文件的各个段的,每个Elf32_Shdr结构体对应一个段,所以Elf32_Shdr又被称为段描述符,对于我们这个例子来说,段表就是有11个元素的结构体数组(第一个元素是无效的段描述符)。结构体如下:


/* Section header.  */

typedef struct
{
  Elf32_Word sh_name;  /* Section name (string tbl index) */
  Elf32_Word sh_type;  /* Section type */
  Elf32_Word sh_flags;  /* Section flags */
  Elf32_Addr sh_addr;  /* Section virtual addr at execution */
  Elf32_Off sh_offset;  /* Section file offset */
  Elf32_Word sh_size;  /* Section size in bytes */
  Elf32_Word sh_link;  /* Link to another section */
  Elf32_Word sh_info;  /* Additional section information */
  Elf32_Word sh_addralign;  /* Section alignment */
  Elf32_Word sh_entsize;  /* Entry size if section holds table */
} Elf32_Shdr;


可以看到每个成员都对应着(次序不是一一对应)上图中的每一列(除了第一列,那个数字表示数组下标)。下面我们用 -s 选项来看看其中的符号表字段.symtab的具体内容:



 注意到未初始化静态局部变量static_uninit_var被放在了数组下标为4的段里,即.bss段,但是未初始化全局变量global_uninit_var被放到一个叫做COM的里面了,只是一个未定义的COMMON符号。编译器的这种处理取决于具体的实现,不同的系统的结果可能是不同的。这个问题跟强符号和弱符号有关,不展开讨论了。反正,就是因为编译器把global_uninit_var放在了别处的原因导致.bss只标记了4个字节。

段表中很重要的还有重定位表,比如上面的.rel.text,这个叫做代码段的重定位表,其他的段也可以有重定位表,只不过这个例子比较简单,只出现了代码段的重定位表,如果数据段.data也需要重定位,那么在ELF文件中就会出现.rel.data段。

重定位表的作用当然是重定位,需要重定位的原因是,当几个目标文件合并在一起成为一个真正可以被OS加载执行的时候,它们之间的各个段需要重新组合,比如把各个文件的代码段合并在一起,各个文件的数据段合并在一起,这个时候就会发生数据的移动,比如一个原来在代码段某个位置的函数被重定位到另一个地方了,这时候如果程序的其他部分对此一无所知,那么调用该函数的代码将来肯定找不到北,因此我们有必要在链接之前把各个目标文件里面每个将来需要移动的段的移动信息放在一个地方保存起来,以便链接的时候有根有据,不至于出乱子。这个地方就是重定位段。

ELF文件还有52个字节的ELF文件头,具体如下:



这个ELF文件头存放了很多信息,对照上面的图,从上到下分别是ELF幻数,文件及其字节长度,数据存储方式,版本,运行平台,ABI版本,ELF重定位类型,硬件平台,硬件平台版本,入口地址,程序头入口和长度,段表的位置和长度及段的数量。

幻数中的前面4个字节是所有ELF文件都必须相同的标识符。分别是0x7f  0x45  0x4c  0x46。OS在加载可执行文件的时候检查这个幻数是否正确,不正确则拒绝加载。后面的一个字节用来标识ELF文件的文件类,0x01代表32位的,0x02代表64位的。接下了的那个字节用来表示这个文件的字节序,1为小端,2为大端。接下来是ELF文件的主版本号,一般就是1了,ELF标准自从1.2版本以来就没有升级过。后面的都是保留的。