Mostrar números con DOS

Resuelto Sep Roland asked hace 7 años • 1 respuestas

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:AXdecimal.

Sep Roland avatar Aug 27 '17 17:08 Sep Roland
Aceptado

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 POPprimero es el primer dígito que queremos que se muestre. Usamos un bucle separado con 5 POPpara 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:AX10.
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:AXes 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:AXel 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
    ...
Sep Roland avatar Aug 27 '2017 10:08 Sep Roland