0%

函数调用栈

介绍

函数调用栈是一块用来保存函数运行状态的连续的内存区域。主调函数(caller)和被调函数(callee)根据调用关系堆叠起来,从内存的高地址向低地址增长。这个过程主要涉及eip、esp、和ebp三个寄存器:

eip用于储存即将执行的指令的地址;esp用于储存栈顶地址,随着数据的压栈和出栈而变化;ebp用于储存栈基址,并参与栈内数据的寻址。

例子

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
int func(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8)
{
int loc1 = arg1 + 1;
int loc8 = arg8 + 8;
return loc1 + loc8;
}
int main(){
return func(11, 22, 33, 44, 55, 66, 77, 88);
}

分别用以下命令编译成32位和64位

1
2
gcc -m32 -o stack stack.c
gcc -o stack64 stack.c

x86

7Kf7NR.png

总体入栈顺序为

7KhZDg.png

接下来看一下两个函数的汇编指令

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
32
33
pwndbg> disassemble main
Dump of assembler code for function main:
0x080483fd <+0>: push ebp # 栈底ebp压栈(esp -= 4)
0x080483fe <+1>: mov ebp,esp # 将栈顶esp的值赋给ebp,此时ebp指向当前函数栈帧的起始地址
0x08048400 <+3>: push 0x58 # 实参88入栈(esp -= 4)
0x08048402 <+5>: push 0x4d # 实参77入栈(esp -= 4)
0x08048404 <+7>: push 0x42 # 实参66入栈(esp -= 4)
0x08048406 <+9>: push 0x37 # 实参55入栈(esp -= 4)
0x08048408 <+11>: push 0x2c # 实参44入栈(esp -= 4)
0x0804840a <+13>: push 0x21 # 实参33入栈(esp -= 4)
0x0804840c <+15>: push 0x16 # 实参22入栈(esp -= 4)
0x0804840e <+17>: push 0xb # 实参11入栈(esp -= 4)
0x08048410 <+19>: call 0x80483db <func> # 调用func()函数(push 0x08048415)
0x08048415 <+24>: add esp,0x20 # 恢复栈顶指针esp
0x08048418 <+27>: leave # mov esp ebp; pop ebp
0x08048419 <+28>: ret # 函数返回,将栈中的返回地址弹出到eip指针
End of assembler dump.
pwndbg> disassemble func
Dump of assembler code for function func:
0x080483db <+0>: push ebp # 栈底ebp压栈(esp -= 4)
0x080483dc <+1>: mov ebp,esp # 将栈顶esp的值赋给ebp,此时ebp指向当前函数栈帧的起始地址
0x080483de <+3>: sub esp,0x10 # 为局部变量开辟栈空间
0x080483e1 <+6>: mov eax,DWORD PTR [ebp+0x8] # 取出arg1
0x080483e4 <+9>: add eax,0x1 # 计算loc1
0x080483e7 <+12>: mov DWORD PTR [ebp-0x8],eax # 将计算之后的loc1入栈
0x080483ea <+15>: mov eax,DWORD PTR [ebp+0x24] # 取出arg8
0x080483ed <+18>: add eax,0x8 # 计算loc8
0x080483f0 <+21>: mov DWORD PTR [ebp-0x4],eax # 将计算之后的loc8入栈
0x080483f3 <+24>: mov edx,DWORD PTR [ebp-0x8] # 取出loc1
0x080483f6 <+27>: mov eax,DWORD PTR [ebp-0x4] # 取出loc8
0x080483f9 <+30>: add eax,edx # 计算函数返回值
0x080483fb <+32>: leave # mov esp ebp; pop ebp
0x080483fc <+33>: ret # 函数返回

首先主调函数将被调函数func()的八个参数入栈(arg8、arg7 ······arg2、arg1),然后执行call指令调用func()函数,当执行call指令时,下一条指令的地址作为函数的返回地址入栈。接下来程序进入到func()函数,先将主调函数的ebp入栈保存,然后更新ebp为func()函数的栈顶地址,作为func()函数的基址。接下来esp下移为局部变量开辟栈空间,然后局部变量入栈。当函数返回时,程序执行leave指令将刚进入func()函数时栈中保存的主调函数ebp赋给栈顶指针esp,然后将保存的主调函数ebp出栈,最后ret指令将保存在栈中的返回地址弹出给eip,程序跳转至该地址,一次完整的函数调用即完成了。

x86-64

7KhK5n.png

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
32
33
34
35
36
37
38
39
pwndbg> disassemble main
Dump of assembler code for function main:
0x000000000040050a <+0>: push rbp # 将栈底rbp入栈,(rsp - = 8)
0x000000000040050b <+1>: mov rbp,rsp # 将栈顶rsp的值赋给rbp
0x000000000040050e <+4>: push 0x58 # arg8入栈,(rsp - = 8)
0x0000000000400510 <+6>: push 0x4d # arg7入栈,(rsp - = 8)
0x0000000000400512 <+8>: mov r9d,0x42 # 将arg6的值赋给r9
0x0000000000400518 <+14>: mov r8d,0x37 # 将arg5的值赋给r8
0x000000000040051e <+20>: mov ecx,0x2c # 将arg4的值赋给ecx
0x0000000000400523 <+25>: mov edx,0x21 # 将arg3的值赋给edx
0x0000000000400528 <+30>: mov esi,0x16 # 将arg2的值赋给esi
0x000000000040052d <+35>: mov edi,0xb # 将arg1的值赋给edi
0x0000000000400532 <+40>: call 0x4004d6 <func> # 调用func函数,push0x400537
0x0000000000400537 <+45>: add rsp,0x10 # 恢复栈顶
0x000000000040053b <+49>: leave # mov esp ebp;pop ebp
0x000000000040053c <+50>: ret # 函数返回
End of assembler dump.
pwndbg> disassemble func
Dump of assembler code for function func:
0x00000000004004d6 <+0>: push rbp # 将栈底rbp入栈,(rsp - = 8)
0x00000000004004d7 <+1>: mov rbp,rsp # 将栈顶rsp的值赋给rbp
0x00000000004004da <+4>: mov DWORD PTR [rbp-0x14],edi
0x00000000004004dd <+7>: mov DWORD PTR [rbp-0x18],esi
0x00000000004004e0 <+10>: mov DWORD PTR [rbp-0x1c],edx
0x00000000004004e3 <+13>: mov DWORD PTR [rbp-0x20],ecx
0x00000000004004e6 <+16>: mov DWORD PTR [rbp-0x24],r8d
0x00000000004004ea <+20>: mov DWORD PTR [rbp-0x28],r9d
0x00000000004004ee <+24>: mov eax,DWORD PTR [rbp-0x14]
0x00000000004004f1 <+27>: add eax,0x1
0x00000000004004f4 <+30>: mov DWORD PTR [rbp-0x8],eax
0x00000000004004f7 <+33>: mov eax,DWORD PTR [rbp+0x18]
0x00000000004004fa <+36>: add eax,0x8
0x00000000004004fd <+39>: mov DWORD PTR [rbp-0x4],eax
0x0000000000400500 <+42>: mov edx,DWORD PTR [rbp-0x8]
0x0000000000400503 <+45>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000400506 <+48>: add eax,edx # 计算返回值
0x0000000000400508 <+50>: pop rbp # 恢复rbp,(rsp + = 8)
0x0000000000400509 <+51>: ret # 函数返回

可以看到,与x86不同的是,对于x86-64的程序,会先将函数前六个参数依次保存在rdi,rsi,rdx,rcx,r8和r9进行传递,多余的参数才会和x86那样入栈。