可执行文件如何被执行

时间:2022-12-29 23:54:00
可执行文件如何被执行?
1.sys_execve 处理execvc系统调用,调用do_execve
2.do_execve 打开该可执行文件,做些准备工作,然后调用search_binary_handler
3.search_binary_handler 确定可执行文件的类型(比如elf, 脚本), 调用相应的处理函数,这里是load_elf_binary
4.load_elf_binary将该执行文件载入内存.分配内存段,padzero BSS.
5.如果是动态链接, 该程序含有INTERP段(包含解释器的全路径,通常是ld.so), 调用load_elf_interp 载入该解释器
6.load_elf_binary最终调用start_thread, 将控制交给解释器或该用户程序

ld.so ?
ld.so 是动态链接器 linker/loader (编译时链接器ld叫做链接编辑器,"link editor"),干以下事情:
1.分析用户程序的DYNAMIC段, 确定所需要的依赖
2.找出并载入这些依赖,递归分析它们的DYNAMIC段以确定是否需要更多的依赖
3.进行任何必要的重定向工作来绑定这些对象
4.调用初始化函数
5.将控制权交给程序

ld.so 如何工作?
ld.so 自己当然不能是动态链接的. 入口地址是_start (ld -verbose | grep ENTRY), 然后调用_dl_start 载入自己的依赖
_dl_start做了什么事情?
1.分配初始TLS(thread local storage) 并且初始化线程指针如果需要的话(这些是ld.so自己的,不是用户程序的)
2.调用_dl_sysdep_start, 里头调用dl_main
3.dl_main 做了主要工作:
process_envvars 处理 LD_* 环境变量
检查程序的DYNAMIC段的NEEDED字段以确定依赖
_dl_init_paths 初始化动态链接库寻找路径
_dl_map_object_from_fd载入动态库,设置读写权限,zeroes out BSS段
_dl_relocate_object 运行时重定向

_dl_start返回后,调用_dl_start_user
1._dl_init_internal, 调用call_init执行每个被载入库的初始化函数(DYNAMIC INIT类型),
2.最后将控制权交给程序的入口地址(通常是.text的初始地址),其中将结束函数_dl_fini作为参数。

tips: 设置LD_DEBUG=all,然后运行程序以获得ld.so所采取的动作


.init, .fini, .preinit_array, .init_array and .finit_array

.init 和.fini 包含初始化和结束代码
如果gcc编译的话, .init 和.fini代码大致如下:
0000000000400510 <_init>:
 400510:       48 83 ec 08               sub    $0x8,%rsp
 400514:       e8 c3 00 00 00          callq  4005dc <call_gmon_start>
 400519:       e8 52 01 00 00          callq  400670 <frame_dummy>
 40051e:       e8 ad 03 00 00          callq  4008d0 <__do_global_ctors_aux>
 400523:       48 83 c4 08               add    $0x8,%rsp
 400527:       c3                              retq

0000000000400908 <_fini>:
 400908:       48 83 ec 08               sub    $0x8,%rsp
 40090c:       e8 ef fc ff ff               callq  400600 <__do_global_dtors_aux>
 400911:       48 83 c4 08               add    $0x8,%rsp
 400915:       c3                              retq

各只有一函数_init/_fini, 都是编译时生成的。
Glibc 为_init/_fini提供自己的开始与结束代码文件,依赖于编译参数(gcc -dumpspec).一般情况下是crti.o 开始,crtn.o 结束。
例外如果没有使用-shared,glibc 总是包含crt1.o, crt1.o含有.text的开始函数_start.
最后包括crtbegin.o,crtbeginS.o,或crtbeginT.o,取决于 -static or -shared

比如如果一个程序使用动态链接,没有profiling, 没有 fast math optimization 来编译,那么链接会以下顺序包括以下文件:
1. crt1.o
2. crti.o (call_gmon_start__)
3. crtbegin.o (frame_dummy,__do_global_ctors_aux)
4. user's code
5. crtend.o (__do_global_dtors_aux)
6. crtn.o

__do_global_ctors_aux 调用所有被标记 __attribute__ ((constructor)) 的函数, 它们存在.ctors区; 同样__do_global_dtors_aux 调用所有被标记 __attribute__ ((destructor)) 的函数 它们存在.dtors区.:

executed sequence:
.preinit_array -> .init -> .init_array -> main -> .finit_arary -> .fini

最好不要放代码到.init区, e.g.
void __attribute__((section(".init"))) foo() {
        ...
}
因为这样会导致__do_global_ctors_aux 不会被调用(自己试试)
同样不要加代码到.fini,会导致segmentation fault

_start ?

_start 是glibc 代码,被编译成crt1.o, 在编译时被链接到用户程序的二进制代码
_start 总是被放在.text的开始处,ld 指定_start的地址为入口地址,所以总是可以保证最开始运行_start (编译时可以通过-e选项指定不同的初始地址)
_start 设置好参数,再调用__libc_start_main
__libc_start_main( int(*main) (int, char **, char **),
                              int argc,
                              char *argv,
                              int (*init) (int, char **, char **),
                              void (*fini) (void),
                              void (*rtld_fini) (void),
                              void *stack_end)
)

__libc_start_main 干了以下事情:
1. 设置好argv和envp
2. __pthread_initialize_minimal 初始化线程本地存储(TLS)
3. 设置好线程栈保护
4. 注册动态链接器的解析器(rtld_fini),如果有的话(使用__cxa_atexit)
5. __libc_init_first 初始化glibc
6. 注册 __libc_csu_init(fini)
7. __libc_csu_init (init):
        1).preinit_array
        2).init
        3).init_array
8. 设置好线程释放/取消所需的数据结构
9. main
10. exit

如果main最后一代码是return XX, XX 会传给exit. 如果没有或只是return,undefined.
当然如果用户程序自己调用了exit/abort, exit 会被调用.

如果一个程序没有main, 将会看到``undefined reference to main''的错误。
如何找到一个可执行二进制文件的main地址?
1. 32-bit x86, 第一个参数是最后一个压入栈,
   objdump -j .text -d a.out | grep -B2 'call.*__libc_start_main' | awk '/push.*0x/ {print $NF}'
2. 64-bit x86, 第一个参数存在RDI寄存器,
   objdump -j .text -d a.out | grep -B5 'call.*__libc_start_main' | awk '/mov.*%rdi/ {print $NF}'


重定向
连接时重定向(链接编辑器ld, .rel.text or .rela.text)/动态时重定向(ld.so, DYNAMIC)
_GLOBAL_OFFSET_TABLE_, .got.plt section, DYNAMIC segment

RUNTIME RELOCATION 重定向
prelinked
R_X86_64_RELATIVE => base + *(base + relative) 存在 base + relative
R_X86_64_GLOB_DAT => base(含该符号ELF的基址) + Symbol Value + Addend
R_X86_64_JUMP_SLOT => same with R_X86_64_GLOB_DAT
R_X86_64_COPY => 与R_X86_64_GLOB_DAT相结合

.plt/.got  
动态链接器默认是懒惰型重定向,即要用到某符号时才重定向
1) PLT(Procedure Linkage Table), 每个记录对应一个输出函数.
比如call  function_call_n # 对应地址是PLT[n+1]
在i386体系中,代码类似于:
PLT[n+1]:jmp *GOT[n+3]
                 push #n
                 jmp PLT[0]

第一次调用时,GOT[n+3] 指向PLT[n+1]+6, 即push #n、然后jmp PLT[0], PLT[0]根据压入栈的n来解决该符号地址,再将该地址填入GOT[n+3].
PLT[0]: push &GOT[1]
            jmp  GOT[2]   # point to resolver()
2) GOT (Global Offset Table)
        开始3条是特殊用途,后面M条是函数PLT,再D条是全局数据
        GOT[0] = linked list pointer used by the dyn-loader
        GOT[1] = pointer to reloc table for this module
        GOT[2] = pointer to the fixed/resolver code, located in the ld.so
        GOT[3..3+M] = indirect function call helper, one per imported function
        GOT[3+M+1..end] = indirect pointers for global data references, one per imported global

每个库和可执行文件都有自己的PLT/GOT

其他详细请看[1]

resources:
<a href="http://www.acsu.buffalo.edu/~charngda/elf.html">http://www.acsu.buffalo.edu/~charngda/elf.html</a>[1]
<a href="http://netwinder.osuosl.org/users/p/patb/public_html/elf_relocs.html">http://netwinder.osuosl.org/users/p/patb/public_html/elf_relocs.html</a>[2]
PIC / noPIC: <a href="http://en.wikipedia.org/wiki/Position-independent_code">http://en.wikipedia.org/wiki/Position-independent_code</a>[3]
GNU HASH: <a href="https://blogs.oracle.com/ali/entry/gnu_hash_elf_sections">https://blogs.oracle.com/ali/entry/gnu_hash_elf_sections</a>[4]