在ELF文件分析(一)中,我们大概了解了ELF文件的结构,知道了ELF文件由文件头和段组成。今天我们继续学习ELF文件的链接。
上篇文章中,我们知道段表中存储着每个段的基本信息,这些基本信息用一个叫段描述符的结构来组织。 想要查看段描述符的结构,可以在linux系统/usr/include/elf.h文件中搜Elf32_Shdr,如下所示:
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;
看到这么多字段,不要想一下子搞清楚所有的字段含义,现阶段,只需要掌握主要的几个字段就够了。
前缀sh表示的是section header。
表示该段在文件中的偏移量。相当于该段在文件中的起始地址。
注意: 这里的偏移单位是字节。
表示该段的长度。有了sh_offset和sh_size,我们就可以从文件中完整地取出该段的内容。
表示段虚拟地址,这是指当进程需要将该段加载到进程地址空间时,请加载到这个地址。
其余的字段,有的看名字就知道含义,不知道的,暂时不用管。
在计算机程序中,我们需要定义很多符号,回顾下上一篇文章中的示例代码:
// simple_section.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;
}
里面的变量名和函数名都是一种符号。符号的本质就是标记一段代码或者数据。
func1和main这两个函数名,标记分别标记了一段指令。我们知道,所有的指令都在.text这个段中。
现在,让我们查看一下这个段的内容。看看func1和main代表的指令在不在里面。我们使用objdump -s -d simple_section.o命令,-d选项会把包含指令的段进行反汇编。如下图所示:
图片中是对.text段进行反汇编的结果。我们看到红框框标识了函数func1和main的起始位置,这里的起始位置指的是它们在段.text中的偏移字节量。
问题来了,.text段中就是一条接一条的指令,非常的纯粹,那么,func1和main1的起始位置信息是从哪里获得的呢?
答案是有一个专门的段,叫符号段.symtab,这个段记录了func1和main这种符号信息。
现在让我们看看符号段的内容。可以使用命令readelf -s simple_section.o,如下图所示:
我们来看看如何通过这个.symtab定位func1和main函数。
主要是用到Ndx和value两个字段。
首先看func1的Ndx值是1,表示的是func1这个符号在段表中第1个段,即.text段。
value是0000000000000000,表示的是func1符号在.text段中的偏移字节量。
TYPE的值FUNC表示func1符号是一个函数。
同理,可以找到main函数的具体位置信息。
Note: 此时我们说的位置,都是指在目标文件中的偏移字节量,是针对目标文件的内容而言的,并不是虚拟内存地址,要注意区分。
在上面的符号段中,我们看到了函数和变量名称的字符串,看起来,这些名称字符串就存在符号段中。但是实际上并不是,而是有一个专门的字符串段.strtab来统一保存这些字符串,符号段中的显示的字符串是从字符串段中取出来的。
如下就是字符串段:
我们来尝试从这个段中找出func1和main。上图中的红框显示,字符串段在目标文件000002a8处,那我们就到这里看看,使用hexdump -c -s 0x000002a8 simple_section.o命令,如下图所示:
哈哈,果然发现了两个符号的字符串。
另外,.shstrtab段也是字符串段,只是它存的是段名的字符串,比如.text,.data这样的字符串。感兴趣的小伙伴可以自己探索一下。
为啥要专门用一个字符串段来存储字符串呢?这是因为,符号段中,记录每个符号信息使用了固定字节数的结构,没法表示长度不同的字符串名称,索性,将所有的字符串放到一个段中,这样,符号段中,符号名处只需要记录该符号名在字符串段中的偏移值即可。
以上,我们学习了段的基本结构,知道了如何根据段表中的段描述信息,从目标文件中提取出段的内容。还学习两种很重要的段,符号段和字符串段,有的资料上也叫符号表和字符串表。
这里,留一个小问题,在符号段中,我们看到printf的Ndx值是UND,TYPE是NOTYPE。我们知道它代表了一个函数,可是现在的目标文件中显然没有这个函数的指令。这个从前面反汇编.text段就可以看到。那么,程序执行时,如何知道printf函数的指令内容呢?答案是需要链接。