Печать целого числа в виде строки с синтаксисом 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 для визуализации ее работы до тех пор, пока функция цикла, но когда она появится в следующем, какое-то случайное значение начнет вводиться в различные регистры. Помогите мне отлаживать, чтобы он мог печатать факториал.
- Почему GCC не использует частичные регистры?
- 64-разрядный формат Mach-O не поддерживает 32-разрядные абсолютные адреса. Доступ к массиву NASM
- Почему инструкции x86-64 на 32-разрядных регистрах обнуляют верхнюю часть полного 64-битного регистра?
- gfortran для макетов: что делает mcmodel = medium?
- Использование дополнительных 16 бит в 64-битных указателях
- Последовательные sys_write syscalls не работают должным образом, ошибка NASM на OS X?
- x86_64 регистры rax / eax / ax / al, переписывающие содержимое полного регистра
- System.BadImageFormatException: не удалось загрузить файл или сборку (из installutil.exe)
- Атомная двойная с плавающей запятой или векторная загрузка / сохранение SSE / AVX на x86_64
- Почему GCC использует умножение на странное число при реализации целочисленного деления?
- Постройте Multiarch OpenSSL на OS X
- __builtin_prefetch, сколько он читает?
- Как напечатать одноточечный поплавок с printf
Несколько вещей:
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.