Как напечатать целое число в Программирование на уровне сборки без printf из библиотеки c?
Может кто-нибудь сказать мне чисто ассемблерный код для отображения значения в регистре в десятичном формате? Пожалуйста, не предлагайте использовать взлом printf, а затем скомпилировать с помощью gcc.
Описание:
Ну, я провел некоторые исследования и эксперименты с NASM и решил, что могу использовать функцию printf из библиотеки c для печати целого числа. Я сделал это, скомпилировав объектный файл с помощью компилятора GCC, и все работает достаточно честно.
Однако я хочу напечатать значение, хранящееся в любом регистре, в десятичной форме.
Я провел некоторое исследование и выяснил, что вектор прерывания 021h для командной строки DOS может отображать строки и символы, в то время как 2 или 9 находятся в регистре ах, а данные - в дх.
Заключение:
Ни один из найденных примеров не показал, как отображать значение содержимого регистра в десятичной форме без использования printf библиотеки C. Кто-нибудь знает как это сделать в сборке?
5 ответов
Вам необходимо написать подпрограмму преобразования двоичного числа в десятичное, а затем использовать десятичные цифры для получения "цифровых символов" для печати.
Вы должны предположить, что что-то где-то напечатает символ на выбранном вами устройстве вывода. Назовите эту подпрограмму "print_character"; предполагается, что он принимает код символа в EAX и сохраняет все регистры.. (Если у вас нет такой подпрограммы, у вас есть дополнительная проблема, которая должна быть основой другого вопроса).
Если у вас есть двоичный код для цифры (например, значение от 0 до 9) в регистре (скажем, EAX), вы можете преобразовать это значение в символ для цифры, добавив код ASCII для символа "ноль" в реестр. Это так просто, как:
add eax, 0x30 ; convert digit in EAX to corresponding character digit
Затем вы можете вызвать print_character, чтобы напечатать код символа цифры.
Чтобы вывести произвольное значение, вам нужно выбрать цифры и распечатать их.
Отбор цифр в основном требует работы с десятью степенями. Проще всего работать с одной степенью десяти, например, с десятью. Представьте, что у нас есть процедура деления на 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
Оставлено в качестве упражнения для читателя: подавить ведущие нули. Кроме того, поскольку мы записываем цифры в память, вместо записи их в стек мы можем записать их в буфер, а затем распечатать содержимое буфера. Также оставлено в качестве упражнения для читателя.
В большинстве операционных систем / сред нет системного вызова, который принимает целые числа и преобразует их в десятичные для вас. Вы должны сделать это самостоятельно, прежде чем отправлять байты в ОС, копировать их в видеопамять самостоятельно или рисовать соответствующие шрифты в видеопамяти...
Безусловно, самый эффективный способ - это сделать один системный вызов, который выполняет всю строку за раз, потому что системный вызов, который записывает 8 байтов, в основном стоит столько же, сколько и запись 1 байта.
Это означает, что нам нужен буфер, но это совсем не увеличивает нашу сложность. 2^32-1 - это всего 4294967295, что составляет всего 10 десятичных цифр. Наш буфер не должен быть большим, поэтому мы можем просто использовать стек.
Обычный алгоритм выдает цифры LSD-первых. Поскольку порядок печати сначала MSD, мы можем просто начать с конца буфера и работать в обратном направлении. Для печати или копирования в другом месте, просто следите за тем, где он начинается, и не беспокойтесь о получении его в начале фиксированного буфера. Не нужно возиться с 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's 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
,
Не могу комментировать, поэтому я отправляю ответ таким образом. @ Ира Бакстер, отличный ответ. Я просто хочу добавить, что вам не нужно делить 10 раз, так как вы отправили сообщение, что в регистре cx установлено значение 10. Просто делите число в топоре до "ax==0"
loop1: call dividebyten
...
cmp ax,0
jnz loop1
Вы также должны хранить, сколько цифр было в оригинальном номере.
mov cx,0
loop1: call dividebyten
inc cx
В любом случае, вы, Айра Бакстер, помогли мне - есть несколько способов оптимизировать код:)
Это касается не только оптимизации, но и форматирования. Когда вы хотите напечатать номер 54, вы хотите напечатать 54, а не 0000000054:)
1 -9 являются 1 -9. после этого должно быть какое-то обращение, которого я тоже не знаю. Скажем, у вас есть 41H в AX (EAX) и вы хотите напечатать 65, а не "A" без какого-либо вызова в службу поддержки. Я думаю, что вам нужно напечатать символьное представление 6 и 5, что бы это ни было. Там должно быть постоянное число, которое можно добавить, чтобы попасть туда. Вам нужен оператор модуля (однако вы делаете это в сборке) и цикл для всех цифр.
Не уверен, но это мое предположение.
Я полагаю, вы хотите напечатать значение для stdout? Если это так
Вы должны использовать системный вызов для этого. Системные вызовы зависят от ОС.
например, Linux: таблица системных вызовов Linux
Программа Hello World в этом учебном пособии может дать вам некоторые идеи.