13

对于以下 C 代码:

struct _AStruct {
    int a;
    int b;
    float c;
    float d;
    int e;
};

typedef struct _AStruct AStruct;

AStruct test_callee5();
void test_caller5();

void test_caller5() {
    AStruct g = test_callee5();
    AStruct h = test_callee5();    
}

我得到以下 Win32 的反汇编:

_test_caller5:
  00000000: lea         eax,[esp-14h]
  00000004: sub         esp,14h
  00000007: push        eax
  00000008: call        _test_callee5
  0000000D: lea         ecx,[esp+4]
  00000011: push        ecx
  00000012: call        _test_callee5
  00000017: add         esp,1Ch
  0000001A: ret

对于 Linux32:

00000000 <test_caller5>:
   0:  push   %ebp
   1:  mov    %esp,%ebp
   3:  sub    $0x38,%esp
   6:  lea    0xffffffec(%ebp),%eax
   9:  mov    %eax,(%esp)
   c:  call   d <test_caller5+0xd>
  11:  sub    $0x4,%esp  ;;;;;;;;;; Note this extra sub ;;;;;;;;;;;;
  14:  lea    0xffffffd8(%ebp),%eax
  17:  mov    %eax,(%esp)
  1a:  call   1b <test_caller5+0x1b>
  1f:  sub    $0x4,%esp   ;;;;;;;;;; Note this extra sub ;;;;;;;;;;;;
  22:  leave
  23:  ret

我试图了解呼叫者在通话后的行为方式的差异。为什么 Linux32 中的调用者会做这些额外的 subs?

我假设两个目标都遵循 cdecl 调用约定。cdecl 不是为返回结构的函数定义调用约定吗?!

编辑:

我添加了被调用者的实现。果然,你可以看到 Linux32 被调用者弹出它的参数,而 Win32 被调用者没有:

AStruct test_callee5()
{
    AStruct S={0};
    return S;
}

Win32反汇编:

test_callee5:
  00000000: mov         eax,dword ptr [esp+4]
  00000004: xor         ecx,ecx
  00000006: mov         dword ptr [eax],0
  0000000C: mov         dword ptr [eax+4],ecx
  0000000F: mov         dword ptr [eax+8],ecx
  00000012: mov         dword ptr [eax+0Ch],ecx
  00000015: mov         dword ptr [eax+10h],ecx
  00000018: ret

Linux32反汇编:

00000000 <test_callee5>:
   0:   push   %ebp
   1:   mov    %esp,%ebp
   3:   sub    $0x20,%esp
   6:   mov    0x8(%ebp),%edx
   9:   movl   $0x0,0xffffffec(%ebp)
  10:   movl   $0x0,0xfffffff0(%ebp)
  17:   movl   $0x0,0xfffffff4(%ebp)
  1e:   movl   $0x0,0xfffffff8(%ebp)
  25:   movl   $0x0,0xfffffffc(%ebp)
  2c:   mov    0xffffffec(%ebp),%eax
  2f:   mov    %eax,(%edx)
  31:   mov    0xfffffff0(%ebp),%eax
  34:   mov    %eax,0x4(%edx)
  37:   mov    0xfffffff4(%ebp),%eax
  3a:   mov    %eax,0x8(%edx)
  3d:   mov    0xfffffff8(%ebp),%eax
  40:   mov    %eax,0xc(%edx)
  43:   mov    0xfffffffc(%ebp),%eax
  46:   mov    %eax,0x10(%edx)
  49:   mov    %edx,%eax
  4b:   leave
  4c:   ret    $0x4  ;;;;;;;;;;;;;; Note this ;;;;;;;;;;;;;;
4

2 回答 2

9

为什么 Linux32 中的调用者会做这些额外的 subs?

原因是使用了由编译器注入的隐藏指针(称为返回值优化),用于按值返回结构。在 SystemV 的ABI中,第 41 页,在关于“函数返回结构或联合”的部分中,它说:

被调用的函数必须在返回之前从堆栈中删除该地址。

这就是为什么你ret $0x4在末尾得到一个test_callee5(),这是为了遵守 ABI。

现在关于sub $0x4, %esp每个调用站点之后的存在test_callee5(),它是上述规则的副作用,结合 C 编译器生成的优化代码。由于本地存储堆栈空间完全由以下方式预先保留:

3:  sub    $0x38,%esp

无需推入/弹出隐藏指针,它只是写在预保留空间的底部(由 指向esp),使用mov %eax,(%esp)第 9 行和第 17 行。由于堆栈指针没有递减,sub $0x4,%esp所以可以否定的效果ret $0x4,并保持堆栈指针不变。

在 Win32 上(我猜是使用 MSVC 编译器),没有这样的 ABI 规则,ret使用了一个简单的规则(正如 cdecl 中所预期的那样),隐藏指针在第 7 行和第 11 行被压入堆栈。虽然,这些插槽在之后没有被释放调用,作为一种优化,但仅在被调用者退出之前,使用add esp,1Ch释放隐藏指针堆栈槽(2 * 0x4 字节)和本地AStruct结构(0x14 字节)。

cdecl 不是为返回结构的函数定义调用约定吗?!

不幸的是,它没有,它因 C 编译器和操作系统而异

于 2017-04-24T07:05:16.797 回答
1

没有单一的“cdecl”调用约定。它由编译器和操作系统定义。

在阅读程序集时,我实际上并不确定约定是否真的不同——在这两种情况下,调用者都为输出提供了缓冲区作为额外的参数。只是 gcc 选择了不同的指令(第二个额外的 sub 很奇怪;代码优化了吗?)。

于 2011-02-08T09:21:15.517 回答