Печать целого числа в виде строки с синтаксисом AT & T с системными вызовами Linux вместо printf

Я написал программу сборки для отображения факториала числа, следующего за AT и t синтаксисом. Но это не работает. Вот мой код

.text 

.globl _start

_start:
movq $5,%rcx
movq $5,%rax


Repeat:                     #function to calculate factorial
   decq %rcx
   cmp $0,%rcx
   je print
   imul %rcx,%rax
   cmp $1,%rcx
   jne Repeat
# Now result of factorial stored in rax
print:
     xorq %rsi, %rsi

  # function to print integer result digit by digit by pushing in 
       #stack
  loop:
    movq $0, %rdx
    movq $10, %rbx
    divq %rbx
    addq $48, %rdx
    pushq %rdx
    incq %rsi
    cmpq $0, %rax
    jz   next
    jmp loop

  next:
    cmpq $0, %rsi
    jz   bye
    popq %rcx
    decq %rsi
    movq $4, %rax
    movq $1, %rbx
    movq $1, %rdx
    int  $0x80
    addq $4, %rsp
    jmp  next
bye:
movq $1,%rax
movq $0, %rbx
int  $0x80


.data
   num : .byte 5

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

Ответ 1

Несколько вещей:

0) Я предполагаю, что это 64-битная среда linux, но вы должны были заявить об этом (если это не так, некоторые из моих точек будут недействительными)

1) int 0x80 - вызов 32b, но вы используете регистры 64b, поэтому вы должны использовать syscall (и разные аргументы)

2) int 0x80, eax=4 требует, чтобы ecx содержал адрес памяти, в котором хранится контент, в то время как вы передаете ему символ ASCII в ecx= незаконный доступ к памяти (первый вызов должен возвращать ошибку, т.е. eax - отрицательное значение). Или с помощью strace <your binary> следует выявить неправильные аргументы + возвращенную ошибку.

3) почему addq $4, %rsp? Не имеет для меня никакого смысла, вы наносите ущерб rsp, поэтому следующий pop rcx будет ошибочным значением, и в итоге вы будете запускать "вверх" в стек.

... может быть, еще немного, я не отлаживал его, этот список - это просто чтение источника (поэтому я могу даже ошибаться в чем-то, хотя это было бы редко).

Кстати, ваш код работает. Он просто не делает то, что вы ожидали. Но работайте отлично, точно так же, как процессор сконструирован и точно, что вы написали в коде. Достигает ли это то, что вы хотели или имеет смысл, это другая тема, но не вините HW или ассемблера.

... Я могу быстро понять, как может быть исправлена ​​процедура (только частичное исправление хака, по-прежнему нужно переписать для syscall под 64b linux):

  next:
    cmpq $0, %rsi
    jz   bye
    movq %rsp,%rcx    ; make ecx to point to stack memory (with stored char)
      ; this will work if you are lucky enough that rsp fits into 32b
      ; if it is beyond 4GiB logical address, then you have bad luck (syscall needed)
    decq %rsi
    movq $4, %rax
    movq $1, %rbx
    movq $1, %rdx
    int  $0x80
    addq $8, %rsp     ; now rsp += 8; is needed, because there no POP
    jmp  next

Снова не пробовал себя, просто написал его с головы, поэтому дайте мне знать, как это изменило ситуацию.

Ответ 2

Как указывает @ped7g, вы делаете несколько ошибок: используя 32-битный ABI int 0x80 в 64-битном коде и передавая символьные значения вместо указателей на системный вызов write().

Здесь, как печатать целое число в 64-разрядной Linux, простой и несколько эффективный способ. См. Почему GCC использует умножение на странное число в реализации целочисленного деления? для избежания div r64 для деления на 10, потому что это очень медленно (от 21 до 83 циклов на Intel Skylake). Мультипликативный обратный сделает эту функцию эффективной, а не просто "несколько". (Но, конечно, все еще есть место для оптимизации...)

Системные вызовы стоят дорого (возможно, тысячи циклов для write(1, buf, 1)) и делают syscall внутри шагов цикла на регистрах, поэтому они неудобны и неуклюжи, а также неэффективны. Мы должны записать символы в небольшой буфер, в порядке печати (наиболее значимая цифра на самом нижнем адресе) и сделать один системный вызов write().

Но тогда нам нужен буфер. Максимальная длина 64-битного целого составляет всего 20 десятичных цифр, поэтому мы можем просто использовать некоторое пространство стека. В x86-64 Linux мы можем использовать пространство стека ниже RSP (до 128B) без "резервирования" его путем изменения RSP. Это называется .

Вместо системных номеров системного кодирования использование GAS упрощает использование констант, определенных в файлах .h. Обратите внимание на mov $__NR_write, %eax ближе к концу функции. x86-64 SystemV ABI передает аргументы системного вызова в аналогичных регистрах в соглашение о вызовах функций. (Так что это совершенно разные регистры из 32-битного int 0x80 ABI.)

#include <asm/unistd_64.h>    // This is a standard glibc header file
// It contains no C code, only only #define constants, so we can include it from asm without syntax errors.

.p2align 4
.globl print_integer            #void print_uint64(uint64_t value)
print_uint64:
    lea   -1(%rsp), %rsi        # We use the 128B red-zone as a buffer to hold the string
                                # a 64-bit integer is at most 20 digits long in base 10, so it fits.

    movb  $'\n', (%rsi)         # store the trailing newline byte.  (Right below the return address).
    # If you need a null-terminated string, leave an extra byte of room and store '\n\0'.  Or  push $'\n'

    mov    $10, %ecx            # same as  mov $10, %rcx  but 2 bytes shorter
    # note that newline (\n) has ASCII code 10, so we could actually have used  movb %cl to save code size.

    mov    %rdi, %rax           # function arg arrives in RDI; we need it in RAX for div
.Ltoascii_digit:                # do{
    xor    %edx, %edx
    div    %rcx                 #  rax = rdx:rax / 10.  rdx = remainder

                                # store digits in MSD-first printing order, working backwards from the end of the string
    add    $'0', %edx           # integer to ASCII.  %dl would work, too, since we know this is 0-9
    dec    %rsi
    mov    %dl, (%rsi)          # *--p = (value%10) + '0';

    test   %rax, %rax
    jnz  .Ltoascii_digit        # } while(value != 0)
    # If we used a loop-counter to print a fixed number of digits, we would get leading zeros
    # The do{}while() loop structure means the loop runs at least once, so we get "0\n" for input=0

    # Then print the whole string with one system call
    mov   $__NR_write, %eax     # SYS_write, from unistd_64.h
    mov   $1, %edi              # fd=1
    # %rsi = start of the buffer
    mov   %rsp, %rdx
    sub   %rsi, %rdx            # length = one_past_end - start
    syscall                     # sys_write(fd=1 /*rdi*/, buf /*rsi*/, length /*rdx*/); 64-bit ABI
    # rax = return value (or -errno)
    # rcx and r11 = garbage (destroyed by syscall/sysret)
    # all other registers = unmodified (saved/restored by the kernel)

    # we don't need to restore any registers, and we didn't modify RSP.
    ret

Чтобы проверить эту функцию, я помещаю ее в тот же файл, чтобы вызвать ее и выйти:

.p2align 4
.globl _start
_start:
    mov    $10120123425329922, %rdi
#    mov    $0, %edi    # Yes, it does work with input = 0
    call   print_uint64

    xor    %edi, %edi
    mov    $__NR_exit, %eax
    syscall                             # sys_exit(0)

Я построил это в статическом двоичном (без libc):

$ gcc -Wall -nostdlib print-integer.S && ./a.out 
10120123425329922
$ strace ./a.out  > /dev/null
execve("./a.out", ["./a.out"], 0x7fffcb097340 /* 51 vars */) = 0
write(1, "10120123425329922\n", 18)     = 18
exit(0)                                 = ?
+++ exited with 0 +++
$ file ./a.out 
./a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=69b865d1e535d5b174004ce08736e78fade37d84, not stripped

Связанный: цикл с расширенной точностью Linux x86-32, который печатает 9 десятичных цифр из каждой 32-разрядной "конечности": см. .toascii_digit: в моем Экстремальный ответ на кодовое слово Фибоначчи. Он оптимизирован для размера кода (даже за счет скорости), но хорошо комментируется.

Он использует div, как вы, потому что это меньше, чем использование быстрого мультипликативного обратного). Он использует loop для внешнего цикла (более множественного целого для расширенной точности), снова для размера кода за счет скорости.

Он использует 32-битный int 0x80 ABI и печатает в буфер, в котором хранилось "старое" значение Фибоначчи, а не текущее.


Другой способ получить эффективный asm - это компилятор C. Для просто цикла над цифрами посмотрите, какие gcc или clang производят для этого источника C (что в основном используется asm). Исследователь компилятора Godbolt позволяет легко использовать разные варианты и различные версии компиляторов.

См. gcc7.2 -O3 asm output, который является почти заменой для цикла в print_uint64 (потому что я выбрал args, чтобы войти в одни и те же регистры):

void itoa_end(unsigned long val, char *p_end) {
  const unsigned base = 10;
  do {
    *--p_end = (val % base) + '0';
    val /= base;
  } while(val);

  // write(1, p_end, orig-current);
}

Я тестировал производительность на Skylake i7-6700k, комментируя инструкцию syscall и помещая цикл повторения вокруг вызова функции. Версия с mul %rcx/shr $3, %rdx примерно в 5 раз быстрее, чем версия с div %rcx для хранения длинной числовой строки (10120123425329922) в буфер. Версия div выполнялась с 0,25 инструкциями за такт, в то время как версия mul работала с 2,65 инструкциями за такт (хотя требуется еще много инструкций).

Это может стоить разворачиваться на 2 и делиться на 100 и разделять оставшуюся часть на 2 цифры. Это даст намного лучший уровень parallelism на уровне инструкций, в случае более простых узких мест в версии mul + shr. Цепочка операций умножения/сдвига, которая приносит val в ноль, будет вдвое длиннее, при этом больше работы в каждой короткой независимой цепочке зависимостей обрабатывать остаток 0-99.