linux elf 格式详解

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shenhuxi_yu

感谢投币,继续输出

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值