C / C ++ возвращает структуру по значению под капотом

(Этот вопрос относится к архитектуре моей машины и соглашениям о вызовах, 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);

Моя память послужила мне правильно, или я ошибаюсь? Как большие объекты (то есть шире, чем ширина регистра) возвращаются значением из функций?

2

Решение

Вот простая дизассемблирование кода с примерами того, что вы говорите

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-битной системы, и что Соглашение о вызовах почти одинаково

5

Другие решения

Это точно правильно. Вызывающая сторона передает дополнительный аргумент, который является адресом возвращаемого значения. Обычно это будет в кадре стека вызывающего, но нет никаких гарантий.

Точная механика определяется платформой ABI, но этот механизм очень распространен.

Различные комментаторы оставили полезные ссылки с документацией по соглашениям о вызовах, поэтому некоторые из них я добавлю в этот ответ:

5

По вопросам рекламы [email protected]