5
\$\begingroup\$

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                             ;
\$\endgroup\$
  • \$\begingroup\$ Optimization for size? Or speed? A bit of both? \$\endgroup\$ – harold Oct 15 at 14:25
  • 1
    \$\begingroup\$ @harold, for size, I guess. \$\endgroup\$ – eanmos Oct 15 at 14:36
2
\$\begingroup\$

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]?

\$\endgroup\$
4
\$\begingroup\$
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.

\$\endgroup\$
  • \$\begingroup\$ So great answer! More than I expected. Thanks you :) \$\endgroup\$ – eanmos Oct 18 at 13:26
  • \$\begingroup\$ 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? \$\endgroup\$ – eanmos Oct 18 at 13:28
  • \$\begingroup\$ @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. \$\endgroup\$ – Fifoernik Oct 18 at 13:32
  • \$\begingroup\$ We do not care about another 8086's segment register (DS, ES)? \$\endgroup\$ – eanmos Oct 18 at 13:39
  • \$\begingroup\$ @eanmos I don't get this question. Segment registers are always important in real address programming. \$\endgroup\$ – Fifoernik Oct 18 at 13:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.