函数栈调用解析复习

函数栈调用解析复习

由于时间过长,中间有些遗忘的,在此总结归纳一下知识点并进行一个回顾

复习参考资料

1 共性

一个函数都可以分为三部分

  • prologue: 这部分负责相关栈和寄存器的初始化
  • body: 这部分负责函数运算主题部分
  • Epilogue: 这部分负责对函数栈的清理和恢复工作

2 x86的函数调用

1587216724069
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 链接过程

1586185898823

主要函数是 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,在第一次执行的时候绑定过程如下:

  1. 进入puts@plt,执行2中的case2:

  2. 1
    2
    3
    4
    5
    6
    case1:
    jump GOT[x] # address

    case2:
    push n
    jump PLT[0] # dynmic
  3. 1
    2
    3
    PLT[0]:
    push GOT[1] # link_map
    jump GOT[2] # call dl_runtime_resolve(栈上的参数为 n 以及 link_map)
  4. 上面dl_runtime_resolve函数得到的puts函数的真实地址写入GOT[x]