'How do i write a function that prints a null terminated string in NASM 16 bit real mode?

I have a simple program which moves some null-terminated strings to the bx register:

[org 0x7c00]    ; Tells the assembler where the code will be loaded
    mov ah, 0x0e

    mov bx, HELLO_MSG   ; Moves string inside of HELLO_MSG to bx
    call print_string   ; Calls the print_string function inside of ./print_string.asm

    mov bx, GOODBYE_MSG ; Moves string inside of GOODBYE_MSG to bx
    call print_string

    jmp $   ; Hangs

%include "print_string.asm"

HELLO_MSG:
    db 'Hello, World!', 0

GOODBYE_MSG:
    db 'Goodbye!', 0

    times 510-($-$$) db 0
    dw 0xaa55

and a print_string function inside of ./print_string.asm:

print_string:
    mov ah, 0x0e
    int 0x10
    ret

The print_string function doesn't work. From my understanding, ah has the value 0x0e stored, so if al has a value of X and int 0x10 is ran, it tells the BIOS to display the value of al on the screen. How would I replicate this for strings?



Solution 1:[1]

print_string:
  mov ah, 0x0e
  int 0x10
  ret

Your print_string routine uses the BIOS.Teletype function 0Eh. This function will display the single character held in the AL register. Since this BIOS function additionally expects you to supply the desired DisplayPage in BH and the desired GraphicsColor in BL (only for when the display is in a graphics video mode), it's perhaps not the best idea to use the BX register as an argument to this print_string routine.

Your new routine will have to loop over the string and use the single character output function for every character contained in the string. Because your strings are zero-terminated, you stop looping as soon as you encounter that zero byte.

[org 7C00h]

cld                  ; This makes sure that below LODSB works fine
mov  si, HELLO_MSG
call print_string
mov  si, GOODBYE_MSG
call print_string

jmp  $

print_string:
    push bx          ; Preserve BX if you need to!
    mov  bx, 0007h   ; DisplayPage BH=0, GraphicsColor BL=7 (White)
    jmp  .fetch
  .print:
    mov  ah, 0Eh     ; BIOS.Teletype
    int  10h
  .fetch:
    lodsb            ; Reads 1 character and also advances the pointer
    test al, al      ; Test if this is the terminating zero
    jnz  .print      ; It's not, so go print the character
    pop  bx          ; Restore BX
    ret

Solution 2:[2]

I am almost certain you are asking this question in the context of reading this document by Nick Blundell on how to write your own OS from scratch, and trying to figure out question 4 on page 21.

Sep Roland's answer is great, but it's more advanced than what the author was trying to teach with the exercise. This point in the text hasn't covered graphics mode yet or the lodsb and test instructions. Blundell is going for something simpler that is trying to draw on your understanding of the tools you've been taught thus far in the reading: cmp, jmp, add, labels, and the various conditional jumps (je, jne, etc...).

My solution looks like this, which works well:

[org 0x7c00]     ; tell NASM what address this will be loaded at
; execution starts here
 mov ax, 0
 mov ds, ax      ; make segmentation agree with NASM about data addresses
; your code can start here, after the magic boilerplate

mov bx, HELLO_MSG
call print_string

mov bx, GOODBYE_MSG
call print_string

jmp $               ; infinite loop because there's nothing to exit to

print_string:
    pusha           ; preserve our general purpose registers on the stack
    mov ah, 0x0e    ; teletype function
.work:
    mov al, [bx]    ; move the value pointed at by bx to al
    cmp al, 0       ; check for null termination
    je .done        ; jump to finish if null
    int 0x10        ; fire our interrupt: int 10h / AH=0E
    add bx, 1       ; increment bx pointer by one
    jmp .work       ; loop back
.done:
    popa            ; pop our preserved register values back from stack
    ret

;; Data placed where execution won't fall into it
HELLO_MSG:
    db `Hello World!\n\r`, 0   ; use backticks to allow C style escape

GOODBYE_MSG:
    db 'Goodbye', 0

;; More boilerplate to make this a bootable MBR
times 510-($-$$) db 0    ; pad out to 510 bytes
dw 0xaa55                ; 2-byte signature so BIOS can recognize this as a bootable MBR

Again this answer can obviously be done better - I am just answering this in the way the author most likely intended you to learn here.

Solution 3:[3]

This question is quite old but I'm using the same document I found on the web as you are so I thought I'd share the solution I found. The writer (Nick Blundell of School of Computer Science, University of Birmingham, UK) wanted you to use the register bx to store the memory address that points to the beginning of the string. Then after you print one value you increment bx and so on until zero. Here's my solution.

[org 0x7c00]

mov bx, HELLO_MSG
call print_string_mem

mov bx, GOODBYE_MSG
call print_string_mem

jmp $                   ; Hang
    
%include "print_string.asm"

; Data
HELLO_MSG:
    db 'Hello, World!', 0
    
GOODBYE_MSG:
    db 'Goodbye!', 0

times 510-($-$$) db 0
dw 0xaa55
print_string_mem:
    jmp test_mem
    
    test_mem:
        mov al, [bx]
        cmp al, 0
        je end_mem
        jmp print_mem
        
    print_mem:
        mov ah, 0x0e
        int 0x10
        add bx, 1
        jmp test_mem
        
    end_mem:
        ret

    ret

The answer above mine works, and is better suited to real development, but the author wanted you to use the tools you learned in the previous pages to formulate your answer. I wracked my brain thinking about how the hell I was supposed to get this answer until I read the docs where he mentions setting bx to the memory address of the message. Hope this helps someone who was in my place.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Sep Roland
Solution 2 Peter Cordes
Solution 3