GNU as, ставит работы, но printf не

Это код, который я сейчас играю:

# file-name: test.s
# 64-bit GNU as source code.
    .global main

    .section .text
main:
    lea message, %rdi
    push %rdi
    call puts

    lea message, %rdi
    push %rdi
    call printf

    push $0
    call _exit

    .section .data
message: .asciz "Hello, World!"

Инструкции по компиляции: gcc test.s -o test

Версия 1:

    .global main
    .section .text
main:
    lea message, %rdi
    call puts

    lea message, %rdi
    call printf

    mov $0, %rdi
    call _exit

    .section .data
message: .asciz "Hello, World!"

Окончательный вариант (Works):

    .global main
    .section .text
main:
    lea message, %rdi
    call puts

    mov $0, %rax
    lea message, %rdi
    call printf

    # flush stdout buffer.
    mov $0, %rdi
    call fflush

    # put newline to offset PS1 prompt when the program ends.  
    # - ironically, doing this makes the flush above redundant and can be removed.
    # - The call to  fflush is retained for display and 
    #      to keep the block self contained.  
    mov $'\n', %rdi
    call putchar

    mov $0, %rdi
    call _exit

    .section .data
message: .asciz "Hello, World!"

Я изо всех сил пытаюсь понять, почему вызов ставится успешно, но вызов printf приводит к ошибке сегментации.

Может кто-нибудь объяснить это поведение и как printf предназначен для вызова?

Спасибо заранее.


Резюме

  • printf получает строку печати из% rdi и число дополнительных аргументов в% rax lower DWORD.
  • Результаты printf не могут быть просмотрены до тех пор, пока в stdout не будет введена новая строка, или вызывается fflush (0).

Ответ 1

puts неявно добавляет новую строку, а stdout - буферизируется по строке (по умолчанию на терминалах). Поэтому текст из printf может просто сидеть там в буфере. Ваш вызов _exit(2) не очищает буферы, потому что это exit_group(2), а не exit(3) функция библиотеки. (См. Мою версию вашего кода ниже).

Ваш вызов printf(3) также не совсем прав, потому что вы не сделали нуль %al перед вызовом функции var-args без аргументов FP. (Хорошо поймать @RossRidge, я пропустил это). xor %eax,%eax - лучший способ сделать это. %al будет отличным от нуля (из puts() возвращаемого значения), что, по-видимому, связано с тем, что printf segfaults. Я тестировал в своей системе, и printf, кажется, не возражает, когда стек смещается (что есть, поскольку вы дважды нажали, прежде чем называть его, в отличие от puts).


Кроме того, вам не нужны инструкции push в этом коде. Первый arg находится в %rdi. Первые 6 целых аргументов идут в регистры, 7-е и более поздние - в стек. Вы также пренебрегаете появлением стека после возвращения функций, что работает только потому, что ваша функция никогда не пытается вернуться после испорчения стека.

ABI требует выравнивания стека на 16В, а push - это один из способов сделать это, который на самом деле может быть более эффективным, чем sub $8, %rsp на последних процессорах Intel с движком стека, и он занимает меньше байтов. (См. x86-64 SysV ABI и другие ссылки в x86 tag wiki).


Улучшенный код:

.text
.global main
main:
    lea     message, %rdi     # or  mov $message, %edi  if you don't need the code to be position-independent: default code model has all labels in the low 2G, so you can use shorter 32bit instructions
    push    %rbx              # align the stack for another call
    mov     %rdi, %rbx        # save for later
    call   puts

    xor     %eax,%eax         # %al = 0 = number of FP args for var-args functions
    mov     %rbx, %rdi        # or mov %ebx, %edi  will normally be safe, since the pointer is known to be pointing to static storage, which will be in the low 2G
    call   printf

    # optionally putchar a '\n', or include it in the string you pass to printf

    #xor    %edi,%edi    # exit with 0 status
    #call  exit          # exit(3) does an fflush and other cleanup

    pop     %rbx         # restore caller rbx, and restore the stack

    xor     %eax,%eax    # return 0
    ret

    .section .rodata     # constants should go in .rodata
message: .asciz "Hello, World!"

lea message, %rdi дешево, и для этого дважды требуется меньшее количество инструкций, чем две инструкции mov, чтобы использовать %rbx. Но поскольку нам нужно было отрегулировать стек на 8B, чтобы строго следовать гарантии ABI 16B, мы могли бы также сделать это, сохранив регистр с сохранением вызова. mov reg,reg является очень дешевым и маленьким, поэтому использование сохраненного вызова является естественным.

Использование mov %edi, %ebx и прочее подобное сохраняет префикс REX в кодировке машинного кода. Если вы не уверены/не понимаете, почему безопасно копировать только низкие 32 бит, обнуляя верхние 32b, тогда используйте 64-битные регистры. Как только вы поймете, что происходит, вы узнаете, когда вы можете сохранить байты машинного кода, используя 32-разрядный размер операнда.