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

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

.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 для визуализации ее работы до тех пор, пока функция цикла, но когда она появится в следующем, какое-то случайное значение начнет вводиться в различные регистры. Помогите мне отлаживать, чтобы он мог печатать факториал.

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

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

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

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

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's no POP jmp next 

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

Как указывает @ 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  // 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: в моем ответе на код Extreme Fibonacci . Он оптимизирован для размера кода (даже за счет скорости), но хорошо комментируется.

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

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


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

См. Вывод gcc7.2-O3 asm, который является почти заменой для цикла в 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 цифры. Это даст намного лучший параллелизм на уровне инструкций, в случае более простых узких мест в версии с задержкой mul + shr . Цепочка операций умножения / сдвига, которая приносит val к нулю, будет наполовину длиннее, при этом больше работы в каждой короткой независимой цепочке зависимостей обрабатывать остаток 0-99.

  • Почему системы x86-64 имеют только 48-битное виртуальное адресное пространство?
  • Может ли x86 переупорядочить узкий магазин с более широкой нагрузкой, которая полностью его содержит?
  • Приобретать / выпускать семантику с невременными магазинами на x64
  • Адресная каноническая форма и арифметика указателя
  • Имеет ли смысл использовать инструкцию LFENCE для процессоров x86 / x86_64?
  • Почему неуправляемый доступ к памяти mmap'а иногда может возникнуть на AMD64?
  • Как найти, если собственный DLL-файл скомпилирован как x64 или x86?
  • x86_64 - Условия сборки и выход из строя
  • Почему% eax обнуляется перед вызовом printf?
  • Что означают префиксы E и R в именах 32-битных и 64-битных регистров Intel?
  • Что будет использоваться для обмена данными между streamами, выполняются на одном ядре с HT?
  • Давайте будем гением компьютера.