前言
之前在学栈溢出的时候,对函数调用栈一直没有很明白,直到最近栈溢出学得差不多的时候,才逐渐了解函数调用栈...这实际上差不多就是栈溢出的核心。
基础知识
栈
栈:一个先进后出的线性表,在计算机中,栈由高地址向低地址增长,栈底为高地址,栈顶为低地址。
栈帧:为区分 主调函数 与 被调函数 之间的关系,给每个函数单独分配的一部分栈空间。当要调用一个函数时,会为被调函数开辟一个栈空间作为它的栈帧;当被调函数执行完后,会回到主调函数的栈帧,继续执行没有执行完的指令。
寄存器
32位
通用寄存器:eax、ebx,ecx、edx、edi、esi、esp、ebp
指令寄存器:eip
eax、ebx、ecx、edx:数据寄存器,主要用来存放操作数和运算结果等;
esp:堆栈指针寄存器,一般指向栈帧的顶部,用于访问栈顶的数据;
ebp:基指针寄存器,一般指向栈帧的底部,用于访问栈帧中的数据。
eip:CPU 通过 eip 寄存器读取即将要执行的指令。每次CPU执行完相应的汇编指令之后,eip 寄存器的值就会增加,指向下一条指令。
特别注明:eax 通常用来保存 函数返回值、某些计算结果、系统调用参数值等
64位
通用寄存器:rax ~ rdx、rdi、rsi、esp、ebp、r8 ~ r15(相比32位新增8个)
指令寄存器:rip
作用与32位大同小异
汇编指令(Intel 风格,以32位为例)
add eax, ebx:把 eax + ebx 的值存到 eax 中
sub eax, ebx:把 eax - ebx 的值存到 eax 中
mov eax, ebx:把 ebx 的值赋给 eax
jmp func:把 eip 指向 func 函数的初始位置
push eax = sub esp 4; mov [esp], eax(64位则将 4 改成 8)
pop eax = mov eax, [esp]; add esp 4
call func = push eip + x; jmp func
leave = mov esp, ebp; pop ebp
retn = pop eip
栈帧的结构
函数的栈帧由高地址向低地址延伸
依次是传入函数的参数、返回地址、栈基、本地变量。
传入的函数参数
函数传参顺序为从右往左传参,如 func(1, 2),则 2 在高地址(更靠近栈底),1在低地址
返回地址
在函数结束后,指令寄存器会跳向返回地址,继续执行主调函数的指令。
因此当可以栈溢出修改返回地址时,即可控制程序流,进行 rop (它不是一个寄存器)。
栈基
ebp(rbp)指向栈基,其保存着主调函数的栈基地址,程序通过栈基访问栈帧的数据(包括传入的参数和本地变量),如 [ebp-4],[ebp+8] 等
本地变量
保存着被调函数内定义的本地变量
函数调用栈过程
以下为演示所用程序代码,以32位编译
gcc -m32 -no-pie -fno-stack-protector a.c -o a
#include<stdio.h>
void func1();
int func2(int a, int b);
int main(int argc, char *argv[]) {
func1();
return 0;
}
void func1() {
func2(1, 2);
}
int func2(int a, int b) {
int c = a + b;
char s[] = "This is func2";
puts(s);
return c;
}
下图为函数调用栈过程中使用的汇编指令(便于演示,稍有修改)
1. 传入被调函数的参数
2. call 被调函数
即 把 call func2 的下一条指令地址压栈,然后把 eip 跳到被调函数
3. 为被调函数开栈
4. 函数一顿操作后,清栈,返回到主调函数
全过程
注:64位程序,除传参时前六个参数从左到右依次传给 rdi、rsi、rdx、rcx、r8、r9,其他过程基本相同