文章目录
- 函数调用过程机制和栈
- 过程控制流
- 过程数据流
- 栈式语言的核心机制
- [x86-64/Linux 栈帧布局](#x86-64/Linux 栈帧布局)
函数调用过程机制和栈
-
控制权传递:跳转至过程代码起始处,最后返回到调用点。
-
数据传递:传递参数,最后返回值。
-
内存管理:在执行期间分配内存,返回时释放内存。
-
所有机制均通过机器指令实现,x86-64仅使用必要的机制支持过程调用。

-
应用二进制接口(Application Binary Interface,ABI):机器指令实现机制,但具体设计由ABI规范决定,ABI定义了控制流,数据传递和内存布局的标准。
栈结构
- 栈的基本概念:一段按栈规则管理的内存区域,内存被视为字节数组,不同区域有不同用途。

- 栈的增长方向为:由高地址向低地址方向增长。寄存器%rsp指向站定元素的地址(最低有效地址)。栈低在高地址,栈顶在低地址。

栈操作:压栈和出栈
- 压栈(pushq):操作形式为
pushq Src,从源操作数读取值,将%rsp减少8字节(64位),将读取的值存入目标寄存器(通常为寄存器)。

- 出栈(popq):操作形式为
popq Dest,从 %rsp 指向的地址读取值,将%rsp增加8,将读取的值存入目标寄存器(通常为寄存器),内存内容不变,仅%rsp更新。

过程控制流

- 利用栈来实现过程调用与返回。
- 调用指令call:利用call标签,将返回地址(call下一条指令地址)压入栈,跳转到标签指定的过程起始地址。
- 返回指令ret:从栈中弹出返回地址,跳转到该地址后继续执行。
指令指针寄存器与栈指针寄存器
- %rip(指令指针寄存器(Instruction Pointer)):**程序控制流的核心,**存储当前正在执行的指令地址,CPU通过%rip获取下一条要执行的指令。
- %rsp(栈指针寄存器(Stack Pointer)):指向当前栈顶的位置,管理函数调用时的栈内存分配和释放,控制参数传递、局部变量存储和返回地址保存。
- 在函数调用场景中,两者协同工作机制,:
- 调用前 :%rip指向
callq指令,%rsp指向调用前的栈顶 - 调用时 :
callq将返回地址压栈(%rsp向下移动),%rip跳转到目标函数 - 返回时 :
retq从栈顶弹出返回地址恢复%rip,%rsp向上移动恢复栈顶
- 调用前 :%rip指向
- 这种机制确保函数调用后能正确返回到调用点继续执行,是x86-64架构中过程调用的基础。
函数调用详细过程

%rip = 0x400544:指向callq 400550 <mult2>指令,表示当前CPU正在执行这条函数调用指令。当callq执行后,%rip将被修改为0x400550(mult2函数的入口地址)。%rsp = 0x120:指向当前的栈顶指针。- 分析 :程序正准备调用
mult2函数,此时还在调用者函数中执行。%rip指向即将执行的callq指令,栈处于调用前的状态。

%rip = 0x400550:跳转到mult2函数入口。%rsp = 0x118:%rsp从0x120减到0x118,为返回地址分配空间,栈顶存储callq指令压入的返回地址0x400549。- 关键机制:
- 控制权传递 :
callq指令将下一条指令地址0x400549压入栈 - 跳转 :将
mult2函数入口地址0x400550加载到%rip

- 控制权传递 :
%rip = 0x400557:指向retq指令%rsp = 0x118:栈指针保持不变- 栈顶:
0x400549(返回地址) - 分析 :
mult2函数即将执行完毕,当前停在retq指令处。此时:函数已完成计算逻辑,栈中保存的返回地址0x400549准备被使用,等待retq指令触发返回操作。

状态变化: %rip = 0x400549:返回到调用点下一条指令。%rsp = 0x120:%rsp从0x118增到0x120,释放返回地址空间(出栈)。- 栈内容:恢复为
0x130,0x128,0x120。 - 关键机制:
- 返回地址恢复 :
retq从栈顶弹出0x400549到%rip。 - 控制权返回 :程序继续执行
mov %rax,(%rbx)指令。
- 返回地址恢复 :
控制权传递机制
bash
调用者(callq) → 被调用者(mult2) → 调用者(retq)
0x400544 → 0x400550 → 0x400549
栈内存管理
bash
调用前: %rsp = 0x120
调用时: %rsp = 0x118 (压入返回地址)
返回后: %rsp = 0x120 (弹出返回地址)
数据传递
- 参数传递:通过寄存器(图中未显示具体参数)
- 返回值 :通过
%rax寄存器返回(从后续mov %rax,(%rbx)可推断) - 返回地址:通过栈传递,确保能正确返回调用点
这个过程完美体现了x86-64架构中函数调用的三个核心机制:
- 控制权传递 :通过
%rip和callq/retq指令实现 - 数据传递:通过寄存器和栈实现参数和返回值传递
- 内存管理 :通过栈指针
%rsp的动态调整实现内存的分配和释放
整个过程保证了函数调用的正确性和效率,是现代计算机系统程序执行的基础机制。
过程数据流
- 参数传递方式:前6个整型参数通过寄存器传递
%rdi, %rsi, %rdx, %rcx, %r8, %r9,超过6个的参数通过栈传递,返回值通过 %rax 寄存器传递。


- 局部变量存储:使用栈帧(stack frame)保存局部状态,包括局部变量、临时数据等,如 mult2 中的变量 s。
- 寄存器保存被调用者保存寄存器(如 %rbx),multstore 中先 push %rbx 保存现场,最后 pop %rbx 恢复,确保跨过程调用时不破坏原有值。
栈式语言的核心机制
- 在编程语言的底层设计中,"栈"(Stack)是支撑函数调用、递归执行和状态管理的核心数据结构。
- 栈式语言是指依赖栈来管理程序运行时状态的编程语言。这类语言通过栈存储函数调用的上下文信息(如参数、局部变量、返回地址),从而实现递归、嵌套调用等功能。常见的栈式语言包括C、Pascal、Java等。
递归的支持:可重入性与状态存储需求
可重入性:递归的前提
递归的本质是"函数自我调用",因此需要满足可重入性------即同一函数可以同时存在多个实例(instantiation),且每个实例的状态互不干扰。
- 例如,计算阶乘
factorial(n)时,当n=3,会依次产生factorial(3)→factorial(2)→factorial(1)三个实例 - 若代码不可重入(如使用全局变量共享状态),后续调用会覆盖前序实例的状态,导致结果错误
状态存储:栈的作用
每个函数实例都需要存储自己的状态信息,这些信息必须独立于其他实例。栈作为"后进先出"(LIFO)的数据结构,天然适合存储这种"临时且有序"的状态,主要包括三类:
- Arguments(参数):传递给函数的输入值
- Local variables(局部变量):函数内部定义的变量
- Return address(返回地址):函数执行完毕后,需要回到调用者的位置
栈规则:保证调用顺序的正确性
栈式语言的栈操作遵循严格的栈规则,确保函数调用的逻辑正确。
- 状态的生命周期:从调用到返回。函数的状态(参数、局部变量、返回地址)仅在函数被调用期间有效,一旦函数返回,其状态会被弹出栈并销毁。
- 调用者与被调用者的顺序:callee先返回。栈的LIFO特性决定了被调用者(callee)必须先于调用者(caller)返回。若违反此规则,会导致caller无法恢复正确的状态,引发程序崩溃或逻辑错误。
栈帧:函数实例的状态容器
栈式语言通过**栈帧(Frame)**来组织每个函数实例的状态。栈帧是栈中的一个连续内存块,专门存储单个函数实例的所有状态信息。
每个栈帧包含:
- 参数区:存储调用者传递的参数
- 局部变量区:存储函数内部的局部变量
- 返回地址区:存储调用者的下一条指令地址
- 可能还包括保存的寄存器值

- 栈帧管理:帧指针
%rbp(可选),栈指针%rsp,进入函数时分配空间,包含 call 指令的压栈操作,返回时释放空间,包含 ret 指令的出栈操作。
x86-64/Linux 栈帧布局
当前栈帧(从顶到底)
- 参数构建区:为即将调用的函数准备参数。
- 本地变量:无法放入寄存器时使用。
- 保存的寄存器上下文。
- 旧的帧指针(%rbp,可选)。
调用者栈帧
- 返回地址(由 call 指令压栈)。
- 当前调用的参数。

incr 函数示例

C代码实现:
bash
- 接收指针 p 和值 val
- 读取 *p 到 x,计算 y = x + val
- 写回 *p = y,返回 x
对应汇编代码:
movq (%rdi), %rax:加载 *p
addq %rax, %rsi:计算 x + val
movq %rsi, (%rdi):写回 *p
ret:返回
调用 incr 示例
主调函数 call_incr 结构
- 定义 v1 = 15213
- 调用 incr(&v1, 3000)
- 返回 v1 + v2
初始栈结构
- 保留 16 字节栈空间(subq $16, %rsp)
- 将 15213 存入 8(%rsp)
- 设置寄存器:%rdi 指向 &v1,%esi 设为 3000
- 执行 call incr


- movl 写入 32 位并清零高 32 位,更高效且足够表达 int 范围值。
- leaq 计算地址:leaq 8(%rsp), %rdi 正确计算 &v1 地址,利用地址计算机制获取局部变量地址。





寄存器保存规则
- 调用者保存(Caller-Saved):调用者需在调用前将重要值保存到栈,被调用者可自由修改这些寄存器。
- 被调用者保存(Callee-Saved):被调用者若使用某些寄存器,必须先保存其原始值,在返回前恢复这些寄存器的原始内容。
寄存器使用规范
调用者保存寄存器
%rax:返回值,也是调用者保存,可被过程修改。%rdi ~ %r9:整型参数传递(共6个)。%r10、%r11:额外的调用者保存临时寄存器。

被调用者保存寄存器
- %rbx、%r12 ~ %r14:必须由被调用者保存和恢复
- %rbp:通常作为帧指针,由被调用者保存
- 编译器决定是否启用 %rbp 作为帧指针
- %rsp:特殊形式的被调用者保存,退出函数时必须恢复原始值

递归函数
c
/* Recursive popcount */
long pcount_r(unsigned long x) {
if (x == 0)
return 0;
else
return (x & 1)
+ pcount_r(x >> 1);
}
- 对应的汇编代码:
c
pcount_r:
movl $0, %eax
testq %rdi, %rdi
je .L6
pushq %rbx
movq %rdi, %rbx
andl $1, %ebx
shrq %rdi
call pcount_r
addq %rbx, %rax
popq %rbx
.L6:
rep; ret
| 寄存器 | 用途 | 类型 |
|---|---|---|
| %rdi | x | 参数 |
| %rax | 返回值 | 返回值 |





