C++ 汇编如何进行参数传递:按值、引用、不同类型/数组的指针?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/19858980/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-27 23:12:44  来源:igfitidea点击:

How does assembly do parameter passing: by value, reference, pointer for different types/arrays?

c++carraysassemblyx86

提问by atkayla

In attempt to look at this, I wrote this simple code where I just created variables of different types and passed them into a function by value, by reference, and by pointer:

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

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

the function bodies were left blank because i am just looking at how parameters are passed in.

函数体留空是因为我只是在看参数是如何传入的。

passByValue(i, c, p, f, tc); 
passByReference(i, c, p, f, tc); 
passByPointer(&i, &c, &p, &f, &tc);

wanted to see how this is different for an array and also how the parameters are then accessed.

想看看这与数组有何不同,以及如何访问参数。

int numbers[] = {1, 2, 3};
passArray(numbers); 

assembly:

集会:

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

I'm assuming I'm looking at the right assembly pertaining to the parameter passing because there are calls at the end of each!

我假设我正在查看与参数传递有关的正确程序集,因为每个程序集的末尾都有调用!

But due to my very limited knowledge of assembly, I can't tell what's going on here. I learned about ccall convention, so I'm assuming something is going on that has to do with preserving the caller-saved registers and then pushing the parameters onto the stack. Because of this, I'm expecting to see things loaded into registers and "push" everywhere, but have no idea what's going on with the movs and leas. Also, I don't know what DWORD PTRis.

但是由于我对装配的了解非常有限,我无法判断这里发生了什么。我了解了 ccall 约定,所以我假设正在发生的事情与保留调用者保存的寄存器然后将参数推送到堆栈上有关。正因为如此,我期待看到东西加载到寄存器中并在任何地方“推送”,但不知道movs 和leas 发生了什么。另外,我不知道是什么DWORD PTR

I've only learned about registers: eax, ebx, ecx, edx, esi, edi, espand ebp, so seeing something like XMM0or DLjust confuses me as well. I guess it makes sense to see leawhen it comes to passing by reference/pointer because they use memory addresses, but I can't actually tell what is going on. When it comes to passing by value, it seems like there are many instructions, so this could have to do with copying the value into registers. No idea when it comes to how arrays are passed and accessed as parameters.

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

If someone could explain the general idea of what's going on with each block of assembly to me, I would highly appreciate it.

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

回答by Igor Popov

Using CPU registers for passing arguments is faster than using memory, i.e. stack. However there is limited number of registers in CPU (especially in x86-compatible CPUs) so when a function has many parameters then stack is used instead of CPU registers. In your case there are 5 function arguments so the compiler uses stack for the arguments instead of registers.

使用 CPU 寄存器传递参数比使用内存(即堆栈)更快。然而,CPU 中的寄存器数量有限(尤其是在 x86 兼容的 CPU 中),因此当一个函数有很多参数时,将使用堆栈而不是 CPU 寄存器。在您的情况下,有 5 个函数参数,因此编译器使用堆栈作为参数而不是寄存器。

In principle compilers can use pushinstructions to push arguments to stack before actual callto function, but many compilers (incl. gnu c++) use movto push arguments to stack. This way is convenient as it does not change ESP register (top of the stack) in the part of code which calls the function.

原则上,编译器可以push在实际call运行之前使用指令将参数推入堆栈,但许多编译器(包括 gnu c++)使用mov将参数推入堆栈。这种方式很方便,因为它不会在调用函数的代码部分更改 ESP 寄存器(堆栈顶部)。

In case of passByValue(i, c, p, f, tc)values of arguments are placed on the stack. You can see many movinstruction from a memory location to a register and from the register to an appropriate location of the stack. The reason for this is that x86 assembly forbids direct moving from one memory location to another (exception is movswhich moves values from one array (or string as you wish) to another).

如果passByValue(i, c, p, f, tc)参数值放置在堆栈中。您可以看到mov从内存位置到寄存器以及从寄存器到堆栈的适当位置的许多指令。这样做的原因是 x86 程序集禁止直接从一个内存位置移动到另一个内存位置(例外是movs将值从一个数组(或您希望的字符串)移动到另一个)。

In case of passByReference(i, c, p, f, tc)you can see many 5 lea instructions which copy addressesof arguments to CPU registers, and these values of the registers are moved into stack.

在这种情况下,passByReference(i, c, p, f, tc)您可以看到许多 5 条 lea 指令将参数的地址复制到 CPU 寄存器,并将这些寄存器的值移入堆栈。

The case of passByPointer(&i, &c, &p, &f, &tc)is similar to passByValue(i, c, p, f, tc). Internally, on the assembly level, pass by reference uses pointers, while on the higher, C++, level a programmer does not need to use explicitely the &and *operators on references.

的情况passByPointer(&i, &c, &p, &f, &tc)类似于passByValue(i, c, p, f, tc)。在内部,在程序集级别,按引用传递使用指针,而在更高的 C++ 级别,程序员不需要显式地在引用上使用&*运算符。

After the parameters are moved to the stack callis issued, which pushes instruction pointer EIPto stack before transferring the program execution to the subroutine. All movesof the parameters to the stack account for the coming EIPon stack after the callinstruction.

参数移入堆栈后call发出,EIP在将程序执行转移到子程序之前将指令指针压入堆栈。moves堆栈中的所有参数都占指令EIP后进入堆栈的原因call

回答by greatwolf

There's too much in your example above to dissect all of them. Instead I'll just go over passByValuesince that seems to be the most interesting. Afterwards, you should be able to figure out the rest.

上面的例子太多了,无法剖析所有这些。相反,我会过去,passByValue因为这似乎是最有趣的。之后,你应该能够弄清楚其余的。

First some important points to keep in mind while studying the disassembly so you don't get completely lost in the sea of code:

首先,在学习反汇编时要记住一些要点,以免您完全迷失在代码的海洋中:

  • There are no instructions to directly copy data from one mem location to another mem location. eg. mov [ebp - 44], [ebp - 36]is not a legalinstruction. An intermediate register is needed to store the data first and then subsequently copied into the memory destination.
  • Bracket operator []in conjunction with a movmeans to access data from a computed memory address. This is analogous to derefing a pointer in C/C++.
  • When you see lea x, [y]that usually meanscompute address of yand save into x. This is analogous to taking the address of a variable in C/C++.
  • Data and objects that needs to be copied but are too big to fit into a register are copied onto the stack in a piece-meal fashion. IOW, it'll copy a native machine word at a time until all the bytes representing the object/data is copied. Usually that means either 4 or 8 bytes on modern processors.
  • The compiler will typically interleaveinstructions together to keep the processor pipeline busy and to minimize stalls. Good for code efficiency but bad if you're trying to understand the disassembly.
  • 没有直接将数据从一个内存位置复制到另一个内存位置的说明。例如。mov [ebp - 44], [ebp - 36]不是合法的指令。首先需要一个中间寄存器来存储数据,然后再将其复制到内存目标中。
  • 括号运算符[]mov从计算出的内存地址访问数据的方法结合使用。这类似于在 C/C++ 中取消引用指针。
  • 当您看到lea x, [y]通常意味着计算y 的地址并保存到x 中。这类似于在 C/C++ 中获取变量的地址。
  • 需要复制但太大而无法放入寄存器的数据和对象以零碎的方式复制到堆栈中。IOW,它会一次复制一个本地机器字,直到所有代表对象/数据的字节都被复制。通常这意味着现代处理器上的 4 或 8 个字节。
  • 编译器通常会将指令交错在一起,以保持处理器流水线忙碌并最大限度地减少停顿。对代码效率有好处,但如果您试图理解反汇编则不好。

With the above in mind here's the call to passByValuefunction rearranged a bit to make it more understandable:

考虑到上述内容,这里对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)

The code above is unmangled and instruction mixing undone to make it clear what is actually happening but some still needs explaining. First, the char is signedand it is a single byte in size. The instructions here:

上面的代码没有被破坏,指令混合也被取消,以明确实际发生的事情,但有些仍然需要解释。首先,char 是signed单个字节的大小。这里的说明:

; copy second parameter
mov DL, [EBP - 17]
movsx   EAX, DL
mov [arg2], EAX

reads a byte from [ebp - 17](somewhere on stack) and stores it into the lower first byte of edx. That byte is then copied into eaxusing sign-extended move. The full 32-bit value in eaxis finally copied onto the stack that passByValuecan access. See register layoutif you need more detail.

[ebp - 17](堆栈上的某处)读取一个字节并将其存储到edx. 然后eax使用符号扩展移动将该字节复制到。最后将完整的 32 位值eax复制到passByValue可以访问的堆栈中。如果您需要更多详细信息,请参阅寄存器布局

The fourth argument:

第四个论点:

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

Uses the SSE movssinstruction to copy the floating point value from stack into a xmm0register. In brief, SSE instructions let you perform the same operation on multiple pieces of data simultaneously but here the compiler is using it as an intermediate storage for copying floating-point values on the stack.

使用 SSEmovss指令将浮点值从堆栈复制到xmm0寄存器中。简而言之,SSE 指令允许您同时对多条数据执行相同的操作,但在这里编译器将其用作中间存储,用于在堆栈上复制浮点值。

The last argument:

最后一个论点:

; copy intermediate copy of TestClass?
mov ESI, [EBP - 40]
mov [EBP - 48], ESI
mov ESI, [EBP - 36]
mov [EBP - 44], ESI

corresponds to the TestClass. Apparently this class is 8-bytes in size located on the stack from [ebp - 40]to [ebp - 33]. The class here is being copied 4-bytes at a time since the object cannot fit into a single register.

对应于TestClass. 显然,这个类的大小为 8 字节,位于从[ebp - 40]到的堆栈上[ebp - 33]。此处的类一次被复制 4 个字节,因为该对象无法放入单个寄存器中。

Here's what the stack approximately looks like prior to call passByValue:

这是堆栈之前的大致样子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

回答by Peter Cordes

What you're looking for are ABI calling conventions. Different platforms have different conventions. e.g. Windows on x86-64 has different conventions than Unix/Linux on x86-64.

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

http://www.agner.org/optimize/has a calling-conventions doc detailing the various ones for x86 / amd64.

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

You can write code in ASM that does whatever you want, but if you want to call other functions, and be called by them, then pass parameters / return values according to the ABI.

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

It could be useful to make an internal-use-only helper function that doesn't use the standard ABI, but instead uses values in the registers that the calling function allocates them in. This is esp. likely if you're writing the main program in something other than ASM, with just a small part in ASM. Then the asm part only needs to care about being portable to systems with different ABIs for being called from the main program, not for its own internals.

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