3

I have the following assembly code. I'm trying to create a small bootloader to go into 32-bit protected mode. Once in protected mode, I need to print to VGA text mode video memory (0xb8000) for testing purposes. My code doesn't work. I found code from various resources around the web and learned that most have similar code which works properly, like this example: Printing characters to screen ASM in protected mode . My code that doesn't work:

bits 16

mov ah, 0x00  ;Set up video mode
mov al, 0x03
int 0x10

gdt_start:
        dq 0x0
gdt_code:
        dw 0xFFFF
        dw 0x0
        db 0x0
        db 10011010b
        db 11001111b
        db 0x0
gdt_data:
        dw 0xFFFF
        dw 0x0
        db 0x0
        db 10010010b
        db 11001111b
        db 0x0  

gdtr:
        dw 24
        dd gdt_start

lgdt [gdtr]

cli

mov eax, cr0
or al, 1
mov cr0, eax

jmp 0x08:protectedMode 

bits 32

protectedMode:
    mov ax, 0x10
    mov ds, ax
    mov es, ax 
    mov fs, ax
    mov gs, ax
    mov ss, ax

    mov word [0xb8000], 0x0769 

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

I compile the code with:

nasm -fbin boot.asm -oboot.bin

and run the result with:

qemu-system-x86_64 -fda boot.bin

It doesn't do anything.

When I disassemble the code with:

ndisasm boot.bin

it outputs the following result:

enter image description here

Why is the instruction before appending the zeroes

mov dword [di], 0xb8000

while it should be

mov word [0xb8000], 0x0769
Michael Petch
  • 46,082
  • 8
  • 107
  • 198
user123
  • 2,510
  • 2
  • 6
  • 20

1 Answers1

3

When you have this block of data:

gdt_start:
        dq 0x0
gdt_code:
        dw 0xFFFF
        dw 0x0
        db 0x0
        db 10011010b
        db 11001111b
        db 0x0
gdt_data:
        dw 0xFFFF
        dw 0x0
        db 0x0
        db 10010010b
        db 11001111b
        db 0x0  

gdtr:
        dw 24
        dd gdt_start

it's located in the path of execution. This data will be executed as code by the processor as the next instructions after int 0x10. Move this lower, after mov word [0xb8000], 0x0769.

You also need to add an infinite loop after executing that instruction to prevent execution from falling down to whatever garbage follows (GDT table if you put it there).

Always keep in mind that assembly is very low-level. Whatever you stick in your code, whether actual meaningful instructions or not, will be treated as code if the processor ever gets to it. It won't skip data, and it won't stop after the last instruction you write.


As to why the instruction disassembly is wrong, the disassembler doesn't know when to switch to 32-bit mode. It's just a disassembler, not a simulator, so it can't see the effect of the far jmp that gets the CPU to execute that part in 32-bit mode.

You can disassemble the whole thing in 32-bit mode, and then (after some mess before disassembly happens to get back in sync with actual instruction boundaries) it disassembles to what you intended with this:

ndisasm -b 32 boot.bin
...                 ;; some earlier mess of 16-bit code disassembled as 32
0000003B  8ED8              mov ds,eax
0000003D  8EC0              mov es,eax
0000003F  8EE0              mov fs,eax
00000041  8EE8              mov gs,eax
00000043  8ED0              mov ss,eax
00000045  66C70500800B0069  mov word [dword 0xb8000],0x769   ; correct disassembly
         -07
0000004E  0000              add [eax],al
00000050  0000              add [eax],al
Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Thomas Jager
  • 4,836
  • 2
  • 16
  • 30
  • 2
    The OP's bootloader is only switching to 32-bit mode, not 64. Note that part of the code uses `BITS 32`. `ndisasm` doesn't know how to switch bitness part way through. I guess 32-bit `mov word [0xb8000], 0x0769` disassembles as 16-bit `mov dword [di], 0xb8000`, unless the disassembler was already out of sync from something else. But yes, `ndisasm -b 32` might work. Or simply check at runtime with BOCHS's debugger that will be aware of what mode the CPU is in. – Peter Cordes May 11 '20 at 13:00
  • @PeterCordes Good point, it's been a while since I've done this stuff, I forgot it all starts in 16-bit real mode. Fixed my answer to use 32-bit. Thanks for calling me out. – Thomas Jager May 11 '20 at 13:02
  • @Thomas Jager Thanks for the answer. It disassembles correctly when using -b 32. But now I get an error when launching with qemu. It says boot failed: could not launch the boot disk – user123 May 11 '20 at 13:17
  • @user123 In moving the GDT data below the code, do you still have the offset to the boot signature setup correctly? – Thomas Jager May 11 '20 at 13:22
  • Yes I do still have the signature. It is under the GDT declaration and the boot.bin is still 512 bytes with the signature at the end. I don't see what's wrong. – user123 May 11 '20 at 13:27
  • @user123 It's difficult to help without more information. I wouldn't expect the emulator to care about anything other than the signature, and possibly size, unless it's encountering invalid instructions and knows how to deal with that. I hesitantly suggest adding a new section to your question below the original with your revised assembly, and with the output of QEMU. – Thomas Jager May 11 '20 at 13:32
  • Maybe another time. For now I'll go back to something else. Thank you for your time. I did learn something with your answer and comments. I tried using dd if=boot.bin of=/dev/sdb to output the code to a usb key. But when I launch the key it still doesn't print anything just a blinking cursor. Your answer seems pretty logical to me though. I don't see what's wrong. I would expect it to work. – user123 May 11 '20 at 13:38
  • 3
    @user123 : Your bootloader *appears* to be showing _boot failed: could not launch the boot disk_ because QEMU is continually rebooting because of a tripe fault. That is because you haven't set an origin point for the code. At the top of your ASM file place an `org 0x7c00` directive. You haven't specified one so the origin point defaulted to 0x0000 and thus all the absolute memory references will be wrong. The LGDT command will use the wrong offset for `gdtr` and when you attempt to jump into protected mode it will triple fault because of bad GDT entries. – Michael Petch May 11 '20 at 15:51
  • @user123 : If you manage to get things working in an emulator like QEMU after you add the `org 0x7c00` (along with the other fixes suggested in the answer and comments) - If you attempt to run code via USB and BIOS is using FDD (Floppy DIsk Emulation) then it may fail for another reason. Because of BIOS quirks with USB booting you may need a BIOS Parameter Block (BPB) to get it booting via USB on real hardware. I have an answer about that issue here: https://stackoverflow.com/a/47320115/3857942 – Michael Petch May 11 '20 at 15:57
  • 4
    And one last thing. And this isn't mentioned in the answer. You can't rely on the segment registers being set to 0 when the BIOS transfer control to your code (This can often be the case if testing on real hardware). You should at a minimum set the DS register to 0x0000 explicitly at the beginning of your bootloader with `xor ax, ax` `mov ds, ax`. It's also a good idea to set your own stack pointer (especially if you intend to read more disk sectors to memory with BIOS calls as you build a real bootloader). I have general bootloader tips here: https://stackoverflow.com/a/32705076/3857942 – Michael Petch May 11 '20 at 16:03
  • 3
    Failure to explicitly set DS to 0 when using an ORG of 0x7c00 can lead to the wrong memory being referenced and may cause your code to fail. – Michael Petch May 11 '20 at 16:04
  • Thanks it worked with `org 0x7c00` and `xor ax, ax mov ds, ax`. As to real hardware I ended up creating a partition table using fdisk and now it works perfectly on both QEMU and USB key. – user123 May 11 '20 at 18:42
  • @user123 : Yes, if you boot the USB as HDD (hard disk emulation) most BIOSes require a partition table with one partition marked active. – Michael Petch May 11 '20 at 18:49
  • Why is it that `lgdt [gdtr]` use absolute memory references? Since I reference the gdt with labels, shouldn't it use relative adresses of the labels instead. – user123 May 11 '20 at 19:06
  • 1
    @user123 In real mode there is a segment and offset that combine together to create a physical address. Each memory operand has a segment associated with it. If BP is part of the memory operand the segment is assumed to be SS while everything else by default uses DS. To get a physical address the value in the segment is multiplied by 16 and the offset added to it. So a segment:offset pair of 0x0000:7c00 is physical address 0x0000*16+0x7c00=0x07c00 . segment:offset pair 0x07c0:0x0000 is also phys address 0x07c0*16+0x0000=0x07c00. Seg:offset 0xb800:0x1234 = 0xb9234 – Michael Petch May 11 '20 at 19:12
  • 1
    @user123 Hypothetically if some BIOS sets DS to some random or unknown value and you combine it with an offset to form a physical address then you don't know where in memory it actually points. That's why setting DS explicitly is a good idea because `lgdt [gdtr]` is actually `lgdt DS:[gdtr]`. If you don't know what DS is you can't know what physical address this actually points to. Starman's segment offset page is a good discussion of this: https://thestarman.pcministry.com/asm/debug/Segments.html – Michael Petch May 11 '20 at 19:14