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 предоставляет более общие регистры общего назначения, чаще используется соглашение быстрого вызова.