My-CSAPP

Machine Level Programming III: Procedures

Mechanisms in Procedures

ABI & API

ABI (Application Binary Interface) 是计算机体系结构中 运行环境(Runtime)目标代码(Object Code) 之间的一套完全确定的低级接口协议。它在二进制层面强制规范了机器码执行时的所有细节:

其核心意义在于确保遵循同一规范的独立编译单元(如可执行文件与动态链接库)无需重新编译,即可在符合该规范的硬件与操作系统上实现二进制级别的互操作性(Interoperability)

Mechanisms

从程序运行的角度来说,往往需要关注如下的流程:

Mechanisms all implemented with machine instructions.

Stack Structures

栈存在在内存中一段连续访问的区域:

stack

栈底指针的位置不会随着栈的扩展而变化(并且在内存中的索引为最高位),栈的扩充过程伴随着栈顶指针的不断下移(寄存器递减栈顶指针)

栈顶指针的内存索引低于栈底指针。

例如,在 BombLab 中,函数入口处往往存在:

sub    rsp,0x18

这样的指令,这就代表函数正在处理参数的输入,将栈顶指针的位置不断下移。

Push

pushq src: Decrement %rsp by 8. 例如 pushq %rbx 代表把寄存器 RBX 的 64 位值压入栈中,对应的,栈顶指针会进行相应的移动。

Pop

popq src: Read value at address given by %rsp, and increment %rsp by 8. Store value at Dest (must be register).

Calling Conventions

Passing Control

Using call label to pass control flow (function call).

具体来说,当一次函数调用发生时:

long mult2(long a, long b) {
  long s = a * b;
  return s;
}

void multstore(long x, long y, long *dest) {
  long t = mult2(x, y);
  *dest = t;
}
	.file	"multstore.c"
	.intel_syntax noprefix
	.text
	.globl	mult2
	.type	mult2, @function
mult2:
	push	rbp
	mov	rbp, rsp
	mov	QWORD PTR [rbp-24], rdi
	mov	QWORD PTR [rbp-32], rsi
	mov	rax, QWORD PTR [rbp-24]
	imul	rax, QWORD PTR [rbp-32]
	mov	QWORD PTR [rbp-8], rax
	mov	rax, QWORD PTR [rbp-8]
	pop	rbp
	ret
	.size	mult2, .-mult2
	.globl	multstore
	.type	multstore, @function
multstore:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 40
	mov	QWORD PTR [rbp-24], rdi
	mov	QWORD PTR [rbp-32], rsi
	mov	QWORD PTR [rbp-40], rdx
	mov	rdx, QWORD PTR [rbp-32]
	mov	rax, QWORD PTR [rbp-24]
	mov	rsi, rdx
	mov	rdi, rax
	call	mult2
	mov	QWORD PTR [rbp-8], rax
	mov	rax, QWORD PTR [rbp-40]
	mov	rdx, QWORD PTR [rbp-8]
	mov	QWORD PTR [rax], rdx
	nop
	leave
	ret
	.size	multstore, .-multstore
	.ident	"GCC: (GNU) 15.2.0"
	.section	.note.GNU-stack,"",@progbits

Passing Data

Managing Local Data

Stack Based Languages

栈帧只会处理当前的函数,栈中其他函数部分在此处被冻结了。

对于全局变量,存储在栈内存的一个特定区域.data 段专门存储已经初始化的全局变量

Stack Frame

Examples

long incr(long *p, long val) {
  long x = *p;
  long y = x + val;
  *p = y;
  return x;
}

long call_incr() {
  long v1 = 15213;
  long v2 = incr(&v1, 3000);
  return v1 + v2;
}
	.file	"incr.c"
	.intel_syntax noprefix
	.text
	.globl	incr
	.type	incr, @function
incr:
	push	rbp
	mov	rbp, rsp
	mov	QWORD PTR [rbp-24], rdi
	mov	QWORD PTR [rbp-32], rsi
	mov	rax, QWORD PTR [rbp-24]
	mov	rax, QWORD PTR [rax]
	mov	QWORD PTR [rbp-8], rax
	mov	rdx, QWORD PTR [rbp-8]
	mov	rax, QWORD PTR [rbp-32]
	add	rax, rdx
	mov	QWORD PTR [rbp-16], rax
	mov	rax, QWORD PTR [rbp-24]
	mov	rdx, QWORD PTR [rbp-16]
	mov	QWORD PTR [rax], rdx
	mov	rax, QWORD PTR [rbp-8]
	pop	rbp
	ret
	.size	incr, .-incr
	.globl	call_incr
	.type	call_incr, @function
call_incr:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16
	mov	QWORD PTR [rbp-16], 15213
	lea	rax, [rbp-16]
	mov	esi, 3000
	mov	rdi, rax
	call	incr
	mov	QWORD PTR [rbp-8], rax
	mov	rdx, QWORD PTR [rbp-16]
	mov	rax, QWORD PTR [rbp-8]
	add	rax, rdx
	leave
	ret
	.size	call_incr, .-call_incr
	.ident	"GCC: (GNU) 15.2.0"
	.section	.note.GNU-stack,"",@progbits

经典的函数调用三段式出现了:

call_incr:
	push	rbp
	mov	rbp, rsp
	sub	rsp, 16

Registers Conventions

一部分寄存器承担着固定的功能,因此建立一个统一的契约是至关重要的:

Caller Saved

Callee Saved

Illustration of Recursions

栈保证递归的实现和其他函数的调用并无二异。

long pcount_r(unsigned long x) {
  if (x == 0) {
    return 0;
  } else {
    return (x & 1) + pcount_r(x >> 1);
  }
}
pcount_r:
    movl    $0, %eax        # 将返回值寄存器 %eax 清零(准备好基准情况的返回值)
    testq   %rdi, %rdi      # 测试参数 %rdi (n) 是否为 0
    je      .L6             # 如果 n == 0,直接跳转到 .L6 返回 0

    pushq   %rbx            # 因为我们要用 %rbx 存中间值,它是被调用者保存寄存器 Callee-Saved,必须先压栈备份
    movq    %rdi, %rbx      # 将当前的 n 复制到 %rbx
    andl    $1, %ebx        # 取 n 的最低位:n & 1,结果存入 %ebx
    
    shrq    %rdi            # 逻辑右移 1 位:n >>= 1,为递归调用准备参数
    call    pcount_r        # 递归调用 pcount_r(n >> 1),结果会返回到 %rax 中
    
    addq    %rbx, %rax      # 将之前保存的最低位 (%rbx) 加到递归返回的结果 (%rax) 上
    popq    %rbx            # 恢复之前备份的 %rbx 值,维持栈平衡和寄存器原样

.L6:
    rep; ret                # 返回。rep; ret 是为了避免某些处理器在直接跳转到 ret 时产生分支预测性能问题

Recursion