Skip to content

L07 Machine-Level Programming III: Procedures

Source: https://www.bilibili.com/video/BV1iW411d7hd/?p=7

过程(procedure):在不同语言中可能称为函数(function)、过程(procedure)、以及 OOP 中的方法(method)

应用程序二进制接口(ABI, application binary interface),L07 就重在讲 ABI. Windows ABI 和 Linux ABI 不完全相同,苹果的 OSX 也有自己的 ABI,甚至 FreeBSD(Unix 操作系统的另一种变体)也有自己的 ABI,它们都很类似但在细节上不同。

过程的机制(mechanisms in procedures)

P(...) {
    ...
    y = Q(x)
    print(y)
    ...
}
int Q(int i) {
    int t = 3 * i;
    int v[10];
    ...
    return v[t]; 
}
  • 传递控制权 passing control
  • 调用另一个过程 to beginning of procedure code
  • 调用结束后返回调用位置 back to return point
  • 传递数据 passing data
  • 传递参数 procedure arguments
  • 返回返回值 return value
  • 内存管理 memory management
  • 函数执行时分配空间 allocate during procedure execution
  • 函数返回时释放空间 deallocate upon return
  • mechanism all implemented with machine instructions
  • x86-64 implementation of a procedure uses only those mechanisms required

今天:过程

  • 栈结构 stack structure
  • 调用惯例 calling conventions
  • passing control
  • passing data
  • managing local data
  • 刻画递归 illustration of recursion

x86-64 栈

---------------------  地址增加  【栈底】
|                   |    ↑
---------------------
|       ......      |
---------------------
|                   |    ↓
---------------------  栈往下
|                   |
---------------------  <- 栈指针 %rsp  【栈顶】

程序用栈来管理过程调用与返回的状态,我们有 pushpop 这两个明确的指令来对栈进行操作。

push src :1. 获取操作数 src (可以来自立即数、寄存器、内存) 2. 栈指针减去 8 3. 入栈了,被 %rsp 指向。

pop dest:1. 获取栈顶(%rsp 指向的数据)2. 栈指针加上 8 3. 将获取的栈顶赋值给 destdest 必须是寄存器。

为什么是 8?数据都是 64 比特即 8 字节的。下面是一个例子:

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;
}
0000000000400540 <multstore>:
  400540:  push   %rbx       # save %rbx
  400541:  mov    %rdx, %rbx # save dest
  400544:  callq  400550 <mult2> # mult2(x, y)
  400549:  mov    %rax, (%rbx)   # save at dest
  50054c:  pop    %rbx       # restore %rbx
  40054d:  retq              # return
0000000000400550 <mult2>:
  400550:  mov    %rdi, %rax # a
  400553:  imul   %rsi, %rax # a
  400557:  retq              # Return

过程的控制流(procedure control flow)

  • 使用栈来帮助过程调用和返回
  • 过程调用:call label
  • 将返回地址入栈
  • 跳转到 label
  • 返回地址
  • 调用后的下一条指令地址 address of the next instruction right after call
  • 反汇编得到的例子(如上)
  • 过程返回:ret
  • 弹栈得到地址
  • 跳转这个地址
400544:  callq  400550 <mult2> # mult2(x, y)
400549:  mov    %rax, (%rbx)   # save at dest

400550:  mov    %rdi, %rax # a

400557:  retq              # Return

过程的数据流(procedure data flow)

ABI 约定:开头的六个参数固定由 %rdi%rsi%rdx%rcx%rcx%r8%r9 传递,之后的参数倒序压入栈中,返回值固定由 %rax 传递。

IA32 所有的参数都在栈中传递。

  400541:  mov    %rdx, %rbx #
  400544:  callq  400550 <mult2> #
  400549:  mov    %rax, (%rbx)   #

  400550:  mov    %rdi, %rax #
  400553:  imul   %rsi, %rax #
  400557:  retq              #

管理本地数据(manage local data)

单线程:每时每刻只有一个过程在运行,调用相当于冻结调用前过程。

因为单线程,所以我们可以为当前运行的过程分配无限的内存而不必担心和其他过程冲突,当这个过程返回时,之前用到的数据全部可以不保留。

stack frame:栈帧,用于特定调用的每个内存块。调用链(call chain)【28:30】

Stack-Based Language
    Languages that support recursion
        e.g. C, Pascal, Java
        Code must be "Reentrant"
            Multiple simultaneous instantiations of single procedure
        Need some place to store state of each instantiation
            Arguments
            Local variables
            Return pointer
    Stack discipline
        state for given procedure needed for limited time
            From when called to when return
        Callee returns before caller does
    Stack allocated in FRAMES
        state for single procedure instatiation

栈帧,包含

  • 返回信息 return information
  • 本体存储(如果需要)local storage (if needed)
  • 临时空间(如果需要)temporary space (if neede)
--------------------
|                  |
| previous frame   |
|                  |
-------------------- <- frame pointer %rbp (optional)
|                  |
| frame for proc   |
|                  |
-------------------- <- stack pointer %rsp
     STACK TOP

管理

​ 当进入过程的时候分配的空间

​ 「启动」代码

​ 包括 push 指令,这被包含在 call 指令中。

​ 当返回的时候释放

​ 「结束」代码

​ 包括 pop 指令,这被包含在 ret 指令中。

大多数系统会限制栈的深度,目的是为了防止无限递归。

x86-64/Linux 栈帧

--------------------
|                  |
|      ......      |
|                  |
--------------------
|                  |
| Arguments 7+     |
|                  |
--------------------    ^
| Return Address   |    |  caller frame
-------------------- -------
| Old %rbp (Op)    | <- %rbp (Optional) 
--------------------
|                  |
| Save Registers   |
|        +         |
| Local Variables  |
|                  |
--------------------
|                  |
| Argument Build   |
| (Optional)       |
|                  |
-------------------- <- %rsp
     STACK TOP

​ 当前栈帧(从顶到底):

​ 参数构建:用于调用的过程

​ 本地变量:不能保存在寄存器中

​ 保存的寄存器上下文

​ 旧栈帧(可选)

​ 调用者栈帧:

​ 返回地址,这被 call 指令压入栈中

​ 这次调用的各个参数。

下面为一个例子

long incr(long *p, long val) {
    long x = *p;
    long y = x + val;
    *p = y;
    return x;
}
/*
%rdi  参数 p
%rsi  参数 val 和 y
%rax  参数 x 和返回值
*/
incr:
    movq    (%rdi), %rax
    addq    %rax, %rsi
    movq    %rsi, (%rdi)
    ret
long call_incr() {
    long v1 = 15213;
    long v2 = incr(&v1, 3000);
    return v1 + v2;
}
call_incr:
    subq    $16, %rsp        # 
    movq    $15213, 8(%rsp)  # 此两条为强制,为了避免其中数据丢失
    movl    $3000, %esi      # 这里用 movl 是因为其指令短一些,少用一些空间(这也省...)
    leaq    8(%rsp), %rdi
    call    incr
    addq    8(%rsp), %rax
    addq    $16, %rsp
    ret 
--------------------
|                  |
|      ......      |
|                  |
--------------------
| Return Address   |
--------------------
| 15213            | <- %rsp + 8 
--------------------
| Unused           | <- %rsp
--------------------
         | Updated Stack Structure
         v
--------------------
|                  |
|      ......      |
|                  |
--------------------
| Return Address   | <- %rsp
--------------------     

寄存器可以进行临时存储吗?

​ 可以,但是可能会被覆写,因此会存一遍。

我们有两种方法可以管理寄存器,一种称为调用者保存(caller saved):调用者在调用之前将临时值保存在它的栈帧中;另一种称为被调用者保存(callee saved):被调用者在使用之前将临时值保存在它的栈帧中,被调用者在返回之前恢复这些临时值。显然选择哪种方式由 ABI 这个约定规定。

x86-64 Linux 寄存器使用(书P173) %rax:返回值,调用者保存,可能被过程修改

​ %rdi, ..., %r9:参数,调用者保存,可能被过程修改

​ %r10, %r11:调用者保存,可能被过程修改,用于调用者保存的临时值(caller-saved temporaries)。

​ %rbx, %r12, %r13, %r14:被调用者保存,被调用者必须保存且恢复。

​ %rbp:被调用者保存,被调用者必须保存且恢复,可能被用作基指针(frame pointer),可以混合、匹配(mix & match)

​ %rsp:被调用者保存的特殊形式,退出过程前恢复原值。

long call_incr2(long x) {
    long v1 = 15213;
    long v2 = incr(&v1, 3000);
    return x + v2;
}
call_incr2:
    pushq   %rbx
    subq    $16, %rsp
    movq    %rdi, %rbx
    movq    $15213, 8(%rsp)
    movl    $3000, %esi
    leaq    8(%rsp), %rdi
    call    incr
    addq    %rbx, %rax
    popq    %rbx
    ret
--------------------
|                  |
|      ......      |
|                  |
--------------------
| Return Address   | <- %rsp
--------------------
         |
         v
--------------------
|                  |
|      ......      |
|                  |
--------------------
| Return Address   | 
--------------------      
| Saved %rbx       |
--------------------
| 15213            | <- %rsp + 8 
--------------------
| Unused           | <- %rsp
--------------------
         | Updated Stack Structure
         v
--------------------
|                  |
|      ......      |
|                  |
--------------------
| Return Address   | <- %rsp
--------------------     

递归

如上所述,因为栈帧机制和(被)调用者保存,调用自己没什么特殊的。

下面是一个例子:

/* Recursive popcount */
long pcount_r(unsigned long x) {
    if (x == 0)
        return 0;
    else 
        return (x & 1) + pcount_r(x >> 1);
}
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
Observations About Recursion
    Handled without special consideration
        Satck frames mean that each function call has private storage
            saved registers & local variables
            saved return pointer
        Registers saving conventions prevent one function call from corrupting another's data
            Unless the C code explicitly does so (e.g. buffer overflow in L09)
        Stack discipline follows call/return pattern
            If P calls Q, then Q returns before P
            Last-In, First-Out
    Also works for mutual recursion
        P calls Q; Q calls P
x86-64 Procedure Summary
    Important Points
        Stack is the right data structure for procedure call/return
            If P calls Q, then Q returns before P
    Recursion (& mutual recursion) handled by normal calling conventions
        Can safely store values in local stack frame and in callee-saved registers
        Put function arguments at top of stack
        Result return in %rax
    Pointers are addresses of values
        On stack or global