Печать целого числа в виде строки с синтаксисом 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. Это называется red-zone.
Вместо системных номеров системного кодирования использование 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.