Search code examples
assemblynasmx86-16bootloaderbios

How to display register value using INT 10H


I know I can print an ASCII character if it's representation is in AL with:

DrawChar:
  MOV AL, 0x45
  MOV AH, 0x0E
  MOV BL, 0x07
  MOV BH, 0x00
  INT 0x10
  RET

Is there a way I can use INT 10H to print the actual register value? Like AL is 0x45 in the e.g. above, so it would print 45 (doesn't have to be the hex rep.). I'm doing this in a 16-bit real-mode bootloader.


Solution

  • I'll give you one method to print a 16-bit register. This prints a nibble at a time (4 bits) by rotating the 16-bit register left 4 bits into the least significant bits and then isolating those 4 bits. We then print the hexadecimal digit of that value which will be 0 to 9 and A to F. We do this 4 times for each of the 4 nibbles in a 16-bit word.

    There are TWO 16-bit hex printing functions provided in the examples. Choose one appropriate for your environment:

    • print_hex_word which will work on any 8086/8088 processor or later.
    • print_hex_word that is a smaller version that works on any 80186/80188 processor in 16-bit real mode.

    Both variants require you to:

    • push a 2-byte value on the stack containing the page number and foreground color (graphics mode). see Ralf Brown's interrupt list for more information on Int 10h/AH=0eh
    • push the 2-byte value on the stack you wish to print.

    The sample code below contains a minimal bootloader as an example:

    bits 16
    org 0x7c00
    
    section .text
        global      _start
    
    _start:
        ; Set segment registers to 0
        xor ax, ax
        mov ds, ax
        mov es, ax
        ; Set stack pointer just below bootloader
        mov ss, ax
        mov sp, 0x7c00
        ; Clear direction flag (forward movement)
        cld
        ; print_hex_word/print_hex_word_186 take a second parameter
        ; that is the page number (upper 8 bits) and foreground color
        ; in lower 8 bits. We just want 0x0000 so push it on the
        ; stack first
        push ax
    
        ; This test just prints SS and SP to the display
        push ss                 ; Push on stack as 1st parameter
                                ;    In this case display value in SS
        call print_hex_word     ; Print 16-bit value as hex
        add  sp, 2              ; Cleanup stack after call
        push sp                 ; Push on stack as 1st parameter
                                ;    In this case display value in SP
        call print_hex_word     ; Print 16-bit value as hex
        add  sp, 2              ; Cleanup stack after call
    
        ; Print value 0xaa55 to the display
        mov ax, 0xaa55
        push ax                 ; Push on stack as 1st parameter
                                ;    In this case display value in 0xAA55
        call print_hex_word     ; Print 16-bit value as hex
    
        cli                     ; Disable interrupts
        hlt                     ; Halt processor
    
    ; Print 16 bit value passed on stack as first parameter
    ; in hexadecimal. Use page number and foreground color
    ; passed in second parameter. This routine will work on 8086+
    ; processors. This code takes advantage of packed BCD to
    ; determine the ASCII values to print. This code could have
    ; used compare and branch to do the same or a translation table.
    
    print_hex_word:
        push bp
        mov bp, sp      ; BP=SP, on 8086 can't use sp in memory operand
        push dx         ; Save all registers we clobber
        push cx
        push bx
        push ax
    
        mov cx, 0x0404  ; CH = number of nibbles to process = 4 (4*4=16 bits)
                        ; CL = Number of bits to rotate each iteration = 4 (a nibble)
        mov dx, [bp+4]  ; DX = word parameter on stack at [bp+4] to print
        mov bx, [bp+6]  ; BX = page / foreground attr is at [bp+6]
    
    .loop:
        rol dx, cl      ; Roll 4 bits left. Lower nibble is value to print
        mov ax, 0x0e0f  ; AH=0E (BIOS tty print),AL=mask to get lower nibble
        and al, dl      ; AL=copy of lower nibble
        add al, 0x90    ; Work as if we are packed BCD
        daa             ; Decimal adjust after add.
                        ;    If nibble in AL was between 0 and 9, then CF=0 and
                        ;    AL=0x90 to 0x99
                        ;    If nibble in AL was between A and F, then CF=1 and
                        ;    AL=0x00 to 0x05
        adc al, 0x40    ; AL=0xD0 to 0xD9
                        ; or AL=0x41 to 0x46
        daa             ; AL=0x30 to 0x39 (ASCII '0' to '9')
                        ; or AL=0x41 to 0x46 (ASCII 'A' to 'F')
        int 0x10        ; Print ASCII character in AL
        dec ch
        jnz .loop       ; Go back if more nibbles to process
    
        pop ax          ; Restore registers
        pop bx
        pop cx
        pop dx
        pop bp
        ret
    
    TIMES 510-($-$$) db 0
    DW 0xaa55
    

    The 80186+ version of print_hex_word that uses PUSHA and POPA to save and restore registers AX, CX, DX, BX, original SP, BP, SI, and DI:

    ; Print 16 bit value passed on stack as first parameter
    ; in hexadecimal. This routine will work on 80186+ processors
    ; Use page number and foreground color passed in second parameter
    
    print_hex_word:
        pusha           ; Save all registers, 16 bytes total
        mov bp, sp      ; BP=SP, on 8086 can't use sp in memory operand
        mov cx, 0x0404  ; CH = number of nibbles to process = 4 (4*4=16 bits)
                        ; CL = Number of bits to rotate each iteration = 4 (a nibble)
        mov dx, [bp+18] ; DX = word parameter on stack at [bp+18] to print
        mov bx, [bp+20] ; BX = page / foreground attr is at [bp+20]
    
    .loop:
        rol dx, cl      ; Roll 4 bits left. Lower nibble is value to print
        mov ax, 0x0e0f  ; AH=0E (BIOS tty print),AL=mask to get lower nibble
        and al, dl      ; AL=copy of lower nibble
        add al, 0x90    ; Work as if we are packed BCD
        daa             ; Decimal adjust after add.
                        ;    If nibble in AL was between 0 and 9, then CF=0 and
                        ;    AL=0x90 to 0x99
                        ;    If nibble in AL was between A and F, then CF=1 and
                        ;    AL=0x00 to 0x05
        adc al, 0x40    ; AL=0xD0 to 0xD9
                        ; or AL=0x41 to 0x46
        daa             ; AL=0x30 to 0x39 (ASCII '0' to '9')
                        ; or AL=0x41 to 0x46 (ASCII 'A' to 'F')
        int 0x10        ; Print ASCII character in AL
        dec ch
        jnz .loop       ; Go back if more nibbles to process
        popa            ; Restore all the registers
        ret
    

    The code uses some packed Binary Coded Decimal(BCD) tricks to convert a 4 bit value to a hexadecimal digit. More on packed BCD arithmetic can be found in this tutorial in the section Processing Packed BCD Numbers.

    To assemble this bootloader you could use:

    nasm -f bin boot.asm -o boot.bin
    

    It could be tested with QEMU at a Linux command line like this:

    qemu-system-i386 -fda boot.bin
    

    Debug Recommendation

    If you use BOCHS you can step through your bootloader with its built-in debugger, and display the contents of the registers and memory as your code executes.