Вызов printf в x86 64 с помощью ассемблера GNU

Я написал программу, использующую синтаксис AT&T для использования с GNU assembler:

            .data
format:   .ascii "%dn"  

            .text
            .global main  
main:
            mov    $format, %rbx
            mov    (%rbx), %rdi
            mov    , %rsi
            call     printf
            ret

Я использую GCC собрать и связать с:

GCC-o main main.s

я запускаю его с помощью этой команды:

./ main

когда я запускаю программу, я получаю ошибку seg. Используя gdb, он говорит printf не нашли. Я пытался ".extern printf", который не работает. Кто-то предложил мне хранить указатель стека перед вызовом printf и восстановить перед RET, как мне это сделать?

2 ответов


есть несколько проблем с этим кодом. The amd64 System V ABI соглашение о вызове, используемое Linux, требует нескольких вещей. Это требует, что непосредственно перед вызов чтобы стек был по крайней мере 16-байтовым (или 32-байтовым) выровненным:

конец области входного аргумента должен быть выровнен по 16 (32, Если __m256 передано по стеку) граница байта.

после C среда выполнения вызывает ваш main функция стек смещен на 8, потому что указатель возврата был помещен в стек на вызов. Перестроить до 16-байтовой границы можно просто пуш любой регистр общего назначения в стек и поп его в конце.

соглашение о вызове также требует, чтобы АЛ содержит количество векторных регистров, используемых для функции переменного аргумента:

%al используется для укажите количество векторных аргументов, переданных функции, требующей переменного числа аргументов

printf является функцией переменного аргумента, поэтому АЛ необходимо установить. В этом случае вы не передаете никаких параметров в векторном регистре, поэтому вы можете установить АЛ к 0.

вы также разыменовываете указатель $format, когда он уже является адресом. Так что это неправильно:

mov  $format, %rbx
mov  (%rbx), %rdi 

это принимает адрес формата и помещает его в RBX. Затем вы берете 8 байт по этому адресу в RBX и поместите их в RDI. RDI должен быть указатель к строке символов, а не сами символы. Эти две строки можно было заменить на:

lea  format(%rip), %rdi

это использует относительную адресацию RIP.

вы должны также нуль завершить ваши строки. Вместо использования .ascii можно использовать .asciz на платформе x86.

рабочая версия вашей программы может выглядеть так:

# global data  #
    .data
format: .asciz "%d\n"
.text
    .global main
main:
  push %rbx
  lea  format(%rip), %rdi
  mov  , %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, что соглашение о вызовах также требует функций, которые вы пишете, чтобы уважать сохранение определенных регистров. Список регистров и следует ли их сохранять выглядит следующим образом:

enter image description here

любой регистр, который говорит Yes на сохраняется между Регистрация столбец-это те, которые вы должны убедиться, что они сохранены в вашей функции. Функция main - это как и любой другой C С .section .rodata, а не .data


в 64-разрядном режиме: если у вас есть операнд назначения, который является 32-разрядным регистром, процессор будет нулевым регистр по всему 64-разрядному регистру. Это может сохранить байты на кодировке инструкции.


возможно, ваш исполняемый файл компилируется как позиционно-независимый код. Может появиться сообщение об ошибке:

перемещение R_X86_64_PC32 против символа `printf@@GLIBC_2.2.5 ' не может использоваться при создании общего объекта; перекомпилировать с-fPIC

чтобы исправить это, вам придется вызвать внешнюю функцию printf этот путь:

call printf@plt 

это вызывает функцию внешней библиотеки через таблица рычага процедуры (PLT)


вы можете посмотреть на ассемблерный код, генерируемый из эквивалентной файл c.
Бег!--3--> С испытанием.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    , %esi
        movl    $.LC0, %edi
        movl    , %eax
        call    printf
        popq    %rbp
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 6.1.1 20160602"
        .section        .note.GNU-stack,"",@progbits

это дает вам пример кода сборки, вызывающего printf,который вы можете изменить.


по сравнению с вашим кодом, вы должны изменить 2 вещи:

  • %rdi должен указывать на формат, вы не должны unreferenced %rbx, это можно сделать с помощью mov $format, %rdi
  • printf имеет переменное количество аргументов, тогда вы должны добавить mov , %eax

применение этих модификаций даст что-то вроде :

    .data
format: .ascii "%d\n"  
.text
    .global main  
main:
  mov  $format, %rdi
  mov  , %rsi
  mov  , %eax
  call printf
  ret

и затем запустите его print:

1