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

Другие вопросы по тегам