x86_64 ABI:反汇编问题
x86_64 ABI: disassembly issue
我有以下 C 代码:
#include <stdio.h>
int function(int a, int b)
{
int res = a + b;
return res;
}
int main(){
function(1,2);
exit(0);
}
我用 gcc 4.8.2(在 Ubuntu 14 下)为 x86-64 编译它并产生这个代码:
000000000040052d <function>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: 89 7d ec mov %edi,-0x14(%rbp)
400534: 89 75 e8 mov %esi,-0x18(%rbp)
400537: 8b 45 e8 mov -0x18(%rbp),%eax
40053a: 8b 55 ec mov -0x14(%rbp),%edx
40053d: 01 d0 add %edx,%eax
40053f: 89 45 fc mov %eax,-0x4(%rbp)
400542: 8b 45 fc mov -0x4(%rbp),%eax
400545: 5d pop %rbp
400546: c3 retq
有些东西我看不懂。
一开始我们pushrbp,将rsp保存在rbp中。然后在上面
然后堆栈(在 %rbp)我们已经保存了 rbp。那么 rbp 以下的所有内容都是免费的 space.
但随后我们将来自 edi 和 esi 的传递参数放置在 -0x14(%rbp) 及以下。
但为什么我们不能将它们直接放在 rbp/rsp 指向的下方? edi 和 esi 都是 4 个字节长,为什么不是 -0x8(%rbp) 和 -0xc(%rbp) 呢?它与内存对齐有关吗?
为什么在return之前有一个奇怪的保存eax到堆栈并读回?
首先,请注意您正在查看未优化的编译器输出。在关闭优化的情况下,编译器输出通常最终看起来有点愚蠢,因为编译器将 C 的每一行都直接翻译成等效的 运行 汇编,甚至没有费心去做最简单、最明显的优化。
对于你的第一个问题,答案是"because that's where your compiler decided the variables should go"。没有更好的答案——编译器在堆栈分配方案上差异很大。例如,我机器上的 Clang 输出的是:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %esi
addl -8(%rbp), %esi
movl %esi, -12(%rbp)
movl -12(%rbp), %eax
popq %rbp
retq
你可以清楚地看到a
存储在-4,b
存储在-8,result
存储在-12。这比你的 GCC 给你的包装更紧,但这只是 GCC 的一个怪癖,仅此而已。
关于你的第二个问题,让我们看看指令如何映射到 C:
标准函数序言(设置栈帧):
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
将两个参数存储到堆栈变量 a
和 b
:
400531: 89 7d ec mov %edi,-0x14(%rbp)
400534: 89 75 e8 mov %esi,-0x18(%rbp)
为 a + b
加载 a
和 b
400537: 8b 45 e8 mov -0x18(%rbp),%eax
40053a: 8b 55 ec mov -0x14(%rbp),%edx
实际上a + b
40053d: 01 d0 add %edx,%eax
设置result = (result of a+b)
40053f: 89 45 fc mov %eax,-0x4(%rbp)
将result
复制到return值(return result;
)
400542: 8b 45 fc mov -0x4(%rbp),%eax
其实return:
400545: 5d pop %rbp
400546: c3 retq
所以你可以看到eax
的冗余保存和加载只是因为保存和加载对应于你原来的C文件的不同语句:保存来自result =
和加载来自 return result;
.
为了比较,这里是 Clang 的优化输出 (-O
):
pushq %rbp
movq %rsp, %rbp
addl %esi, %edi
movl %edi, %eax
popq %rbp
retq
更聪明:没有栈操作,整个函数体只有addl
和movl
两条指令。 (当然,如果您声明函数 static
,那么 GCC 和 Clang 都会很高兴地检测到该函数从未被有效使用并直接将其删除。)
我有以下 C 代码:
#include <stdio.h>
int function(int a, int b)
{
int res = a + b;
return res;
}
int main(){
function(1,2);
exit(0);
}
我用 gcc 4.8.2(在 Ubuntu 14 下)为 x86-64 编译它并产生这个代码:
000000000040052d <function>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: 89 7d ec mov %edi,-0x14(%rbp)
400534: 89 75 e8 mov %esi,-0x18(%rbp)
400537: 8b 45 e8 mov -0x18(%rbp),%eax
40053a: 8b 55 ec mov -0x14(%rbp),%edx
40053d: 01 d0 add %edx,%eax
40053f: 89 45 fc mov %eax,-0x4(%rbp)
400542: 8b 45 fc mov -0x4(%rbp),%eax
400545: 5d pop %rbp
400546: c3 retq
有些东西我看不懂。
一开始我们pushrbp,将rsp保存在rbp中。然后在上面 然后堆栈(在 %rbp)我们已经保存了 rbp。那么 rbp 以下的所有内容都是免费的 space.
但随后我们将来自 edi 和 esi 的传递参数放置在 -0x14(%rbp) 及以下。
但为什么我们不能将它们直接放在 rbp/rsp 指向的下方? edi 和 esi 都是 4 个字节长,为什么不是 -0x8(%rbp) 和 -0xc(%rbp) 呢?它与内存对齐有关吗?
为什么在return之前有一个奇怪的保存eax到堆栈并读回?
首先,请注意您正在查看未优化的编译器输出。在关闭优化的情况下,编译器输出通常最终看起来有点愚蠢,因为编译器将 C 的每一行都直接翻译成等效的 运行 汇编,甚至没有费心去做最简单、最明显的优化。
对于你的第一个问题,答案是"because that's where your compiler decided the variables should go"。没有更好的答案——编译器在堆栈分配方案上差异很大。例如,我机器上的 Clang 输出的是:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %esi
addl -8(%rbp), %esi
movl %esi, -12(%rbp)
movl -12(%rbp), %eax
popq %rbp
retq
你可以清楚地看到a
存储在-4,b
存储在-8,result
存储在-12。这比你的 GCC 给你的包装更紧,但这只是 GCC 的一个怪癖,仅此而已。
关于你的第二个问题,让我们看看指令如何映射到 C:
标准函数序言(设置栈帧):
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
将两个参数存储到堆栈变量 a
和 b
:
400531: 89 7d ec mov %edi,-0x14(%rbp)
400534: 89 75 e8 mov %esi,-0x18(%rbp)
为 a + b
a
和 b
400537: 8b 45 e8 mov -0x18(%rbp),%eax
40053a: 8b 55 ec mov -0x14(%rbp),%edx
实际上a + b
40053d: 01 d0 add %edx,%eax
设置result = (result of a+b)
40053f: 89 45 fc mov %eax,-0x4(%rbp)
将result
复制到return值(return result;
)
400542: 8b 45 fc mov -0x4(%rbp),%eax
其实return:
400545: 5d pop %rbp
400546: c3 retq
所以你可以看到eax
的冗余保存和加载只是因为保存和加载对应于你原来的C文件的不同语句:保存来自result =
和加载来自 return result;
.
为了比较,这里是 Clang 的优化输出 (-O
):
pushq %rbp
movq %rsp, %rbp
addl %esi, %edi
movl %edi, %eax
popq %rbp
retq
更聪明:没有栈操作,整个函数体只有addl
和movl
两条指令。 (当然,如果您声明函数 static
,那么 GCC 和 Clang 都会很高兴地检测到该函数从未被有效使用并直接将其删除。)