本次实验源码:
#include<stdio.h>
int global_init_var = 10;
int global_uninit_var;
void func(int sum){
printf("%d\n", sum);
}
int main(){
static int local_static_init_var = 20;
static int local_static_uninit_var;
int local_init_val = 30;
int local_uninit_var;
func(global_init_var + local_init_val + local_static_init_var);
}
编译
gcc example.c -o example.exec
gcc -static example.c -o example_static.exec
gcc -c example.c -o example.rel
gcc -c -fPIC example.c -o example_pic.rel && gcc -shared example_pic.rel -o example.dyn
编译后会生成5个文件,可以发现,ELF文件分为三类:可执行文件.exec,可重定位文件.rel和共享目标文件.dyn:
在审视一个目标文件时,有两种视角:一是链接视角,通过节(Section)划分;另一种是运行视角,通过段(Segment)进行划分。
链接视角来看,目标文件通常都会包括代码(.text),数据(.data)和BSS(.bss)三个节,分别存储可执行的机器指令、已初始化的全局变量和局部静态变量、为初始化的全局变量和局部静态变量。除了这三个节之外,简化的目标文件还应包含一个文件头。
将程序指令和程序数据分开,从安全角度考虑,数据和指令分别被映射到两个虚拟区域,数据是可读写的,而指令是只读的,防止程序的指令被改写和利用。
ELF文件头位于目标文件最开始的位置,记录了文件的一些基本信息,由魔术字符7f 45 4c 46标志开始,即字符串“\177ELF”,当文件被映射到内存时,可以通过搜索该字符确定映射地址。
节头表记录了文件中含有的节的信息,由于它对于程序运行不是必须的,所以常有程序去掉节头表,以增加反编译的分析难度。
之前写到,.text存储可执行指令。
如图所示,本次的.text占0x57个字节。
下面是.text的内容,最左侧是偏移量
下面紧跟的是反汇编的结果
可以发现,.data占0x08个字节,.rodata占0x04个字节。.bss没有contents属性,表示该节其实并不存在。只是为它预留了位置
根据contents的内容,可以看到global_init_var(0a000000)和local_static_init_var(14000000)会存放在.data中,而局部变量local_init_val则会直接存储在指令中;.rodata保存只读数据,存放了printf中的参数。
符号表记录了目标文件中所用到的所有符号信息,通常分为.dynsym和.symtab,前者是后者的子集。.dynsym保存了引用自外部文件的符号,只能在运行时被解析,而.symtab还保存了本地符号,用于调试和链接。目标文件通过符号在符号表中的索引值来使用该符号。
系统其实并不关心每个节的实际内容,而是关心这些节的读写执行的权限,所以可以将相同权限的节统一为同一段,进行装载,从而节省资源。