计算系统安全速成之机器级编程【2】

文章目录

函数调用过程机制和栈

  • 控制权传递:跳转至过程代码起始处,最后返回到调用点。

  • 数据传递:传递参数,最后返回值。

  • 内存管理:在执行期间分配内存,返回时释放内存。

  • 所有机制均通过机器指令实现,x86-64仅使用必要的机制支持过程调用。

    ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/c4a3183ef10a4fefa3d3c7fc2976f205.png#pic_center =400px)

  • 应用二进制接口(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)):指向当前栈顶的位置,管理函数调用时的栈内存分配和释放,控制参数传递、局部变量存储和返回地址保存。
  • 在函数调用场景中,两者协同工作机制,:
    1. 调用前 :%rip指向callq指令,%rsp指向调用前的栈顶
    2. 调用时callq将返回地址压栈(%rsp向下移动),%rip跳转到目标函数
    3. 返回时retq从栈顶弹出返回地址恢复%rip,%rsp向上移动恢复栈顶
  • 这种机制确保函数调用后能正确返回到调用点继续执行,是x86-64架构中过程调用的基础。

函数调用详细过程

  • %rip = 0x400544:指向callq 400550 <mult2>指令,表示当前CPU正在执行这条函数调用指令。当callq执行后,%rip将被修改为0x400550(mult2函数的入口地址)。
  • %rsp = 0x120:指向当前的栈顶指针。
  • 分析 :程序正准备调用mult2函数,此时还在调用者函数中执行。%rip指向即将执行的callq指令,栈处于调用前的状态。
  • %rip = 0x400550:跳转到mult2函数入口。
  • %rsp = 0x118%rsp0x120减到0x118,为返回地址分配空间,栈顶存储callq指令压入的返回地址0x400549
  • 关键机制:
    1. 控制权传递callq指令将下一条指令地址0x400549压入栈
    2. 跳转 :将mult2函数入口地址0x400550加载到%rip
  • %rip = 0x400557:指向retq指令
  • %rsp = 0x118:栈指针保持不变
  • 栈顶:0x400549(返回地址)
  • 分析mult2函数即将执行完毕,当前停在retq指令处。此时:函数已完成计算逻辑,栈中保存的返回地址0x400549准备被使用,等待retq指令触发返回操作。

    状态变化:
  • %rip = 0x400549:返回到调用点下一条指令。
  • %rsp = 0x120%rsp0x118增到0x120,释放返回地址空间(出栈)。
  • 栈内容:恢复为0x130, 0x128, 0x120
  • 关键机制:
    1. 返回地址恢复retq从栈顶弹出0x400549%rip
    2. 控制权返回 :程序继续执行mov %rax,(%rbx)指令。

控制权传递机制

bash 复制代码
调用者(callq) → 被调用者(mult2) → 调用者(retq)
0x400544 → 0x400550 → 0x400549

栈内存管理

bash 复制代码
调用前: %rsp = 0x120
调用时: %rsp = 0x118 (压入返回地址)
返回后: %rsp = 0x120 (弹出返回地址)

数据传递

  • 参数传递:通过寄存器(图中未显示具体参数)
  • 返回值 :通过%rax寄存器返回(从后续mov %rax,(%rbx)可推断)
  • 返回地址:通过栈传递,确保能正确返回调用点

这个过程完美体现了x86-64架构中函数调用的三个核心机制:

  1. 控制权传递 :通过%ripcallq/retq指令实现
  2. 数据传递:通过寄存器和栈实现参数和返回值传递
  3. 内存管理 :通过栈指针%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(返回地址):函数执行完毕后,需要回到调用者的位置

栈规则:保证调用顺序的正确性

栈式语言的栈操作遵循严格的栈规则,确保函数调用的逻辑正确。

  1. 状态的生命周期:从调用到返回。函数的状态(参数、局部变量、返回地址)仅在函数被调用期间有效,一旦函数返回,其状态会被弹出栈并销毁。
  2. 调用者与被调用者的顺序: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 返回值 返回值






相关推荐
shinelord明3 个月前
【大数据技术实战】流式计算 Flink~生产错误实战解析
大数据·架构·flink·实时计算·计算机技术
shinelord明5 个月前
【计算机网络架构】环型架构简介
计算机网络·架构·计算机科学·通信·计算机技术
Thanks_ks6 个月前
计算机技术、互联网与 IT 前沿:量子计算、Web3.0 等趋势洞察及行业应用
量子计算·web3.0·计算机技术·人工智能应用·互联网趋势·it 行业创新·大数据云计算融合
Thanks_ks6 个月前
计算机组成原理核心剖析:CPU、存储、I/O 与总线系统全解
计算机组成原理·计算机技术·存储系统·cpu 结构·i/o 设备·总线系统·硬件原理
AI技术学长7 个月前
训练神经网络的批量标准化(使用 PyTorch)
人工智能·pytorch·神经网络·数据科学·计算机技术·批量标准化
AI技术学长8 个月前
深度学习-python猫狗识别tensorflow2.0
人工智能·深度学习·计算机视觉·图像识别·计算机技术·tensorflow2·猫狗识别
编程在手天下我有8 个月前
解码 Web Service:从技术原理到应用场景的深度剖析
软件开发·分布式计算·计算机技术·系统集成·网络技术·web 开发
AI_小站1 年前
实践教程|Transformer Decoder-Only 模型批量生成 Trick
人工智能·深度学习·大模型·llm·transformer·大语言模型·计算机技术
爱喝白开水a1 年前
基于Transformer的图像处理预训练模型
图像处理·人工智能·深度学习·transformer·预训练·ai大模型·计算机技术