Как C возвращает структуру?

(gdb) disas func
Dump of assembler code for function func:
0x00000000004004b8 <func+0>:    push   %rbp
0x00000000004004b9 <func+1>:    mov    %rsp,%rbp
0x00000000004004bc <func+4>:    movl   $0x64,0xfffffffffffffff0(%rbp)
0x00000000004004c3 <func+11>:   movb   $0x61,0xfffffffffffffff4(%rbp)
0x00000000004004c7 <func+15>:   mov    0xfffffffffffffff0(%rbp),%rax
0x00000000004004cb <func+19>:   leaveq
0x00000000004004cc <func+20>:   retq
End of assembler dump.


t_test func()
{
    t_test t;
    t.i = 100;
    t.c = 'a';
    return t;
}

Таким образом, кажется, что он возвращает локальную переменную t, но гарантируется ли такая работа, разве она не должна ссылаться на какие-либо локальные переменные при возврате??


person new_perl    schedule 18.07.2011    source источник
comment
Вы возвращаете структуру в том смысле, что тип возвращаемого объекта является структурой. Но на самом деле вы возвращаете значение структуры. Таким образом, int f() { ... означает, что тип объекта, который возвращает f, равен int. И return 5; означает, что мы возвращаем значение 5. Точно так же int q = 3; return q; означает, что мы возвращаем значение q, то есть 3. На самом деле мы не возвращаем q .   -  person David Schwartz    schedule 31.03.2016


Ответы (5)


По моему опыту, не существует стандартного способа возврата структуры C. Чтобы иметь возможность передать структуру, компилятор обычно (незаметно для пользователя) передает указатель на структуру, в которую функция может скопировать содержимое. Способ передачи этого указателя (первым или последним в стеке) зависит от реализации. Некоторые компиляторы, такие как 32-битный MSVC++, возвращают небольшие структуры в регистрах, таких как EAX и EDX. Судя по всему, GCC возвращает такую ​​структуру в RAX в 64-битном режиме.

Но, опять же, нет стандартного способа, как это сделать. Это не проблема, когда остальная часть кода, использующего функцию, также компилируется тем же компилятором, но это проблема, если функция является экспортированной функцией DLL или библиотеки. Я был укушен этим несколько раз, когда использовал такие функции из другого языка (Delphi) или из C с другим компилятором. См. эту ссылку< /а> тоже.

person Rudy Velthuis    schedule 18.07.2011
comment
Есть ли какие-либо ссылки, говорящие, что это поведение зависит от реализации? - person Je Rog; 18.07.2011
comment
Я знаю, что это зависит от реализации. Каждая реализация имеет свой собственный способ сделать это. Я боролся с этим не раз, потому что не всегда легко узнать, как получить доступ к функции (в DLL), которая возвращает структуру из программы C или Delphi. Особенно, если func возвращает в регистрах, прямого способа обработки этого почти нет (кроме ассемблера). - person Rudy Velthuis; 18.07.2011
comment
@JeRog: вам нужна документация по соглашению о вызовах. В этом случае он указан как часть x86-64 System V ABI, на которую GCC ориентируется при компиляции для x86-64 Linux, MacOS или любой другой системы, отличной от Windows. - person Peter Cordes; 24.10.2020

RAX достаточно большой, чтобы вместить всю конструкцию. По адресу 0x00000000004004c7 вы загружаете всю структуру (с mov), а не ее адрес (вместо этого вы бы использовали lea).

Соглашение о вызовах x86-64 System V ABI возвращает C-структуры размером до 16 байтов в RDX:RAX или RAX. С++ на x86-64: когда структуры/классы передаются и возвращаются в регистрах?

Для больших структур есть скрытый выходной указатель arg, переданный вызывающей стороной.

person BlackBear    schedule 18.07.2011
comment
на что это будет похоже, если rax НЕ будет достаточно большим? - person new_perl; 18.07.2011
comment
@new_perl Создайте большую структуру и убедитесь сами. - person Justin; 18.07.2011
comment
я думаю, он скопирует структуру в стеке. Пытаться ;) - person BlackBear; 18.07.2011
comment
@Rudy: movs, наверное, но я совсем не уверен :P - person BlackBear; 18.07.2011
comment
Я знаю, как скопировать структуру. Но как функция возвращает данные в стек? Функция не знает, следует ли присвоить результат локальной или глобальной переменной. Поэтому необходимо передать указатель на структуру, а возвращаемое значение скопировать в это место. Возврат в стек работает только в том случае, если вызывающая сторона резервирует дополнительную память в стеке и функция знает об этом. Затем вызывающий абонент может выполнить задание. Я не знаю ни одного компилятора, который работает таким образом. - person Rudy Velthuis; 18.07.2011

Это не совсем стандартно, как вещи возвращаются, но обычно это в RAX. В вашем примере, предполагая, что t_test::i и t_test::c являются единственными членами t_test и имеют не более 32 бит каждый, вся структура может поместиться в 64-битный регистр, поэтому она просто возвращает значения непосредственно через RAX , и обычно вещи, которые могут поместиться в 2 регистра, возвращаются в RAX:RDX (или RDX:RAX, я забыл общий порядок).

Для более чем двух регистров обычно используется скрытый параметр-указатель, передаваемый в качестве первого параметра, который указывает на объект в вызывающей функции (обычно тот, которому напрямую присваивается возвращаемое значение). Затем этот объект записывается перед возвратом из вызываемой функции (обычно копируется из локальной структуры, используемой в вызываемой функции), и обычно тот же самый указатель, который был передан, возвращается в RAX.

EAX/EDX можно заменить на RAX/RDX в 32-разрядных системах x86.

В соглашениях, которые передают указатель this в стек (например, стандартные соглашения GCC x86), указатель возвращаемого значения обычно передается как скрытый второй параметр, а не первый.

person Kevin M    schedule 18.07.2011
comment
Итак, скрытый указатель указывает на кадр стека вызываемой функции, срок действия которого логически истек? - person Je Rog; 18.07.2011
comment
вызывающая функция выделяет для нее место (обычно это уже локальная структура в этой функции, а не что-то, выделенное специально для вызова функции), которое затем записывается вызываемой (та, которая возвращает большую структуру) перед возвратом. С другой стороны, то, что вы упомянули, все еще, вероятно, будет в порядке, поскольку кадр стека функции, которая ТОЛЬКО вернула, еще не был перезаписан. - person Kevin M; 18.07.2011
comment
То, что вы описываете, может быть верно для некоторых компиляторов C и C++ в Windows, но даже не для всех. Это определенно не всегда так (тем более, что не каждый процессор имеет эти регистры). - person Rudy Velthuis; 18.07.2011
comment
Я только что проверил это на gcc. Я взял 4-байтовую структуру. Я создал функцию и создал там локальную структуру и вернул ее в структуру, созданную в main. Затем посмотрел на сгенерированный ассемблерный код. Все произошло так, как вы сказали. Вероятно, компилятор использовал это соглашение о вызовах. - person crisron; 01.02.2015

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

Правильный способ обработки структуры — использовать их в качестве выходных параметров:

void func(t_test* t)
{
    t->i = 100;
    t->c = 'a';
}
person Eli Iser    schedule 18.07.2011
comment
На самом деле правильным способом является возврат по значению. Этот метод требует, чтобы любой, кто читает код, учитывал нелокальные эффекты, чтобы понять, что происходит. Реентерабельность и идемпотентность облегчают доказательство правильности вашей программы. Семантика значений Google. - person spraff; 18.07.2011
comment
@spraff - у меня встроенный фон, и для меня возвращать структуры по значению - плохая привычка. То же самое можно сказать и о любом неявном копировании параметров (массивы, классы и т.д.). В основном это связано с причинами производительности и различными ошибками, которые могут возникнуть из-за ошибки компилятора или вуду generic-HW (слабое оправдание, но это случается чаще, чем мне нужно). - person Eli Iser; 18.07.2011
comment
Некоторые компиляторы реализуют в этом маннаре возврат по значению. Дело в том, что вы не должны принимать решение на этом уровне. Возврат по значению лучше поддается оптимизации, потому что копии могут быть опущены и потенциально подвержены некоторым другим преобразованиям. Лучше починить компилятор, чем сломать приложение. - person spraff; 18.07.2011
comment
@spraff - я понимаю твою точку зрения. Однако из-за очень статичного характера встроенных приложений (по крайней мере, в моей области) более распространенным случаем является то, что у вас уже есть выделенная память, и вы хотите ее заполнить. Думаю, для этого сценария ничто не сравнится с передачей по ссылке. - person Eli Iser; 18.07.2011
comment
Справедливо. Иногда хороший дизайн и хорошая инженерия — не одно и то же ;-) - person spraff; 18.07.2011
comment
@spraff - ты действительно хорошо выразился :) - person Eli Iser; 18.07.2011
comment
Правильный способ вернуть маленькую структуру (умещается в один или два регистра) — это вернуть ее по значению, особенно в соглашении о вызовах, таком как x86-64 SysV, которое на самом деле делает возвращать небольшие структуры в регистрах. Если вызывающая сторона на самом деле не хочет сразу получать значения структуры, тогда вы можете передать указатель на функцию, чтобы сохранить их в памяти (если она не встроена). - person Peter Cordes; 24.10.2020

Указатель стека не изменяется в начале функции, поэтому выделение t_test не выполняется внутри функции и, следовательно, не освобождается функцией. Как это обрабатывается, зависит от используемого соглашения о вызовах. Если вы посмотрите, как вызывается функция, вам будет легче увидеть, как она выполняется.

person Anders Abel    schedule 18.07.2011
comment
Я не уверен, можно ли сказать, что t_test не выделяется внутри функции. Он не выделяется явно, но, например. Соглашение о вызовах Win64 требует, чтобы вызывающая сторона зарезервировала специальный блок памяти для сохранения регистров перед вызовом (т.е. в памяти вызывающей стороны), и здесь, по-видимому, используется что-то подобное (RBP-16 - это используемый адрес). IOW, в конце концов, он может быть локальным, даже если он явно не выделен из стека. - person Rudy Velthuis; 18.07.2011
comment
Для справки, это x86-64 System V, использующая красную зону ниже RSP. Теневое пространство Windows x64 будет выше RSP, но да, функция может использовать его так же, как свободное пространство. - person Peter Cordes; 24.10.2020