Заинтригованный постом об UB , я решил начать читать Джонатана Бартлетта «Программирование с нуля», чтобы поиграться с C++ UB и посмотреть, как выглядит сборка.
Но пробуя разные вещи, я обнаружил кое-что странное в довольно простом случае. Рассмотрим этот код
int foo(int * p) {
int y = 7;
if (p)
++y;
return y;
}
Его сборка
foo(int*):
cmpq $1, %rdi
movl $7, %eax
sbbl $-1, %eax
ret
(Обозреватель компиляторов)
Теперь я понимаю, что movl $7, %eax помещает значение 7 в регистр eax, а затем то, которое будет возвращено вызывающей стороне ret. Поэтому я также понял, что sbbl $-1, %eax — это инструкция, которая заботится о вычитании -1 из содержимого eax и сохранении результата в самом eax, и что эта инструкция выполняется только в том случае, если p не равно нулю. Это заставляет меня предположить, что sbbl использует скрытое логическое значение, вычисленное предыдущими строками. Единственный кандидат, даже по имени, это cmpq $1, %rdi.
Но что это делает? Из вышеупомянутой книги я понял, что аргументы функций передаются от вызывающей стороны к вызываемой через стек: вызывающая сторона помещает аргументы в стек, а вызываемая сторона извлекает эти значения. Но здесь такого нет.
Так %rdi что? Регистр первого (и в данном случае единственного) аргумента функции? Почему это так? Существуют ли другие регистры, относящиеся к дополнительным аргументам? Сколько? И кроме того, что является хорошим источником информации по этой теме?
Что касается того, как GCC организует возврат 7 или 8 в соответствии с условием if (p), да, cmp $1, %rdi устанавливает CF, если RDI был равен нулю, в противном случае очищает его. Таким образом, более поздний SBB добавит 0 (EAX -= -1 + CF=1) или 1 (EAX -= -1 + CF=0).
sbb
, felixcloutier.com/x86/sbb
%rdi ссылка на реестр rdi.
В этом случае создается впечатление, что компилятор передает первый параметр в регистр, а не в стек.
Передача параметров в основном является соглашением: пока компилятор согласован в том, как он передает параметры, компилятор может переключаться с передачи параметров одним способом (например, всегда в стеке) на другой (некоторые в регистрах) практически в любое время, когда сочтет нужным. (новая версия компилятора или даже просто передача какого-либо переключателя в командной строке компилятора).
В зависимости от того, когда и где вы смотрите, один компилятор может поддерживать несколько соглашений о вызовах. Например, долгое время 32-разрядный компилятор Microsoft поддерживал четыре: cdecl, fastcall, stdcall и thiscall (последний использовался только для функций-членов C++). Из них cdecl и stdcall были основаны исключительно на стеке, а fastcall и thiscall оба использовали регистры для некоторых аргументов.
Программирование с нуля использует сборку i386 с синтаксисом AT&T в Linux; это бесплатная книга, написанная до того, как x86-64 получила широкое распространение (но она довольно хороша, судя по тому, что я просмотрел). Такие термины, как «cdecl», не существуют в ABI i386 System V или x86-64 System V. (Единственный способ получить 32-битный код, используя что-либо, кроме аргументов стека, — это GCC __attribute__((regparm(3))), по умолчанию — regparm(0), если вы не используете параметр командной строки.)
Компиляторы C не могут по прихоти переключать соглашения о вызовах в новой версии, что нарушит двоичную совместимость с существующими библиотеками. (Теоретически они могли бы использоваться для функций static, но эта функция никоим образом не является частной.)
@PeterCordes: Да, могут, и да. Я полагаю, что «по прихоти» немного преувеличивает, но они, безусловно, время от времени менялись от одной версии к другой.
GCC для x86-64 (который использует OP) никогда не менял свой C ABI несовместимыми с предыдущими способами, о чем я не знаю, и вряд ли это произойдет в будущем. Да, вы можете изменить его с помощью аргументов командной строки, например. -mpreferred-stack-boundary=3 чтобы поддерживать только 2**3 = 8-байтовое выравнивание стека. GCC для i386 случайно (?) начал требовать выравнивания 16-байтового стека с SSE code-gen, в конечном итоге изменив ABI, чтобы потребовать его, как только была обнаружена проблема с библиотеками, скомпилированными таким образом, существующими в дикой природе. (И вы можете изменить соглашение о вызовах с помощью -mregparm=3.)
Я думаю, что некоторые другие ISA, которые чаще используются во встроенных системах, изменили ABI, возможно, потому, что они чаще используются в системах, где все можно без особых проблем перестроить из исходного кода. Но я не знаю подробностей тех. Учитывая ваше обсуждение 32-битных имен соглашений о вызовах Windows, вы говорите об изменении значений по умолчанию MSVC или что-то в этом роде?
@PeterCordes: MSVC — один из примеров, но далеко не единственный.
%rdi
— это 64-битный регистр (когда-то он обозначал «индекс назначения», а r
означает 64-битный регистр). Функция, написанная на C, обычно предполагает, что первый параметр находится в этом регистре (или указатель на него, если он больше 64 бит). В прошлом он часто использовался для хранения указателя на некоторый массив, в который вы записывали, или назначение функций C memcpy()
или memset()
, поскольку аппаратное обеспечение x86 имеет специальные команды для многократной записи в память, на которую указывает %rdi
.
RDI содержит первый целочисленный аргумент/указатель в соглашении о вызовах x86-64 System V. В книге, которую вы читаете, используется 32-разрядный ассемблер x86, где стандартное соглашение о вызовах намного старше и менее эффективно, только с использованием аргументов стека. Если вы используете gcc -O3 -m32 -mregparm=3, вы получите 32-битный код, используя аргументы регистра. Если вы используете gcc -O3 -m32, вы получите более знакомый код.