news 2026/4/23 21:05:29

函数调用与栈基础-pwn入门第1课

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
函数调用与栈基础-pwn入门第1课

可以把栈想成内存里一段从高地址向低地址增长**的区域,用来存局部变量、函数返回地址、保存寄存器等。

x86-64 栈是向下生长的(从高地址往低地址延伸):

● rbp:栈帧基指针,指向当前函数栈帧的底部(高地址)
● rsp:栈指针,指向当前函数栈帧的顶部(低地址)
● 函数内的局部变量,都存在 rsp 和 rbp 之间的这段内存里,且往 rsp 方向(低地址)分布

形象的想 栈就像一堆摞的很高的盘子 盘子之间是想通的

1️⃣ 四个寄存器的身份

模式栈顶指针栈基指针(帧指针)
64 位RSP(Stack Pointer)RBP(Base Pointer)
32 位ESPEBP
RSP/ESP:始终指向当前栈帧 最顶端(低地址),也就是最新一次 push 后的地址。 RBP/EBP:在函数调用时保存上一个栈帧的基址,方便通过固定偏移访问局部变量和参数

文字描述

假设从 main 调用 foo,栈的关键步骤是

步骤汇编动作RSP 变化RBP 变化说明
进入 foo 前call fooRSP 减 8,压入返回地址RBP 不变call 自动 push 返回地址
函数序言push rbp
mov rbp, rsp
RSP 再减 8 出空间用来保存旧 RBP;RBP=当前 RSP建立新栈帧
分配局部变量sub rsp, NRSP 向下减 NRBP 保持不变预留栈空间
调用子函数/保存寄存器push rbx 等RSP 每 push 一次减 8RBP 仍不动
函数结尾leave 即
mov rsp, rbp + pop rbp
RSP 还原到 栈基(rbp 所在处)
再弹出旧 RBP
恢复上层 RBP
返回ret弹出返回地址到 RIP,RSP 加 8RBP = 上层 RBP栈帧销毁

(1)对于为什么 RSP 减 8:

这是因为在 x86-64 架构下,栈上的每个“槽”是按 8 字节(64 bit)对齐的,而 call、push 这些指令本质上就是:先让 RSP 减去一个 8 字节元素的大小,再把数据写入 [RSP] 所指的地址。

call 指令会把下一条要执行的指令地址也就是返回地址压入栈里。 在 64 位模式下,返回地址是 8 字节

push 的动作也是“栈顶指针先减去操作数宽度,再写值”。rbp 宽度是 64 bit → 8 字节,

(2)leave中的pop rbp是直接用原rbp覆盖了当时mov rbp,rsp的那个rbp

(3)ret=pop rip 取出最初存储的返回地址进 rip

且(rsp)=(rsp)+8 回去,销栈 附一张手写图

调用过程

这是函数调用的基础,函数调用的流程,简单的说就是

RSP永远指向 栈顶(更准确:指向当前栈帧里最低地址的已用位置)。
x86 栈通常 由高向低地址增长:压栈会让 RSP -= 8(64 位)

RBP(frame/base pointer):传统上的栈底。

进入函数把旧 RBP 保存起来,然后把 RBP 设为当前 RSP,这样可以用固定偏移访问局部变量和参数。

但注意 tips:现代编译器常常省略 frame pointer(-fomit-frame-pointer),此时 RBP 可能被当作普通寄存器用,或者根本不建立RBP 栈帧链。

一次典型调用:**caller → call → callee → ret **

x86-64 的前 6 个整型/指针参数用寄存器:

RDI, RSI, RDX, RCX, R8, R9

Caller 侧:调用函数

mov edi,7;a=7放到 RDI/EDI call foo;调用

callfoo 做了 两件事:

第一步(压栈):把 返回地址(也就是 call 指令下一条指令的地址)压入栈中保存;

第二步(跳转):把 RIP 寄存器的值 直接设置为被调用函数(比如 foo)第一条指令的内存地址,CPU 从此开始执行该函数的代码。

栈变化为 :rsp-=8 为接下来入栈的返回地址腾空间

然后把返回地址压入 rsp 当前指向的新栈顶的位置

Callee 侧:函数序言

foo:push rbp mov rbp,rsp sub rsp,16...

push rbp:包含 rsp-=8,把被调用函数的 rbp 地址保存在栈上

mov rbp , rsp:建立当前的栈帧基准,以当前的栈顶为栈底开始拓展栈

然后对 rsp 操作给被调用函数的局部变量和临时空间腾地方

Ret: 函数返回

mov rsp,rbp pop rbp (上面两条等价于leave) ret

让 rsp=rbp,是直接丢弃了被调用函数在下面生长的新栈空间,

然后 pop rbp,先恢复 rbp 的值然后 rsp+=8

最后 ret 弹出返回地址:让 rip=[ rsp ], rsp+=8

特殊帧栈变化

push rbp mov rbp,rsp sub rsp,N...leave ret

这是上面的 用 rbp 始终指向当前函数栈帧的基准点,

局部变量用 [rbp-…],参数/返回地址用 [rbp+…] 来表示和访问

但有的时候会省略掉 rbp 和这个过程

省略帧指针

省略之后 函数入口是

sub rsp,32...add rsp,32ret

这个时候 rbp 变成了一个通用寄存器,编译器用 rsp 为基准或者把某些地址算出来放进寄存器

sub rsp,32mov DWORD PTR[rsp+12],edi;局部变量放在 rsp+offset

——局部变量放在了 rsp,并用 rsp+来访问

sub rsp,32lea rax,[rsp+16];rax 当临时“frame base” mov[rax-4],edi

—— 把 栈基准地址 算进另一个寄存器

这里把拓展出 rsp 的 32 字节一分为二,在正中间的[rsp+16] 作为栈基准存放进 rax,此时的 rax 和原本的 rbp 作用差不多

下面就用 [rax-4] 这样的 rax 基准来访问数据

这时的 rbp 就是一个普通的寄存器 用来干什么都行

不会出现大量 [rbp-xx] / [rbp+xx]

叶子函数(leaf function)

叶子函数是不再调用别的函数 没有 call,

并且局部变量和计算结果直接能放寄存器 不用放在栈里 自然用不到栈和 rbp

foo:lea eax,[rdi+1]ret

这里 foo 没有 push rbp 和 mov rbp,rsp

这种函数只需计算 lea eax, [rdi+1] 用寄存器完成

栈对齐导致的 rsp 调整

通常要求在 call 前满足

于是经常有:sub rsp, 8 / sub rsp, 40

或者在 call 前临时 sub/add rsp, 8 做对齐这种操作

这不是在建拆栈 只是在做栈对齐

tail call 尾调用优化

尾调用就是函数的最后一条语句是调用另一个函数并直接返回其结果

正常调用是 f call g 建立帧栈,g 执行后 ret 回到 f,f 再 ret 回到调用者

这里面有两层帧栈

所以会把 call g 优化成 **jmp g **不压入 f 的返回地址也不在调用 g 的时候建栈

直接 jmp 跳转到 g 的入口执行 ,g 执行完 ret 回到 f

这样相当于 g 没有再建立帧栈 而是利用了 f 的栈帧

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 21:03:08

别再为随机车烦恼了!手把手教你自定义highway-env中所有车辆的初始状态(附完整代码)

彻底掌控highway-env车辆初始状态:从随机到精确控制的进阶指南 在强化学习研究中,仿真环境的可控性直接决定了实验结果的可靠性和可重复性。highway-env作为一款专注于高速公路场景的强化学习环境,因其轻量级和高度模块化的特点受到广泛欢迎。…

作者头像 李华
网站建设 2026/4/23 21:02:53

Steam成就管理器完全指南:三步掌握所有游戏成就的终极方案

Steam成就管理器完全指南:三步掌握所有游戏成就的终极方案 【免费下载链接】SteamAchievementManager A manager for game achievements in Steam. 项目地址: https://gitcode.com/gh_mirrors/st/SteamAchievementManager 还在为那些永远无法完成的Steam游戏…

作者头像 李华
网站建设 2026/4/23 21:02:17

ABB机器人图形绘制代码

MODULE Module1!机器人图形绘制!定义一个原点位置VAR robtarget pHome:[[310.14,-0.00,552.71],[0.00299118,0.0294504,-0.999562,-8.81301E-5],[-1,0,-1,0],[9E9,9E9,9E9,9E9,9E9,9E9]];PROC main()sjx;zfx;yx;lbx;ENDPROC!三角形图形绘制PROC sjx()!运动到原点位置MoveL pHom…

作者头像 李华
网站建设 2026/4/23 20:59:25

电源管理电路设计全解析:从LDO到DC-DC,工业应用的实战要点

电源管理是嵌入式硬件设计中最基础也最容易出问题的环节。一个不稳定的电源会让单片机频繁复位、ADC读数跳变、通信丢包,甚至直接烧毁器件。本文系统梳理电源管理电路的核心知识点,从线性稳压器(LDO)到开关电源(DC-DC),再到工业环境下的特殊设计考量,适合硬件工程师和嵌…

作者头像 李华
网站建设 2026/4/23 20:59:20

零基础玩转Qwen2.5-VL:图文对话AI一键部署与实战体验

零基础玩转Qwen2.5-VL:图文对话AI一键部署与实战体验 1. 认识Qwen2.5-VL图文对话模型 1.1 什么是Qwen2.5-VL Qwen2.5-VL-7B-Instruct-GPTQ是一款基于Qwen2.5-VL-7B-Instruct模型的GPTQ量化版本,专门用于图文对话任务。这个模型能够理解图片内容&#…

作者头像 李华