Как напечатать целое число в программировании уровня сборки без printf в библиотеке c?
Может ли кто-нибудь сказать мне чисто ассемблерный код для отображения значения в регистре в десятичном формате? Пожалуйста, не предлагайте использовать printf hack, а затем скомпилировать с помощью gcc.
Описание:
Хорошо, я провел некоторое исследование и несколько экспериментов с NASM и решил, что могу использовать функцию printf из библиотеки c для печати целого числа. Я сделал это путем компиляции объектного файла с помощью компилятора GCC, и все работает достаточно справедливо.
Однако я хочу, чтобы напечатать значение, хранящееся в любом регистре в десятичной форме.
Я провел некоторое исследование и вычислил, что вектор прерывания 021h для командной строки DOS может отображать строки и символы, а либо 2, либо 9 - в регистре ah, а данные - в dx.
Вывод:
Ни один из примеров, которые я нашел, не показал, как отображать значение содержимого регистра в десятичной форме без использования библиотеки C printf. Кто-нибудь знает, как это сделать в сборке?
Ответы
Ответ 1
Вам нужно написать процедуру преобразования двоичного кода в десятичный код, а затем использовать десятичные цифры для создания "цифровых символов" для печати.
Вы должны предположить, что что-то, где-то, напечатает символ на вашем выходном устройстве по выбору. Вызовите эту подпрограмму "print_character"; предполагает, что он принимает код символа в EAX и сохраняет все регистры.. (Если у вас нет такой подпрограммы, у вас есть дополнительная проблема, которая должна быть основой другого вопроса).
Если у вас есть двоичный код для цифры (например, значение от 0 до 9) в регистре (скажем, EAX), вы можете преобразовать это значение в символ для цифры, добавив код ASCII для "ноль" в регистр. Это так же просто, как:
add eax, 0x30 ; convert digit in EAX to corresponding character digit
Затем вы можете вызвать print_character для печати символьного кода.
Чтобы вывести произвольное значение, вам нужно выбрать цифры и распечатать их.
Выбор цифр принципиально требует работы с десятью десятью. Проще всего работать с одной мощностью десять, например, самой 10. Представьте, что у нас есть подпрограмма "разделить на 10", которая взяла значение в EAX, и произвела коэффициент в EDX и остальную часть EAX. Я оставляю это как упражнение для вас, чтобы выяснить, как реализовать такую процедуру.
Тогда простая процедура с правильной идеей состоит в том, чтобы произвести одну цифру для всех цифр, которые могут иметь значение. 32-битный регистр хранит значения до 4 миллиардов, поэтому вы можете напечатать 10 цифр. Итак:
mov eax, valuetoprint
mov ecx, 10 ; digit count to produce
loop: call dividebyten
add eax, 0x30
call printcharacter
mov eax, edx
dec ecx
jne loop
Это работает... но печатает цифры в обратном порядке. К сожалению! Ну, мы можем воспользоваться стеком pushdown для хранения произведенных цифр, а затем вывести их в обратном порядке:
mov eax, valuetoprint
mov ecx, 10 ; digit count to generate
loop1: call dividebyten
add eax, 0x30
push eax
mov eax, edx
dec ecx
jne loop1
mov ecx, 10 ; digit count to print
loop2: pop eax
call printcharacter
dec ecx
jne loop2
Слева как упражнение для читателя: подавляйте начальные нули. Кроме того, поскольку мы пишем цифры в памяти, вместо того, чтобы записывать их в стек, мы можем записать их в буфер, а затем распечатать содержимое буфера. Также оставлен в качестве упражнения для читателя.
Ответ 2
Большинство операционных систем/сред не имеют системного вызова, который принимает целые числа и преобразует их в десятичные для вас. Вы должны сделать это самостоятельно, прежде чем отправлять байты в ОС или копировать их в видеопамять самостоятельно или рисовать соответствующие символы шрифта в видеопамяти...
Самым эффективным способом является создание единого системного вызова, который выполняет всю строку сразу, поскольку системный вызов, который записывает 8 байтов, в основном такой же, как и запись 1 байта.
Это означает, что нам нужен буфер, но это совсем не добавляет нашей сложности. 2 ^ 32-1 - это только 4294967295, что составляет всего 10 десятичных цифр. Наш буфер не должен быть большим, поэтому мы можем просто использовать стек.
Обычный алгоритм генерирует цифры LSD-first. Поскольку порядок печати MSD-first, мы можем просто начать работу в конце буфера и работать в обратном направлении. Для печати или копирования в другом месте просто отслеживайте, где он начинается, и не беспокойтесь о том, чтобы получить его до начала фиксированного буфера. Не нужно возиться с push/pop, чтобы отменить что-либо, просто произведите его в обратном направлении.
char *itoa_end(unsigned long val, char *p_end) {
const unsigned base = 10;
char *p = p_end;
do {
*--p = (val % base) + '0';
val /= base;
} while(val); // runs at least once to print '0' for val=0.
// write(1, p, p_end-p);
return p; // let the caller know where the leading digit is
}
gcc/clang проделайте отличную работу, используя множитель магической константы вместо div
, чтобы делить на 10 эффективно. (Исследователь компилятора Godbolt для выхода asm).
Здесь простая прокомментированная версия NASM, использующая div
(медленный, но более короткий код) для 32-разрядных целых без знака и системный вызов Linux write
. Это должно быть легко переносить на 32-битный режим, просто изменив регистры на ecx
вместо rcx
. (Вы также должны сохранить/восстановить esi
для обычных 32-битных соглашений вызова, если только вы не делаете это в макрос или функцию внутреннего использования.)
Часть системного вызова специфична для 64-разрядной Linux. Замените это на все, что подходит для вашей системы, например. вызовите страницу VDSO для эффективных системных вызовов в 32-разрядной Linux или используйте int 0x80
непосредственно для неэффективных системных вызовов. См. соглашения о вызовах для 32 и 64-разрядных системных вызовов в Unix/Linux.
ALIGN 16
; void print_uint32(uint32_t edi)
; x86-64 System V calling convention. Clobbers RSI, RCX, RDX, RAX.
global print_uint32
print_uint32:
mov eax, edi ; function arg
mov ecx, 0xa ; base 10
push rcx ; newline = 0xa = base
mov rsi, rsp
sub rsp, 16 ; not needed on 64-bit Linux, the red-zone is big enough. Change the LEA below if you remove this.
;;; rsi is pointing at '\n' on the stack, with 16B of "allocated" space below that.
.toascii_digit: ; do {
xor edx, edx
div ecx ; edx=remainder = low digit = 0..9. eax/=10
;; DIV IS SLOW. use a multiplicative inverse if performance is relevant.
add edx, '0'
dec rsi ; store digits in MSD-first printing order, working backwards from the end of the string
mov [rsi], dl
test eax,eax ; } while(x);
jnz .toascii_digit
;;; rsi points to the first digit
mov eax, 1 ; __NR_write from /usr/include/asm/unistd_64.h
mov edi, 1 ; fd = STDOUT_FILENO
lea edx, [rsp+16 + 1] ; yes, it safe to truncate pointers before subtracting to find length.
sub edx, esi ; length, including the \n
syscall ; write(1, string, digits + 1)
add rsp, 24 ; undo the push and the buffer reservation
ret
Общественное достояние. Не стесняйтесь копировать/вставлять это во все, над чем вы работаете. Если он ломается, вы можете сохранить обе части.
И вот код, чтобы вызвать его в цикле, считая до 0 (включая 0). Поместить его в тот же файл удобно.
ALIGN 16
global _start
_start:
mov ebx, 100
.repeat:
lea edi, [rbx + 0] ; put whatever constant you want here.
call print_uint32
dec ebx
jge .repeat
xor edi, edi
mov eax, 231
syscall ; sys_exit_group(0)
Соберите и соединитесь с
yasm -felf64 -Worphan-labels -gdwarf2 print-integer.asm &&
ld -o print-integer print-integer.o
./print_integer
100
99
...
1
0
Используйте strace
, чтобы увидеть, что только системные вызовы этой программы составляют write()
и exit()
. (См. Также подсказки gdb/debugging в нижней части x86, а также другие ссылки.)
Я разместил версию AT & T-синтаксиса для 64-битных целых чисел в качестве ответа на
Печать целого числа в виде строки с синтаксисом AT & T с системными вызовами Linux вместо printf. См. Это для получения дополнительных комментариев о производительности и эталоном кода div
по сравнению с компилятором с помощью mul
.
Ответ 3
Не могу комментировать, поэтому я отправляю ответ таким образом.
@Ira Baxter, отличный ответ. Я просто хочу добавить, что вам не нужно разделить 10 раз, когда вы разместили, что вы установили регистр cx в значение 10. Просто разделите число в топе, пока "ax == 0"
loop1: call dividebyten
...
cmp ax,0
jnz loop1
Вам также нужно сохранить количество цифр в исходном номере.
mov cx,0
loop1: call dividebyten
inc cx
В любом случае, вы помогли мне Ирак Бакстер, есть только несколько способов оптимизации кода:)
Это не только оптимизация, но и форматирование. Если вы хотите напечатать номер 54, вы хотите напечатать 54 не 0000000054:)
Ответ 4
Я предполагаю, что вы хотите напечатать значение для stdout? Если это так.
для этого вам необходимо использовать системный вызов . Системные вызовы зависят от ОС.
например. Linux:
Таблица системных вызовов Linux
Всемирная программа hello в этом Tutorial может дать вам некоторые идеи.
Ответ 5
1 -9 равны 1 -9. после этого должно быть какое-то преобразование, о котором я тоже не знаю. Скажем, у вас есть 41H в AX (EAX), и вы хотите распечатать 65, а не "A", не выполняя какой-либо служебный вызов. Я думаю, вам нужно напечатать представление символа 6 и 5, что бы это ни было. Должно быть постоянное число, которое можно добавить туда. Вам нужен оператор модуля (как вы это делаете в сборке) и цикл для всех цифр.
Не уверен, но это мое предположение.