欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

c-目标文件的三魂七魄

程序员文章站 2022-07-13 23:17:34
...

本文基于一个简单的目标文件例子,来窥探目标文件的三魂七魄。


首先,展示一下例子的源码,了解一下这段代码做了些什么。代码中的注释部分进行了详细说明。
c-目标文件的三魂七魄
然后,通过gcc将main.c编译成main.o
c-目标文件的三魂七魄

注意:绿色部分的main.o文件大小是1896(0x768)。

由于目标文件涉及的知识点非常的多,本文不可能讲解所有细节,主要讲解目标文件的主*分---“三魂七魄”。"三魂七魄"到底是什么鬼!!!

目标文件的整体轮廓

通过readelf命令先来感受一下main.o里面到底长啥样!
c-目标文件的三魂七魄

c-目标文件的三魂七魄

上面两张图中的ELF Header、Section Headers、Symbol table就是目标文件的"三魂"

ELF Header

ELF文件类型

ELF(Executable Linkable Format)是LINUX系统中的一种文件格式。通常我们见到的目标文件(.o)、可执行文件、动态库(.so)都是ELF文件类型。

可以通过file命令查看文件类型
c-目标文件的三魂七魄

从上图可以看出,main.o 是ELF relocatable文件,可重定位文件能被链接成可执行文件或动态库;main.c是c源文件;/bin/ls是ELF executable文件,可执行文件;/usr/lib/libcpupower.so.4.4.0-154是ELF shared object文件,即动态库。从这里其实可以看出(如果你对gcc编译的流程熟悉的话),relocatable file通常被链接成 executable file或 shared object file。

了解了什么是ELF,接下来再看看什么是ELF Header。

ELF Header描述

为什么我将elf header看作是三魂之一,说明了它非常重要。elf header描述了整个目标文件的基础属性,如:
c-目标文件的三魂七魄

ELF Magic

​ ELF Magic由16个字节组成,前四个字节(第0到第3个字节)是所有ELF文件都相同的标识码0x7f、0x45、0x4c、0x46,如果你对ASCII码有所了解的话,你会发现,0x7f刚好对应DEL字符,0x45、0x4c、0x46分别对应大写的’E’ ‘L’ ‘F’。第4个字节(0x02)表示文件CLASS,也即文件位数,因为我的机器是64位的,所以0x02表示64位,如果是32位的机器,第4个字节的值应该是0x01。第5个字节(0x01)表示字节序,0x01表示小端字节序,0x02表示大端字节序。第6个字节表示ELF的主版本号,ELF标准从1.2版本后就再没有更新了,所以一般是1。剩下的9个字节通常都为0。

​ 其实可以发现,ELF Header的16字节的魔数包含了Class(机器位数)、Data(字节序)、Version(ELF版本)信息。

OS/ABI 表示运行的平台为unix-system v。

ABI Version 表示ABI版本。

ELF Type

​ REL 表示relocatable file,说明该文件是目标文件(可重定位文件),即.o文件
​ EXEC 表示executable file,说明该文件是可执行文件
​ DYN 表示shared object file,说明该文件是动态库文件,即.so文件
​ 可分别执行readelf -h /bin/ls 和 readelf -h /usr/lib/libcpupower.so.4.4.0-154 查看ELF Type。

Machine

​ 表示该ELF文件的可运行的平台属性。

Entry point address

​ 入口地址,也就是ELF程序的虚拟入口地址,OS在加载完程序后从这个地址开始执行。relocatable文件因为还没有链接,所以一般没有入口地址,其值为0。

Start of program headers

​ program header描述了ELF文件是怎样被操作系统加载到进程的虚拟地址空间的。由于目标文件(relocatable file)没有program header(可通过readelf -l main.o查看program header),所以这里不展开讲解program header。

Start of section headers

​ 段表在文件中的偏移。main.o的start of section headers值为1064(0x428),表示段表从文件的1065个字节开始。section headers将在后面详细讲解。

Flags

​ 描述平台架构相关的属性。可以查看/usr/include/elf.h 中以EF_开头的宏。

Size of this header

​ 表示elf文件头本身的大小。main.o的文件头大小为64字节。

Size of program headers

​ 由于目标文件没有program header,所以这里暂时不管。

Number of program headers

​ 由于目标文件没有program header,所以这里暂时不管。

Size of section headers

​ 表示段表描述符的大小。一般等于sizeof(Elf64_Shdr)。64字节。

Number of section headers

​ 表示段表中段的个数。main.o中段的个数为13,说明有13个段

Section header string table index

​ 表示段表字符串表 所在段的下标。

说明:本文中,用中文表达的段都是指section,因为还有一个概念叫segment,但在中文表达上不好区分,所以这里特别说明一下,以免产生混淆。

我个人觉得,可以将section翻译为"节",将segment翻译为"段"。但是由于书籍或已经约定俗成的叫法中已经将section表达为段了,例如,代码段、数据段等,这里的段是指section。

为了在中文上尽量表达清晰而不产生混淆,我后面的系列文章会将section表达成"节",例如,代码节、数据节;而将segment表达成"段"。"段"是比"节"更大一点的概念。当然,如果你习惯用英文表达,就无所谓了。

section headers(节表头)

首先,我们来看看main.o的节表信息
c-目标文件的三魂七魄

可以看到 section(节)的个数是13个,和我们前面分析elf header的number of section headers的值一致。starting at offset 0x428 也就是1064,也就是前面分析elf header的start of section headers。在代码层面,每个section是通过Elf64_Shdr来描述的,所以Elf64_Shdr也叫做节描述符(section descriptor)

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

可以发现,Elf64_Shdr的10个成员刚好和readelf命令显示的每个section的属性字段相对应。接下来,详细分析每个section及其相应字段的含义。

Name

节名,通常 节名的字符串是存放在.shstrtab中的,sh_name是节名字符串在.shstrtab中的偏移。

Type

NULL 表示 无效节
PROGBITS 表示程序节,代码节、数据节都是这种类型
RELA 表示重定位表
NOBITS 表示该节在目标文件中没有内容,如 .bss节
STRTAB 表示该节的内容为字符串表
SYMTAB 表示该节的内容为符号表

Address

表示节被加载后的进程虚拟地址。这里暂不做过多说明。

Off

表示节偏移,即节在文件中的偏移,如果节的内容不在文件中,那么off就没有意义

Size

表示节的大小

Es(section entry size)

有些节包含了一些固定大小的项(entry),而Es就表示这种项的大小。如果Es为0表示该节不包含固定大小的项。例如,符号表,它包含的每个符号所占的大小都是一样的。

Flg

节的标志位 表示 节在进程虚拟地址空间中的属性。

A 表示该节在进程空间中需要分配相应的空间。

E 表示该节在进程空间中可以被执行,一般都是指代码节。

W 表示该节在进程空间中可写。

Lk Inf

表示节的链接信息。一般节的类型与链接相关,这两个成员才有意义。例如节的类型是重定位表、符号表等。

Al

表示节地址对齐。

.strtab 字符串表,用来保存普通字符串

.shstrtab 段表字符串表,用来保存节表中用到的字符串,最常见的就是节名(section name)

Symbol(符号表)

在目标文件的链接过程中,本质上是对“地址”的引用,而“地址”即是函数或变量的地址。同时,为了方便引用,函数或变量都有自己的名字,这个名字就是所以为的符号(symbol),函数名或变量名就是符号名(symbol name)。

我们可以将符号看作是链接过程的粘合剂,整个链接过程正是基于符号才能够完成。链接过程中很关键的一部分就是符号的管理,每一个目标文件都会有一个相应符号表(symbol table),这个表里面记录了目标文件中所用到的所有符号。每个定义的符号有一个对应的值,叫做符号值(symbol value),对于变量和函数来说,符号值就是他们地址。

符号的分类:

  • 定义在本目标文件中的全局符号,可以被其他目标文件引用;如main.c中的g_var, func, main;
  • 在目标文件中引用的全局符号,却没有定义在本目标文件中,一般叫做外部符号(external symbol);如,printf
  • 节名,这种符号通常由编译器产生,它的值就是该段的起始地址;如,.data,.text;
  • 局部符号,这类符号只在编译单元内可见,这种符号通常对于链接过程没有作用,链接器往往会忽略他们;如,l_static_var,l_static_var2;
  • 行号信息,目标文件指令与源代码中代码行的对应关系,可选;

在链接过程中,"可见的"还是全局符号和外部符号。

接下来,先展示一下main.o里面的符号表到底长得是什么样子,然后再详细解释里面的内容。
c-目标文件的三魂七魄
符号表通过readelf -s main.o命令查看,符号表的section name 一般叫做’.symtab’。接下来依次解释符号表资格字段的含义。

Num

表示符号表数组的索引。

### Size

表示相应符号的内容的大小

Type

NOTYPE 表示未知类型符号

FILE 该符号为文件名

SECTION 该符号表示一个节,这种符号的Bind一定是LOCAL,他们的Name没有显示,起始默认就是指相应的section name,所以没有必要显示出来。

OBJECT 表示一个数据对象,如,变量、数组等

FUNC 一般表示函数名

Bind

LOCAL 表示局部符号,目标文件外部不可见

GLOBAL 表示全局符号,目标文件外部可见

Ndx

一般表示该符号所在节的节索引(查看节索引命令:readelf -S -W main.o),如,g_static_val,g_var在.data节。

ABS 表示该符号包含了一个绝对值。文件名的符号就是这一类

COM “COMMON块”,一般未初始化的全局变量就是这种

UND 表示该符号未定义,说明该符号不在本目标文件中定义,但在其他文件中定义

Value

  • 如果符号在目标文件内定义,则value表示该符号所在节的偏移值。
  • 如果符号是COM,value表示对齐属性
  • 在可执行文件中,value表示符号的虚拟地址

了解了前面的5个字段的含义,再来解读符号表的内容就显得更加的从容啦!

main和func都是函数名,显然属于.text,所以Ndx为1,类型为FUNC;Bind为GLOBAL表示全局可见;value表示相对于.text起始位置的偏移量

g_static_val、l_static_val.2813、g_var,初始化的静态存储的外部或内部链接变量 都属于.data。

g_static_val2、l_static_val2.2814 未初始化的静态存储的内部或空链接的变量 都属于.bss。

g_var2 Ndx为COM,它的value为对齐属性,外部可见。

从这里例子,可以发现,我们平常所说的未初始化的全局变量(g_var2)放在.bss中是不准确的。

相关标签: c gcc