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

Другие вопросы по тегам