复习参考资料
1 共性
一个函数都可以分为三部分
- prologue: 这部分负责相关栈和寄存器的初始化
- body: 这部分负责函数运算主题部分
- Epilogue: 这部分负责对函数栈的清理和恢复工作
2 x86的函数调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | .486 .MODEL FLAT .CODE PUBLIC _myFunc _myFunc PROC ; Subroutine Prologue push ebp ; Save the old base pointer value. mov ebp, esp ; Set the new base pointer value. sub esp, 4 ; Make room for one 4-byte local variable. push edi ; Save the values of registers that the function push esi ; will modify. This function uses EDI and ESI. ; (no need to save EBX, EBP, or ESP) ; Subroutine Body mov eax, [ebp+8] ; Move value of parameter 1 into EAX mov esi, [ebp+12] ; Move value of parameter 2 into ESI mov edi, [ebp+16] ; Move value of parameter 3 into EDI mov [ebp-4], edi ; Move EDI into the local variable add [ebp-4], esi ; Add ESI into the local variable add eax, [ebp-4] ; Add the contents of the local variable ; into EAX (final result) ; Subroutine Epilogue pop esi ; Recover register values pop edi mov esp, ebp ; Deallocate local variables pop ebp ; Restore the caller's base pointer value ret _myFunc ENDP END |
注意:
ebp
保存栈帧
esp
保存栈底函数参数都在栈上,使用
ebp
作为基准进行调用
call func
等于push eip
+jump func
ret
等于pop eip
2 ARM
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | .global main main:8 push {r11, lr} /* Start of the prologue. Saving Frame Pointer and LR onto the stack */ add r11, sp, #0 /* Setting up the bottom of the stack frame */ sub sp, sp, #16 /* End of the prologue. Allocating some buffer on the stack */ mov r0, #1 /* setting up local variables (a=1). This also serves as setting up the first parameter for the max function */ mov r1, #2 /* setting up local variables (b=2). This also serves as setting up the second parameter for the max function */ bl max /* Calling/branching to function max */ sub sp, r11, #0 /* Start of the epilogue. Readjusting the Stack Pointer */ pop {r11, pc} /* End of the epilogue. Restoring Frame pointer from the stack, jumping to previously saved LR via direct load into PC */ max: push {r11} /* Start of the prologue. Saving Frame Pointer onto the stack */ add r11, sp, #0 /* Setting up the bottom of the stack frame */ sub sp, sp, #12 /* End of the prologue. Allocating some buffer on the stack */ cmp r0, r1 /* Implementation of if(a<b) */ movlt r0, r1 /* if r0 was lower than r1, store r1 into r0 */ add sp, r11, #0 /* Start of the epilogue. Readjusting the Stack Pointer */ pop {r11} /* restoring frame pointer */ bx lr /* End of the epilogue. Jumping back to main via LR register */ |
注意:
r11(fp)
保存栈帧lr
保存返回地址sp
保存栈底- 参数前四个存在
r0-r3
,后面存在栈bl func
等于mov lr,PC+4
b func
- PC 指向当前正在运行的指令,编码,译码,执行导致pc的改变
3 动态加载
动态链接处理共享库的时候非常高效,当一个程序被加载进内存的时候,动态链接器会将需要的共享库,加载并绑定到进程的地址空间,在调用某个函数的时候,对函数地址进行解析,达到对函数调用的目的
3.1 两个表
3.1.1 PLT(Procedure Linkage Table)
过程连接表,在程序中以
.plt
节表示,表在代码段,每个表项表示了一个需要重定位的函数的相关若干条指令,每个表项长度为16字节,存储用于延迟绑定的代码可执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | PLT[0] --> 与每个函数第一次链接相关指令 例: 0x4004c0: 0x4004c0: ff 35 42 0b 20 00 push QWORD PTR [rip+0x200b42] // push [GOT[1]] 0x4004c6: ff 25 44 0b 20 00 jmp QWORD PTR [rip+0x200b44] // jmp [GOT[2]] 0x4004cc: 0f 1f 40 00 nop DWORD PTR [rax+0x0] 即: 第一条指令为 push 一个值,该值为 GOT[1] 处存放的地址, 第二条指令为 jmp 到一个地址执行,该值为 GOT[2] 处存放的地址 PLT[1] --> 某个函数链接时所需要的指令,与 got 表一一对应 例: 0x4004d0 <__stack_chk_fail@plt>: 0x4004d0: ff 25 42 0b 20 00 jmp QWORD PTR [rip+0x200b42] // jmp GOT[3] 0x4004d6: 68 00 00 00 00 push 0x0 // push reloc_arg 0x4004db: e9 e0 ff ff ff jmp 0x4004c0 <_init+0x20> // jmp PLT[0] 即: 第一条指令为: jmp 到一个地址执行,该地址为对应 GOT 表项处存放的地址,在下文中会具体讨论这种结构 第二条指令为: push 一个值,该值作用在下文提到 第三个指令为: jmp 一个地址执行,其实该地址就是上边提到的 PLT[0] 的地址, 也就是说接下来要执行 PLT[0] 中保存的两条指令 |
3.1.2 GOT(Global Offset Table)
全局偏移表,在程序中以
.gpt
和.got.plt
节区表示,存放于数据段,可读可写,每一个表项存储的都是一个地址,每个表项的长度都是当前程序对应的需要寻址的长度(32位程序:4个字节,64位程序8个字节)
1 2 3 4 5 6 7 8 9 10 | GOT[0] --> 此处存放的是 .dynamic 的地址;该节(段)的作用会在下文讨论 GOT[1] --> 此处存放的是 link_map 的地址;该结构也会在下文讨论 GOT[2] --> 此处存放的是 dl_runtime_resolve 函数的地址 GOT[3] --> 与 PLT[1] 对应,存放的是与该表项 (PLT[1]) 要解析的函数相关地址, 由于延迟绑定的原因,开始未调用对应函数时该项存的是 PLT[1] 中第二条指令的地址, 当进行完一次延迟绑定之后存放的才是所要解析的函数的真实地址 GOT[4] --> 与 PLT[2] 对应,所以存放的是与 PLT[2] 所解析的函数相关的地址 . . . |
3.1.3 表与表之间的关系
1 2 3 4 5 6 7 8 9 10 | GOT[0]: .dynamic 地址 PLT[0]: 与每个函数第一次链接相关指令 GOT[1]: link_map 地址 GOT[2]: dl_runtime_resolve 函数地址 GOT[3] --> PLT[1] // 一一对应 GOT[4] --> PLT[2] // 相互协同,作用于一个函数 GOT[5] --> PLT[3] // 一个保存的是该函数所需要的延迟绑定的指令 GOT[6] --> PLT[4] // 一个是保存个该函数链接所需要的地址 . . . . . . |
3.2 三个节
3.2.1 .dynamic
加载的时候
.dynamic
节区也是.dynamic
段,这部分主要与动态链接的整个过程有关,保存的是与动态链接相关的信息。通过
.dynamic
能够找到与动态链接相关的其他节区(.dynsym
,.dynstr
,.rel.plt
等节区)
结构:
1 2 3 4 5 6 7 8 9 | struct Elf64_Dyn { Elf64_Sxword d_tag; // type of dynamic table entry // 识别该结构体表示哪一节,通过这个字段能够寻找不同节区 union { Elf64_Xword d_val; // Integer value of entry // 对应表在文件中的偏移地址 Elf64_Addr d_ptr; // Pointer value of entry } d_un; } |
3.2.2 .dynsym
动态符号表,存储着在动态链接中需要的每个函数的符号信息,每个结构体对应一个符号,结构体数组,
d_tag = DT_SYMTAB(0x06)
.symtab
在运行时会将运行时需要的符号表复制一份到.dynsym
,而不需要的并不需要加载进内存,因此可以优化- 同理
strtab
也可以优化
结构:
1 2 3 4 5 6 7 8 9 10 | typedef struct { Elf64_Word st_name; // Symbol name(String table index) // 保存了函数名在.dynstr中的偏移,结合.dynstr可以找到准确的函数名 unsigned char st_info; // Synbol type and binding unsigned char st_other; // Symbol visibility Elf64_Section st_shndx; // Section index Elf64_Addr st_value; // Symbol value // 如果符号被导出,则存在这个导出函数的虚拟地址,否则为null Elf64_Xword st_size; // Symbol size } Elf64_Sym; |
3.2.3 .dynstr
动态字符串表,存放了一系列的字符串,表示了符号的名称,是一组字符串数组,
d_tag = DT_STRTAB(0x05)
strtab
在运行的时候会将运行时需要的字符串赋值到.dynstr
,所以strtab
可以优化
3.2.4 .rel.plt(.rela.plt)
重定位表,保存了重定位相关的信息,描述了如何在链接或者运行时,对ELF目标文件的某部分内容或者进程镜像进行补充和修改
每个结构体也对应一个需要重定位的函数,
d_tag = DT_REL(0x11)/DT_RELA(0x7)
结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 | typedef struct { Elf64_Addr r_offset; // Address // 解析完的函数的真是地址存放的位置 // 对应解析函数的GOT表项地址 Elf64_Xword r_info; // Relocation type and symbol index // 高位表示索引,低位表示信息 } Elf64_Rel; typedef struct { Elf32_Addr r_offset; /* Address */ Elf32_Word r_info; /* Relocation type and symbol index */ Elf32_Sword r_addend; /* Addend */ } Elf32_Rela; |
3.3 链接过程
主要函数是
dl_runtime__resolve(link_map_obj, reloc_arg)
第一个参数是一个link_map
第二个参数是一个重定位参数,即在运行PLT代码时push进去的n
这个函数主要是调用了一个
dl_fixup(link_map_obj, reloc_arg)
结构完成主要功能第一个参数的作用是获得重定位函数所在的library的基址地址,以及或许在library中寻找重定位函数时所需要的Section。
第二个函数主要是确定需要解析的函数名,以及解析完之后写回的地址。
因此整个过程大概可以理解为,dl_fixup,通过reloc_arg参数确定当前正在解析的函数名
然后拿着函数名,利用link_map找到
.dynsym
,然后进行函数名匹配,如果成功,则从.dynsym
中获取对应符号的函数地址
1 2 3 4 5 | reloc_arg : 函数名A linkmap --> .dynsym --> 遍历匹配函数名称 A 若 某一个 Elf64_Sym(符号) 的 st_name + .dynstr == A 则 该 Elf64_Sym 表示的符号即为函数 A |
如果存在一个函数put,在第一次执行的时候绑定过程如下:
-
进入puts@plt,执行2中的case2:
-
1 2 3 4 5 6
case1: jump GOT[x] # address case2: push n jump PLT[0] # dynmic
-
1 2 3
PLT[0]: push GOT[1] # link_map jump GOT[2] # call dl_runtime_resolve(栈上的参数为 n 以及 link_map)
-
上面
dl_runtime_resolve
函数得到的puts
函数的真实地址写入GOT[x]