内存参数在堆栈上的位置 x86_64 gcc
Position of Memory parameters on the Stack x86_64 gcc
我从汇编开始,为了测试,我编写了一个简单的 C 程序,对其进行了编译和反汇编,以查看参数是如何传递的。这是 C 代码:
#include <stdio.h>
#include <stdlib.h>
void calc (float*a,float*b,float*c,float*d) {
a[0]=1000;
b[0]=100.0;
c[0]=99.9;
d[0]=10000;
}
int main() {
float a[100];
float b[100];
float c[100];
float d[100];
calc(a,b,c,d);
}
这是它的反汇编:
default rel
global calc: function
global main: function
SECTION .text align=1 execute ; section number 1, code
calc: ; Function begin
push rbp ; 0000 _ 55
mov rbp, rsp ; 0001 _ 48: 89. E5
mov qword [rbp-8H], rdi ; 0004 _ 48: 89. 7D, F8
mov qword [rbp-10H], rsi ; 0008 _ 48: 89. 75, F0
mov qword [rbp-18H], rdx ; 000C _ 48: 89. 55, E8
mov qword [rbp-20H], rcx ; 0010 _ 48: 89. 4D, E0
; 0054 _ 90
pop rbp ; 0055 _ 5D
ret ; 0056 _ C3
; calc End of function
main: ; Function begin
push rbp ; 0057 _ 55
mov rbp, rsp ; 0058 _ 48: 89. E5
sub rsp, 1600 ; 005B _ 48: 81. EC, 00000640
lea rcx, [rbp-640H] ; 0062 _ 48: 8D. 8D, FFFFF9C0
lea rdx, [rbp-4B0H] ; 0069 _ 48: 8D. 95, FFFFFB50
lea rsi, [rbp-320H] ; 0070 _ 48: 8D. B5, FFFFFCE0
lea rax, [rbp-190H] ; 0077 _ 48: 8D. 85, FFFFFE70
mov rdi, rax ; 007E _ 48: 89. C7
call calc ; 0081 _ E8, 00000000(rel)
mov eax, 0 ; 0086 _ B8, 00000000
leave ; 008B _ C9
ret ; 008C _ C3
; main End of function
我不明白为什么堆栈上的参数大小不同。第一个在 [ebp-8H]
中,这是可以理解的,因为它是一个 64 位地址,但下一个只多了两个字节,在 [ebp-10H]
而不是 [ebp-16H]
中。
为什么会这样,而且最重要的是,当我编写一个采用这些确切参数的汇编程序时,我应该使用 ebp
?
中的哪些地址
这个好像说了很多,估计你还没听过,所以还是要重复一遍:分析未优化代码的反汇编,很大程度上是浪费时间。禁用优化后,编译器会专注于两件事:
- 尽可能快地生成代码,以便您获得尽可能快的编译,并且
- 使您调试代码变得容易(例如,确保您可以在每个高级语言语句上设置断点,并且不对指令重新排序以允许您单步执行代码)。
未优化的代码混乱、丑陋且令人困惑。它包含大量冗余指令,看起来不像人类会编写的代码,并且与实际应用程序中的代码不匹配(在启用优化的情况下编译)。
当你想分析汇编代码时,打开优化器。
当我们这样做时,我们看到您的代码编译为:
calc(float*, float*, float*, float*):
mov DWORD PTR [rdi], 0x447a0000
mov DWORD PTR [rsi], 0x42c80000
mov DWORD PTR [rdx], 0x42c7cccd
mov DWORD PTR [rcx], 0x461c4000
ret
main:
xor eax, eax
ret
等等,发生了什么事?好吧,优化器发现 main
除了 return 0(隐式地;甚至没有在您的代码中表示)之外, 不会 做 任何事情,因此它转换了整个函数只是一条清除 EAX
寄存器然后 returns.
的指令
不过,从这里我们可以看出函数的结果是 returned in EAX
。这在 Unix 系统上常见的 System V AMD64 调用约定中是正确的,在 Windows 上使用的 64 位调用约定中也是如此,甚至在您将要使用的所有 32 位 x86 调用约定中也是如此在野外寻找。 (32 位结果在 EAX
中被 return 编辑;64 位结果在 EDX:EAX
中被 return 编辑,其中高位在 EDX
中低位在 EAX
.)
我们还可以通过查看 calc
函数的反汇编来判断它是如何接收参数的。第一个整数参数在 RDI
中传递,第二个在 RSI
中传递,第三个在 RDX
中传递,第四个在 RCX
中传递。根据 System V AMD64 调用约定,如果有第五个参数,它将在 R8
中传递,第六个参数将在 R9
.
中传递
换句话说,寄存器中最多传递前六个整数参数。之后,任何额外的整数参数都会在堆栈上传递。
浮点参数在XMM寄存器中传递(XMM0
到XMM7
),方便SSE指令的使用。同样,任何额外的浮点参数都在堆栈上传递。
你试图在评论中区分"integer parameters"和"memory parameters",但没有后者。当您传递指针(或 C++ 中的引用,编译器根据指针实现)时,您实际上传递的是 地址 。由于地址只是整数,它们就像任何其他整数值一样在寄存器中传递。
如果在栈上传递参数,都是8字节(64位)大小,一个接一个。第一个与堆栈指针的偏移量为 8,RBP
。第二个偏移量为 16,等等。当您查看问题中的代码时,似乎有点混乱,因为偏移量以 hexadecimal 表示,其中 10h
相当于十进制的 16,而 18h
相当于十进制的 24。 (为什么第一个参数从偏移量8开始?因为第一个位置RBP+0
被return指针占用了。)
这基本上涵盖了调用约定的基础知识。但坦率地说,分析反汇编 不是 学习调用约定的好方法。还有很多你不一定会看到的细节,你也不会得到全局视图。你真的需要 read the fine manual. If you hate manuals, there are more concise (and more simplified) summaries available various places online, e.g., Wikipedia.
我从汇编开始,为了测试,我编写了一个简单的 C 程序,对其进行了编译和反汇编,以查看参数是如何传递的。这是 C 代码:
#include <stdio.h>
#include <stdlib.h>
void calc (float*a,float*b,float*c,float*d) {
a[0]=1000;
b[0]=100.0;
c[0]=99.9;
d[0]=10000;
}
int main() {
float a[100];
float b[100];
float c[100];
float d[100];
calc(a,b,c,d);
}
这是它的反汇编:
default rel
global calc: function
global main: function
SECTION .text align=1 execute ; section number 1, code
calc: ; Function begin
push rbp ; 0000 _ 55
mov rbp, rsp ; 0001 _ 48: 89. E5
mov qword [rbp-8H], rdi ; 0004 _ 48: 89. 7D, F8
mov qword [rbp-10H], rsi ; 0008 _ 48: 89. 75, F0
mov qword [rbp-18H], rdx ; 000C _ 48: 89. 55, E8
mov qword [rbp-20H], rcx ; 0010 _ 48: 89. 4D, E0
; 0054 _ 90
pop rbp ; 0055 _ 5D
ret ; 0056 _ C3
; calc End of function
main: ; Function begin
push rbp ; 0057 _ 55
mov rbp, rsp ; 0058 _ 48: 89. E5
sub rsp, 1600 ; 005B _ 48: 81. EC, 00000640
lea rcx, [rbp-640H] ; 0062 _ 48: 8D. 8D, FFFFF9C0
lea rdx, [rbp-4B0H] ; 0069 _ 48: 8D. 95, FFFFFB50
lea rsi, [rbp-320H] ; 0070 _ 48: 8D. B5, FFFFFCE0
lea rax, [rbp-190H] ; 0077 _ 48: 8D. 85, FFFFFE70
mov rdi, rax ; 007E _ 48: 89. C7
call calc ; 0081 _ E8, 00000000(rel)
mov eax, 0 ; 0086 _ B8, 00000000
leave ; 008B _ C9
ret ; 008C _ C3
; main End of function
我不明白为什么堆栈上的参数大小不同。第一个在 [ebp-8H]
中,这是可以理解的,因为它是一个 64 位地址,但下一个只多了两个字节,在 [ebp-10H]
而不是 [ebp-16H]
中。
为什么会这样,而且最重要的是,当我编写一个采用这些确切参数的汇编程序时,我应该使用 ebp
?
这个好像说了很多,估计你还没听过,所以还是要重复一遍:分析未优化代码的反汇编,很大程度上是浪费时间。禁用优化后,编译器会专注于两件事:
- 尽可能快地生成代码,以便您获得尽可能快的编译,并且
- 使您调试代码变得容易(例如,确保您可以在每个高级语言语句上设置断点,并且不对指令重新排序以允许您单步执行代码)。
未优化的代码混乱、丑陋且令人困惑。它包含大量冗余指令,看起来不像人类会编写的代码,并且与实际应用程序中的代码不匹配(在启用优化的情况下编译)。
当你想分析汇编代码时,打开优化器。
当我们这样做时,我们看到您的代码编译为:
calc(float*, float*, float*, float*):
mov DWORD PTR [rdi], 0x447a0000
mov DWORD PTR [rsi], 0x42c80000
mov DWORD PTR [rdx], 0x42c7cccd
mov DWORD PTR [rcx], 0x461c4000
ret
main:
xor eax, eax
ret
等等,发生了什么事?好吧,优化器发现 main
除了 return 0(隐式地;甚至没有在您的代码中表示)之外, 不会 做 任何事情,因此它转换了整个函数只是一条清除 EAX
寄存器然后 returns.
不过,从这里我们可以看出函数的结果是 returned in EAX
。这在 Unix 系统上常见的 System V AMD64 调用约定中是正确的,在 Windows 上使用的 64 位调用约定中也是如此,甚至在您将要使用的所有 32 位 x86 调用约定中也是如此在野外寻找。 (32 位结果在 EAX
中被 return 编辑;64 位结果在 EDX:EAX
中被 return 编辑,其中高位在 EDX
中低位在 EAX
.)
我们还可以通过查看 calc
函数的反汇编来判断它是如何接收参数的。第一个整数参数在 RDI
中传递,第二个在 RSI
中传递,第三个在 RDX
中传递,第四个在 RCX
中传递。根据 System V AMD64 调用约定,如果有第五个参数,它将在 R8
中传递,第六个参数将在 R9
.
换句话说,寄存器中最多传递前六个整数参数。之后,任何额外的整数参数都会在堆栈上传递。
浮点参数在XMM寄存器中传递(XMM0
到XMM7
),方便SSE指令的使用。同样,任何额外的浮点参数都在堆栈上传递。
你试图在评论中区分"integer parameters"和"memory parameters",但没有后者。当您传递指针(或 C++ 中的引用,编译器根据指针实现)时,您实际上传递的是 地址 。由于地址只是整数,它们就像任何其他整数值一样在寄存器中传递。
如果在栈上传递参数,都是8字节(64位)大小,一个接一个。第一个与堆栈指针的偏移量为 8,RBP
。第二个偏移量为 16,等等。当您查看问题中的代码时,似乎有点混乱,因为偏移量以 hexadecimal 表示,其中 10h
相当于十进制的 16,而 18h
相当于十进制的 24。 (为什么第一个参数从偏移量8开始?因为第一个位置RBP+0
被return指针占用了。)
这基本上涵盖了调用约定的基础知识。但坦率地说,分析反汇编 不是 学习调用约定的好方法。还有很多你不一定会看到的细节,你也不会得到全局视图。你真的需要 read the fine manual. If you hate manuals, there are more concise (and more simplified) summaries available various places online, e.g., Wikipedia.