[英]How does assembly do parameter passing: by value, reference, pointer for different types/arrays?
为了看看这个,我写了这个简单的代码,我刚刚创建了不同类型的变量,并通过值,引用和指针将它们传递给函数:
int i = 1;
char c = 'a';
int* p = &i;
float f = 1.1;
TestClass tc; // has 2 private data members: int i = 1 and int j = 2
函数体留空,因为我只是看看如何传入参数。
passByValue(i, c, p, f, tc);
passByReference(i, c, p, f, tc);
passByPointer(&i, &c, &p, &f, &tc);
想知道数组的不同之处以及如何访问参数。
int numbers[] = {1, 2, 3};
passArray(numbers);
部件:
passByValue(i, c, p, f, tc)
mov EAX, DWORD PTR [EBP - 16]
mov DL, BYTE PTR [EBP - 17]
mov ECX, DWORD PTR [EBP - 24]
movss XMM0, DWORD PTR [EBP - 28]
mov ESI, DWORD PTR [EBP - 40]
mov DWORD PTR [EBP - 48], ESI
mov ESI, DWORD PTR [EBP - 36]
mov DWORD PTR [EBP - 44], ESI
lea ESI, DWORD PTR [EBP - 48]
mov DWORD PTR [ESP], EAX
movsx EAX, DL
mov DWORD PTR [ESP + 4], EAX
mov DWORD PTR [ESP + 8], ECX
movss DWORD PTR [ESP + 12], XMM0
mov EAX, DWORD PTR [ESI]
mov DWORD PTR [ESP + 16], EAX
mov EAX, DWORD PTR [ESI + 4]
mov DWORD PTR [ESP + 20], EAX
call _Z11passByValueicPif9TestClass
passByReference(i, c, p, f, tc)
lea EAX, DWORD PTR [EBP - 16]
lea ECX, DWORD PTR [EBP - 17]
lea ESI, DWORD PTR [EBP - 24]
lea EDI, DWORD PTR [EBP - 28]
lea EBX, DWORD PTR [EBP - 40]
mov DWORD PTR [ESP], EAX
mov DWORD PTR [ESP + 4], ECX
mov DWORD PTR [ESP + 8], ESI
mov DWORD PTR [ESP + 12], EDI
mov DWORD PTR [ESP + 16], EBX
call _Z15passByReferenceRiRcRPiRfR9TestClass
passByPointer(&i, &c, &p, &f, &tc)
lea EAX, DWORD PTR [EBP - 16]
lea ECX, DWORD PTR [EBP - 17]
lea ESI, DWORD PTR [EBP - 24]
lea EDI, DWORD PTR [EBP - 28]
lea EBX, DWORD PTR [EBP - 40]
mov DWORD PTR [ESP], EAX
mov DWORD PTR [ESP + 4], ECX
mov DWORD PTR [ESP + 8], ESI
mov DWORD PTR [ESP + 12], EDI
mov DWORD PTR [ESP + 16], EBX
call _Z13passByPointerPiPcPS_PfP9TestClass
passArray(numbers)
mov EAX, .L_ZZ4mainE7numbers
mov DWORD PTR [EBP - 60], EAX
mov EAX, .L_ZZ4mainE7numbers+4
mov DWORD PTR [EBP - 56], EAX
mov EAX, .L_ZZ4mainE7numbers+8
mov DWORD PTR [EBP - 52], EAX
lea EAX, DWORD PTR [EBP - 60]
mov DWORD PTR [ESP], EAX
call _Z9passArrayPi
// parameter access
push EAX
mov EAX, DWORD PTR [ESP + 8]
mov DWORD PTR [ESP], EAX
pop EAX
我假设我正在查看与参数传递相关的正确程序集,因为每个结尾都有调用!
但由于我对装配的知识非常有限,我不知道这里发生了什么。 我学习了ccall约定,所以我假设正在进行的事情与保留调用者保存的寄存器然后将参数推送到堆栈有关。 因此,我希望看到东西被加载到寄存器中并“推”到各处,但不知道mov
和lea
是怎么回事。 另外,我不知道DWORD PTR
是什么。
我只学习了寄存器: eax, ebx, ecx, edx, esi, edi, esp
和ebp
,所以看到像XMM0
或DL
这样的东西也让我感到困惑。 我想通过引用/指针传递lea
是有意义的,因为它们使用内存地址,但我实际上无法分辨出发生了什么。 当涉及到传递值时,似乎有很多指令,所以这可能与将值复制到寄存器有关。 不知道何时将数组作为参数传递和访问。
如果有人能够向我解释每个装配块的一般概念,我将非常感激。
使用CPU寄存器传递参数比使用内存更快,即堆栈。 但是,CPU中的寄存器数量有限(特别是在x86兼容的CPU中),因此当函数有许多参数时,则使用堆栈而不是CPU寄存器。 在您的情况下,有5个函数参数,因此编译器使用堆栈作为参数而不是寄存器。
原则上,编译器可以使用push
指令在实际call
函数之前将参数推送到堆栈,但是许多编译器(包括gnu c ++)使用mov
将参数推送到堆栈。 这种方式很方便,因为它不会在调用函数的代码部分中更改ESP寄存器(堆栈顶部)。
在passByValue(i, c, p, f, tc)
,参数的值被放置在堆栈上。 您可以看到许多mov
指令从存储器位置到寄存器,从寄存器到堆栈的适当位置。 原因是x86程序集禁止直接从一个内存位置移动到另一个内存位置(例外情况是将值从一个数组(或字符串)移动到另一个数组的movs
)。
在passByReference(i, c, p, f, tc)
您可以看到许多5个lea指令,它们将参数的地址复制到CPU寄存器,并将这些寄存器值移入堆栈。
passByPointer(&i, &c, &p, &f, &tc)
情况类似于passByValue(i, c, p, f, tc)
。 在内部,在程序集级别,按引用传递使用指针,而在较高的C ++级别,程序员不需要明确地使用引用上的&
和*
运算符。
在将参数移动到堆栈之后,发出call
,在将程序执行转移到子例程之前,将call
指令指针EIP
。 在call
指令之后,所有参数moves
到堆栈都会考虑堆栈中即将到来的EIP
。
在上面的例子中有太多的东西来剖析所有这些。 相反,我只是通过passByValue
因为这似乎是最有趣的。 之后,你应该能够弄清楚其余部分。
首先要研究反汇编时要记住的一些要点,这样你就不会完全迷失在代码的海洋中:
mov [ebp - 44], [ebp - 36]
不是法律指令。 首先需要一个中间寄存器来存储数据,然后将其复制到存储器目的地。 []
与mov
一起用于从计算的存储器地址访问数据。 这类似于在C / C ++中解析指针。 lea x, [y]
通常意味着 y的计算地址并保存到x中 。 这类似于在C / C ++中获取变量的地址。 考虑到上面这一点,对passByValue
函数的调用重新排列了一些,使其更容易理解:
.define arg1 esp
.define arg2 esp + 4
.define arg3 esp + 8
.define arg4 esp + 12
.define arg5.1 esp + 16
.define arg5.2 esp + 20
; copy first parameter
mov EAX, [EBP - 16]
mov [arg1], EAX
; copy second parameter
mov DL, [EBP - 17]
movsx EAX, DL
mov [arg2], EAX
; copy third
mov ECX, [EBP - 24]
mov [arg3], ECX
; copy fourth
movss XMM0, DWORD PTR [EBP - 28]
movss DWORD PTR [arg4], XMM0
; intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI
;copy fifth
lea ESI, [EBP - 48]
mov EAX, [ESI]
mov [arg5.1], EAX
mov EAX, [ESI + 4]
mov [arg5.2], EAX
call passByValue(int, char, int*, float, TestClass)
上面的代码是无法解释的,并且指令混合,以清楚地说明实际发生了什么,但有些仍然需要解释。 首先,char被signed
,它的大小是一个字节。 这里的说明:
; copy second parameter
mov DL, [EBP - 17]
movsx EAX, DL
mov [arg2], EAX
从[ebp - 17]
(堆栈中的某处)读取一个字节并将其存储到edx
的低位字节。 然后使用符号扩展移动eax
字节复制到eax
。 eax
的完整32位值最终被复制到passByValue
可以访问的堆栈中。 如果需要更多细节, 请参阅寄存器布局 。
第四个论点:
movss XMM0, DWORD PTR [EBP - 28]
movss DWORD PTR [arg4], XMM0
使用SSE movss
指令将堆栈中的浮点值复制到xmm0
寄存器中。 简而言之,SSE指令允许您同时对多个数据执行相同的操作,但此处编译器将其用作复制堆栈上浮点值的中间存储。
最后一个论点:
; copy intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI
对应于TestClass
。 显然这个类的大小是8字节,位于从[ebp - 40]
到[ebp - 33]
的堆栈中。 这里的类一次被复制4个字节,因为该对象不能适合单个寄存器。
这是call passByValue
之前堆栈大致的样子:
lower addr esp => int:arg1 <--.
esp + 4 char:arg2 |
esp + 8 int*:arg3 | copies passed
esp + 12 float:arg4 | to 'passByValue'
esp + 16 TestClass:arg5.1 |
esp + 20 TestClass:arg5.2 <--.
...
...
ebp - 48 TestClass:arg5.1 <-- intermediate copy of
ebp - 44 TestClass:arg5.2 <-- TestClass?
ebp - 40 original TestClass:arg5.1
ebp - 36 original TestClass:arg5.2
...
ebp - 28 original arg4 <--.
ebp - 24 original arg3 | original (local?) variables
ebp - 20 original arg2 | from calling function
ebp - 16 original arg1 <--.
...
higher addr ebp prev frame
您正在寻找的是ABI呼叫约定 。 不同的平台有不同的约定。 例如x86-64上的Windows与x86-64上的Unix / Linux有不同的约定。
http://www.agner.org/optimize/有一个调用约定文档,详细介绍了x86 / amd64的各种文档。
您可以在ASM中编写任何您想要的代码,但如果您想调用其他函数并由它们调用,则根据ABI传递参数/返回值。
制作一个不使用标准ABI的内部使用辅助函数可能很有用,而是使用调用函数分配它们的寄存器中的值。这是特别的。 可能如果你用ASM以外的其他东西编写主程序,只有一小部分在ASM中。 然后,asm部分只需要关心是否可以移植到具有不同ABI的系统,以便从主程序调用,而不是为了它自己的内部。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.