x64 nasm: вставка адресов памяти в стек и вызов функции
Я довольно новичок в x64-сборке на Mac, так что я путаюсь с переносом 32-битного кода в 64-битную.
Программа должна просто распечатать сообщение через printf
функция из стандартной библиотеки C.
Я начал с этого кода:
section .data
msg db 'This is a test', 10, 0 ; something stupid here
section .text
global _main
extern _printf
_main:
push rbp
mov rbp, rsp
push msg
call _printf
mov rsp, rbp
pop rbp
ret
Компилируем его с помощью nasm следующим образом:
$ nasm -f macho64 main.s
Возвращена следующая ошибка:
main.s:12: error: Mach-O 64-bit format does not support 32-bit absolute addresses
Я попытался исправить эту проблему, изменив код следующим образом:
section .data
msg db 'This is a test', 10, 0 ; something stupid here
section .text
global _main
extern _printf
_main:
push rbp
mov rbp, rsp
mov rax, msg ; shouldn't rax now contain the address of msg?
push rax ; push the address
call _printf
mov rsp, rbp
pop rbp
ret
Скомпилировано нормально с nasm
Команда выше, но теперь есть предупреждение при компиляции объектного файла с gcc
к актуальной программе:
$ gcc main.o
ld: warning: PIE disabled. Absolute addressing (perhaps -mdynamic-no-pic) not
allowed in code signed PIE, but used in _main from main.o. To fix this warning,
don't compile with -mdynamic-no-pic or link with -Wl,-no_pie
Так как это предупреждение, а не ошибка, я выполнил a.out
файл:
$ ./a.out
Segmentation fault: 11
Надеюсь, кто-нибудь знает, что я делаю не так.
3 ответа
64-битный OS X ABI в целом соответствует System V ABI - дополнению к архитектуре AMD64. Его модель кода очень похожа на модель кода, независимую от малого положения (PIC), с различиями, объясненными здесь. В этой модели кода все локальные и небольшие данные доступны напрямую с использованием RIP-относительной адресации. Как отмечено в комментариях Z boson, база изображений для 64-битных исполняемых файлов Mach-O находится за пределами первых 4 ГиБ виртуального адресного пространства, поэтому push msg
это не только неверный способ поставить адрес msg
в стеке, но это также невозможно, так как PUSH
не поддерживает 64-битные непосредственные значения. Код должен выглядеть примерно так:
lea rax, [rel msg] ; RIP-relative addressing
push rax
Но в этом конкретном случае не нужно вообще помещать значение в стек. 64-битное соглашение о вызовах требует, чтобы первые 6 целочисленных аргументов / указатели передавались в регистрах RDI
, RSI
, RDX
, RCX
, R8
, а также R9
именно в таком порядке. Первые 8 аргументов с плавающей точкой или вектор входят в XMM0
, XMM1
,..., XMM7
, Только после того, как будут использованы все доступные регистры или есть аргументы, которые не могут вписаться ни в один из этих регистров (например, 80-битный long double
значение) стек используется. 64-битные немедленные нажатия выполняются с использованием MOV
(QWORD
вариант) а не PUSH
, Простые возвращаемые значения передаются обратно в RAX
регистр. Вызывающая сторона также должна предоставить стековую область для вызываемой стороны, чтобы сохранить некоторые регистры.
printf
это специальная функция, потому что она принимает переменное количество аргументов. При вызове таких функций RAX
должно быть установлено количество аргументов с плавающей точкой, передаваемых в векторных регистрах. Также обратите внимание, что RIP
Относительная адресация предпочтительна для данных, которые находятся в пределах 2 Гбайт кода.
Вот как gcc
переводит printf("This is a test\n");
в сборку на OS X:
xorb %al, %al (1)
leaq L_.str(%rip), %rdi (2)
callq _printf (3)
L_.str:
.asciz "This is a test\n"
(это сборка в стиле AT&T, источник слева, пункт назначения справа, имена регистров имеют префикс %
, ширина данных закодирована как суффикс к имени инструкции)
В (1)
ноль ставится в RAX
поскольку аргументы с плавающей точкой не передаются. В (2)
адрес строки загружается в RDI
, Обратите внимание, что значение на самом деле является смещением от текущего значения RIP
, Поскольку ассемблер не знает, каким будет это значение, он помещает запрос на перемещение в объектный файл. Затем компоновщик видит перемещение и устанавливает правильное значение во время соединения.
Я не гуру NASM, но я думаю, что следующий код должен сделать это:
section .data
msg db 'This is a test', 10, 0 ; something stupid here
section .text
global _main
extern _printf
_main:
push rbp
mov rbp, rsp
xor al, al
lea rdi, [rel msg]
call _printf
mov rsp, rbp
pop rbp
ret
Пока нет ответа, объяснил, почему отчеты NASM
Mach-O 64-bit format does not support 32-bit absolute addresses
Причина, по которой NASM этого не делает, объясняется в руководстве Agner Fog по оптимизации сборки в разделе 3.3 "Режимы адресации" в подразделе " 32-разрядная абсолютная адресация в 64-разрядном режиме", который он пишет.
32-разрядные абсолютные адреса нельзя использовать в Mac OS X, где адреса больше 2^32 по умолчанию.
Это не проблема в Linux или Windows. На самом деле я уже показал, что это работает на static-linkage-with-glibc-без-call-main. Этот привет код мира использует 32-битную абсолютную адресацию с elf64 и работает нормально.
@HristoIliev предложил использовать относительную адресацию rip, но не объяснил, что 32-разрядная абсолютная адресация в Linux также будет работать. На самом деле, если вы измените lea rdi, [rel msg]
в lea rdi, [msg]
он собирается и работает нормально nasm -efl64
но терпит неудачу с nasm -macho64
Как это:
section .data
msg db 'This is a test', 10, 0 ; something stupid here
section .text
global _main
extern _printf
_main:
push rbp
mov rbp, rsp
xor al, al
lea rdi, [msg]
call _printf
mov rsp, rbp
pop rbp
ret
Вы можете проверить, что это абсолютный 32-битный адрес, а не рип относительно objdump
, Тем не менее, важно отметить, что предпочтительным методом по-прежнему является относительная адресация. Агнер в том же руководстве пишет:
Нет абсолютно никакой причины использовать абсолютные адреса для простых операндов памяти. Rip-относительные адреса делают инструкции короче, они устраняют необходимость перемещения во время загрузки и безопасны для использования во всех системах.
Так когда же использовать 32-битные абсолютные адреса в 64-битном режиме? Статические массивы - хороший кандидат. См. Следующий подраздел " Адресация статических массивов в 64-битном режиме". Простой случай будет, например:
mov eax, [A+rcx*4]
где A - абсолютный 32-битный адрес статического массива. Это прекрасно работает с Linux, но еще раз вы не можете сделать это с Mac OS X, потому что база изображений больше, чем 2^32 по умолчанию. Для этого в Mac OS X см. Примеры 3.11c и 3.11d в руководстве Агнера. В примере 3.11c вы можете сделать
mov eax, [(imagerel A) + rbx + rcx*4]
Где вы используете внешнюю ссылку от Mach O __mh_execute_header
чтобы получить базу изображений. В примере 3.11c вы используете относительную адресацию rip и загружаете адрес следующим образом
lea rbx, [rel A]; rel tells nasm to do [rip + A]
mov eax, [rbx + 4*rcx] ; A[i]
Согласно документации для 64-битного набора команд x86 http://download.intel.com/products/processor/manual/325383.pdf
PUSH принимает только 8, 16 и 32-битные непосредственные значения (хотя разрешены 64-битные регистры и регистры адресованных блоков памяти).
PUSH msg
Где msg является 64-битным непосредственным адресом, не скомпилируется, как вы узнали.
Какое соглашение о вызовах определено _printf в вашей 64-битной библиотеке?
Ожидается ли параметр в стеке или используется соглашение о быстрых вызовах, когда параметры включены в регистрах? Поскольку x86-64 делает более доступными регистры общего назначения, соглашение о быстрых вызовах используется чаще.