c++ 函数调用的参数推送顺序不反映堆栈中参数的地址

Parameter push order of a c++ function call does not reflect the parameter's address in stack

本文关键字:参数 堆栈 地址 顺序 函数调用 c++      更新时间:2023-10-16

我做了一个实验:

class A {
public:
  A() {}
  A(const A& a) {
    printf("A - %pn", this);
  }
};
class B {
public:
  B() {}
  B(const B& b) {
    printf("B - %pn", this);
  }
};
void func(A a, B b) {}
int main() {
  A a;
  B b;
  func(a, b);
  return 0;
}

输出为:

B - 0x7fff636e2c48
A - 0x7fff636e2c50

既然参数是从右向左推的,为什么B的地址比A的要低呢?困惑。(堆栈从较高的地址开始(。

简而言之:func的参数从右到左正确地"推送"在堆栈上; 您打印的不是这些参数的堆栈地址,而是这些参数的值,这些参数的值恰好也是堆栈上的一些地址。

稍微详细一点...

首先,您在x64机器上。你应该忘记_cdecl_stdcall等调用约定。只有一个调用约定(简化版(:前四个函数参数(在函数调用中从左到右列出(将在寄存器上传递,其余的 - 在堆栈上传递。现在,也就是说,调用方需要在自己的堆栈上分配足够的"家庭空间",因此被调用方可以"溢出"寄存器上传递的参数。因此,原则上,如果被调用方决定通过"家庭空间"使用堆栈,则前四个参数也可以在堆栈上找到。

其次,如果寄存器上传递的参数被被调用方"溢出",它们仍然被"推送"在堆栈上从右到左

:被调用方将使用从右到左的顺序将寄存器"溢出"到"主空间"中,这意味着第一个函数参数最终在"主空间"区域中的堆栈上的地址将低于第二个参数。因此,在这方面,参数总是从右到左"推送"在堆栈上。

第三,你的输出与参数传递给func的方式无关,而与创建临时变量的位置有关。这是发生的事情:main保留了足够的堆栈空间来创建临时ab; b首先在main堆栈上的较低地址创建,这很好,因为函数调用func中调用函数(在我们的例子中是复制构造函数A(const A&)B(const B&)(的顺序是明确未定义的;临时ab的地址存储在寄存器上(并且已经保留了足够的"家庭空间"func"溢出"(; func被称为; func可以将寄存器"溢出"到"家庭空间"中;如果它"溢出"它们,那么b的地址将被"溢出"到堆栈地址高于堆栈地址的堆栈地址中,a的地址将被"溢出"到堆栈地址 - 这是传递参数的从右到左的顺序。

这是一些代码和相应的汇编程序。请注意,我使用了修改后的代码(更多参数和两个函数(来显示如何传递参数。int参数funcI函数是为了说明要点,而不会乱七八糟地调用复制构造函数。具有两个参数的函数版本本质上是已发布funcC的"切片"版本 - 它被"切片"为处理RCXRDX寄存器(到最后一个寄存器(的代码。另外,请注意,RSP包含堆栈指针和内部funcI并且funcC小于RSP内部main 8(这解释了检索funcI时的偏移量funcC堆栈上推送的最后两个参数(:

class A
{
 public:
  int mData;
  A()
  { // mov     qword ptr [rsp+8],rcx    ; "spilling" rcx
  } // mov     rax,qword ptr [rsp+8]    ; RAX (return value) = pointer to the object;
  A( const A& )
  {// mov     qword ptr [rsp+10h],rdx   ;"spilling" RDX and RCX
   // mov     qword ptr [rsp+8],rcx
  }// mov     rax,qword ptr [rsp+8] ; notice, RAX has value of RCX
};
class B
{
 public:
  int mData;
  B()
  { // mov     qword ptr [rsp+8],rcx    ; "spilling" rcx
  } // mov     rax,qword ptr [rsp+8]    ; RAX (return value) = pointer to the object
  B( const B& )
  {// mov     qword ptr [rsp+10h],rdx   ;"spilling" RDX and RCX
   // mov     qword ptr [rsp+8],rcx
  }// mov     rax,qword ptr [rsp+8] ; notice, RAX has value of RCX
};
void __cdecl funcI( int a, int b, int c, int d, int e, int g )
{ // mov     dword ptr [rsp+20h],r9d    ; "spilling" registers, notice right-to-left order
 // mov     dword ptr [rsp+18h],r8d
 // mov     dword ptr [rsp+10h],edx
 // mov     dword ptr [rsp+8],ecx
 a = 1; // mov dword ptr [rsp+8],1  ; accessing first 4 "spilled" function parameters
 b = 2; // mov dword ptr [rsp+10h],2    ; notice, first function parameter has lower stack address than second parameter etc
 c = 3; // mov dword ptr [rsp+18h],3    ; so parameters pushed right-to-left by callee's "spilling"
 d = 4; // mov dword ptr [rsp+20h],4    ;
 e = 5; // mov dword ptr [rsp+28h],5    ; here, accessing 2 parameters pushed on stack explicitly by caller
 g = 6; // mov dword ptr [rsp+30h],6    ; they were pushed right-to-left
       //  also notice the offset of 8 in RSP ("g" is accessed through [rsp+30h] while it was put into [rsp+28h] in main
} // ret

void __cdecl funcC( A a1, A a2, A a3, A a4, A a5, B b1 )
{// mov qword ptr [rsp+20h],r9  ; "spilling"
 // mov qword ptr [rsp+18h],r8
 // mov qword ptr [rsp+10h],rdx
 // mov qword ptr [rsp+8],rcx
 a1.mData = 1;
 // mov rax,qword ptr [rsp+8]   ; same right-to-left order: a1 itself has lower stack address 'rsp+8' than a2 'rsp+18h'
 // mov dword ptr [rax],1   ; HOWEVER, stack address value 'rsp+88h' stored in a1 is HIGHER than stack address value 'rsp+78h' stored in a2!!!!
 a2.mData = 2;
 // mov dword ptr [rax],2
 // mov rax,qword ptr [rsp+18h]
 a3.mData = 3;
 // mov rax,qword ptr [rsp+18h]
 // mov dword ptr [rax],3
 a4.mData = 4;
 // mov rax,qword ptr [rsp+20h]
 // mov dword ptr [rax],4
 a5.mData = 5;
 // mov rax,qword ptr [rsp+28h]
 // mov dword ptr [rax],5
 b1.mData = 6;
 // mov rax,qword ptr [rsp+30h]
 // mov dword ptr [rax],6
} // ret
int main( void )
{
 // sub     rsp,0C8h            ; reserving stack for `main`
 A a;
 // lea     rcx,[rsp+30h]   ; putting into RCX stack address of local 'a'
 // call    A::A()
 B b;
 // lea     rcx,[rsp+34h]   ; putting into RCX stack address of local 'b'
 // call    B::B()
 funcI( 1, 2, 3, 4, 5, 6 );
 // mov     dword ptr [rsp+28h],6   ; passing parameters to `funcI`
 // mov     dword ptr [rsp+20h],5   ; last 2 on stack, first 4 on registers
 // mov     r9d,4           ; notice the right-to-left order:
 // mov     r8d,3           ; "6" is put on stack at higher address than "5" etc.
 // mov     edx,2                   ; notice also that "6" is put into [rsp+28h]: during call to `funcI` RSP will be less by 8
 // mov     ecx,1                   ; and inside `funcI` parameter will be accessed accordingly through [rsp+30h]
 // call    funcI
 funcC( a, a, a, a, a, b );
 // lea rax,[rsp+38h]           ; some preparations: putting stack addresses of temporaries into stack variables
 // mov qword ptr [rsp+40h],rax     ; there are few indirections in debug mode, we can ignore them, noticing the addresses
 // lea rax,[rsp+48h]
 // mov qword ptr [rsp+50h],rax
 // lea rax,[rsp+58h]
 // mov qword ptr [rsp+60h],rax
 // lea rax,[rsp+68h]
 // mov qword ptr [rsp+70h],rax
 // lea rax,[rsp+78h]
 // mov qword ptr [rsp+80h],rax
 // lea rax,[rsp+88h]
 // mov qword ptr [rsp+90h],rax
 // lea rdx,[rsp+34h]           ; putting into RDX stack address of local 'b'
 // mov rcx,qword ptr [rsp+40h]     ; putting into RCX stack address of temporary - this is right-most temporary of type `B` in `funcC`:
 // call B::B(const B&)         ; [rsp+40h] = (rsp+38h) which is stack address of temporary `b`
 // mov qword ptr [rsp+98h],rax     ; putting on stack value of RAX: [rsp+98h] now contains (rsp+38h)

 // lea rdx,[rsp+30h]
 // mov rcx,qword ptr [rsp+50h]
 // call A::A(const B&)
 // mov qword ptr [rsp+0A0h],rax
 // lea rdx,[rsp+30h]
 // mov rcx,qword ptr [rsp+60h]
 // call A::A(const A&)
 // mov qword ptr [rsp+0A8h],rax
 // lea rdx,[rsp+30h]
 // mov rcx,qword ptr [rsp+70h]
 // call A::A(const A&)
 // mov qword ptr [rsp+0B0h],rax
 // lea rdx,[rsp+30h]
 // mov rcx,qword ptr [rsp+80h]
 // call A::A(const A&)
 // mov qword ptr [rsp+0B8h],rax
 // lea rdx,[rsp+30h]
 // mov rcx,qword ptr [rsp+90h]
 // call A::A(const A&)         ; notice, RAX is not copied onto stack, it's preserved (see below)
 // mov rcx,qword ptr [rsp+98h]     ; passing parameters (ignoring indirections): putting stack address of right-most temporary `b` onto stack
 // mov qword ptr [rsp+28h],rcx     ; notice stack address 'rsp+28h' where stack address of 'b' 'rsp+38h' is put
 // mov rcx,qword ptr [rsp+0A0h]    ; same for right-most temporary of type 'A' (indirection again)
 // mov qword ptr [rsp+20h],rcx     ; stack address 'rsp+20h' where stack address 'rsp+48h' of right-most `a` is put
 // mov rcx,qword ptr [rsp+0A8h]    ; NOW: this is RIGHT-TO-LEFT order - take a closer look:
 // mov r9,rcx              ; rsp+28h > rsp+20h but value [rsp+28h] = rsp+38h  < rsp+48h = [rsp+20h]
 // mov rcx,qword ptr [rsp+0B0h]    ; parameters are pushed right-to-left, parameters' values are not ordered this way (and, really, can be anything)
 // mov r8,rcx              ; other 4 parameters are put on registers
 // mov rcx,qword ptr [rsp+0B8h]
 // mov rdx,rcx
 // mov rcx,rax             ; notice, preserved RAX is put into RCX  - it has stack address of first temporary of type 'A'
 // call funcC
 return ( 0 ); // xor     eax,eax      ; return value is 0
} 
// add     rsp,0C8h       ; restoring stack
// ret

因为你把这些对象推到堆栈上,堆栈通常会向下增长。因此,这不是有保证的行为,并且特定于平台。不过,这与如何将参数传递给函数无关。