15

为了了解这一点,我编写了这个简单的代码,其中我只是创建了不同类型的变量,并通过值、引用和指针将它们传递给函数:

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 约定,所以我假设正在发生的事情与保留调用者保存的寄存器然后将参数推入堆栈有关。正因为如此,我希望看到东西被加载到寄存器中并到处“推送”,但不知道movs 和leas 发生了什么。另外,我也不知道是什么DWORD PTR

我只了解了寄存器:eax, ebx, ecx, edx, esi, edi, espebp,所以看到类似XMM0DL只是让我感到困惑的东西。lea我想看看何时通过引用/指针传递是有意义的,因为它们使用内存地址,但我实际上无法判断发生了什么。当谈到按值传递时,似乎有很多指令,所以这可能与将值复制到寄存器中有关。不知道何时将数组作为参数传递和访问。

如果有人可以向我解释每个组装块的大致情况,我将不胜感激。

4

3 回答 3

18

使用 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++ 级别,程序员不需要在引用上显式使用&and*运算符。

参数移动到堆栈后call发出,EIP在将程序执行转移到子程序之前将指令指针压入堆栈。堆栈的所有moves参数都说明EIP了指令之后的堆栈call

于 2013-11-08T12:56:04.510 回答
8

上面的示例中有太多内容无法剖析所有内容。相反,我会过去,passByValue因为这似乎是最有趣的。之后,您应该能够弄清楚其余部分。

首先,在学习反汇编时要记住一些重要的点,这样你就不会完全迷失在代码的海洋中:

  • 没有直接将数据从一个内存位置复制到另一个内存位置的说明。例如。mov [ebp - 44], [ebp - 36]不是法律指令。需要一个中间寄存器来首先存储数据,然后将其复制到内存目的地。
  • 括号运算符[]mov从计算的内存地址访问数据的方法结合使用。这类似于在 C/C++ 中取消引用指针。
  • 当您看到lea x, [y]通常意味着计算y的地址并保存到x中。这类似于在 C/C++ 中获取变量的地址。
  • 需要复制但太大而无法放入寄存器的数据和对象以零碎的方式复制到堆栈中。IOW,它将一次复制一个本机机器字,直到复制代表对象/数据的所有字节。通常这意味着现代处理器上的 4 或 8 个字节。
  • 编译器通常会将指令交错在一起,以保持处理器流水线繁忙并最大限度地减少停顿。有利于代码效率,但如果您试图理解反汇编,则不好。

考虑到上述内容,这里对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然后使用符号扩展移动将该字节复制到其中。最终将完整的 32 位值eax复制到passByValue可以访问的堆栈中。如果您需要更多详细信息,请参阅寄存器布局。

第四个论点:

movss   XMM0, DWORD PTR [EBP - 28]
movss   DWORD PTR [arg4], XMM0

使用 SSEmovss指令将浮点值从堆栈复制到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
于 2013-11-08T14:43:59.653 回答
4

您正在寻找的是ABI 调用约定。不同的平台有不同的约定。例如,x86-64 上的 Windows 与 x86-64 上的 Unix/Linux 有不同的约定。

http://www.agner.org/optimize/有一个调用约定文档,详细说明了 x86 / amd64 的各种内容。

您可以在 ASM 中编写任何您想做的代码,但如果您想调用其他函数并被它们调用,则根据 ABI 传递参数/返回值。

制作一个不使用标准 ABI 的仅供内部使用的辅助函数可能很有用,而是使用调用函数分配它们的寄存器中的值。这是 esp。如果你用 ASM 以外的东西编写主程序,而 ASM 中只有一小部分,则很可能。然后 asm 部分只需要关心是否可移植到具有不同 ABI 的系统,以便从主程序调用,而不是它自己的内部。

于 2015-07-02T18:40:05.210 回答