¿Cómo imprimo un número entero en la Progtwigción de nivel de ensamblaje sin printf de la biblioteca c?

¿Alguien puede decirme el código de ensamblaje para mostrar el valor en un registro en formato decimal? Por favor, no sugiera usar el truco printf y luego comstackr con gcc.

Descripción:

Bueno, investigué un poco y experimenté con NASM y pensé que podría utilizar la función printf de la biblioteca c para imprimir un entero. Lo hice al comstackr el archivo objeto con el comstackdor GCC y todo funciona bastante bien.

Sin embargo, lo que quiero lograr es imprimir el valor almacenado en cualquier registro en forma decimal.

Investigué un poco y me di cuenta de que el vector de interrupción 021h para la línea de comandos de DOS puede mostrar cadenas y caracteres, mientras que 2 o 9 están en el registro ah y los datos están en el dx.

Conclusión:

Ninguno de los ejemplos que encontré mostró cómo mostrar el valor del contenido de un registro en forma decimal sin utilizar el printf de la biblioteca C. ¿Alguien sabe cómo hacer esto en el assembly?

Necesita escribir una rutina de conversión de binario a decimal y luego usar los dígitos decimales para producir “caracteres de dígitos” para imprimir.

Debe suponer que algo, en algún lugar, imprimirá un carácter en su dispositivo de salida de su elección. Llame a esta subrutina “print_character”; asume que toma un código de carácter en EAX y preserva todos los registros. (Si no tiene una subrutina de este tipo, tiene un problema adicional que debería ser la base de una pregunta diferente).

Si tiene el código binario para un dígito (por ejemplo, un valor de 0-9) en un registro (por ejemplo, EAX), puede convertir ese valor en un carácter para el dígito agregando el código ASCII para el carácter “cero” al registro. Esto es tan simple como:

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

A continuación, puede llamar print_character para imprimir el código de caracteres de dígitos.

Para generar un valor arbitrario, debe seleccionar los dígitos e imprimirlos.

Seleccionar fundamentalmente los dígitos requiere trabajar con poderes de diez. Es más fácil trabajar con una potencia de diez, por ejemplo, 10 en sí misma. Imagine que tenemos una rutina de división por 10 que tomó un valor en EAX y produjo un cociente en EDX y un rest en EAX. Lo dejo como ejercicio para que descubras cómo implementar esa rutina.

Entonces, una rutina simple con la idea correcta es producir un dígito para todos los dígitos que el valor pueda tener. Un registro de 32 bits almacena valores en 4 mil millones, por lo que puede obtener 10 dígitos impresos. Asi que:

  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 

Esto funciona … pero imprime los dígitos en orden inverso. Oops! Bueno, podemos aprovechar la stack de pushdown para almacenar los dígitos producidos, y luego hacer que salgan en orden inverso:

  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 

Se deja como un ejercicio para el lector: suprime los ceros a la izquierda. Además, dado que estamos escribiendo caracteres de dígitos en la memoria, en lugar de escribirlos en la stack, podríamos escribirlos en un búfer y luego imprimir el contenido del búfer. También se dejó como un ejercicio para el lector.

La mayoría de los sistemas / entornos operativos no tienen una llamada al sistema que acepte enteros y los convierta a decimales por usted. Debe hacerlo usted mismo antes de enviar los bytes al SO, o copiarlos a la memoria de video usted mismo, o dibujar los glifos de fuente correspondientes en la memoria de video …

Con mucho, la forma más eficiente es hacer una única llamada al sistema que haga toda la cadena a la vez, porque una llamada al sistema que escribe 8 bytes es básicamente el mismo costo que escribir 1 byte.

Esto significa que necesitamos un buffer, pero eso no agrega mucho a nuestra complejidad. 2 ^ 32-1 es solo 4294967295, que tiene solo 10 dígitos decimales. Nuestro buffer no necesita ser grande, así que solo podemos usar la stack.

El algoritmo habitual produce dígitos LSD primero. Como el orden de impresión es primero en MSD, podemos simplemente comenzar al final del buffer y trabajar hacia atrás. Para imprimir o copiar en cualquier otro lugar, solo haga un seguimiento de dónde comienza y no se moleste en llevarlo al inicio de un búfer fijo. No hay necesidad de meterse con push / pop para invertir cualquier cosa, solo producirlo hacia atrás en primer 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 } 

gcc / clang hace un excelente trabajo, utilizando un multiplicador mágico constante en lugar de div para dividir por 10 de manera eficiente. ( Godbolt compiler explorer for asm output).

Aquí hay una versión NASM comentada simple de eso, usando div (código lento pero más corto) para enteros sin signo de 32 bits y una llamada al sistema de write Linux. Debería ser fácil rcx esto al código de modo de 32 bits simplemente cambiando los registros a ecx lugar de a rcx . (También debe guardar / restaurar esi para las convenciones habituales de llamadas de 32 bits, a menos que esté convirtiendo esto en una función macro o de uso interno solamente).

La parte de llamada al sistema es específica para Linux de 64 bits. Reemplace eso con lo que sea apropiado para su sistema, por ejemplo, llame a la página de VDSO para realizar llamadas de sistema eficientes en Linux de 32 bits o use int 0x80 directamente para llamadas de sistema ineficientes. Consulte las convenciones de llamadas para llamadas al sistema de 32 y 64 bits en 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 

Dominio publico. Siéntase libre de copiar / pegar esto en lo que sea que esté trabajando. Si se rompe, puedes conservar ambas piezas.

Y aquí está el código para llamarlo en un ciclo que cuenta hasta 0 (incluido 0). Ponerlo en el mismo archivo es 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) 

Montar y vincular con

 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 las llamadas únicas del sistema que hace este progtwig son write() y exit() . (Consulte también las sugerencias de gdb / depuración en la parte inferior de la wiki de la etiqueta x86 y los otros enlaces allí).


Publiqué una versión de syntax de AT & T para enteros de 64 bits como respuesta a la impresión de un entero como una cadena con syntax de AT & T, con llamadas al sistema Linux en lugar de printf . Véalo para más comentarios sobre el rendimiento, y un punto de referencia de div frente a código generado por el comstackdor usando mul .

No puedo comentar, así que publico una respuesta de esta manera. @Ira Baxter, respuesta perfecta. Solo quiero agregar que no es necesario dividir 10 veces como lo publicaste para establecer el registro cx en el valor 10. Solo divide el número en hacha hasta “ax == 0”

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

También debe almacenar cuántos dígitos había en el número original.

  mov cx,0 loop1: call dividebyten inc cx 

De todos modos, Ira Baxter me ayudó, solo hay algunas formas de optimizar el código 🙂

Esto no solo se trata de optimización sino también de formateo. Cuando quiera imprimir el número 54, quiere imprimir 54, no 0000000054 🙂

Supongo que quieres imprimir el valor de stdout? Si este es el caso
tienes que usar una llamada al sistema para hacerlo. Las llamadas al sistema dependen del sistema operativo.

por ejemplo, Linux: tabla de llamadas del sistema Linux

El progtwig hello world en este Tutorial puede brindarle algunas ideas.

1 -9 son 1 -9. después de eso, debe haber alguna conversión que yo tampoco sé. Supongamos que tiene un 41H en AX (EAX) y desea imprimir un 65, no una ‘A’ sin hacer una llamada de servicio. Creo que necesitas imprimir una representación de personaje de un 6 y un 5 sea lo que sea. Debe haber un número constante que se puede agregar para llegar allí. Necesita un operador de módulo (como haga eso en el ensamblaje) y un bucle para todos los dígitos.

No estoy seguro, pero esa es mi suposición.