目录 Table of Contents
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
指令来调用过程