| Instruction | Mnemonic | C example | Flags |
|---|---|---|---|
| j (jmp) | Jump | break; | (Unconditional) |
| je (jz) | Jump if equal (zero) | if (x == y) | ZF |
| jne (jnz) | Jump if not equal (nonzero) | if (x != y) | !ZF |
| jg (jnle) | Jump if greater | if (x > y), signed | !ZF && !(SF ^ OF) |
| jge (jnl) | Jump if greater or equal | if (x >= y), signed | !(SF ^ OF) |
| jl (jnge) | Jump if less | if (x < y), signed | SF ^ OF |
| jle (jng) | Jump if less or equal | if (x <= y), signed | (SF ^ OF) || ZF |
| ja (jnbe) | Jump if above | if (x > y), unsigned | !CF && !ZF |
| jae (jnb) | Jump if above or equal | if (x >= y), unsigned | !CF |
| jb (jnae) | Jump if below | if (x < y), unsigned | CF |
| jbe (jna) | Jump if below or equal | if (x <= y), unsigned | CF || ZF |
| js | Jump if sign bit | if (x < 0), signed | SF |
| jns | Jump if not sign bit | if (x >= 0), signed | !SF |
| jc | Jump if carry bit | N/A | CF |
| jnc | Jump if not carry bit | N/A | !CF |
| jo | Jump if overflow bit | N/A | OF |
| jno | Jump if not overflow bit | N/A | !OF |
Some basic rules of the x86-64/Linux calling convention are:
- The first six function arguments are passed in registers
%rdi,%rsi,%rdx,%rcx,%r8, and%r9(in this order; see the register list from last lecture). - The seventh and subsequent arguments are passed on the stack (see more below).
- The return value is passed in register
%rax.
✅ 在 Buffer Overflow 中,我们覆盖的是函数调用时压入栈的返回地址(return address),从而控制程序在
ret时跳转去你想让它执行的代码(比如hack())。
为了帮你更直观地理解,我们下面用一个简单例子从“call → ret”全过程来说明:
🧪 示例:一个简单的 C 函数调用
#include <stdio.h>
void bar() {
printf("In bar\n");
}
void foo() {
bar();
printf("Returning from foo\n");
}
int main() {
foo();
printf("Back in main\n");
return 0;
}
🧰 汇编视角下的栈调用过程
🧱 假设地址如下(用于举例)
-
main()地址:0x400100 -
foo()地址:0x400150 -
bar()地址:0x400180
📦 执行流程:main → foo → bar
第一步:main 调用 foo
main:
call foo ; 等于 push 0x400105 ; jmp 0x400150
栈变化(栈向低地址增长):
| ... |
| return to 0x400105 | ← main 调用 foo 后返回地址被压栈
|----------------------| ← rsp
第二步:foo 调用 bar
foo:
call bar ; 等于 push 0x400155 ; jmp 0x400180
栈变化:
| return to 0x400105 | ← 仍然是 main 的返回地址
| return to 0x400155 | ← foo 调用 bar 的返回地址
|----------------------| ← rsp
第三步:bar 执行完,执行 ret
bar:
ret ; pop -> 0x400155 (跳回 foo)
栈变化:
| return to 0x400105 | ← 栈顶,foo 的返回地址
|----------------------| ← rsp
第四步:foo 执行完,执行 ret
foo:
ret ; pop -> 0x400105 (跳回 main)
🎯 那么我们是怎么 “控制流” 的?
假设 foo() 有个像下面这样的本地缓冲区:
char buf[32];
gets(buf);
栈布局如下
| return to main (0x400105) | ← ✅我们想覆盖这里!
| saved rbp |
| local buf[32] |
|---------------------------| ← rsp
如果你用 gets(buf) 输入:
"A"*40 + "\x80\x01\x40\x00\x00\x00\x00\x00"
你就是覆盖掉:
-
A* 32 → 填满buf -
A* 8 → 覆盖rbp -
"\x80\x01\x40..."→ 覆盖返回地址为bar()的地址
这样,当 foo() 执行 ret,就跳到了 bar(),而不是 main()。
🛠️ 总结:call 和 ret 之间栈上发生了什么?
| 步骤 | 栈行为 | 栈顶值(%rsp 指向) |
|---|---|---|
| call | 压入调用者的下一条指令地址 | return address |
| 函数执行 | 分配局部变量空间(sub %rsp, N) | 本地变量 |
| ret | 从栈顶弹出返回地址并跳转 | 跳转地址 |
🎯 你在 buffer overflow 中做的事
你伪造了一个假的返回地址,用数据覆盖了真实返回地址,使 ret 跳转到你控制的位置(如 hack())。
如果你还想看看在你当前项目(CSCI0300 Lab3)里:
- 怎么一步步用 GDB 确认你覆盖成功
- 怎么自动化写脚本找地址、写 payload
To prepare for a function call, the caller performs the following tasks:
- The caller stores the first six arguments in the corresponding registers.
- If the callee takes more than six arguments, or if some of its arguments are large, the caller must store the surplus arguments on its stack frame (in increasing order). The 7th argument must be stored at
(%rsp)(that is, the top of the stack) when the caller executes itscallqinstruction. - The caller saves any caller-saved registers (see last lecture’s list). These are registers whose values the callee might overwrite, but which the caller needs to retain for later use.
- The caller executes
callq FUNCTION. This has an effect likepushq $NEXT_INSTRUCTION; jmp FUNCTION(or, equivalently,subq $8, %rsp; movq $NEXT_INSTRUCTION, (%rsp); jmp FUNCTION), whereNEXT_INSTRUCTIONis the address of the instruction immediately followingcallq.
To return from a function, the callee does the following:
- The callee places its return value in
%rax. - The callee restores the stack pointer to its value at entry (“entry
%rsp”), if necessary. - The callee executes the
retqinstruction. This has an effect likepopq %rip, which removes the return address from the stack and jumps to that address (because the instruction writes it into the special%ripregister). - Finally, the caller then cleans up any space it prepared for arguments and restores caller-saved registers if necessary.
一、函数调用前(caller 调用者做的事)
-
前六个参数放到固定的寄存器里(如
%rdi,%rsi,%rdx,%rcx,%r8,%r9)。 -
如果有超过六个参数,或者某些参数太大,多出来的参数就放在调用者的栈上(caller stack)。
- 比如第7个参数就要放在当前
%rsp指向的栈顶上。
- 比如第7个参数就要放在当前
-
保存 caller-saved(调用者自己负责保存的)寄存器的值,防止被函数修改。
- 这些寄存器可能在函数里会被用掉,比如
%r10,%r11,%rax等。
- 这些寄存器可能在函数里会被用掉,比如
-
执行
callq FUNCTION:-
它的作用是:
-
把返回地址(下一条指令地址)压栈;
-
然后跳转到 FUNCTION 的代码开始执行。
-
-
二、函数执行完要返回时(callee 被调用者做的事)
-
把返回值放在
%rax里。 -
如果函数改动了栈指针
%rsp(比如分配了局部变量),就把%rsp恢复到函数刚开始时的值。 -
执行
retq:- 相当于:从栈上弹出 return address,跳回到原来调用这个函数的位置继续执行。
三、函数返回后,调用者还要
- 清理栈上为参数预留的空间(如果用了栈传参)。
- 恢复之前保存的 caller-saved 寄存器。
这是个非常关键的问题!理解 %rbp(base pointer)和它的保存(saved %rbp)是理解函数调用过程、栈帧结构、以及如何进行 buffer overflow 攻击的基础。我们来清楚地解释一下。
🧠 %rbp 是什么?(x86-64)
-
%rbp是 Base Pointer(基址指针),又叫 Frame Pointer。 -
用来标记当前函数的栈帧起始地址。
-
它在函数调用时被
push(保存)下来,以便返回上一层函数时能还原栈帧结构。
🚀 函数调用时,call 和 prologue 会做什么?
调用一个函数 foo() 时,发生的事情如下:
1. call foo
-
将当前
rip(下一条指令地址)压入栈,也就是return address。 -
跳转到
foo()的入口。
2. 进入 foo() 后,函数前几行是标准 prologue
push %rbp ; 保存 caller 的 rbp 到栈中(这就是 saved %rbp)
mov %rsp, %rbp ; 设置当前函数的栈帧基地址(rbp = rsp)
sub $n, %rsp ; 留出 n 字节空间给局部变量
栈此时结构如下
↑ High Address
[ caller saved %rbp ] ← saved %rbp
[ return address ] ← call foo 时压入
[ foo 局部变量们 ] ← rsp 向下减了空间
↓ Low Address
🧯 这个 saved %rbp 的作用是什么?
当 foo() 执行完毕时,要恢复到 main() 的上下文,这时靠 leave 和 ret:
leave ; 等价于:mov %rbp, %rsp ; pop %rbp
ret ; 等价于:pop %rip
所以:
-
leave让%rsp回到%rbp,然后把 saved%rbp弹出还原; -
ret弹出栈顶的 return address,跳转回 caller。
✅ 总结:两者的作用
| 项 | 作用 |
|---|---|
%rbp | 当前函数栈帧的起始地址;用来访问局部变量(如 mov -0x4(%rbp), %eax) |
saved %rbp | 保存 caller 的 %rbp,供返回时 leave 恢复;也表示上一栈帧起点 |
🔓 为什么 overflow 会覆盖 saved %rbp 和 return address?
因为局部变量(如 char buf[32])是按 rsp 分配的,如果用 gets() 或 scanf() 写超出 buf 的空间:
[ buf (32 bytes) ] ← overflow 从这里开始
[ saved %rbp (8 bytes) ] ← 会被覆盖
[ return address (8 bytes)] ← 被覆盖后就能“劫持控制流”
📌 例子总结
void foo() {
char buf[32];
gets(buf); // 如果你输入超过 32 字节,就会覆盖 saved %rbp 和 ret addr
}
是否要我给你画个栈帧结构图?或者展示一次 leave 和 ret 的 GDB 步骤?你就会更清楚 saved %rbp 是如何被使用的。