0%

栈溢出基础

一、栈的介绍

(1)栈的基本概念

栈(Stack):一种后进先出(LIFO)的数据结构,用于存储程序运行时的临时数据

栈帧(Stack Frame):每个函数调用时,栈会分配一个栈帧,用于存储函数的局部变量、参数、返回地址等信息

栈指针(SP):指向当前栈顶的指针,随着压栈(Push)和出栈(Pop)操作动态变化

基指针(BP):指向当前栈帧的底部,用于在栈帧中定位局部变量和参数

指令指针(IP/EIP/RIP):指向当前执行的指令地址,函数调用时会保存返回地址到栈中

(2)栈的内存布局

函数调用栈是程序运行时内存中一段连续的区域,用于保存函数运行时的状态信息。这些状态信息包括:

函数参数:调用函数时传递给被调用函数的参数

局部变量:被调用函数中定义的变量

返回地址:被调用函数执行完毕后,程序需要返回的位置

保存的寄存器值:某些情况下,函数会保存一些寄存器的值,以便在函数返回时恢复

(3)栈的先进后出特性

函数调用栈被称为“栈”,是因为它遵循后进先出的原则

压栈(Push):当发生函数调用时,调用函数(caller)的状态被保存到栈中,被调用函数(callee)的状态被压入栈顶。

退栈(Pop):当函数调用结束时,栈顶的函数(callee)状态被弹出,栈顶恢复到调用函数(caller)的状态。

(4)栈的生长方向

在大多数系统中,函数调用栈在内存中是从高地址向低地址生长的

压栈时:栈顶指针(SP)向低地址移动,栈顶对应的内存地址变小。

退栈时:栈顶指针(SP)向高地址移动,栈顶对应的内存地址变大。

二、 函数调用过程

(1)参数传递的步骤

1.逆序压栈

调用函数(caller)按照逆序将参数依次压入栈中。这意味着最后一个参数先被压入栈,第一个参数最后被压入栈。

例如,调用函数 callee(1,2,3) 时,参数的压栈顺序是3,2,1

1
2
3
push 3    ; 第三个参数,实际是第一个一个压入栈
push 2 ; 第二个参数
push 1 ; 第一个参数,实际是最后一个压入栈

2.参数保存

这些参数被压入栈后,会保存在调用函数(caller)的栈帧中。此时,调用函数的栈帧仍然包含这些参数

3.被调用函数的状态保存

在参数压栈之后,调用函数会将控制权交给被调用函数(callee)。此时,被调用函数会创建自己的栈帧,并将这些参数视为自己的输入

(2)栈帧的切换

当被调用函数(callee)开始执行时,栈帧的切换过程如下:

1.保存返回地址

当调用函数使用 call 指令调用被调用函数时,call 指令会自动将调用函数的下一条指令的地址(即返回地址)压入栈中

1
call callee  ; 调用 callee 函数

2.创建被调用函数的栈帧

被调用函数(callee)会创建自己的栈帧,包括保存的寄存器值(也就是调用函数的基地址)、局部变量等

被调用函数会从栈中读取参数,并在自己的栈帧中使用这些参数

1
2
push ebp       ; 保存调用者的基地址
mov ebp, esp ; 建立被调用者的基地址

3.执行被调用函数

被调用函数执行其逻辑,操作局部变量和参数

image-20250120172851935

4.清理栈帧

被调用函数执行完毕后,清理自己的栈帧,释放局部变量占用的空间

返回地址从栈中弹出,存储到指令指针(IP/EIP/RIP)中,程序跳回调用函数的下一条指令

1
2
3
mov esp, ebp  ; 恢复栈指针到调用者调用时的位置,释放局部变量占用的空间
pop ebp ; 恢复调用者的基地址
ret ; 从栈中弹出返回地址并跳转到该地址

注:

x86

使用栈来传递参数使用 eax 存放返回值

amd64

前6个参数依次存放于 rdi、rsi、rdx、rcx、r8、r9 寄存器中第7个以后的参数存放于栈中

// 在最后添加