# Display hexadecimal value stored at a register

I read a book about OS development and faced with a simple exercise: write a function that prints a hexadecimal value stored at a register. The program runs as a boot sector.

I would really appreciate suggestions and help for size optimization and functionality.

[BITS 16]
[ORG 0x7C00]

EntryPoint:
; Setup the stack:

xor AX, AX                          ; First of all set SS to 0x0000.
mov SS, AX                          ;
mov BP, 0x7BFF                      ; BIOS memory map description found on the Internet states that
; address space from 0x500 to 0x7BFF is guaranteed free for use.
mov SP, BP                          ; We will use it for our stack.

; Test of the PrintHex procedure:

mov BX, 0x2019                      ; Load some number into BX.
call PrintHex                       ; Print it.

; Hang:

jmp $;============================================================================== ; PROCEDURE PrintHex ; ; Description: ; Prints content of BX in form "0x0000" in teletype mode. ; Inputs: ; BX: number to print. ; Outputs: ; No. ;============================================================================== PrintHex: pusha mov CX, BX ; Save the original number to CX. mov SI, .alphabet ; Use SI as base for .alphabet array. shr BX, 12 ; Get the first 4 bits of the original number (0x[1]234). mov AL, [BX + SI] ; Use it as index in the array of hexadecimal digits. Thus get the appropriate character. mov [.result + 2], AL ; Copy the character to the output array. ; In other words, these instuctions mean result[2] = alphabet[BX]. mov BX, CX ; Restore the original number. shr BX, 8 ; Get the second 4 bits of the original number (0x1[2]34). and BX, 0x0F ; We have to apply mask 0x0F to the value in order to get exactly 4 bits. mov AL, [BX + SI] ; AL = alphabet[BX]. mov [.result + 3], AL ; result[3] = AL. mov BX, CX ; Restore the original number. shr BX, 4 ; Get the third 4 bits of the original number (0x12[3]4). and BX, 0x0F ; mov AL, [BX + SI] ; AL = alphabet[BX]. mov [.result + 4], AL ; result[4] = AL. mov BX, CX ; Restore the original number. and BX, 0x0F ; Get the last 4 bits of the original number (0x123[4]). mov AL, [BX + SI] ; AL = alphabet[BX]. mov [.result + 5], AL ; result[5] = AL. mov BX, .result ; Print the result. call WriteString ; popa ret .alphabet: db '0123456789ABCDEF', 0x0 .result: db '0x0000', 0x0 ;============================================================================== ; PROCEDURE WriteString ; ; Description: ; Writes a null-terminated string to the screen using BIOS teletype mode text ; writing. ; Inputs: ; BX: address of the string. ; Outputs: ; No. ;============================================================================== WriteString: pusha mov SI, 0 ; Use SI as index in the string. .loop: mov AL, [BX + SI] ; Move the current character at the string into AL. cmp AL, 0x0 ; If the current character is null-terminator, then je .break ; break the loop. call WriteCharInTeletypeMode ; Otherwise print it. inc SI ; Increment index and jmp .loop ; jump to the beginning. .break: popa ret ;============================================================================== ; PROCEDURE WriteCharInTeletypeMode ; ; Description: ; Writes a character in teletype mode using BIOS interrupts. ; Inputs: ; AL: character. ; Outputs: ; No. ;============================================================================== WriteCharInTeletypeMode: pusha mov AH, 0xE ; Select BIOS function teletype mode text writing. int 0x10 popa ret ;============================================================================== ; Padding and the Bootloader Signature ;============================================================================== times 510 - ($ - ) db 0x0         ; Fill the rest of the file by zeroes.

db 0x55                             ; The bootloader signature. Required by some BIOSes.
db 0xAA                             ;

• Optimization for size? Or speed? A bit of both? – harold Oct 15 '19 at 14:25
• @harold, for size, I guess. – eanmos Oct 15 '19 at 14:36

When setting up the SS:SP registers, there is a slight chance that an interrupt might happen between setting SS and SP. The interrupt handler would then write to an unintended memory address. To prevent this, enclose the code between cli and sti.

I'd rather set SP to 07FEh, to align memory reads to 2-byte boundaries.

Instead of jmp \$ you should not waste energy by replacing it with:

forever:
hlt
jmp short forever


Since you are programming in assembly and not in C, you don't have to use the inefficient C-style strings. You can also use the (start, length) format, which does not need a trailing null character.

I'd allocate the output buffer on the stack instead of using a static buffer, so that you can put something else in that section.

I dimly remember that I initialized CS, DS and ES as well in the boot sector. I don't remember if they have guaranteed values at startup, or maybe that was the common sequence in .com files.

"Found on the internet" is an inappropriate citation. Just mention the URL where you found that information.

Instead of the complicated times expression, can you just say [org 7CFEh]?

mov SS, AX                          ;
mov BP, 0x7BFF
mov SP, BP


This is a dangerous construct! To maintain a consistent SS:SP pair of registers you should always set these back to back (No instruction(s) in between).

The code uses pusha, popa, shr bx, 12, ...
So your code is clearly targetting x86 (and not the infamous 8086). Then you don't need to use CLI nor STI around this code.

To optimize the stack access, setting the stackpointer offset SP to an even address is best and since you want to place the stack beneath the bootloader, I suggest you write:

xor ax, ax
mov ss, ax
mov sp, 0x7C00


Your program depends on a correct DS segment register. You can't trust this to be so when BIOS starts your program. For good measure this applies to ES also, so make it happen:

xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00


The BIOS Teletype function uses BH to specify the display page and you will want this to be zero. Therefore it's not a good idea to use BX for addressing the text.

In your program it works because the label .result has a very small address, meaning BH=0.

These are the necessary changes to WriteString and WriteCharInTeletypeMode:

WriteString:
pusha
.loop:
mov  al, [si]
cmp  al, 0
je   .break
call WriteCharInTeletypeMode
inc  si
jmp  .loop
.break:
popa
ret

WriteCharInTeletypeMode:
pusha
mov bh, 0      ;Displaypage
mov ah, 0x0E
int 0x10
popa
ret


### You want to optimize for code size

(1) Then at least use a loop to convert to hexadecimal instead of using an unrolled approach.

  mov  di, .result+6
More:
mov  si, bx
and  si, 15
mov  al, [.alphabet + si]
dec  di
mov  [di], al
shr  bx, 4
cmp  di, .result+2
ja   More
mov  si, .result        ;Using SI here!!
call WriteString


(2) Do you really need a separate WriteCharInTeletypeMode?
You save a lot of bytes if you inline this code:

WriteString:
pusha
.loop:
mov  al, [si]
cmp  al, 0
je   .break

mov bh, 0      ;Displaypage
mov ah, 0x0E
int 0x10

inc  si
jmp  .loop
.break:
popa
ret


(3) You don't need a terminating zero with that .alphabet string.

.alphabet:
db '0123456789ABCDEF'


(4) A conversion that doesn't use this 16 characters string is probably shorter, code-wise. Should not be too difficult to find an example.

• So great answer! More than I expected. Thanks you :) – eanmos Oct 18 '19 at 13:26
• Can you please explain this moment: "So your code is clearly targetting x86 (and not the infamous 8086). Then you don't need to use CLI nor STI around this code". Why I don't need use CLI and STI around this code? – eanmos Oct 18 '19 at 13:28
• @eanmos The instructions that I mention (and that you used) didn't exist on the original 8086. That's why I can say that you wrote a program for an architecture later than 8086. We refer to this as x86. All x86 processors protect the instruction following a write to SS from interruption but some old 8086's had an error in them requiring us to use cli / sti as a protection. – Fifoernik Oct 18 '19 at 13:32
• We do not care about another 8086's segment register (DS, ES)? – eanmos Oct 18 '19 at 13:39
• @eanmos I don't get this question. Segment registers are always important in real address programming. – Fifoernik Oct 18 '19 at 13:41