Вызов соглашения для возврата функции 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 - странный, оптимизирован ли этот код?).