1.elf (Executable and Linkable Format) 是unix和类unix系统下得标准文件格式,包括常见地可执行文件,目标文件,库文件,coredump文件类型。维基百科里有elf格式的具体信息。elf wikihttps://en.wikipedia.org/wiki/Executable_and_Linkable_Format
2.elf 文件构成 file header segment section
The segments contain information that is needed for run time execution of the file
The sections contain important data for linking and relocation.
3.以可执行文件为例说明header segment section组织关系
header :描述了elf类型版本和平台信息,entry point是指程序的在内存里的执行入口。readelf -S a.out 能看到这个entry point 既是.text section的地址
program header:将多个具有相同权限和a对其属性的section合并成一个segment,在程序加载的时候,一起建立mem映射
section header:程序中某个具体的段比如.bss .init .text
以ubuntu 上gcc编译出的elf为例,section合并成segment(一个segment包含1个或多个section)的map如下
4.elf可执行程序的加载与运行过程
在linux终端执行elf,如果没有指定命令解释工具的话(比如 bash -c),没有指定会用默认的sh 解释命令并与内核交互。sh会fork一个新的进程然后调用用户空间库函数execve然后内核系统调用do_execve。execve系统调用为elf的执行准备内存空间和用户权限,挂到某个cpu调度队列中。
在内核空间 struct linux_binprm用来记录一个可执行程序信息,struct linux_binfmt 是用来解释bin格式行为的结构体,以elf为例系统init阶段会注册好elf fmt的各种方法作为elf格式的binary hander
bprm_mm_init初始化创建的进程的虚拟地址空间。参数和环境变量都设置完成后,prepare_binprm 中调用kernel_read 从filebuf中获取的前128字节,这里包含了elf header和program header,可以获取到文件的格式,之后execv_binprm --> search_binary_hadler(prm),找到提前注册好的elf文件的方法开始load_binary,load_elf_binary这个函数很重要
load_elf_binary
(1)从128字节elf header和program header中检查elf magic 文件类型和架构支持
(2) loops over the program header entries, checking for an interpreter (PT_INTERP) and whether the program's stack should be executable (from the PT_GNU_STACK entry).
(3)flush_old_exec,执行命令时候是由sh fork出的进程(old_exec),从old_exec进程组分离出来,释放old_exec的mm设置bprm的mm 为new_mm, exec_mmap(bprm->mm),关闭old_exec的打开的文件
(4) set_new_exec,设置new_exec的task->comm(a.out)和signal handler
(5)设置程序的栈空间, randomize_stack_top 随机栈顶
(6)段描述的vaddr和size等信息为PT_LOAD类型的段map到对应的程序内存空间中
(7)设置bss段然后arch_setup_additional_pages (比如vdso page)
(8) create_elf_tables 把elf辅助向量 argc envc argv 等信息塞到stack之前的位置。on Linux systems it sits at the high end of the user address space, just above the (downwardly growing) stack, the command-line arguments (argv), and environment variables (environ). 动态链接器在加载so完成后会从辅助向量中的找到程序的entry和elf header。
(9)start_thread(current->regs, elf_entry, bprm->p), elf_entry作为返回用户空间后的pc。the execve() syscall returns to user space — but to a completely different user space, where the process's memory has been remapped, and the restored registers have values that start the execution of the new program
5.动态链接与elf
动态链接程序是在runtime时加载so的,.inter section指明了程序的链接器的绝对路径
readelf -p 1 ./a.out 可以查看.inter section中的内容
动态链接程序的本质也是一个类型是DYN的elf文件,在load_elf_binary中会将动态链接器load进内存,将interp的entry_point作为程序的elf_entry
bin和so都有entry point,使用objdump -d,查看bin的entry point的symbol是_start, so的entry point函数是deregister_tm_clones 这两处的代码都是gcc生成的
返回userspace后interpret程序查找和加载so然后解析原程序中的未定义符号,完成这些之后会根据auxiliary vector 中的AT_ENTRY,启动程序。
6.动态链接与位置无关代码(PIC)
动态库的加载地址是不固定的,程序运行的时候没办法对符号(func & data)正确的解析,有两个解决该问题的主流方法,加载时重定位与位置无关代码。对于gcc编译选项为 -shared 与 -fPIC
-shared 代表了当前编译目标为动态库,如果不加-fpic的话,默认加载时重定位,(区别于链接时重定位,编译可执行文件的时候在.rela(重定位表)中建立对外部符号一个引用的表,外部目标文件合并后,每个section的加载地址就固定了,外部symbol的地址也产生了,然后再去根据.rela中的信息更新可执行文件中对外部符号的引用),加载时重定位是重定位的时机发生在加载阶段,加载时重定位有两个缺点,对大量的符号的重定位会影响程序的启动时间,另外重定位需要对代码段做修改,会导致代码不能共享,每个进程仍然要单独一份的代码copy,造成ram的浪费。
-fpic 添加一层位于data section的中间层帮助程序间接的访问外部符号,实现当前程序code的地址无关,由于加载时重定位的缺点,pic已是如今最流行的方法。readelf -a *.so | grep TEXTREL ,如果有textrel 代表支持动态重定位是不支持pic的(待验证)。另一个方法readelf -l 可以查看代码段的LOAD地址是否是指定的来判断是否支持pic
pic位置无关代码的实现依赖如下两个重要概念
GOT(global_offset_table) and PLT(procedure linkage table)
got的内容是一张symbol的地址表
plt的内容是中间层跳转函数
#file a.c
int test_pp(int a)
{
if(a > 10)
return 10;
else
return a;
}
#file main.c
extern int test_pp(int a);
int main()
{
int a = 42;
a = test_pp(a);
return 0;
}
#gcc -o main.bin main.c -L./ -la
objdump -D main.bin > main.s
vim main.s
000000000000070a <main>:
70a: 55 push %rbp
70b: 48 89 e5 mov %rsp,%rbp
70e: 48 83 ec 10 sub $0x10,%rsp
712: c7 45 fc 2a 00 00 00 movl $0x2a,-0x4(%rbp)
719: 8b 45 fc mov -0x4(%rbp),%eax
71c: 89 c7 mov %eax,%edi
71e: e8 bd fe ff ff callq 5e0 <test_pp@plt>
723: 89 45 fc mov %eax,-0x4(%rbp)
726: b8 00 00 00 00 mov $0x0,%eax
72b: c9 leaveq
72c: c3 retq
72d: 0f 1f 00 nopl (%rax)
Disassembly of section .plt:
00000000000005d0 <.plt>:
5d0: ff 35 ea 09 20 00 pushq 0x2009ea(%rip)# 200fc0 <_GLOBAL_OFFSET_TABLE_+0x8>
5d6: ff 25 ec 09 20 00 jmpq *0x2009ec(%rip)#200fc8<_GLOBAL_OFFSET_TABLE_+0x10>
5dc: 0f 1f 40 00 nopl 0x0(%rax)
00000000000005e0 <test_pp@plt>:
5e0: ff 25 ea 09 20 00 jmpq *0x2009ea(%rip) # 200fd0 <test_pp>
5e6: 68 00 00 00 00 pushq $0x0
5eb: e9 e0 ff ff ff jmpq 5d0 <.plt>
Disassembly of section .got:
0000000000200fb8 <_GLOBAL_OFFSET_TABLE_>:
200fb8: b8 0d 20 00 00 mov $0x200d,%eax
...
200fcd: 00 00 add %al,(%rax)
200fcf: 00 e6 add %ah,%dh
200fd1: 05 00 00 00 00 add $0x0,%eax
...
readelf -r ./main.bin
Relocation section '.rela.plt' at offset 0x5a0 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000200fd0 000100000007 R_X86_64_JUMP_SLO 0000000000000000 test_pp + 0
编译完后会生成test_pp函数的影子函数test_pp@plt,把对库函数的调用换成对影子函数的调用,影子函数的实际内容是跳到当前pc的某个相对地址处0x200fd0,该位置处在.got段,属于_GLOBAL_OFFSET_TABLE_。动态库中的symbol在不同进程中被加载到的位置是不确定的,rela.plt section 指示加载器将名为test_pp的symbol地址加载到0x200fd0位置处,对于main.bin来说运行时只需要从0x200fd0中找test_pp的地址即可。
readelf -S 是可以查看到.got段属性是可写的,got的内容是symbol的地址,动态库被加载后加载器会初始化GOT,动态库被加载的位置是不确定的,GOT与PLT实现了对动态库的函数的引用与函数实际的加载地址无关。
7.阅读文章
elf 辅助向量阅读https://lwn.net/Articles/519085/
arch_setup_additional_pages vdsohttps://lwn.net/Articles/615809/stack randomize 查阅文章
https://en.wikipedia.org/wiki/Stack_buffer_overflowplt got阅读
https://en.wikipedia.org/wiki/Stack_buffer_overflow
elf GNU_STACK vdso 阅读文章https://en.wikipedia.org/wiki/Executable_space_protection