Mostrar números con DOS
Me encargaron escribir un programa que mostrara 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. ¿Me lo perdí y qué puedo hacer?
Quiero mostrar el número en DX:AX
decimal.
Es cierto que DOS no nos ofrece una función para generar un número directamente.
Primero deberá convertir el número usted mismo y luego hacer que DOS lo muestre usando una de las funciones de salida de texto.
Visualización del número de 16 bits sin firmar contenido en AX
Al abordar el problema de convertir un número, es útil ver cómo se relacionan entre sí los dígitos que lo componen.
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 potencias decrecientes de 10
Procesar el número 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 carácter 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 carácter 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 carácter 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 carácter de inmediato. Aquí el resto siempre será 0. (Para evitar esta tonta división entre 1 se requiere algún 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 usáramos el mismo proceso de 5 pasos, obtendríamos "00255". Esos 2 ceros iniciales no son deseables 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. Es posible calcular dinámicamente estos divisores, pero introduce muchas divisiones adicionales.
Si quisiéramos aplicar este método para mostrar números aún mayores, digamos de 32 bits, y eventualmente querríamos hacerlo, las divisiones involucradas se volverían realmente problemáticas.
Por tanto, el método 1 no es práctico y, por tanto, rara vez se utiliza.
Método 2: división por constante 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) entre 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) entre 10 , obtenemos un cociente (655) que se convertirá en el dividendo del 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) entre 10 , obtenemos un cociente (65) que se convertirá en el dividendo del 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.
Dividiendo el cociente del paso anterior (65) entre 10 , obtenemos un cociente (6) que se convertirá en el dividendo del 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) entre 10 , obtenemos un cociente (0) que señala que esta fue la última división. También obtenemos un resto (6) que podríamos generar como personaje de inmediato, pero abstenernos 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 de los cuales es un número de un solo dígito en el rango [0,9]. Dado que la pila es LIFO (Último en entrar, primero en salir), el valor que pondremos POP
primero es el primer dígito que queremos que se muestre. 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 tienen menos de 5 dígitos, contaremos los dígitos a medida que lleguen y luego haremos esa cantidad 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:
- Como nos detenemos cuando un cociente se vuelve cero, nunca hay ningún problema con los ceros iniciales feos.
- El divisor está fijo. Eso es bastante fácil.
- Es realmente sencillo aplicar este método para mostrar números más grandes y eso es precisamente lo que viene a continuación.
Visualización del número de 32 bits sin firmar contenido en DX:AX
En8086Se necesita una cascada de 2 divisiones para dividir el valor de 32 bits entre
DX:AX
10.
La primera división divide el dividendo alto (ampliado con 0) produciendo un cociente alto. La segunda división divide el dividendo bajo (ampliado con el resto de la primera división) dando como resultado el cociente bajo. Es el resto de la 2ª división lo que guardamos en la pila.
Para verificar si el dword DX:AX
es cero, puse OR
-ed ambas mitades en un registro temporal.
En lugar de contar los dígitos, lo que requería un registro, elegí poner un centinela 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 ciclo 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 de 32 bits firmado contenido en DX:AX
El procedimiento es el siguiente:
Primero averigüe si el número con signo 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
el proceso.
El resto del fragmento es el mismo que para un número sin firmar.
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?
In a program where you need to display on occasion AL
, AX
, or DX:AX
, you could
just include the 32-bit version and use next little wrappers for the smaller
sizes:
; 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
...
Alternatively, if you don't mind the clobbering of the AX
and DX
registers use
this fall-through solution:
; 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
...