'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 |
