Дан фрагмент на Assembly (x86): краткая последовательность push/pop и вызов функции — объясните, как работает соглашение о вызовах (calling convention), какие регистры сохраняются вызывающим, а какие — вызываемым, и как это влияет на совместимость с кодом на высокоуровневых языках;
Кратко и по делу. Что такое calling convention - Это соглашение о том, как передавать аргументы, где возвращать значение, кто очищает стек и какие регистры должны сохраняться при вызове функции. Оно гарантирует, что вызывающий код и вызываемая функция понимают расположение данных в стеке/регистрaх. Типичное поведение на x86 (32‑бит) - Передача аргументов: обычно по стеку, справа‑налево (последний аргумент оказывается сверху стека). (например, push argN; ...; push arg1; call func). - Возврат значения: в регистре EAX. - Очистка стека: - cdecl: стек очищает вызывающий (caller). - stdcall: стек очищает вызываемый (callee). - Сохраняемые регистры: - Caller‑saved (volatile, могут быть повреждены вызовом): EAX, ECX, EDX. - Callee‑saved (nonvolatile, обязан сохранить вызываемый): EBX, ESI, EDI, EBP. - ESP (указатель стека) должен быть восстановлен вызовом — нельзя менять базовую позицию стека при возврате. (все числа и важные смещения: 32\,3232-bit слова по 4\,44 байта). Пример с push/pop - Набор аргументов: push arg2; push arg1; call func. - Если вызывающий хочет сохранить EAX через вызов (потому что EAX volatile), он делает: push EAX; call func; pop EAX. - Если вызываемый обязан сохранить EBX (callee‑saved), он обычно в начале функции делает: push EBX; ...; в конце: pop EBX; ret. - Если используется cdecl, после call вызывающий должен убрать аргументы: add ESP, 4⋅n\,4\cdot n4⋅n (где nnn — число аргументов). В stdcall этот add делает функция перед ret. Влияние на совместимость с кодом на ВЯП (высокоуровневых языках) - Компилятор генерирует код под конкретное соглашение; смешение разных соглашений (например, функция с stdcall, а вызывают как cdecl) приводит к рассинхронизации стека и авариям. - Для varargs (например, printf) обычно требуется cdecl, потому что вызывающий должен убирать стек. - При межъязыковом вызове (C↔C++, C# P/Invoke, ассемблер↔C) нужно точно указать/согласовать convention (extern "C", атрибуты __cdecl/__stdcall/__fastcall и т.п.). - Частые оптимизации (fastcall, аргументы в регистрах) меняют набор регистров, используемых для передачи аргументов — значит, обе стороны должны знать ABI. Короткая справка по x86-64 (для контраста) - System V AMD64 (Linux): аргументы в регистрах RDI, RSI, RDX, RCX, R8, R9; возврат в RAX; caller‑saved: RAX, RCX, RDX, RSI, RDI, R8–R11; callee‑saved: RBX, RBP, R12–R15. - Microsoft x64: аргументы в RCX, RDX, R8, R9; похожие правила по сохранению регистров. Вывод - Push/pop вокруг call — это явное сохранение/восстановление регистров или аргументов. Нужно знать, какое соглашение используется: кто чистит стек и какие регистры обещаны сохраняться; несоответствие соглашений вызывает ошибки в совместимости.
Что такое calling convention
- Это соглашение о том, как передавать аргументы, где возвращать значение, кто очищает стек и какие регистры должны сохраняться при вызове функции. Оно гарантирует, что вызывающий код и вызываемая функция понимают расположение данных в стеке/регистрaх.
Типичное поведение на x86 (32‑бит)
- Передача аргументов: обычно по стеку, справа‑налево (последний аргумент оказывается сверху стека). (например, push argN; ...; push arg1; call func).
- Возврат значения: в регистре EAX.
- Очистка стека:
- cdecl: стек очищает вызывающий (caller).
- stdcall: стек очищает вызываемый (callee).
- Сохраняемые регистры:
- Caller‑saved (volatile, могут быть повреждены вызовом): EAX, ECX, EDX.
- Callee‑saved (nonvolatile, обязан сохранить вызываемый): EBX, ESI, EDI, EBP.
- ESP (указатель стека) должен быть восстановлен вызовом — нельзя менять базовую позицию стека при возврате.
(все числа и важные смещения: 32\,3232-bit слова по 4\,44 байта).
Пример с push/pop
- Набор аргументов: push arg2; push arg1; call func.
- Если вызывающий хочет сохранить EAX через вызов (потому что EAX volatile), он делает: push EAX; call func; pop EAX.
- Если вызываемый обязан сохранить EBX (callee‑saved), он обычно в начале функции делает: push EBX; ...; в конце: pop EBX; ret.
- Если используется cdecl, после call вызывающий должен убрать аргументы: add ESP, 4⋅n\,4\cdot n4⋅n (где nnn — число аргументов). В stdcall этот add делает функция перед ret.
Влияние на совместимость с кодом на ВЯП (высокоуровневых языках)
- Компилятор генерирует код под конкретное соглашение; смешение разных соглашений (например, функция с stdcall, а вызывают как cdecl) приводит к рассинхронизации стека и авариям.
- Для varargs (например, printf) обычно требуется cdecl, потому что вызывающий должен убирать стек.
- При межъязыковом вызове (C↔C++, C# P/Invoke, ассемблер↔C) нужно точно указать/согласовать convention (extern "C", атрибуты __cdecl/__stdcall/__fastcall и т.п.).
- Частые оптимизации (fastcall, аргументы в регистрах) меняют набор регистров, используемых для передачи аргументов — значит, обе стороны должны знать ABI.
Короткая справка по x86-64 (для контраста)
- System V AMD64 (Linux): аргументы в регистрах RDI, RSI, RDX, RCX, R8, R9; возврат в RAX; caller‑saved: RAX, RCX, RDX, RSI, RDI, R8–R11; callee‑saved: RBX, RBP, R12–R15.
- Microsoft x64: аргументы в RCX, RDX, R8, R9; похожие правила по сохранению регистров.
Вывод
- Push/pop вокруг call — это явное сохранение/восстановление регистров или аргументов. Нужно знать, какое соглашение используется: кто чистит стек и какие регистры обещаны сохраняться; несоответствие соглашений вызывает ошибки в совместимости.