原来一直对64位程序的汇编函数调用比较熟悉, 32位程序的函数调用总觉得十分复杂. 本文主要对64位程序在不同优化级别下的汇编函数调用实现情况, 后篇再结合32位进行对比分析

说明

汇编格式

如果对汇编指令还不熟悉可以看上一篇文章, 本文还是采用AT&T汇编格式

优化级别

在编译时, 可以指定代码的优化级别, 大致优化级别有如下几个

  • -O (相当于 -O1)
  • -O0 (不优化, 默认级别)
  • -O1 (不影响编译速度的前提下,尽量采用一些优化算法降低代码大小和可执行代码的运行速度)
  • -O2 (牺牲部分编译速度, 优化更多)
  • -O3 (采取很多向量化算法,提高代码的并行执行程度)
  • -Ofast (不会严格遵循语言标准, 优化更多)
  • -Og (提供合理的优化水平,同时产生较好的可调试信息)
  • -Os (尽量降低目标代码的大小)

如果优化级别太高会出现和源代码完全不符的情况, 如函数调用会被优化掉, 出现汇编中没有调用函数的现象

本文主要分析不优化和-Og两种情况

源代码

为了简便, 下面分析将采用以下源代码

#include<stdio.h>

int add(int a, int b) {
int array[10];
return a + b + array[2] + array[1];
}

int main() {
int array[] = {4, 5, 6};
int a = 7, b = 8;
printf("sum: %d\n", add(a, b));
for (int i = 0; i < 3; i++) {
printf("%d ", array[i]);
}
}

x86_64程序分析

-Og优化级别下

首先看add函数的汇编代码(已省去无关指令)

subq $56, %rsp
/* 栈保护代码
movl $40, %edx
movq %fs:(%rdx), %rax
movq %rax, 40(%rsp)
*/
xorl %eax, %eax
addl %esi, %edi // a + b
addl 8(%rsp), %edi // array[2]
movl %edi, %eax
addl 4(%rsp), %eax // array[1]
/* 栈保护代码
movq 40(%rsp), %rcx
xorq %fs:(%rdx), %rcx
jne .L4
*/
addq $56, %rsp
ret

可以看到函数中存在栈保护代码, 这里暂且不说, 与函数调用关系不大, 后面的代码中将省略

首先看出程序会计算出add函数内需要的栈空间, 这里我们开了长度为10的int数组, 需要40字节, 栈保护需要8字节,
在使用call调用函数时已经将返回地址8字节压入栈中, 为了使栈内存和16字节对齐, 所以第一步将栈顶指针下移了56字节(56+8 是16的倍数)

rsp + 40的位置用于存放栈保护内容, 而rsp, rsp + 4 则依次存放数组元素array[0], array[1]

eax寄存器作为返回值, 首先通过xor置0

函数的参数依次存放在edi, esi 寄存器中, 使用add指令累加

最后将栈指针上移56字节, 回到调用函数前的状态

再看main函数

pushq  %rbx
subq $32, %rsp
movl $4, 12(%rsp)
movl $5, 16(%rsp)
movl $6, 20(%rsp)
movl $8, %esi
movl $7, %edi
call add
movl %eax, %edx
leaq .LC0(%rip), %rsi
movl $1, %edi
movl $0, %eax
call __printf_chk@PLT

除了一开始在栈上保存了rbx以外, 其它步骤基本和add函数一致, 也可以看到函数传参的过程(mov到相应寄存器)

注意这里编译器自动优化, 将printf转换成了更安全的__printf_chk(int flag, const char * format)函数, 所以会多一个参数flag

无优化下

首先还是先看add函数

pushq %rbp
movq %rsp, %rbp
subq $64, %rsp
movl %edi, -52(%rbp)
movl %esi, -56(%rbp)
/* 栈保护
movq %fs:40, %rax
movq %rax, -8(%rbp)
*/
xorl %eax, %eax
movl -52(%rbp), %edx
movl -56(%rbp), %eax
addl %eax, %edx
movl -40(%rbp), %eax
addl %eax, %edx
movl -44(%rbp), %eax
addl %edx, %eax
/* 栈保护
movq -8(%rbp), %rcx
xorq %fs:40, %rcx
*/
leave
ret

与优化过的代码相比, 最大的变化就是使用了rbp寄存器

rbp存储当前函数的基地址, 一个正在执行的函数A, rsp是 A 的栈顶, rbp 是 A 的栈底
stack-frame

函数调用规定了被调用者需存储调用者的rbp信息(Callee Saved), 并且在函数执行结束时恢复

于是, 使用rbp这一套的模板大概如下

pushq %rbp // 存储调用者的rbp
movq %rsp, %rbp // 将rbp设置为当前函数的基地址
subq $64, %rsp // 为局部变量预留空间

... // 使用rbp作为内存寻址的基地址

leave // mov %rbp, %rsp 和 pop %rbp 指令的结合

可以看到, 尽管使用rbp寻址, 但仍有subq $64, %rsp操作, 这是为了避免在调用函数时可能的push操作造成和局部变量数据冲突

多参数的情况

在64位程序中, 函数的参数会依次存在%rdi, %rsi, %rdx, %rcx, %r8, %r9, 这6个寄存器中, 如果参数多于6个怎么处理呢? 下面看一个例子

修改add函数接收7个参数

int add(int a, int b, int c, int d, int e, int f, int g) {
int array[10];
return a + b + c + d + e + f + g + array[2] + array[1];
}

对应的汇编代码为(-Og优化)

// add 函数
subq $56, %rsp
addl %esi, %edi //a + b
addl %edx, %edi // c
addl %ecx, %edi // d
addl %r8d, %edi // e
addl %r9d, %edi // f
movl %edi, %eax
addl 64(%rsp), %eax // g
addl 8(%rsp), %eax // array[1]
addl 4(%rsp), %eax // array[0]
addq $56, %rsp

// main 函数
pushq $0 // g
movl $0, %r9d // f
movl $0, %r8d // e
movl $0, %ecx // d
movl $0, %edx // c
movl $8, %esi // b
movl $7, %edi // a
call add
addq $16, %rsp

可以看到, 在main函数中通过push操作将第7个参数压栈, 调用完成后通过add释放栈空间
这也是上面说的为什么即使有rbp作为基地址, 仍然要移动rsp的原因


本文大致讲了两种函数调用的方式, 可以看出, 仅使用rsp代码会简洁不少, 而rbp方式仅仅是方便于调试, 所以在开了优化的情况下, 一般会被编译为rsp方式

下一篇将讲述32位程序的情况, 并与64位程序做一个对比总结

转载申请

知识共享许可协议

本文知识共享署名 4.0 国际许可协议进行许可,转载时请注明原文链接