x86架构 函数调用栈过程简述

前言

之前在学栈溢出的时候,对函数调用栈一直没有很明白,直到最近栈溢出学得差不多的时候,才逐渐了解函数调用栈...这实际上差不多就是栈溢出的核心。



基础知识

栈:一个先进后出的线性表,在计算机中,栈由高地址向低地址增长,栈底为高地址,栈顶为低地址。

栈帧:为区分 主调函数 与 被调函数 之间的关系,给每个函数单独分配的一部分栈空间。当要调用一个函数时,会为被调函数开辟一个栈空间作为它的栈帧;当被调函数执行完后,会回到主调函数的栈帧,继续执行没有执行完的指令。

寄存器

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,其他过程基本相同

评论

Anonymous : 🕊
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×