2

I'm relatively new to assembly programming and was wondering why my code does not print the expected strings. This project is supposed to be a bootloader when finished. I am compiling using the command nasm -f bin boot.asm -o boot.bin. There are no errors during compilation.

boot.asm

bits 16
org 0x7C00

%include "print.asm"
%include "text.asm"

boot:
        mov si, boot_string_00
        call print
        mov si, boot_string_01
        call print

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

print.asm

print:
        mov ah, 0x0E

.print_loop:
        lodsb
        or al, al
        je .print_done
        int 0x10
        jmp .print_loop

.print_done:
        cli
        ret

text.asm

boot_string_00: db "Placeholder OS Title v0.0.1", 0
boot_string_01: db "Loading Operating system", 0

Expected Output:

PlaceHolder OS Title v0.0.1Loading Operating System

Actual Output:

S

Also, I was wondering how i could implement newlines in assembly so that i could just use '\n' in my strings.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
rnfudge
  • 23
  • 3
  • 3
    You included your extra files at the top of your bootloader where they'll execute first. Use a debugger like BOCH's built-in one, this should be pretty easy to see in the disassembly view. asm doesn't have functions; you have to implement them yourself out of labels and branches. – Peter Cordes Apr 07 '20 at 02:48
  • 1
    The `cli` is misplaced. – ecm Apr 07 '20 at 10:29

2 Answers2

4

You included stuff at the top of your bootloader, where it will executes first. Instead include extra functions where they aren't in the main path of execution and are only reached by call.


This should work, placing the %include directives where it's safe to put extra function or data, just like if you were writing them all in one file.

boot.asm:

[bits 16]
[org 0x7c00]

boot:
  xor ax, ax
  mov ds, ax        ; set up DS to make sure it matches our ORG

  mov si, boot_string_00
  call println

  mov si, boot_string_01
  call println

finish:       ; fall into a hlt loop to save power when we're done
  hlt
  jmp finish
 

%include "printf.asm"      ; not reachable except by call to labels in this file
%include "text.S"


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

printf.asm:

print:
        mov ah, 0x0E      ; call number for int 0x10 screen output

print_loop:
        lodsb
        test al, al
        je print_done
        int 0x10
        jmp print_loop

print_done:
        ret
           
println:
  call print
  mov si, line_end
  call print
  ret

text.S:

boot_string_00: db "Placeholder OS Title v0.0.1", 0
boot_string_01: db "Loading Operating system", 0
line_end:       db 0xD, 0xA, 0
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • 1
    Linebreak should be a CR (13, 0Dh) followed by LF (10, 0Ah). – ecm Apr 07 '20 at 10:27
  • 1
    You should add the following after the last call to `print`: `halt:` \ `sti` \ `hlt` \ `jmp halt` – ecm Apr 07 '20 at 10:28
  • 1
    You should also add segment register initialisation, before calling the function: `xor ax, ax` \ `mov ds, ax` – ecm Apr 07 '20 at 10:31
  • 1
    Why would you want `print` to disable interrupts before returning? That doesn't make sense as part of printing. If the main bootloader wants that, it can do it itself. Also, use `test al,al` not `or al,al`. They both work but [`test` is better for efficiency](https://stackoverflow.com/questions/33721204/test-whether-a-register-is-zero-with-cmp-reg-0-vs-or-reg-reg/33724806#33724806) and is more idiomatic. You have your `line_end` backwards. It probably works here but more standard to do `\r\n` not `\n\r`. There's also no need to put the `finish` loop after the includes; before is fine. – Peter Cordes Apr 07 '20 at 16:27
  • 1
    As Peter Cordes listed, the order of the linebreak should be CR 13 LF 10, not 10 13. The `cli` is wrong but to be fair that was already present in the question. However, you should change "[the halting loop from `JMP $` to `sti` \ `hlt` \ `jmp` to idle while halting. This means the qemu process won't waste CPU time while running this loop.](https://stackoverflow.com/questions/60980527/how-to-solve-bootloader-asm30-error-times-value-44-is-negative-problem-in/60988502#60988502)" – ecm Apr 07 '20 at 16:47
  • Thanks for these informative comment. I editied the code. @PeterCordes – Furkan Çetinkaya Apr 07 '20 at 18:43
  • @ecm Thanks for these informative comments. I editied the code. – Furkan Çetinkaya Apr 07 '20 at 18:43
  • 1
    Now your finish loop isn't a loop. You `jmp` to a `hlt` and then fall off the end. Instead, fall into the `hlt` and then `jmp` back to before the `hlt`. – Peter Cordes Apr 07 '20 at 18:45
  • @PeterCordes I did not understand. Is it for that "Don't finish the program. If an interrupt occurs, do the another job." for future extension of the code or something else? – Furkan Çetinkaya Apr 07 '20 at 19:04
  • 1
    Try it in an emulator + debugger like BOCHS or QEMU. Set a breakpoint on the `times 510-($-$$) db 0` and note that it's reached after the first timer or keyboard interrupt after `hlt` runs. You should put `hlt` *in* a loop because it only sleeps until the next interrupt. (The other via option is `cli` / `hlt` (because no NMIs should happen so nothing will wake hlt up) but then you can't reboot with ctrl+alt+del because interrupts are disabled so the keyboard interrupt is ignored.) – Peter Cordes Apr 07 '20 at 19:10
  • I edited your answer since apparently you're not understanding what I was trying to describe. The reason for it is in my previous comment. – Peter Cordes Apr 07 '20 at 19:16
  • 1
    What is the point of putting `halt_op:` after the includes? There's zero benefit to looping there instead of at the end of the main code. IMO every part of your last edit made your answer worse, removing comments (especially the ones that explain the key change) and making the code more complicated with an extra jmp. Adding the `magic_num:` label was almost an improvement, but you put it on the padding, not the actual boot signature. But it's your answer, feel free to do whatever you want. – Peter Cordes Apr 07 '20 at 19:25
  • Sorry for reediting. I did not realize it. I am converting it back. @PeterCordes – Furkan Çetinkaya Apr 07 '20 at 19:27
  • 1
    Yeah, looks fine now. The key parts you initial answer was missing was some text explaining what the problem was, and why the change fixed it. That's part of what I added with my edit, along with comments in the code pointing that out. – Peter Cordes Apr 07 '20 at 19:40
  • @PeterCordes Thanks for comments – Furkan Çetinkaya Apr 07 '20 at 19:44
2

Includes don't go at the top

When using a directive like %include "print.asm", NASM will insert the contents of the file print.asm right where you have written the line. The same is true for %include "text.asm". The expanded source text thus becomes:

bits 16
org 0x7C00

print:                                               \ %include "print.asm"
        mov ah, 0x0E                                 |
                                                     |
.print_loop:                                         |
        lodsb                                        |
        or al, al                                    |
        je .print_done                               |
        int 0x10                                     |
        jmp .print_loop                              |
                                                     |
.print_done:                                         |
        cli                                          |
        ret                                          /
boot_string_00: db "Placeholder OS Title v0.0.1", 0  \ %include "text.asm"
boot_string_01: db "Loading Operating system", 0     /

boot:
        mov si, boot_string_00
        call print
        mov si, boot_string_01
        call print

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

Now, when BIOS has finished loading your bootsector code at the address 0x7C00, it will transfer control to that same address in memory. The first instruction that the CPU encounters will be mov ah, 0x0E and so our printing loop starts. Problem is, we haven't yet setup the pointer to the message. The intend was for the code to start executing at the boot label, and the includes made that go wrong.
A quick solution would be to have a jmp boot instruction as the very first instruction beneath the org 0x7C00 directive. But why waste 2 or 3 bytes when we could just as well, and better, place the includes below the rest of the code? That's going to be the preferred solution:

bits 16
org 0x7C00

boot:
        mov si, boot_string_00
        call print
        mov si, boot_string_01
        call print

%include "print.asm"
%include "text.asm"

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

Let's expand the includes again and verify that the problem is resolved.

bits 16
org 0x7C00

boot:
        mov si, boot_string_00
        call print
        mov si, boot_string_01
        call print

print:                                               \ %include "print.asm"
        mov ah, 0x0E                                 |
                                                     |
.print_loop:                                         |
        lodsb                                        |
        or al, al                                    |
        je .print_done                               |
        int 0x10                                     |
        jmp .print_loop                              |
                                                     |
.print_done:                                         |
        cli                                          |
        ret                                          /
boot_string_00: db "Placeholder OS Title v0.0.1", 0  \ %include "text.asm"
boot_string_01: db "Loading Operating system", 0     /

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

Both messages print fine, but, as you can see, once the second call print returns, the code falls through in the print routine and starts printing an empty message (because the SI register is pointing at the first zero-byte inserted by the times directive).
A far worse problem is, that because this (third) time, the ret instruction has no sensible return address on the stack, the computer will crash, but in a dangerous way because there's no telling about where execution will go to!

The solution is to prevent the code falling through in the print subroutine. Because our program has nothing more to do, we will insert a halting loop so the CPU doesn't waste precious cycles in a tight loop. The preferred way is cli hlt jmp $-2.

bits 16
org 0x7C00

boot:
    mov si, boot_string_00
    call print
    mov si, boot_string_01
    call print

    cli
    hlt
    jmp $-2

%include "print.asm"
%include "text.asm"

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

There's room for improvement in the present print routine

  • The BIOS.Teletype function 0Eh expects the BH register to contain the desired DisplayPage and the BL register the desired GraphicsColor for when the screen is in a graphics mode.
  • There's no sense in having the cli instruction in this code.
  • Testing the AL register for zero, would better use test al, al.
  • A simple loop like this should never use 2 branch instructions (while iterating).
  • This code depends on a correct DS segment register. Make sure the calling code has xor ax, ax mov ds, ax in accordance with the org 0x7C00 directive.
print:
    mov     bx, 0x0007   ; DisplayPage BH=0, GraphicsColor BL=7
    jmp     .begin       ; This is what makes 'empty messages' OK
.print:
    mov     ah, 0x0E     ; BIOS.Teletype
    int     0x10
.begin:
    lodsb
    test    al, al
    jnz     .print
    ret

Answering the additional question will improve the code even further

Also, I was wondering how i could implement newlines in assembly so that i could just use '\n' in my strings.

In NASM you can define string litterals in 3 different ways. Using single quotation marks ', using double quotation marks ", or using backticks `.

Only with the backticks can you include escape sequences in the text. In order to insert newlines \r\n, your text.asm would need to become:

boot_string_00: db `Placeholder OS Title v0.0.1\r\n`, 0
boot_string_01: db `Loading Operating system\r\n`, 0

to produce

Placeholder OS Title v0.0.1
Loading Operating system

Now that the newlines are embedded in the messages, you could consider simplifying the code and output the 2-line message as a whole, for a total savings of 7 bytes:

boot_string: db `Placeholder OS Title v0.0.1\r\nLoading Operating system\r\n`, 0

Why the \r\n sequence, and not simply \n?

For reference a quote from the Wikipedia article about escape sequences:

\n produces one byte, despite the fact that the platform may use more than one byte to denote a newline, such as the DOS/Windows CR-LF sequence, 0x0D 0x0A. The translation from 0x0A to 0x0D 0x0A on DOS and Windows occurs when the byte is written out to a file or to the console, and the inverse translation is done when text files are read.

The \n (newline) escape sequence only inserts the linefeed byte (10), but since this is bootloader code, and not DOS/Windows, BIOS will require both the carriage return byte (13) and the linefeed byte (10) in order to perform a move to the beginning of the next line. That's why we need to write \r\n.

Sep Roland
  • 33,889
  • 7
  • 43
  • 76
  • 1
    I believe you should use `\r\n` in those backticks to create the 13,10 sequence needed by int 10.0E to display a complete linebreak. – ecm Oct 29 '21 at 11:32
  • 1
    @ecm You're right. This is bootloader code, so BIOS will need both bytes to execute the newline, not just the code for linefeed. I will edit the answer now. Thanks. – Sep Roland Oct 30 '21 at 18:06