x86-64 Stack

Region of memory managed with stack discipline.Grows toward lower address.Register %rsp contains lowest stack address.(address of “top” element)

Push

pushq Src

  • Fetch operand at Src
  • Decrement %rsp by 8
  • Write operand at address given by %rsp

Pop

popq Dest

  • Read Value at address given by %rsp
  • Increment %rsp by 8
  • Store value at Dest (must be register)

Calling Conventions

Passing control

在下面的C程序中,我们在main函数调用了multsotre,在multsotre中调用了mult2。我们通过这个例子来观察函数调用的过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

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

void mulstore(long x, long y, long *dest) {
long t = mult2(x, y);
*dest = t;
}

int main() {
long ans;
multstore(1, 2, &ans);
}

mult2

1
2
3
movq        %rdi, %rax    # a
imulq %rsi, %rax # a*b
ret # return

multstore

1
2
3
4
5
6
pushq        %rbx        # Save %rbx
movq %rdx, %rbx # Save dest
call mult2 # mult2 (x, y)
movq %rax, (%rbx) # Save at dest
popq %rbx # Restore
ret # Return

我们使用栈来支持过程的调用与返回。

过程调用:call label

  • Push return address on stack
  • Jump to label

返回地址:

  • 函数调用的下一条指令的地址。
  • 在我们这个例子中,multstore进行call时,会把movq %rax, (%rbx)这条指令的地址压入栈中。

过程返回:ret

  • Pop address from stack(此前压入的返回地址)
  • Jump to adress

我们使用gdb来更加直观地查看具体的调用过程与指令的跳转。

1
2
3
4
5
6
0x0000000000001159 <+4>:     push   %rbx
0x000000000000115a <+5>: mov %rdx,%rbx
0x000000000000115d <+8>: call 0x1149 <mult2>
0x0000000000001162 <+13>: mov %rax,(%rbx)
0x0000000000001165 <+16>: pop %rbx
0x0000000000001166 <+17>: ret

当我们执行到<+8>call时,程序首先会将rsp-8,也就是申请8个字节的栈空间(因为64位下,指令地址的大小就是8个字节),用来保存call的下一条指令的地址,在本例子中是0x0000000000001162<+13> mov,接着将%rip(保存了当前待执行指令的地址)设置为mult2首条指令的地址。

1
2
3
4
0x0000000000001149 <+0>:     endbr64 
0x000000000000114d <+4>: mov %rdi,%rax
0x0000000000001150 <+7>: imul %rsi,%rax
0x0000000000001154 <+11>: ret

在本例子中%rip被设置成0x0000000000001149。然后随着程序的执行%rip跳转到0x0000000000001154 <+11> ret时,代表过程结束,我们将%rsp保存的地址pop出,并且赋值给%rip。这样程序的执行回到了multstore中的

0x0000000000001162 <+13>: mov %rax,(%rbx)

并且接着往下执行,这样就完成了一次简单的函数调用。

Passing data

Registers(only first 6 arguments):%rdi, %rsi, %rdx, %rcx, %r8, %r9

Return value:%rax

stack:Arg 7, Arg 8,…,Arg n.

还是以上面那个例子来说。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
multsotre:
# x in %rdi, y in %rsi, dest in %rdx
0x0000000000001159 <+4>: push %rbx
0x000000000000115a <+5>: mov %rdx,%rbx
0x000000000000115d <+8>: call 0x1149 <mult2>
# t in %rax
0x0000000000001162 <+13>: mov %rax,(%rbx)
0x0000000000001165 <+16>: pop %rbx
0x0000000000001166 <+17>: ret
mult2:
# a in %rdi, b in %rsi
0x000000000000114d <+4>: mov %rdi,%rax
0x0000000000001150 <+7>: imul %rsi,%rax
# s in rax
0x0000000000001154 <+11>: ret

Managing local data

Linux Stack Frame

img

Example:incr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
long incr(long *p, long val) {
long x = *p;
long y = x + val;
*p = y;
return x;
}
# rdi : p, rsi : y, rax : x
0x000000000000114d <+4>: mov (%rdi),%rax
0x0000000000001150 <+7>: add %rax,%rsi
0x0000000000001153 <+10>: mov %rsi,(%rdi)
0x0000000000001156 <+13>: 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
leaq 8(%rsp), %rdi
call incr
addq 8(%rsp), %rax
addq $16, %rsp
ret

暂时无法在飞书文档外展示此内容

下表给出了寄存器的保存规则。

寄存器 用途 状态 是否可被修改 备注
%rax 返回值 调用者保存
%rdi 第一个参数 调用者保存 后续参数 %rsi 到 %r9 也遵循相同的规则
%rsi 第二个参数 调用者保存
后续参数 调用者保存 后续参数 %rdi 到 %r9 也遵循相同的规则
%r9 第九个参数 调用者保存
%r10 通用寄存器 调用者保存
%r11 通用寄存器 调用者保存
%rbx 通用寄存器 被调用者保存 否(需保存) 被调用者必须在函数结束前恢复其原始值
%r12 通用寄存器 被调用者保存 否(需保存) 被调用者必须在函数结束前恢复其原始值
%r13 通用寄存器 被调用者保存 否(需保存) 被调用者必须在函数结束前恢复其原始值
%r14 通用寄存器 被调用者保存 否(需保存) 被调用者必须在函数结束前恢复其原始值
%rbp 帧指针 被调用者保存 否(需保存) 可以作为帧指针使用,被调用者必须在函数结束前恢复其原始值
%rsp 栈指针 特殊被调用者保存 否(需恢复) 在程序退出时恢复到原始值

Recursion

  • 无需特别考虑即可处理
    • 栈帧意味着每个函数调用都有私有存储。
      • 保存寄存器和局部变量。
      • 保存返回指针。
    • 寄存器保存约定可以防止一个函数调用破坏另一个函数的数据。
      • 除非C代码明确这样做(例如,第9讲中的缓冲区溢出)。
    • 栈的纪律遵循调用/返回模式。
      • 如果P调用Q,那么Q在P之前返回。
      • 后进先出(LIFO)。
  • 也适用于相互递归
    • P调用Q;Q调用P。