x86-64过程调用与运行栈

x86-64 过程调用与运行栈

64 位把 rbp 解放出来, 只保留 rsp 还是指向栈顶的

在新的 32 位版本情况下, gcc 4.x 版本, ebp 实际上也是被释放出来, 也就是说默认情况下, ebp 也是可以作为通用寄存器使用.

x86-64 使用惯例

寄存器 用途
%rax Return value
%rbx 被调用者负责保存与恢复 Callee saved
%rcx 参数 #4
%rdx 参数 #3
%rsi 参数 #2
%rdi 参数 #1
%rsp 栈顶指针
%rbp Callee saved
%r8 参数 #5
%r9 参数 #6
%r10 Callee saved
%r11 Used for linking
%r12 C:Calleed saved
%r13 Callee saved
%14 Callee saved
%r15 Callee saved

除了 callee saved 之外, 像 rax 传参的这些, 这种寄存器肯定是调用者保存以及恢复

r11 有点特殊, 回头再讲

x86-64 寄存器

过程参数 (不超过 6 个) 通过寄存器传递, 大于 6个仍然使用栈传递, 多出来的几个参数用栈传递, 前 6 个还是用寄存器传递

这些传递参数寄存器可以看作是 "调用者保存" 寄存器

所有对于栈帧内容的访问, 都是基于 %rsp 完成的, 就是基于栈顶寄存器来访问.

%rbp 完全用作通用寄存器

swap()

x86-64 下的 swap() 过程

void swap(long *xp, long *yp)
{
    long t0 = *xp;
    long t1 = *yp;
    *xp = t1;
    *yp = t0;
}
swap:
    movq (%rdi), %rdx
    movq (%rsi), %rax
    movq %rax, (%rdi)
    movq %rdx, (%rsi)
    ret

它参数由寄存器传递, xp 放到 rdi, yp 放到 rsi

64 位指针, 因为是 long 类型, 所以后缀为 q

无需任何栈操作, 局部变量也存储于寄存器中

x86-64 下的 swap() 过程_2

/* swap, using local array*/
void swap_a(long *xp, long *yp)
{
    volatile long loc[2];
    loc[0] = *xp;
    loc[1] = *yp;
    *xp = loc[1];
    *yp = loc[0];
}

我们把这个函数变形一下, 加了一个叫 "易失型" 的变量类型. Volatile

这个 "易失型", 简单解释就是, 这么个变量有可能被外部程序或者模块所修改, 既然这样吗就强制编译器把它们存储在内存里面, 那么同时它们又是局部的, 所以它们要存在你的栈帧空间内.

swap_a:
    movq (%rdi), %rax
    movq %rax, -24(%rsp)
    movq (%rsi), %rax
    movq %rax, -16(%rsp)
    movq -16(%rsp), %rax
    movq %rax, (%rdi)
    movq -24(%rsp), %rax
    movq %rax, (%rsi)
    ret
    |--------|
    |rtn addr| <---- %rsp
    |--------|
-8  | unused |
    |--------|
-16 | loc[1] |
    |--------|
-24 | loc[0] |
    |--------|

注意看, 它在整个过程中, 没有对 rsp 进行修改, rsp 一直指向 return address. 它没有显示地分配栈帧, 而是直接去用 rsp 加上偏移量来访问变量, 比如 rsp - 24 访问 local[0].

在 64 位情况下, 从这个地址往下 128 字节, 你当前的函数可以直接去用, 而不需要显式地把 rsp 拉下去

x86-64 下的 swap() 过程_3

我们又把 swap() 变形了一下

long scount = 0;
/* swap a[i] & a[i+1] */
void swap_ele_se(long a[], int i)
{
    swap(&a[i], &a[i+1]);
    scount++;
}
swap_ele_se:
    movslq %esi, %rsi   ; sign extend i
    leaq (%rdi, %rsi, 8), %rdi  ; &a[i]
    leaq 8(%rdi), %rsi  ; &a[i+1]
    call swap   ; swap()
    incq scount(%rip)   ; scount++
    ret

这里面没有分配栈帧

leaq (%rdi, %rsi, 8), %rdi 相当于把 a[i] 的地址取出来, leaq 8(%rdi), %rsi 相当于把 a[i+1] 的地址取出来, 然后 call swap

这里面没有分配栈帧, 原因是这里面它所在过程本身, 它第一没有局部变量, 而且它也没有对传递给它的参数做什么运算操作

值得一提的是 incq scount(%rip), 好像是相对于 %rip 的寻址, 这个是因为 x64 情况下, 支持相对于程序指令寄存器的寻址, 你可以理解为以 rip 作为基址寄存器进行寻址

x86-64 下的 swap() 过程_4

我们又把它变换一下

```C
long scount = 0;
/* swap a[i] & a[i+1] */
void swap_ele(long a[], int i)
{
    swap(&a[i], &a[i+1]);
}

和刚才不一样的是, 这里没有 scount++ 操作

这时候它编译出来的代码就可能是下面这样 :

swap_ele:
    movslq %esi, %rsi   ; sign extend 1
    leaq (%rdi, %rsi, 8), %rdi  ; &a[i]
    leq 8(%rdi), %rsi   ; &a[i+1]
    jmp swap    ; swap()

它这里使用 jmp 来调用函数, 这是可以的, 因为 jmp swap 就相当于直接跳到 swap() 的入口点, swap() 本身有个 return, 它可以 return 回来, 因为你的函数没有对栈进行任何修改, 所以当时的 rsp 还是指向 swap() 的返回地址. 相当于从一个孙子过程直接返回到一个祖父过程.

栈帧使用实例

long sum = 0;
/* swap a[i] & a[i+1] */
void swap_ele_su(long a[], int i)
{
    swap(&a[i], &a[i+1]);
    sum += a[i];
}

这个和上面的有点不同, 这个运算有点麻烦, 因为它是把数组里面的元素取出来, 然后再和全局变量运算. 变量 a 与 i 的值保存在 "被调用者保存"的寄存器中. 这种情况下必须要分配栈帧了.

swap_ele_su:
    movq %rbx, -16(%rsp)        ; save %rbx
    movslq %esi, %rbx           ; extend & save i
    movq %r12, -8(%rsp)         ; save %r12
    movq %rdi, %r12             ; save a
    leaq (%rdi, %rbx, 8), %rdi  ; &a[i]
    subq $16, %rsp              ; Allocate stack frame
    leaq 8(%rdi), %rsi          ; &a[i+1]
    call swap                   ; swap()
    movq (%r12, %rbx, 8), %rax  ; a[i]
    addq %rax, sum(%rip)        ; sum += a[i]
    movq (%rsp), %rbx           ; restore %rbx
    movq 8(%rsp), %r12          ; restore %r12
    addq $16, %rsp              ; deallocate stack frame
    ret

栈操作

movq %rbx, -16(%rsp)

movq %r12, -8(%rsp)

subq $16, %rsp

movq (%rsp), %rbx

movq 8(%rsp), %r12

addq $16, %rsp

我们把栈操作的代码提取出来, 发现它是先 rsp - 16, rsp - 8, 先用上了, 然后再来分配. 当然退出的时候, 恢复.

这说明 x64 下有一些相当不同的操作特性.

x86-64 下栈帧的一些不同的操作特性

首先你可以一次性地分配整个栈帧, 就是用 rsp 一下子减去某个值来分配. (这个值就是栈帧的大小)

然后对于栈帧的内容的访问都是基于 %rsp 完成的, 就是 %rsp 一次减完吗然后通过 %rsp 加几加几来访问栈帧的内容

还有个特性就是刚才上面看到的, 就是延迟分配. 就是说我 %rsp 直接减多少, 就先用上了, 用了之后再 rsp 减去一个值来分配栈帧.

在 x86-64 下可以直接访问不超过当前栈指针 (%rsp) 128 字节的栈上空间, 就是栈指针减去 128 这块栈内存上你是可以直接用的. 这是 64 位下一个软件层面上新的定义. 这么做你针对 %rsp 的使用非常灵活. 这个定义中的 128 字节的这个空间, 叫作 宏区


这个栈的释放也非常简单, 就是 %rsp 直接加上某个值 (栈帧的大小)

小结

  • 频繁使用寄存器

    参数传递

  • 减少对栈的使用

    甚至不使用栈

    一次性分配与释放栈

  • 优化操作

    可以直接使用部分栈空间, 而不用分配

    可以使用 jmp 指令来调用过程