Вызов printf в расширенном встроенном ASM
Я пытаюсь вывести одну и ту же строку дважды в расширенном встроенном ASM в GCC, на 64-битном Linux.
int main()
{
const char* test = "test\n";
asm(
"movq %[test], %%rdi\n" // Debugger shows rdi = *address of string*
"movq $0, %%rax\n"
"push %%rbp\n"
"push %%rbx\n"
"call printf\n"
"pop %%rbx\n"
"pop %%rbp\n"
"movq %[test], %%rdi\n" // Debugger shows rdi = 0
"movq $0, %%rax\n"
"push %%rbp\n"
"push %%rbx\n"
"call printf\n"
"pop %%rbx\n"
"pop %%rbp\n"
:
: [test] "g" (test)
: "rax", "rbx","rcx", "rdx", "rdi", "rsi", "rsp"
);
return 0;
}
Теперь строка выводится только один раз. Я перепробовал много вещей, но я предполагаю, что пропускаю некоторые предостережения о соглашении о вызовах. Я даже не уверен, что список клобберов правильный, или мне вообще нужно сохранять и восстанавливать RBP и RBX.
Почему строка не выводится дважды?
Взгляд с отладчиком показывает мне, что как-то, когда строка загружается в rdi
во второй раз это имеет значение 0
вместо фактического адреса строки.
Я не могу объяснить, почему, похоже, после первого вызова стек поврежден? Должен ли я восстановить его каким-либо образом?
1 ответ
Особая проблема вашего кода: RDI не поддерживается при вызове функции (см. Ниже). Это правильно до первого звонка printf
но забит printf
, Сначала вам нужно будет временно сохранить его в другом месте. Регистр, который не засорен, будет удобен. Затем вы можете сохранить копию до printf
и скопируйте его обратно в RDI после.
Я не рекомендую делать то, что вы предлагаете (вызывать функции во встроенном ассемблере). Компилятору будет очень сложно оптимизировать вещи.
Среди прочего, 64-битный System V ABI требует 128-байтовой красной зоны. Это означает, что вы ничего не можете поместить в стек без потенциального повреждения. Помните: выполнение CALL помещает адрес возврата в стек. Быстрый и грязный способ решить эту проблему - вычесть 128 из RSP, когда ваш встроенный ассемблер запускается, а затем добавить 128 обратно, когда закончите.
128-байтовая область за пределами местоположения, на которое указывает% rsp, считается зарезервированной и не должна изменяться обработчиками сигналов или прерываний.8 Следовательно, функции могут использовать эту область для временных данных, которые не нужны при вызовах функций. В частности, листовые функции могут использовать эту область для всего кадра стека, вместо того, чтобы корректировать указатель стека в прологе и эпилоге. Эта область известна как красная зона.
Другая проблема, о которой следует беспокоиться, - это требование, чтобы стек был выровнен по 16 байтов (или, возможно, выровнен по 32 байта в зависимости от параметров) до любого вызова функции. Это требуется и для 64-битного ABI:
Конец области входного аргумента должен быть выровнен по границе 16 байтов (32, если __m256 передается в стеке). Другими словами, значение (%rsp + 8) всегда кратно 16 (32), когда управление передается в точку входа в функцию.
Примечание. Это требование для 16-байтового выравнивания при вызове функции также требуется в 32-битном Linux для GCC > = 4.5:
В контексте языка программирования C аргументы функции помещаются в стек в обратном порядке. В Linux GCC устанавливает стандарт де-факто для соглашений о вызовах. Начиная с версии 4.5 GCC, стек должен быть выровнен по 16-байтовой границе при вызове функции (в предыдущих версиях требовалось только 4-байтовое выравнивание.)
Так как мы называем printf
во встроенном ассемблере мы должны убедиться, что мы выравниваем стек по 16-байтовой границе перед выполнением вызова.
Вы также должны знать, что при вызове функции некоторые регистры сохраняются при вызове функции, а некоторые - нет. В частности, те, которые могут быть засорены вызовом функции, перечислены на рисунке 3.4 64-битного ABI (см. Предыдущую ссылку). Это регистры RAX, RCX, RDX, RD8-RD11, XMM0-XMM15, MMX0-MMX7, ST0-ST7. Все они потенциально могут быть уничтожены, поэтому их следует поместить в список clobber, если они не отображаются в ограничениях ввода и вывода.
Следующий код должен удовлетворять большинству условий, чтобы гарантировать, что встроенный ассемблер, который вызывает другую функцию, не будет непреднамеренно перегружать регистры, сохраняет красную зону и поддерживает 16-байтовое выравнивание перед вызовом:
int main()
{
const char* test = "test\n";
long dummyreg; /* dummyreg used to allow GCC to pick available register */
__asm__ __volatile__ (
"add $-128, %%rsp\n\t" /* Skip the current redzone */
"mov %%rsp, %[temp]\n\t" /* Copy RSP to available register */
"and $-16, %%rsp\n\t" /* Align stack to 16-byte boundary */
"mov %[test], %%rdi\n\t" /* RDI is address of string */
"xor %%eax, %%eax\n\t" /* Variadic function set AL. This case 0 */
"call printf\n\t"
"mov %[test], %%rdi\n\t" /* RDI is address of string again */
"xor %%eax, %%eax\n\t" /* Variadic function set AL. This case 0 */
"call printf\n\t"
"mov %[temp], %%rsp\n\t" /* Restore RSP */
"sub $-128, %%rsp\n\t" /* Add 128 to RSP to restore to orig */
: [temp]"=&r"(dummyreg) /* Allow GCC to pick available output register. Modified
before all inputs consumed so use & for early clobber*/
: [test]"r"(test), /* Choose available register as input operand */
"m"(test) /* Dummy constraint to make sure test array
is fully realized in memory before inline
assembly is executed */
: "rax", "rcx", "rdx", "rsi", "rdi", "r8", "r9", "r10", "r11",
"xmm0","xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",
"xmm8","xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15",
"mm0","mm1", "mm2", "mm3", "mm4", "mm5", "mm6", "mm6",
"st", "st(1)", "st(2)", "st(3)", "st(4)", "st(5)", "st(6)", "st(7)"
);
return 0;
}
Я использовал ограничение ввода, чтобы шаблон мог выбрать доступный регистр, который будет использоваться для передачи str
адрес через. Это гарантирует, что у нас есть регистр для хранения str
адрес между звонками на printf
, Я также получаю шаблон ассемблера, чтобы выбрать доступное место для временного хранения RSP, используя фиктивный регистр. Выбранные регистры не будут включать в себя ни один из уже выбранных / перечисленных в качестве операнда ввода / вывода / прерывания.
Это выглядит очень грязно, но если вы не сделаете это правильно, это может привести к проблемам позже, когда ваша программа станет более сложной. Вот почему вызов функций, соответствующих 64-битному ABI System V внутри встроенного ассемблера, как правило, не лучший способ сделать что-либо.