GNU as, puts работает, а 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!"

Окончательная редакция (работы):

    .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!"

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

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

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


Обзор:

  1. printf получает строку печати из %rdi и количество дополнительных аргументов в нижнем DWORD %rax.
  2. Результаты printf нельзя увидеть до тех пор, пока в стандартный вывод не будет введена новая строка или не будет вызвана функция fflush(0).

person Dmitry    schedule 14.04.2016    source источник
comment
Если вы работаете в Linux, то при вызове printf (и других функций, которые принимают переменное количество аргументов) вам нужно загрузить AL с количеством аргументов, которые вы передаете в регистры XMM. В этом случае число будет равно 0.   -  person Ross Ridge    schedule 14.04.2016
comment
что такое регистр AL в 64-битной системе?   -  person Dmitry    schedule 14.04.2016
comment
Это младшие 8 бит регистра RAX. Вы можете обратиться к нему с помощью %al, например. mov $0, %al хотя xor %eax,%eax в данном случае, вероятно, предпочтительнее.   -  person Ross Ridge    schedule 14.04.2016
comment
Я понял это, я был немного сбит с толку, передавая 0 в %rax, чувствовал, что ничего не происходит, но я вспомнил, что должен был попробовать сбросить его после печати. Обновлен OP с текущим решением.   -  person Dmitry    schedule 14.04.2016
comment
Я обновил свой ответ некоторыми улучшениями и комментариями к вашей окончательной версии.   -  person Peter Cordes    schedule 15.04.2016


Ответы (1)


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

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


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

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


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

.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's 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. Но поскольку нам нужно было настроить стек на 8 байт, чтобы строго следовать гарантии ABI, выровненной по 16 байтам, мы могли бы сделать это, сохранив регистр, сохраняемый вызовами. mov reg,reg очень дешев и мал, поэтому вполне естественно воспользоваться преимуществами регистрации с сохранением вызовов.

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

person Peter Cordes    schedule 14.04.2016
comment
кстати, почему в этом примере используется xor? Я вижу, что это часто используется, но не могу понять причину этого. Это дешевле? - person Dmitry; 15.04.2016
comment
@Dmitry: я уже внес изменения в текст, добавив ссылку на xor. - person Peter Cordes; 15.04.2016
comment
Виноват. Я запомню эту идиому. Хотя теперь у меня возникает соблазн заменить все мои 0 присваиваний в C на ^=... Я не уверен, насколько хороша эта идея. Спасибо за решение, многое нужно понять. Я потрачу некоторое время на его изучение. - person Dmitry; 15.04.2016
comment
@Dmitry: это ужасная идея для C. Это оптимально для x86, а не вообще. На большинстве архитектур reg = reg^reg будет иметь ложную зависимость от старого значения. Это делает ваш код менее читабельным, и это невозможно сделать без использования исходного кода C неинициализированного значения. (компиляторам это не нравится. обратите внимание на опечатку в этот комментарий: это должно быть _mm_undefined_si128(), а не _mm_uninitialized_si128().) - person Peter Cordes; 15.04.2016
comment
В любом случае, это просто ужасно для удобочитаемости. Это стандартная идиома для x86, а не для C. Компиляторы знают об этом и сделают это правильно. Они всегда будут выдавать xor same,same вместо mov $0, %reg, за исключением -O0, когда они не проверяют эту оптимизацию глазка. - person Peter Cordes; 15.04.2016
comment
еще интересно понять. Мне нравится иметь возможность прочитать, что производит компилятор, и понять его оптимизацию или отсутствие. - person Dmitry; 15.04.2016
comment
@Dmitry: да, конечно. Вы можете многому научиться, понимая вывод компилятора из довольно простых функций. Однако было бы ошибкой отвернуться и применить некоторые приемы к исходному коду C. В этом случае одним из многих аспектов этой ошибки является то, что это только то, как x86 делает что-то, а не оптимально, например. ARM или MIPS. (в MIPS вы обнуляете r3 с помощью addiu r3, r0, #0: добавляете немедленное нулевое значение к архитектурному нулевому регистру и сохраняете результат в r3. Нет move insn, просто псевдооперативная мнемоника для него. Было бы так же глупо писать a = b+0 вместо a = b в C, однако) - person Peter Cordes; 15.04.2016
comment
@Dmitry: На самом деле это не совсем так, что вы не должны применять то, что вы изучаете, к C. Иногда вы можете изменить C, чтобы упростить/возможность для компилятора использовать специфичные для арки оптимизации глазка, такие как LEA. Однако в более общем плане вы можете структурировать код так, чтобы он хорошо работал на ассемблере. Если вы знаете, что ввод не может быть равен нулю, вы можете использовать цикл do { } while() вместо цикла while(), чтобы компилятору не нужно было проверять, выполняется ли цикл хотя бы один раз. Могут помочь подъемные инварианты циклов, а также ручное развертывание с ручным введением/эпилогом для нечетных чисел. - person Peter Cordes; 15.04.2016
comment
Еще один вопрос: как вызвать printf(%u, a_number_in_data_section); в сборе? Мне не удается это сделать, я, кажется, неправильно понимаю роль %eax здесь. - person Dmitry; 15.04.2016
comment
хорошо, я посмотрел на вывод компилятора и обнаружил, что rax по-прежнему равен 0 для printf с 2 аргументами (я думал, что мне нужно передать 1). Все, что мне нужно было сделать, это передать строку как rdi, второй аргумент как rsi, 0 как rax и вызвать printf. Спасибо! - person Dmitry; 16.04.2016
comment
@Дмитрий: ага. %al — это количество аргументов с плавающей запятой (переданных в регистрах xmm) в функцию var-args. Все еще ноль, поскольку указатели и целое число без знака являются целыми аргументами. - person Peter Cordes; 16.04.2016
comment
@Bulat: Попробуйте еще раз отредактировать, я заметил пару вещей после того, как пришел посмотреть, как обстоят дела с этим ответом после получения уведомления о положительном голосовании, и мое редактирование перезаписало ваше. - person Peter Cordes; 19.09.2016