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
Надеюсь, кто-нибудь знает, что я делаю неправильно.
Ответы
Ответ 1
64-битный OS X ABI в целом соответствует System V ABI - дополнению к процессору архитектуры AMD64. Его модель кода очень похожа на модель кода, независимую от малого положения (PIC), с различиями, объясненными здесь. В этой модели кода все локальные и малые данные доступны напрямую с использованием адресации RIP -relative. Как отмечается в комментариях Z boson, база изображений для 64-битных исполняемых файлов Mach-O находится за пределами первых 4 ГиБ виртуального адресного пространства, поэтому push msg
- это не только неверный способ поместить адрес msg
в стек, но это также невозможно, поскольку PUSH
не поддерживает 64-битные непосредственные значения. Код должен выглядеть примерно так:
; this is what you *would* do for later args on the stack
lea rax, [rel msg] ; RIP-relative addressing
push rax
Но в этом конкретном случае не нужно вообще помещать значение в стек. 64-битное соглашение о вызовах требует, чтобы первые 6 целочисленных/указательных аргументов передавались в регистрах RDI
, RSI
, RDX
, RCX
, R8
и R9
, именно в этом порядке. Первые 8 аргументов с плавающей точкой или вектор XMM0
в XMM0
, XMM1
,..., XMM7
. Только после того, как будут использованы все доступные регистры или есть аргументы, которые не могут поместиться ни в один из этих регистров (например, long double
значение long double
80 бит), используется стек. 64-битные немедленные QWORD
выполняются с использованием MOV
(вариант QWORD
), а не PUSH
. Простые возвращаемые значения передаются обратно в регистр RAX
. Вызывающая сторона также должна предоставить стековую область для вызываемой стороны, чтобы сохранить некоторые регистры.
printf
- это специальная функция, потому что она принимает переменное число аргументов. При вызове таких функций AL
(младший байт RAX) должен быть установлен в число аргументов с плавающей запятой, передаваемых в векторных регистрах. Также обратите внимание, что адресация RIP
-relative предпочтительна для данных, которые находятся в пределах 2 Гбайт кода.
Вот как gcc
переводит printf("This is a test\n");
в сборку на OS X:
xorl %eax, %eax # (1)
leaq L_.str(%rip), %rdi # (2)
callq _printf # (3)
L_.str:
.asciz "This is a test\n"
(это сборка в стиле AT & T, источник слева, место назначения справа, имена регистров имеют префикс %
, ширина данных кодируется как суффикс к имени инструкции)
В (1)
ноль помещается в AL
(путем обнуления всего RAX, что позволяет избежать частичных задержек в регистре), поскольку аргументы с плавающей точкой не передаются. На (2)
адрес строки загружается в RDI
. Обратите внимание, что значение на самом деле является смещением от текущего значения RIP
. Поскольку ассемблер не знает, каким будет это значение, он помещает запрос на перемещение в объектный файл. Затем компоновщик видит перемещение и устанавливает правильное значение во время ссылки.
Я не гуру NASM, но я думаю, что следующий код должен сделать это:
default rel ; make [rel msg] the default for [msg]
section .data
msg: db 'This is a test', 10, 0 ; something stupid here
section .text
global _main
extern _printf
_main:
push rbp ; re-aligns the stack by 16 before call
mov rbp, rsp
xor eax, eax ; al = 0 FP args in XMM regs
lea rdi, [rel msg]
call _printf
mov rsp, rbp
pop rbp
ret
Ответ 2
Ответ пока не объяснил, почему отчеты NASM
Mach-O 64-bit format does not support 32-bit absolute addresses
Причина, по которой NASM этого не сделает, объясняется в Руководство по оптимизации Agner Fog Optimization в разделе 3.3. Режимы адресации в подразделе под названием 32- бит абсолютной адресации в 64-битном режиме он пишет
32-разрядные абсолютные адреса не могут использоваться в Mac OS X, где адреса выше 2 ^ 32 на по умолчанию.
Это не проблема для Linux или Windows. Фактически, я уже показывал, что это работает на static-linkage-with-glibc-without-calling-main. Этот мировой код hello использует 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-разрядный адрес, а не rip relative с 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 в руководстве Agner. В примере 3.11c вы можете сделать
mov eax, [(imagerel A) + rbx + rcx*4]
Если вы используете ссылку extern из 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]
Ответ 3
В соответствии с документацией для 64-разрядного набора команд x86 http://download.intel.com/products/processor/manual/325383.pdf
PUSH принимает только 8, 16 и 32-битные значения (допускаются 64-битные регистры и регистровые блоки памяти).
PUSH msg
Если msg - это 64-битный адрес, который не будет скомпилирован, как вы узнали.
Какое соглашение о вызове - _printf определено как в вашей 64-битной библиотеке?
Ожидает ли этот параметр в стеке или использует соглашение быстрого вызова, где параметры в регистре? Поскольку x86-64 предоставляет более общие регистры общего назначения, чаще используется соглашение быстрого вызова.