Вызов соглашения для возврата функции struct

Для следующего кода 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 выполняет эти дополнительные подписки?

Я бы предположил, что обе цели будут следовать за конвенцией вызова cdecl. Не определяет ли cdecl соглашение о вызове для функции, возвращающей структуру?!

EDIT:

Я добавил реализацию вызываемого абонента. И, конечно же, вы можете видеть, что вызывающий 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 ;;;;;;;;;;;;;;

Ответ 1

Почему вызывающий абонент в Linux32 выполняет эти дополнительные подписки?

Причина заключается в использовании скрытого указателя (с именем return value optimization), введенного компилятором, для возврата структуры по значению. В 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

Ответ 2

Не существует единого соглашения о вызове cdecl. Он определяется компилятором и операционной системой.

Также, читая сборку, я на самом деле не уверен, что соглашение на самом деле отличается друг от друга. В обоих случаях вызывающий агент предоставляет буфер для вывода в качестве дополнительного аргумента. Это просто, что gcc выбрал разные инструкции (второй дополнительный sub - странный, оптимизирован ли этот код?).