Четвёртая часть.
Последней архитектурой, которую мы рассмотрим будет архитектура AMD64 (также известная как x86-64).
AMD64 берёт традиционную архитектуру x86 и увеличивает регистры до 64-х бит, именуя их rax, rbx и т.д. И также добавляет восемь дополнительных регистров, называя их просто как R8-R15.
- Первые четыре параметры в функцию передаются в rcx, rdx, r8 и r9. Все другие параметры передаются в стеке. Более того, для параметров в регистре резервируется место в стеке, на случай если вызываемая функция захочет сбросить регистры в стек; это также важно для функций с переменным числом аргументов.
- Параметры меньше 64-бит не дополняются нулями; верхние биты содержат мусор, поэтому не забывайте обнулять их явно, если вы собираетесь их использовать. Параметры размером больше 64-х бит передаются по ссылке.
- Возвращаемое значение помещается в rax. Если возвращаемое значение больше 64-х бит, то в функцию будет передан неявный секретный параметр, который содержит адрес, по которому нужно записать результат.
- Все регистры обязаны сохраняться во всемя вызова, кроме регистров rax, rcx, rdx, r8, r9, r10 и r11, которые свободны.
- Вызываемый не чистит стек. Это работа вызывающего.
- Стек должен быть всё время выровненным на границу 16-ти байт. Поскольку инструкция "call" записывает в стек 8-ми байтовый адрес возврата, то это значит, что каждая не листовая функция должна подправлять стек на значение вида 16n + 8 для восстановления выравнивания на 16 байт.
Вот пример:
void SomeFunction(int a, int b, int c, int d, int e);После входа в CallThatFunction стек выглядит примерно так:
void CallThatFunction()
{
SomeFunction(1, 2, 3, 4, 5);
SomeFunction(6, 7, 8, 9, 10);
}
xxxxxxx0 .. данные на стеке ..Из-за наличия в нём адреса возврата стек оказывается не выровненным. Функция CallThatFunction настраивает свой фрейм примерно так:
xxxxxxx8 адрес возврата <- RSP
sub rsp, 0x28Заметим, что размер локального стекового фрейма равен 16n + 8, так что в результате у нас получается выравненный стек:
xxxxxxx0 .. данные на стеке ..Теперь мы готовим первый вызов:
xxxxxxx8 адрес возврата
xxxxxxx0 (arg5)
xxxxxxx8 (место для arg4)
xxxxxxx0 (место для arg3)
xxxxxxx8 (место для arg2)
xxxxxxx0 (место для arg1) <- RSP
mov dword ptr [rsp+0x20], 5 ; параметр 5Когда функция SomeFunction возвращает управление, стек ещё не очищен и поэтому выглядит так же, как и выше. Тогда для второго вызова нам просто нужно занести новые значения в уже подготовленное место:
mov r9d, 4 ; параметр 4
mov r8d, 3 ; параметр 3
mov edx, 2 ; параметр 2
mov ecx, 1 ; параметр 1
call SomeFunction ; Вперёд, Спиди-Гонщик!
mov dword ptr [rsp+0x20], 10 ; параметр 5Теперь CallThatFunction завершена и она должна очистить стек и вернуть управление:
mov r9d, 9 ; параметр 4
mov r8d, 8 ; параметр 3
mov edx, 7 ; параметр 2
mov ecx, 6 ; параметр 1
call SomeFunction ; Вперёд, Спиди-Гонщик!
add rsp, 0x28Заметьте, что вы практичеси не встречаете инструкций "push" в коде amd64, поскольку парадигмой вызывающего является резервирование пространства и повторное использование его.
ret
"Теперь CallThatFunction завершена и она должна очистить стек и вернуть управление:"
ОтветитьУдалитьв оригинале - "CallThatFunction is now finished and can clean its stack and return."
правильный перевод - Теперь CallThatFunction завершена и она может очистить стек и вернуть управление.
Ибо: "Вызываемый не чистит стек. Это работа вызывающего."
Мне кажется, вы неправильно поняли. CallThatFunction должна чистить стек не за собой, а за SomeFunction. Она и должна и может это сделать :)
ОтветитьУдалить