Вызов printf в x86_64 с использованием ассемблера GNU
Я написал программу с использованием синтаксиса AT&T для использования с ассемблером GNU:
.data
format: .ascii "%d\n"
.text
.global main
main:
mov $format, %rbx
mov (%rbx), %rdi
mov $1, %rsi
call printf
ret
Я использую GCC для сборки и связи с:
gcc -o main main.s
Я запускаю его с помощью этой команды:
./главный
Когда я запускаю программу, я получаю ошибку сегмента. Используя GDB, он говорит printf
не найдено. Я пробовал ".extern printf", который не работает. Кто-то предложил мне сохранить указатель стека перед вызовом printf
и восстановить до RET, как мне это сделать?
2 ответа
Есть несколько проблем с этим кодом. Соглашение о вызовах AMD64 System V ABI, используемое Linux, требует нескольких вещей. Требуется, чтобы непосредственно перед вызовом CALL стек был выровнен как минимум на 16 байтов (или 32 байта):
Конец области входного аргумента должен быть выровнен по границе 16 байтов (32, если __m256 передается в стеке).
После того, как среда выполнения C вызывает main
функция, стек смещен на 8, потому что CALL указатель возврата был помещен в стек. Для выравнивания в 16-байтовую границу вы можете просто ЗАДВИЖИТЬ любой регистр общего назначения в стек и POP отключить его в конце.
Соглашение о вызовах также требует, чтобы AL содержал количество векторных регистров, используемых для функции переменного аргумента:
% al используется для указания количества векторных аргументов, переданных функции, для которой требуется переменное число аргументов
printf
является переменной функцией аргумента, поэтому необходимо установить AL. В этом случае вы не передаете никаких параметров в векторный регистр, поэтому вы можете установить AL в 0.
Вы также разыменовываете указатель формата $, когда он уже является адресом. Так что это неправильно
mov $format, %rbx
mov (%rbx), %rdi
Это берет адрес формата и помещает его в RBX. Затем вы берете 8 байтов по этому адресу в RBX и помещаете их в RDI. RDI должен быть указателем на строку символов, а не самими символами. Две строки можно заменить на:
lea format(%rip), %rdi
При этом используется относительная адресация RIP.
Вы также должны NUL прекратить ваши строки. Вместо того, чтобы использовать .ascii
ты можешь использовать .asciz
на платформе x86.
Рабочая версия вашей программы может выглядеть так:
# global data #
.data
format: .asciz "%d\n"
.text
.global main
main:
push %rbx
lea format(%rip), %rdi
mov $1, %esi # Writing to ESI zero extends to RSI.
xor %eax, %eax # Zeroing EAX is efficient way to clear AL.
call printf
pop %rbx
ret
Другие рекомендации / предложения
Из 64-битного Linux ABI вы также должны знать, что соглашение о вызовах также требует написанных вами функций для сохранения определенных регистров. Список регистров и должны ли они быть сохранены следующим образом:
Любой регистр, который говорит Yes
в столбце " Сохранено через регистр " вы должны убедиться, что они сохранены во всей функции. функция main
как любая другая функция C.
Если у вас есть строки / данные, которые, как вы знаете, будут только для чтения, вы можете поместить их в .rodata
раздел с .section .rodata
скорее, чем .data
В 64-битном режиме: если у вас есть целевой операнд, который является 32-битным регистром, ЦПУ будет расширять регистр нулями по всему 64-битному регистру. Это может сохранить байты в кодировке команд.
Возможно, ваш исполняемый файл компилируется как позиционно-независимый код. Вы можете получить сообщение об ошибке, похожее на:
перемещение R_X86_64_PC32 к символу `printf@@GLIBC_2.2.5'не может использоваться при создании общего объекта; перекомпилировать с -fPIC
Чтобы это исправить, вам нужно вызвать внешнюю функцию printf
сюда:
call printf@plt
Это вызывает функцию внешней библиотеки через таблицу связей процедур (PLT)
Вы можете посмотреть код сборки, сгенерированный из эквивалентного файла c.
Бег gcc -o - -S -fno-asynchronous-unwind-tables test.c
с test.c
#include <stdio.h>
int main() {
return printf("%d\n", 1);
}
Это выводит код сборки:
.file "test.c"
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
pushq %rbp
movq %rsp, %rbp
movl $1, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
popq %rbp
ret
.size main, .-main
.ident "GCC: (GNU) 6.1.1 20160602"
.section .note.GNU-stack,"",@progbits
Это даст вам пример кода сборки, вызывающего printf, который вы затем сможете изменить.
По сравнению с вашим кодом, вы должны изменить 2 вещи:
- % rdi должен указывать на формат, вы не должны ссылаться на%rbx, это можно сделать с помощью
mov $format, %rdi
- printf имеет переменное количество аргументов, тогда вам нужно добавить
mov $0, %eax
Применение этих модификаций даст что-то вроде:
.data
format: .ascii "%d\n"
.text
.global main
main:
mov $format, %rdi
mov $1, %rsi
mov $0, %eax
call printf
ret
А затем запустив его в печать:
1