(Этот вопрос относится к архитектуре моей машины и соглашениям о вызовах, Windows x86_64)
Я точно не помню, где я читал это, или если я вспомнил это правильно, но я слышал, что когда функция должна возвращать некоторую структуру или объект по значению, она либо заполняет ее rax
(если объект может вписаться в регистр шириной 64 бита) или передать указатель на то место, где будет результирующий объект (я предполагаю, что он расположен в кадре стека вызывающей функции) в rcx
где он будет делать все обычные инициализации, а затем mov rax, rcx
для обратной поездки. То есть что-то вроде
extern some_struct create_it(); // implemented in assembly
будет действительно иметь секретный параметр, как
extern some_struct create_it(some_struct* secret_param_pointing_to_where_i_will_be);
Моя память послужила мне правильно, или я ошибаюсь? Как большие объекты (то есть шире, чем ширина регистра) возвращаются значением из функций?
Вот простая дизассемблирование кода с примерами того, что вы говорите
typedef struct
{
int b;
int c;
int d;
int e;
int f;
int g;
char x;
} A;
A foo(int b, int c)
{
A myA = {b, c, 5, 6, 7, 8, 10};
return myA;
}
int main()
{
A myA = foo(5,9);
return 0;
}
и вот разборка функции foo, и основная функция, вызывающая ее
главный:
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 30h
call ___main
lea eax, [esp+20] ; placing the addr of myA in eax
mov dword ptr [esp+8], 9 ; param passing
mov dword ptr [esp+4], 5 ; param passing
mov [esp], eax ; passing myA addr as a param
call _foo
mov eax, 0
leave
retn
Foo:
push ebp
mov ebp, esp
sub esp, 20h
mov eax, [ebp+12]
mov [ebp-28], eax
mov eax, [ebp+16]
mov [ebp-24], eax
mov dword ptr [ebp-20], 5
mov dword ptr [ebp-16], 6
mov dword ptr [ebp-12], 7
mov dword ptr [ebp-8], 9
mov byte ptr [ebp-4], 0Ah
mov eax, [ebp+8]
mov edx, [ebp-28]
mov [eax], edx
mov edx, [ebp-24]
mov [eax+4], edx
mov edx, [ebp-20]
mov [eax+8], edx
mov edx, [ebp-16]
mov [eax+0Ch], edx
mov edx, [ebp-12]
mov [eax+10h], edx
mov edx, [ebp-8]
mov [eax+14h], edx
mov edx, [ebp-4]
mov [eax+18h], edx
mov eax, [ebp+8]
leave
retn
Теперь давайте рассмотрим то, что только что произошло, поэтому при вызове foo параметры передаются следующим образом: 9 — это самый высокий адрес, затем 5, затем начинается адрес myA in main.
lea eax, [esp+20] ; placing the addr of myA in eax
mov dword ptr [esp+8], 9 ; param passing
mov dword ptr [esp+4], 5 ; param passing
mov [esp], eax ; passing myA addr as a param
в foo
есть какой-то местный myA
который хранится в кадре стека, так как стек идет вниз, самый низкий адрес myA
начинается в [ebp - 28]
смещение -28 может быть вызвано выравниванием структуры, поэтому я предполагаю, что размер структуры здесь должен составлять 28 байт, а не 25, как ожидалось. и как мы можем видеть в foo
после местного myA
из foo
был создан и заполнен параметрами и непосредственными значениями, скопирован и перезаписан по адресу myA
прошло от main
(это фактическое значение возврата по значению)
mov eax, [ebp+8]
mov edx, [ebp-28]
[ebp + 8]
где адрес main::myA
был сохранен (адрес памяти идет вверх, следовательно, ebp + старый ebp (4 байта) + адрес возврата (4 байта)) в целом ebp + 8, чтобы добраться до первого байта main::myA
как говорилось ранее foo::myA
хранится в [ebp-28]
как стек идет вниз
mov [eax], edx
место foo::myA.b
в адресе первого члена данных main::myA
который main::myA.b
mov edx, [ebp-24]
mov [eax+4], edx
поместите значение, которое находится в адресе foo::myA.c
в EDX, и поместите это значение в адрес main::myA.b
+ 4 байта, что main::myA.c
как вы можете видеть, этот процесс повторяется через функцию
mov edx, [ebp-20]
mov [eax+8], edx
mov edx, [ebp-16]
mov [eax+0Ch], edx
mov edx, [ebp-12]
mov [eax+10h], edx
mov edx, [ebp-8]
mov [eax+14h], edx
mov edx, [ebp-4]
mov [eax+18h], edx
mov eax, [ebp+8]
что в основном доказывает, что при возврате структуры через val, которая не может быть помещена в качестве параметра, происходит то, что адрес, в котором должно находиться возвращаемое значение, передается как параметр функции и внутри функции, вызываемой значения возвращаемой структуры копируются в адрес, переданный в качестве параметра …
надеюсь, что этот пример помог вам визуализировать, что происходит под капотом немного лучше 🙂
РЕДАКТИРОВАТЬ
Я надеюсь, что вы заметили, что мой пример использовал 32-битный ассемблер и Я ЗНАЮ Вы спрашивали о x86-64, но в настоящее время я не могу разобрать код на 64-битной машине, поэтому я надеюсь, что вы поверите мне на слово, что концепция одинакова как для 64-битной, так и для 32-битной системы, и что Соглашение о вызовах почти одинаково
Это точно правильно. Вызывающая сторона передает дополнительный аргумент, который является адресом возвращаемого значения. Обычно это будет в кадре стека вызывающего, но нет никаких гарантий.
Точная механика определяется платформой ABI, но этот механизм очень распространен.
Различные комментаторы оставили полезные ссылки с документацией по соглашениям о вызовах, поэтому некоторые из них я добавлю в этот ответ:
Статья в Википедии Соглашения о вызовах x86
Коллекция ресурсов оптимизации Agner, включая краткое изложение соглашений о вызовах (Прямая ссылка на 57-страничный PDF документ.)
Документация Microsoft Developer Network (MSDN) по соглашения о вызовах.
Переполнение стека x86 тег вики имеет много полезных ссылок.