assembly - Mostrar números con DOS
x86-16 integer-division (1)
Me encargaron escribir un programa que muestre la dirección lineal de la PSP de mi programa. Escribí lo siguiente:
ORG 256
mov dx,Msg
mov ah,09h ;DOS.WriteStringToStandardOutput
int 21h
mov ax,ds
mov dx,16
mul dx ; -> Linear address is now in DX:AX
???
mov ax,4C00h ;DOS.TerminateWithExitCode
int 21h
; ------------------------------
Msg: db ''PSP is at linear address $''
¡Busqué en la API de DOS (usando la lista de interrupciones de Ralph Brown ) y no encontré una sola función para generar un número! ¿Lo extrañé y qué puedo hacer?
Quiero mostrar el número en
DX:AX
en decimal.
Es cierto que DOS no nos ofrece una función para generar un número directamente.
Primero tendrá que convertir el número usted mismo y luego hacer que DOS lo muestre usando una de las funciones de salida de texto.
Mostrar el número de 16 bits sin firmar que se encuentra en AX
Al abordar el problema de convertir un número, es útil ver cómo los dígitos que forman un número se relacionan entre sí.
Consideremos el número 65535 y su descomposición:
(6 * 10000) + (5 * 1000) + (5 * 100) + (3 * 10) + (5 * 1)
Método 1: división por poderes decrecientes de 10
Procesar el número que va de izquierda a derecha es conveniente porque nos permite mostrar un dígito individual tan pronto como lo extraemos.
-
Al dividir el número (65535) por 10000 , obtenemos un cociente de un solo dígito (6) que podemos generar como un carácter de inmediato. También obtenemos un resto (5535) que se convertirá en el dividendo en el siguiente paso.
-
Al dividir el resto del paso anterior (5535) por 1000 , obtenemos un cociente de un solo dígito (5) que podemos generar como un personaje de inmediato. También obtenemos un resto (535) que se convertirá en el dividendo en el siguiente paso.
-
Al dividir el resto del paso anterior (535) por 100 , obtenemos un cociente de un solo dígito (5) que podemos generar como un personaje de inmediato. También obtenemos un resto (35) que se convertirá en el dividendo en el siguiente paso.
-
Al dividir el resto del paso anterior (35) por 10 , obtenemos un cociente de un solo dígito (3) que podemos generar como un personaje de inmediato. También obtenemos un resto (5) que se convertirá en el dividendo en el siguiente paso.
-
Al dividir el resto del paso anterior (5) por 1 , obtenemos un cociente de un solo dígito (5) que podemos generar como un personaje de inmediato. Aquí el resto siempre será 0. (Evitar esta tonta división por 1 requiere un código adicional)
mov bx,.List
.a: xor dx,dx
div word ptr [bx] ; -> AX=[0,9] is Quotient, Remainder DX
xchg ax,dx
add dl,"0" ;Turn into character [0,9] -> ["0","9"]
push ax ;(1)
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
pop ax ;(1) AX is next dividend
add bx,2
cmp bx,.List+10
jb .a
...
.List:
dw 10000,1000,100,10,1
Aunque este método, por supuesto, producirá el resultado correcto, tiene algunos inconvenientes:
-
Considere el número más pequeño 255 y su descomposición:
(0 * 10000) + (0 * 1000) + (2 * 100) + (5 * 10) + (5 * 1)
Si tuviéramos que usar el mismo proceso de 5 pasos obtendríamos "00255". Esos 2 ceros iniciales son indeseables y tendríamos que incluir instrucciones adicionales para deshacernos de ellos.
-
El divisor cambia con cada paso. Tuvimos que almacenar una lista de divisores en la memoria. El cálculo dinámico de estos divisores es posible pero introduce muchas divisiones adicionales.
-
Si quisiéramos aplicar este método para mostrar números aún mayores, digamos 32 bits, y eventualmente querremos hacerlo, las divisiones involucradas se volverían realmente problemáticas.
Por lo tanto, el método 1 no es práctico y, por lo tanto, rara vez se usa.
Método 2: división por const 10
Procesar el número que va de derecha a izquierda parece contrario a la intuición, ya que nuestro objetivo es mostrar primero el dígito más a la izquierda. Pero como estás a punto de descubrir, funciona de maravilla.
-
Al dividir el número (65535) por 10 , obtenemos un cociente (6553) que se convertirá en el dividendo en el siguiente paso. También obtenemos un resto (5) que todavía no podemos generar, por lo que tendremos que guardarlo en algún lugar. La pila es un lugar conveniente para hacerlo.
-
Al dividir el cociente del paso anterior (6553) por 10 , obtenemos un cociente (655) que se convertirá en el dividendo en el siguiente paso. También obtenemos un resto (3) que todavía no podemos generar, por lo que tendremos que guardarlo en algún lugar. La pila es un lugar conveniente para hacerlo.
-
Al dividir el cociente del paso anterior (655) por 10 , obtenemos un cociente (65) que se convertirá en el dividendo en el siguiente paso. También obtenemos un resto (5) que todavía no podemos generar, por lo que tendremos que guardarlo en algún lugar. La pila es un lugar conveniente para hacerlo.
-
Al dividir el cociente del paso anterior (65) por 10 , obtenemos un cociente (6) que se convertirá en el dividendo en el siguiente paso. También obtenemos un resto (5) que todavía no podemos generar, por lo que tendremos que guardarlo en algún lugar. La pila es un lugar conveniente para hacerlo.
-
Al dividir el cociente del paso anterior (6) por 10 , obtenemos un cociente (0) que indica que esta fue la última división. También obtenemos un resto (6) que podríamos obtener como personaje de inmediato, pero abstenerse de hacerlo resulta ser más efectivo y, como antes, lo guardaremos en la pila.
En este punto, la pila contiene nuestros 5 restos, cada uno con un número de un solo dígito en el rango [0,9].
Como la pila es LIFO (último en entrar, primero en salir), el valor que vamos a
POP
primero es el primer dígito que queremos mostrar.
Usamos un bucle separado con 5
POP
para mostrar el número completo.
Pero en la práctica, dado que queremos que esta rutina también pueda manejar números que tengan menos de 5 dígitos, contaremos los dígitos a medida que lleguen y luego haremos esa cantidad de
POP
.
mov bx,10 ;CONST
xor cx,cx ;Reset counter
.a: xor dx,dx ;Setup for division DX:AX / BX
div bx ; -> AX is Quotient, Remainder DX=[0,9]
push dx ;(1) Save remainder for now
inc cx ;One more digit
test ax,ax ;Is quotient zero?
jnz .a ;No, use as next dividend
.b: pop dx ;(1)
add dl,"0" ;Turn into character [0,9] -> ["0","9"]
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
loop .b
Este segundo método no tiene ninguno de los inconvenientes del primer método:
- Debido a que nos detenemos cuando un cociente se convierte en cero, nunca hay ningún problema con los ceros iniciales feos.
- El divisor es fijo. Eso es bastante fácil.
- Es realmente simple aplicar este método para mostrar números más grandes y eso es precisamente lo que viene después.
Visualización del número de 32 bits sin signo que se encuentra en DX: AX
En
8086
se necesita una cascada de 2 divisiones para dividir el valor de 32 bits en
DX:AX
por 10.
La primera división divide el dividendo alto (extendido con 0) produciendo un cociente alto.
La 2da división divide el dividendo bajo (extendido con el resto de la 1ra división) produciendo el cociente bajo.
Es el resto de la segunda división que guardamos en la pila.
Para verificar si la dword en
DX:AX
es cero, he
OR
-ed ambas mitades en un registro de cero.
En lugar de contar los dígitos, que requieren un registro, elegí poner un sentinel en la pila. Debido a que este centinela obtiene un valor (10) que ningún dígito puede tener ([0,9]), permite determinar cuándo debe detenerse el bucle de visualización.
Aparte de eso, este fragmento es similar al método 2 anterior.
mov bx,10 ;CONST
push bx ;Sentinel
.a: mov cx,ax ;Temporarily store LowDividend in CX
mov ax,dx ;First divide the HighDividend
xor dx,dx ;Setup for division DX:AX / BX
div bx ; -> AX is HighQuotient, Remainder is re-used
xchg ax,cx ;Temporarily move it to CX restoring LowDividend
div bx ; -> AX is LowQuotient, Remainder DX=[0,9]
push dx ;(1) Save remainder for now
mov dx,cx ;Build true 32-bit quotient in DX:AX
or cx,ax ;Is the true 32-bit quotient zero?
jnz .a ;No, use as next dividend
pop dx ;(1a) First pop (Is digit for sure)
.b: add dl,"0" ;Turn into character [0,9] -> ["0","9"]
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
pop dx ;(1b) All remaining pops
cmp dx,bx ;Was it the sentinel?
jb .b ;Not yet
Visualización del número firmado de 32 bits contenido en DX: AX
El procedimiento es el siguiente:
Primero averigüe si el número firmado es negativo probando el bit de signo.
Si es así, niegue el número y genere un carácter "-", pero tenga cuidado de no destruir el número en
DX:AX
en el proceso.
El resto del fragmento es el mismo que para un número sin signo.
test dx,dx ;Sign bit is bit 15 of high word
jns .a ;It''s a positive number
neg dx ;/
neg ax ; | Negate DX:AX
sbb dx,0 ;/
push ax dx ;(1)
mov dl,"-"
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
pop dx ax ;(1)
.a: mov bx,10 ;CONST
push bx ;Sentinel
.b: mov cx,ax ;Temporarily store LowDividend in CX
mov ax,dx ;First divide the HighDividend
xor dx,dx ;Setup for division DX:AX / BX
div bx ; -> AX is HighQuotient, Remainder is re-used
xchg ax,cx ;Temporarily move it to CX restoring LowDividend
div bx ; -> AX is LowQuotient, Remainder DX=[0,9]
push dx ;(2) Save remainder for now
mov dx,cx ;Build true 32-bit quotient in DX:AX
or cx,ax ;Is the true 32-bit quotient zero?
jnz .b ;No, use as next dividend
pop dx ;(2a) First pop (Is digit for sure)
.c: add dl,"0" ;Turn into character [0,9] -> ["0","9"]
mov ah,02h ;DOS.DisplayCharacter
int 21h ; -> AL
pop dx ;(2b) All remaining pops
cmp dx,bx ;Was it the sentinel?
jb .c ;Not yet
¿Necesitaré rutinas separadas para diferentes tamaños de números?
En un programa en el que necesita mostrar ocasionalmente
AL
,
AX
o
DX:AX
, puede incluir la versión de 32 bits y usar los siguientes pequeños
wrappers
para los tamaños más pequeños:
; IN (al) OUT ()
DisplaySignedNumber8:
push ax
cbw ;Promote AL to AX
call DisplaySignedNumber16
pop ax
ret
; -------------------------
; IN (ax) OUT ()
DisplaySignedNumber16:
push dx
cwd ;Promote AX to DX:AX
call DisplaySignedNumber32
pop dx
ret
; -------------------------
; IN (dx:ax) OUT ()
DisplaySignedNumber32:
push ax bx cx dx
...
Alternativamente, si no le importa el golpeteo de los registros
AX
y
DX
use esta solución de caída:
; IN (al) OUT () MOD (ax,dx)
DisplaySignedNumber8:
cbw
; --- --- --- --- -
; IN (ax) OUT () MOD (ax,dx)
DisplaySignedNumber16:
cwd
; --- --- --- --- -
; IN (dx:ax) OUT () MOD (ax,dx)
DisplaySignedNumber32:
push bx cx
...