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


Я разместил версию 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, что бы это ни было. Должно быть постоянное число, которое можно добавить туда. Вам нужен оператор модуля (как вы это делаете в сборке) и цикл для всех цифр.

Не уверен, но это мое предположение.