Вызов 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

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