Como eu imprimo um inteiro em Assembly Level Programming sem printf da biblioteca c?

Alguém pode me dizer o código de assembly puramente para exibir o valor em um registro no formato decimal? Por favor, não sugira usar o printf e então compile com o gcc.

Descrição:

Bem, eu fiz algumas pesquisas e algumas experiências com o NASM e imaginei que poderia usar a function printf da biblioteca c para imprimir um inteiro. Eu fiz isso compilando o arquivo object com o compilador GCC e tudo funciona de forma justa.

No entanto, o que eu quero alcançar é imprimir o valor armazenado em qualquer registro na forma decimal.

Eu fiz algumas pesquisas e percebi que o vetor de interrupção 021h para a linha de comando do DOS pode exibir strings e caracteres enquanto 2 ou 9 está no registrador ah e os dados estão no dx.

Conclusão:

Nenhum dos exemplos que encontrei mostrou como exibir o valor do conteúdo de um registro na forma decimal sem usar o printf da biblioteca C. Alguém sabe como fazer isso na assembly?

Você precisa escrever uma rotina de conversão binária em decimal e, em seguida, usar os dígitos decimais para produzir “caracteres de dígitos” para imprimir.

Você tem que assumir que algo, em algum lugar, imprimirá um caractere no dispositivo de saída escolhido. Chame essa sub-rotina de “print_character”; assume que ele aceita um código de caractere no EAX e preserva todos os registradores. (Se você não tem essa sub-rotina, você tem um problema adicional que deve ser a base de uma questão diferente).

Se você tem o código binário para um dígito (por exemplo, um valor de 0-9) em um registrador (digamos, EAX), você pode converter esse valor em um caractere para o dígito adicionando o código ASCII para o caractere “zero” para o registro. Isso é tão simples quanto:

add eax, 0x30 ; convert digit in EAX to corresponding character digit 

Você pode então chamar print_character para imprimir o código do caractere de dígito.

Para produzir um valor arbitrário, você precisa selecionar dígitos e imprimi-los.

Escolher dígitos requer fundamentalmente trabalhar com poderes de dez. É mais fácil trabalhar com uma potência de dez, por exemplo, 10 em si. Imagine que tenhamos uma rotina de divisão por 10 que tenha um valor em EAX e tenha produzido um quociente em EDX e um restante em EAX. Deixo como um exercício para você descobrir como implementar essa rotina.

Então, uma simples rotina com a idéia correta é produzir um dígito para todos os dígitos que o valor possa ter. Um registrador de 32 bits armazena valores para 4 bilhões, então você pode obter 10 dígitos impressos. Assim:

  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 

Isso funciona … mas imprime os dígitos na ordem inversa. Opa! Bem, podemos aproveitar a pilha de empilhamento para armazenar dígitos produzidos e, em seguida, exibi-los na ordem inversa:

  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 

Deixado como um exercício para o leitor: suprima zeros à esquerda. Além disso, como estamos escrevendo caracteres typescripts na memory, em vez de gravá-los na pilha, podemos gravá-los em um buffer e depois imprimir o conteúdo do buffer. Também foi deixado como um exercício para o leitor.

A maioria dos sistemas operacionais / ambientes não possui uma chamada de sistema que aceite inteiros e os converta em decimal para você. Você tem que fazer isso sozinho antes de enviar os bytes para o sistema operacional, ou copiá-los para a memory de vídeo mesmo, ou desenhar os glifos de fonte correspondentes na memory de vídeo …

De longe, a maneira mais eficiente é fazer uma única chamada de sistema que faça a cadeia inteira de uma só vez, porque uma chamada de sistema que grava 8 bytes é basicamente o mesmo custo que escrever 1 byte.

Isso significa que precisamos de um buffer, mas isso não aumenta muito nossa complexidade. 2 ^ 32-1 é apenas 4294967295, que tem apenas 10 dígitos decimais. Nosso buffer não precisa ser grande, então podemos usar a pilha.

O algoritmo usual produz dígitos LSD primeiro. Desde que a ordem de impressão é MSD-primeiro, podemos apenas começar no final do buffer e trabalhar para trás. Para imprimir ou copiar em outro lugar, apenas acompanhe onde ele inicia e não se preocupe em colocá-lo no início de um buffer fixo. Não há necessidade de mexer com push / pop para reverter qualquer coisa, apenas produzi-lo para trás em primeiro lugar.

 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 } 

O gcc / clang faz um excelente trabalho, usando um multiplicador constante mágico em vez de div para dividir por 10 eficientemente. ( Explorador do compilador Godbolt para saída asm).

Aqui está uma simples versão NASM comentada, usando div (código lento mas curto) para inteiros não assinados de 32 bits e uma chamada de sistema de write Linux. Deve ser fácil portá-lo para o código do modo de 32 bits apenas alterando os registros para ecx vez de rcx . (Você também deve salvar / restaurar o esi para as convenções de chamada de 32 bits usuais, a menos que você faça isso em uma function de macro ou uso interno.)

A parte da chamada do sistema é específica para o Linux de 64 bits. Substitua isso pelo que for apropriado para o seu sistema, por exemplo, chame a página VDSO para chamadas eficientes do sistema no Linux de 32 bits ou use o int 0x80 diretamente para chamadas de sistema ineficientes. Consulte as convenções de chamada para chamadas de sistema de 32 e 64 bits no 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 

Domínio público. Sinta-se à vontade para copiar / colar isso no que você estiver trabalhando. Se quebrar, você consegue manter os dois pedaços.

E aqui está o código para chamá-lo em um loop de contagem regressiva para 0 (incluindo 0). Colocá-lo no mesmo arquivo é conveniente.

 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) 

Monte e conecte com

 yasm -felf64 -Worphan-labels -gdwarf2 print-integer.asm && ld -o print-integer print-integer.o ./print_integer 100 99 ... 1 0 

Use strace para ver que as únicas chamadas do sistema feitas por este programa são write() e exit() . (Veja também as dicas do gdb / debug na parte inferior do wiki da tag x86 , e os outros links lá.)


Eu publiquei uma versão de syntax AT & T para números inteiros de 64 bits como uma resposta para a impressão de um inteiro como uma string com syntax AT & T, com chamadas de sistema Linux em vez de printf . Veja isto para mais comentários sobre desempenho, e um benchmark de div vs. código gerado pelo compilador usando mul .

Não consigo comentar, então postei a resposta dessa maneira. @Ira Baxter, resposta perfeita Eu só quero acrescentar que você não precisa dividir 10 vezes como você postou que você setar registrador cx para valor 10. Apenas divida o número em ax até “ax == 0”

 loop1: call dividebyten ... cmp ax,0 jnz loop1 

Você também precisa armazenar quantos dígitos havia no número original.

  mov cx,0 loop1: call dividebyten inc cx 

De qualquer forma você Ira Baxter me ajudou, há apenas algumas maneiras de como otimizar o código 🙂

Não se trata apenas de otimização, mas também de formatação. Quando você quiser imprimir o número 54, você deve imprimir 54 e não 0000000054 🙂

Eu suponho que você quer imprimir o valor para stdout? Se esse é o caso
você tem que usar uma chamada de sistema para fazer isso. Chamadas do sistema são dependentes do sistema operacional.

eg Linux: Tabela de Chamadas do Sistema Linux

O programa hello world neste Tutorial pode lhe dar algumas ideias.

1 -9 são 1 -9. Depois disso, deve haver alguma conversão que eu também não sei. Digamos que você tenha um 41H em AX (EAX) e você deseja imprimir um 65, não ‘A’ sem fazer alguma chamada de serviço. Eu acho que você precisa imprimir uma representação de caractere de um 6 e um 5 qualquer que seja. Deve haver um número constante que possa ser adicionado para chegar lá. Você precisa de um operador de módulo (no entanto, você faz isso na assembly) e faz um loop para todos os dígitos.

Não tenho certeza, mas esse é o meu palpite.