目录
ELF的文件类型
ELF文件的结构
ELF文件头
节头表
代码节
数据节和只读数据节
bss节
字符串表
符号表
重定位
可执行文件的装载
常见的段
ELF就是可执行可连接格式 为linux运行文件格式
我们使用复杂的例子进行演示
#include
int global_init_var = 10;
int global_uninit_var;
定义全局变量
void func(int sum){
printf("%d\n",sum);
}
定义func函数
void main(void){
static int local_static_init_var=20;
static int local_static_uninit_var;
局部变量
只能在main函数中访问
int local_init_val=30;
int local_uninit_var;
func(global_init_var + local_init_val + local_static_init_var);
打印这个的值 10+20+30
}
发现确实是60
gcc elfDemo.c -o elfDemo.exce
这里把他编译成windows 的pe文件 exe文件类型
gcc -static elfDemo.c -o elfDEmo_static.exec
这里也是windows的但是是静态链接的
gcc -c elfDemo.c -o elfDemo.rel
这里编译成 rel文件 为 可重定位文件 这里就是之前的汇编阶段
gcc -c -fPIC elfDemo.c -o elfDemo_pic.rel && gcc -shared elfDemo_pic.rel -o elfdemo.dyn
这里把文件编译 为pic类型 位置独立代码 就是 代码可以是在不固定的虚拟内存区域 我们无法预测
同时前面成功的话编译一个 共享库的这个文件 名为 .dyn
&&是前面成功 才执行后面的
这样可以实现代码函数的共享和重用
这里可以发现 ELF文件 分为3类
可执行文件 exec :经过链接 可执行的目标文件 叫做程序
可重定位文件 : 预编译文件编译但没有实现链接的文件 .o结尾 通常是PIC类型
共享目标文件 :动态链接库文件 用于在链接过程中和其他共享库进行链接 构建目标文件
或者在可执行文件运行的时候 链接到进程中 成为运行代码 的一部分
在看目标文件的两个视角
链接视角 通过节进行划分
运行视角 通过段进行划分
节和段
节是一种逻辑的划分 一个程序中 具有很多节 可以是代码 调试信息等
段是一种物理的划分 一个段中 具有一组节 段又可以分为代码段 数据段等
这里section header tables 为节头表 用来保存节的信息 内容等
而我们例子的链接视角为
后面的所有是在汇编链接过程中得到的
文件头是在目标文件最开始的位置 包含着基本信息
ELF文件类型 版本 目标机器 程序入口 段表和节表的位置和长度等
ELF的魔术字符 和jpg的文件头字符一样 遇到就说明是这个类型
7f 45 4c 46 \177ELF
我们可以使用readelf来查看文件头
readelf -h elfDemo.rel
一个文件包含很多个节 这些节的信息存放在节头表中 这个表的每一个项都是 Elf64_Shdr结构体
记录着 节的名字 长度 偏移 读写权限等信息
节头表的位置记录在e_shoff域中
但是节头表在程序中并不是必须的
所以一些会除去节头表来增加反编译的难度
我们通过readelf查看
readelf -S elfDemo.rel
我们进行深入查看节头表的.text , .data, .bss节
objdump -x -s -d elfDemo.rel
-x 列出完整的头部信息
-s 显示每一个节的大小
-d 把符号的地址和名字列出来
我们只看text
先看这个 这个内容为
Contents of section .text:
是text数据的十六进制形式 总共有 0x4e个字节 4个算一个字节
最左边为偏移量 中间为内容 最右边为ASCII表示
这里是反汇编的结果
data节保存着初始化的全局变量和局部静态变量
global_init_var (0a000000)
全局变量
local_static_init_var (14000000)
局部静态变量
每一个变量4个字节 一共8个字节
.rodata为只读数据 包括只读变量和字符串常量
源代码中调用printf函数 用到了字符串常量 "%d\n" 这是一种只读数据
所以保存在rodata中
这个节用于保存 未初始化的全局变量和局部静态变量 这个节在文件中其实不存在
只是变量预留的空间
所以下面没有contents属性
这里继续给出其他常见的节
包括了以 null结尾的字符序列 用来表示符号名和节名
所以引用字符串只需要给出在字符序列中的偏移量即可
字符串表达第一个字符和最后一个字符 都是 null
readelf -x .strtab elfDemo.rel
readelf -x .shstrtab elfDemo.rel
能发现第一个和最后一个都是 00000000 null
是记录了 目标文件中 所需要用到的所有符号信息 分为 .dynsym .dyntab
前者是后者的子集
.dynsym 保存了引用外部文件的符合 只能在运行的时候被解析
.dyntab 不仅保存了外部 还保存了本地符合 用于调试和链接
索引值都是从 0 开始计数 但是0不具有实际意义
每一个符号都要符号值
readelf -s elfDemo.rel
这里能看见 printf 为 und 说明是外部 需要程序运行才能知道地址
重定位是连接符号定义和符号引用的过程
可重定位文件在构建可执行文件和共享目标文件的过程中 需要把节中符号引用转换为这些符号在进程空间的虚拟地址
包含这些转换信息的数据 就是重定位项
readelf -r elfDemo.rel
这里是从运行视角进行审视
运行一个可执行文件
将该文件和动态链接库 装载到进程空间中 形成进程镜像
每一个进程都有自己的独立虚拟地址空间 这个空间如何布局是段头表中的程序头决定
readelf -l elfDemo.exce
root@lxz-virtual-machine:~/下载# readelf -l elfDemo.exce
Elf 文件类型为 DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
程序头:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000628 0x0000000000000628 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x00000000000001b1 0x00000000000001b1 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000114 0x0000000000000114 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000260 0x0000000000000270 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x0000000000002008 0x0000000000002008 0x0000000000002008
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section to Segment mapping:
段节...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
这里我们可以从段节发现 一个段中出现了很多的节
段的出现就是对这些节进行分类
实际上 系统并不关心这些节的内容 而是关心这些的读写执行权限
那么就将不同权限的节分组 这样就可以同时装载多个节
.data .bss 具有读写权限
.text .plt.got 具有读和执行权限
可执行文件至少有一个 PT_LOAD类型的段 用于描述可装载的节
而动态链接的可执行文件包含两个 .data 和 .plt 分开存放
动态段 PT_DYNAMIC包含动态链接器所必须的信息
例如 动态共享库 GOT表 和 重定位表等
PT_NOTE 类型的段 保存了系统相关的附加信息
但是程序运行不需要这个内容
PT_INTERP段 将位置和大小信息存放在一个字符串中
是对程序解释器的位置描述
PT_PHDR段保存了 程序头本身的位置和大小
在进程中 使用段是不够的 还需要使用到栈堆vDSO等空间
动态链接的可执行文件装载后 还需要进行动态链接才可以顺利执行