引言

书上说,在子函数调用里面,需要在结束前将减掉的 %rsp 指针再加回来,但实际上自己动手实验之后发现并没有加回来。因此写了这篇文章记录一下自己的折腾与思考。

分析

实际上,gcc 生成了如下的汇编代码:

	nop
	leave
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc

这个 leave 非常值得人注意,而实际上,这条指令的作用就是将栈桢恢复到父程序的状态,而 ret 的作用就是跳转到 (%rsp) 的位置,并将 %rsp 减一。

那么 leave 指令是如何恢复栈帧的呢?

其实有一个 %rbp 寄存器,指向的位置就是栈桢开始的地方,所以子程序一开始就必执行一句指令

  pushq	%rbp            ; push the frame pointer of it's parent
  .cfi_def_cfa_offset 16
  .cfi_offset 6, -16
  movq	%rsp, %rbp
;; And then, creat the frame of itself

将 %rbp 指针入栈,并且将 %rsp 指针的值赋值给 %rbp,然后再创建自己的栈桢。

因此,被调用的程序的栈桢的结构(栈是地址从大到小增长的,这里也是从大到小的顺序)就是:

- 父程序的帧
- 返回地址
-----------------------------------------
- 父程序的 %rbp 指针
- 自己的栈桢
   - 需要保存的寄存器(被调用者保存的寄存器)
   - 临时变量(为什么说某些变量具有函数作用域?不就是因为它们保存在栈桢上面,子程序一返回,为这些变量分配的地址上面的内容就可以被其它代码随意修改了)
   - 子程序的子程序的参数构造
- 返回地址
-----------------------------------------
子程序的子程序的帧

因此,leave 指令只要令 %rsp 等于 %rbp,然后再 pop %rbp ,即可再执行 ret 指令来返回原程序就好。

书上的汇编代码是如此的顺序,但是这里又有实际与理论的区别:书上使用 IMM(%rsp) 来访问栈桢,而且是从地址从大到小的顺序来存放,也就是上面列表里面的第 2 项那样的顺序;而 gcc 会使用 -IMM(%rbp) 来存放数据到栈桢,而且还会按照地址从小到大的顺序来存放数据!

gcc 生成的代码如下:

	subq	$48, %rsp
	movq	%rdi, -40(%rbp) ; save the first parameter
	movq	%rsi, -48(%rbp) ; save the second parameter

这样做的好处是,当出现像 printf 这样可变数量参数的函数的时候,可以非常方便地分配栈桢,反正后面的参数可以尽情占用剩下的空间,而像临时变量与子程序的子程序的参数构造区这样的固定的长度的数据相对于 %rbp 即函数栈桢的开始的偏移量便可以固定下来。

(附) C 代码与相应的汇编码的注释

  • test.c:

          #include "vec.h"
    
          #define OP *
          #define IDENT 1
    
          void combine(vec_ptr v, data_t *dest) {
            long i;
            long length = vec_length(v);
            data_t *data = get_vec_start(v);
            data_t acc = IDENT;
    
            for (i = 0; i < length; i++) {
              acc = acc OP data[i];
            }
    
            *dest = acc;
          }
    
  • vec.h:

          typedef long data_t;
    
          typedef struct {
            long len;
            data_t *data;
          } vec_rec, *vec_ptr;
    
          inline data_t *get_vec_start(vec_ptr v) { return v->data; }
    
          vec_ptr new_vec(long len);   // create new
          long vec_length(vec_ptr v);  // get length
          int get_vec_element(vec_ptr v, long index, data_t *dest);
    
  • test.s:

                  .file	"test.c"
                  .text
                  .globl	combine
                  .type	combine, @function
          combine:
          .LFB1:
                  .cfi_startproc
                  pushq	%rbp            ; push the frame pointer
                  .cfi_def_cfa_offset 16
                  .cfi_offset 6, -16
                  movq	%rsp, %rbp
                  .cfi_def_cfa_register 6
                  subq	$48, %rsp
                  movq	%rdi, -40(%rbp) ; save v
                  movq	%rsi, -48(%rbp) ; save *dest
                  movq	-40(%rbp), %rax
                  movq	%rax, %rdi      ; use v as the first parameter
                  call	vec_length@PLT
                  movq	%rax, -16(%rbp) ; save the "length" at -16(%rbp)
                  movq	-40(%rbp), %rax
                  movq	%rax, %rdi      ; use v as the first parameter
                  call	get_vec_start@PLT ; would return address of the first element of v
                  ;; save data at %rbp - 8, aka just below the %rbp, aka the top of the frame
                  movq	%rax, -8(%rbp)
                  movq	$1, -24(%rbp)   ; acc
                  movq	$0, -32(%rbp)   ; i
                  jmp	.L2
          .L3:
                  movq	-32(%rbp), %rax ; i -> %rax
                  leaq	0(,%rax,8), %rdx ; i*8 -> %rdx, cuz data_t is long(8byte each elem)
                  movq	-8(%rbp), %rax   ; data -> %rax
                  addq	%rdx, %rax       ; %rax = %rax + i*8, aka %rax = (void *)data + 8*i
                  movq	(%rax), %rax     ; movq means move an 8bytes data, in this case, aka long.
                  movq	-24(%rbp), %rdx  ; acc -> %rdx
                  imulq	%rdx, %rax
                  movq	%rax, -24(%rbp) ; save the result to acc
                  addq	$1, -32(%rbp)   ; i++
          .L2:
                  movq	-32(%rbp), %rax ; determine should the for loop continue
                  cmpq	-16(%rbp), %rax
                  jl	.L3
                  movq	-48(%rbp), %rax
                  movq	-24(%rbp), %rdx
                  movq	%rdx, (%rax)    ; *dest = acc
                  nop
                  leave
                  .cfi_def_cfa 7, 8
                  ret
                  .cfi_endproc
          .LFE1:
                  .size	combine, .-combine
                  .ident	"GCC: (Gentoo 9.3.0-r2 p4) 9.3.0"
                  .section	.note.GNU-stack,"",@progbits
    
  • 编译选项

          gcc -S test.c
    

(附)一些碎碎念

看过 CSAPP 的同学可能会发现,这个就是第五章:“优化程序性能”一章,“消除不必要的内存引用”一节的代码,而我只不过将 data_t 的定义从 float 改成了 long,虽然因此我弄明白了一些书上讲的与实际的不同,但我其实在此处遇到了问题。

书上讲的,消除不必要的内存引用,是说 acc 这个局部变量放在了寄存器里面,因此可以不用每次循环都对 data 取址,而是每次都对这个寄存器进行累加就好了。但是现在我发现 acc 根本就没有用寄存器来代表,而是仍然开辟了栈桢,存储在栈桢上面,每次循环都要对这个地址进行内存引用!——这与书上讲的根本不一样嘛!

其实我本来在动手实验之前就有这个疑惑了,为什么新建一个局部变量,它不也是在栈桢上面开辟一个内存来保存吗?为什么一定能保证汇编代码会使用寄存器来存储这个变量?我之前认为是 GCC 的默认优化功能,但是当我动手试了一下后发现,也许是我没有指定优化等级?到底要怎么样才会用寄存器来做这个临时变量啊喂!

后续

后面按照 gcc -S -Og test.c 编译,发现还真是因为优化等级的问题,下面是这次编译的结果。可见,acc 确实是使用寄存器来表示的,栈桢也精简了,而且 %rbp 也没有用来指示栈桢的顶部了,反而是用了书上讲的方法:

    addq $8, %rsp

来 deallocate 栈桢了,真是令人唏嘘不已。

	.file	"test.c"
	.text
	.globl	combine
	.type	combine, @function
combine:
.LFB1:
	.cfi_startproc
	pushq	%rbp            ;callee saving
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	pushq	%rbx            ;callee saving
	.cfi_def_cfa_offset 24
	.cfi_offset 3, -24
	subq	$8, %rsp
	.cfi_def_cfa_offset 32
	movq	%rdi, %rbp      ; %rbp = v
	movq	%rsi, %rbx      ; %rbx = dest
	call	vec_length@PLT  ; %rax = lenth
	movq	8(%rbp), %rsi   ; %rsi = 8+v, skip the `long len'
	movl	$1, %ecx        ;acc
	movl	$0, %edx        ;i
        ;; get_vec_start disappeared, because it is inline function.
.L2:
	cmpq	%rax, %rdx
	jge	.L5
	imulq	(%rsi,%rdx,8), %rcx ;acc += data[i]
	addq	$1, %rdx            ;i++
	jmp	.L2
.L5:
	movq	%rcx, (%rbx)    ;*dest = acc
	addq	$8, %rsp
	.cfi_def_cfa_offset 24
	popq	%rbx
	.cfi_def_cfa_offset 16
	popq	%rbp
	.cfi_def_cfa_offset 8
	ret
	.cfi_endproc
.LFE1:
	.size	combine, .-combine
	.ident	"GCC: (Gentoo 9.3.0-r2 p4) 9.3.0"
	.section	.note.GNU-stack,"",@progbits