CLOSE

Debugging low-level code, such as bootloaders operating in real mode, can be quite challenging due to the lack of sophisticated debugging tools available at higher levels of programming. One effective technique for debugging is to print the values of CPU registers to the screen. This article will walk you through the process of writing a simple bootloader that prints the values of several registers, helping you to understand the state of your program at specific point.

Why Print Register Values?

Printing register values is a straightforward method to:

  • Verify the correctness of your code.
  • Ensure that the CPU is in the expected state at critical points.
  • Debug unexpected behavior by observing the values in registers.

Printing Register Values

As we all know that registers stores data in decimal format. But in low level we mostly deal with the hexadecimal data. So we should have function for printing both decimal as well as hexa-decimal value.

First we will add some helper functions, which ease up our job:

common/print16.inc:

1️⃣ PrintNewLine:

; ********************************
; PrintNewline

; This prints a newline, used to signify the end of a line of text and the beginning of a new one.
; It is equivalent to '\n' in C/C++
; ********************************
PrintNewline:

	pusha
	mov ah, 0x0E            ; BIOS function to print character
	mov al, 0x0A            ; Newline character (Line feed)

	int 0x10                ; Call BIOS interrupt to print character

	mov al, 0x0D            ; Carriage return character
	int 0x10                ; Call BIOS interrupt to print character
	popa
ret
Line Feed (Line Feed, \n, ASCII 0x0A):
  • Moves the cursor down to the next line but does not return to the beginning of the line.
  • In a text environment, it advances the vertical position of the cursor by one line.
Carriage Return (Carriage Return, \r, ASCII 0x0D):
  • Moves the cursor to the beginning of the line but does not advance to the next line.
  • It resets the horizontal position of the cursor to the start of the line.
  • Historically used by old printers and typewriters.
Carriage Return + Line Feed (CR+LF, \r\n):
  • Moves the cursor to the beginning of the line and then down to the next line.
  • This combined effect is similar to pressing the Enter key on a keyboard, which starts a newline for text output.

Visual Representation :-

Consider the following text:

Hello, World!
This is a new line.

If Hello, World! is printed first, the cursor is at the end of the line:

Hello, World!_

Executing PrintNewline will:

  • Move the cursor down to the next line (due to the Line Feed).
  • Move the cursor to the beginning of the line (due to the Carriage Return).

Resulting in:

Hello, World!
_

Where _ represents the cursor position, ready for the next line of text.

2️⃣ Print Number:

We would need a function, which would print the value of an register.

The problem: given a number stored in the AX register, we want to:

  1. Extract digits from the number.
  2. Push them onto the stack as ASCII digits.
  3. Pop each digit from the stack and print it.

To do this, we need to:

  • Use division to extract each decimal digit (starting from the least significant digit).
  • Convert each digit to its ASCII equivalent.
  • Use BIOS interrupt 0x10 to print each character.

Dry Run:

Initialization:
AX = 1234 (decimal)
BX = 0 (counter for the number of digits)
CX = 10 (used to divide AX by 10 to get individual decimal digits).

|     |
|     |
|     |
|     |
|     | <-- Top of Stack
+-----+
Iteration 1:
AX = 1234, CX = 10
div cx:
       AX / CX = 1234 / 10 = 123 (quotiend stored in
       AX), remainded = 4 (stored in DX)
The remainder (4) is converted to an ASCII character:
       DX = 4 -> DX + 48 = 52 (ASCII code for '4').
       Push 52 onto the stack.
Increment BX = BX + 1
          BX = 0 + 1 = 1
 
 |     |
 |     |
 |     |
 |     |
 |  4  | <-- Top of Stack
 +-----+
  Stack
Iteration 2:
AX = 123, CX = 10, BX = 1
div cx:
       AX / CX = 123 / 10 = 12 (quotiend stored in
       AX), remainded = 3 (stored in DX)
The remainder (4) is converted to an ASCII character:
       DX = 4 -> DX + 48 = 51 (ASCII code for '3').
       Push 52 onto the stack.
Increment BX = BX + 1
          BX = 1 + 1 = 2
 
 |     |
 |     |
 |     |
 |  2  | <-- Top of Stack
 |  4  |
 +-----+
  Stack
Iteration 3:
AX = 12, CX = 10, BX = 2
div cx:
       AX / CX = 12 / 10 = 1 (quotiend stored in
       AX), remainded =  (stored in DX)
The remainder (4) is converted to an ASCII character:
       DX = 4 -> DX + 48 = 50 (ASCII code for '2').
       Push 52 onto the stack.
Increment BX = BX + 1
          BX = 2 + 1 = 3
 
 |     |
 |     |
 |  2  | <-- Top of Stack
 |  3  |
 |  4  |
 +-----+
  Stack
Iteration 4:
AX = 1, CX = 10, BX = 3
div cx:
       AX / CX = 1 / 10 = 0 (quotiend stored in
       AX), remainded = 1 (stored in DX)
The remainder (4) is converted to an ASCII character:
       DX = 4 -> DX + 48 = 49 (ASCII code for '1').
       Push 52 onto the stack.
Increment BX = BX + 1
          BX = 3 + 1 = 4
 
 |     |
 |  1  | <-- Top of Stack
 |  2  |
 |  3  |
 |  4  |
 +-----+
  Stack
Print Loop: Print by popping digits from the stack
Print Iteration 1:
 |     |
 |  1  | <-- Top of Stack
 |  2  |
 |  3  |
 |  4  |
 +-----+
  Stack

BX = 4
Pop value from stack top into AX
    AX = 1
    Print character '1' via PrintChar16BIOS
    Decrement BX
              BX = BX - 1
                 = 4 - 1 = 3
Print Iteration 2:
 |     |
 |  2  | <-- Top of Stack
 |  3  |
 |  4  |
 +-----+
  Stack
  
BX = 3
Pop value from stack top into AX
    AX = 2
    Print character '1' via PrintChar16BIOS
    Decrement BX
              BX = BX - 1
                 = 3 - 1 = 2
Print Iteration 3:
 |     |
 |     |
 |  3  | <-- Top of Stack
 |  4  |
 +-----+
  Stack
  
BX = 2
Pop value from stack top into AX
    AX = 3
    Print character '1' via PrintChar16BIOS
    Decrement BX
              BX = BX - 1
                 = 2 - 1 = 1
Print Iteration 4:
 |     |
 |     |
 |     |
 |  4  | <-- Top of Stack
 +-----+
  Stack
  
BX = 1
Pop value from stack top into AX
    AX = 3
    Print character '1' via PrintChar16BIOS
    Decrement BX
              BX = BX - 1
                 = 2 - 1 = 0
Loop Terminate:
 |     |
 |     |
 |     |
 |     | <-- Top of Stack
 +-----+
  Stack

BX = 0

Since BX becomes 0 means we have printed all the digits
then the loop will get terminated.

Function Code:

; ********************************
; PrintWordNumber
; IN:
; 	- AX: NumberToPrint
; ********************************
PrintWordNumber:
	; Save all general-purpose registers
	pusha

	; Initialize variables
	xor 	bx, bx        ; Clear BX to use it as a counter for the number of digits
    mov 	cx, 10         ; Set CX to 10, the divisor for converting number to digits

	.DigitLoop:
	    xor 	edx, edx        ; Clear EDX before division
	    div 	cx             ; Divide AX by 10
	    ; After div: AX contains quotient, DX contains remainder

	    ; Convert remainder to ASCII
	    add dx, 48         ; Convert digit to ASCII ('0' = 48)

	    ; Store ASCII digit on the stack
	    push 	dx
	    inc 	bx             ; Increment digit count

	    ; If quotient (AX) is zero, we're done converting digits
	    cmp ax, 0
	    jnz .DigitLoop       ; If AX is not zero, repeat the loop

	.PrintLoop:
		; Pop ASCII digit from stack into EAX
		pop 	ax

		; Print the character in AX
		call 	PrintChar16BIOS

		; Decrease digit count in BX
		dec 	ebx
		jnz 	.PrintLoop     ; If BX is not zero, print next digit

    ; Restore all general-purpose registers
    popa
ret                       ; Return from the PrintNumber routine

This function prints the number stored in the AX register using BIOS interrupts to display each digit on the screen. This routine converts the number into its individual digits, stores them in ASCII format on the stack, and then prints each digit by popping them off the stack.

3️⃣ Print Hex:

Printing a hexadecimal number requires converting the number into its hexadecimal string representation and then outputting each character. This is slightly more complex than printing a decimal number because each hexadecimal digit ranges from 0 to 15, which corresponds to 0-9 and A-F in ASCII.

To print the register value in hexadecimal

hexString: db '0x0000'
hex_to_ascii: db '0123456789ABCDEF'

; ********************************
; PrintWordHex
; This function prints the value stored in DX register in Hexa decimal format.
; IN:
; 	- DX: Hex Value to Print
; ********************************
PrintWordHex:
        mov cx, 4	; offset in string, counter (4 hex characters)
        .hex_loop:
            mov ax, dx	              ; Hex word passed in DX
            and al, 0Fh               ; Use nibble in AL
            mov bx, hex_to_ascii
            xlatb                     ; AL = [DS:BX + AL]

            mov bx, cx                ; Need bx to index data
            mov [hexString+bx+1], al  ; Store hex char in string
            ror dx, 4                 ; Get next nibble
        loop .hex_loop 

        mov si, hexString             ; Print out hex string
        mov ah, 0Eh
        mov cx, 6                     ; Length of string
        .loop:
            lodsb
            int 10h
        loop .loop
ret

Usage:

We can use these functions as follow in our stage 1 or stage 2, but these depends on the BIOS, so only be used in real mode (16-bit):

;Print Welcome to the Screen
mov si, WelcomeToStage1		; Load the address of the string into si register
call PrintString16BIOS		; String printing function.
call PrintNewline			; \n
image-151.png