15

I have managed to produce a minimal boot sector that works with QEMU 2.0.0 Ubuntu 14.04:

.code16
.global _start
_start:
    cli
    mov $msg, %si
    mov $0x0e, %ah
loop:
    lodsb
    or %al, %al
    jz halt
    int $0x10
    jmp loop
halt:
    hlt
msg:
    .asciz "hello world"
.org 510
.word 0xaa55

Compiled with:

as -o main.o main.S
ld --oformat binary -o main.img -Ttext 0x7C00 main.o

The example is available on this repo: https://github.com/cirosantilli/x86-bare-metal-examples/tree/2b79ac21df801fbf4619d009411be6b9cd10e6e0/no-ld-script

Upon:

qemu -hda main.img

it shows hello world on the emulator screen as expected.

But if I try to burn to a USB:

sudo dd if=main.img of=/dev/sdb

then plug the USB into a ThinkPad T400 or T430, hit F12, and select the USB what I observe is:

  • some boot messages show up quickly
  • then the screen goes blank, with only a underscore cursor at the top

I have also tested the same USB with a Ubuntu 14.04 image, and it booted fine, so the USB is working.

How should I change this example so that it will boot on the hardware and show the hello world message?

What is the difference between the Ubuntu image and the one I've created?

Where is this documented?

I have uploaded the output of sudo dmidecode on the T400 to: https://gist.github.com/cirosantilli/d47d35bacc9be588009f#file-lenovo-t400

Michael Petch
  • 46,082
  • 8
  • 107
  • 198
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
  • 7
    As a bare minimum, you should zero `DS`. – Jester Sep 10 '15 at 18:15
  • 5
    Besides setting `DS` you should also explicitly set up a stack segment (`SS`) especially if you start using BIOS routines. You are also a bit lucky because QEMU and other emulators are much nicer to you by the time they call your boot code. The `CS` segment doesn't necessarily have to be 0 when the bios calls your bootloader. The _segment:offset_ will be equivalent to 0x0000:0x7c00, but `CS` could be other values like 0x07c0 (0x07c0:0x0000). To get around that you should do a far jmp to a label in your code with `CS` set to 0x0000. On real hardware this could be an issue. – Michael Petch Sep 10 '15 at 18:47
  • @MichaelPetch thanks for the further tips! I feel more like unlucky that QEMU does not closely emulate my hardware :-) I will study the register segments you mentioned more closely. I had never understood them well before because the OS hide them from me. – Ciro Santilli OurBigBook.com Sep 10 '15 at 18:55
  • 1
    A number of months ago I took a boot loader I wrote about 25 years ago and updated it to use at&t syntax and compilable with GNU Assembler for #throwbackthursday. You can find it [here](http://www.capp-sysware.com/tbt/boot.s). It uses CLI to turn off interrupts before performing a far jump, and after that sets up the Stack and ES, DS registers. Your code is Intel syntax but you should be get a general idea of what I did. This bootloader was designed to be run from a floppy (this code was for a 2.88MB 3.5" floppy) – Michael Petch Sep 10 '15 at 19:04
  • Oops your code was already using at&t syntax ;-). – Michael Petch Sep 10 '15 at 19:24
  • @MichaelPetch thanks, I'll have a look at the code! It's fine, at this point I can read both already :-) – Ciro Santilli OurBigBook.com Sep 10 '15 at 19:42
  • @MichaelPetch I think you can depend on CS on zero, but it's pretty trivial to make your boot sector not care what CS value is used. – Ross Ridge Sep 10 '15 at 21:03
  • 1
    @RossRidge Unfortunately you can't rely on being `0` it in real hardware. Some Old BIOSes liked to use 0x07C0:0000 in particular. which is the same physical memory location as 0x0000:0x7C00. By doing the far jmp to a local label you ensure you are using a known CS. Of course it doesn't need to be zero, but whatever origin your code was written to has to be matched by the CS. In the old days the far jmp to a local label was the simple way to make sure we had a CS we were expecting rather than the one the BIOS may have used. The sample code I made available does the simple far jmp method. – Michael Petch Sep 10 '15 at 21:09
  • @MichaelPetch In practice it doesn't matter what the value of CS is in a boot sector. Since the near direct JMP and CALL instructions use relative offsets, it's trivial to write boot sector can be loaded at any offset in the code segment and still work. – Ross Ridge Sep 10 '15 at 21:16
  • @RossRidge Yes, if you write your boot sector so that you avoid absolute jmps and calls then you only have to make sure that your DS (and other applicable segment registers) map properly to whatever the origin point being used was. – Michael Petch Sep 10 '15 at 21:42

1 Answers1

11

As mentioned by @Jester, I had to zero DS with:

@@ -4,2 +4,4 @@ _start:
     cli
+    xor %ax, %ax
+    mov %ax, %ds
     mov $msg, %si

Note that it is not possible to mov immediates to ds: we must pass through ax: 8086- why can't we move an immediate data into segment register?

So the root of the problem was difference between QEMU's initial state and that of the real hardware.

I am now adding the following 16-bit initialization code to all my bootloaders to guarantee a cleaner initial state. Not all of those are mandatory as mentioned by Michael Petch on the comments.

 .code16
cli
/* This sets %cs to 0. TODO Is that really needed? */
ljmp $0, $1f
1:
xor %ax, %ax
/* We must zero %ds for any data access. */
mov %ax, %ds
/* The other segments are not mandatory. TODO source */
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
/*
TODO What to move into BP and SP? https://stackoverflow.com/questions/10598802/which-value-should-be-used-for-sp-for-booting-process
Setting BP does not seem mandatory for BIOS.
*/
mov %ax, %bp
/* Automatically disables interrupts until the end of the next instruction. */
mov %ax, %ss
/* We should set SP because BIOS calls may depend on that. TODO confirm. */
mov %bp, %sp

I have also found this closely related question: C Kernel - Works fine on VM but not actual computer?

The Intel Manual Volume 3 System Programming Guide - 325384-056US September 2015 9.10.2 "STARTUP.ASM Listing " contains a large initialization example.

Community
  • 1
  • 1
Ciro Santilli OurBigBook.com
  • 347,512
  • 102
  • 1,199
  • 985
  • 3
    A hint: To zero out registers use `xor %reg,%reg`. This reduces the length of the machine code from three to one byte and is faster on modern 8086 compatibles. Just use `xor %ax,%ax` instead of `mov $0,%ax`. – fuz Sep 11 '15 at 07:53
  • @FUZxxl thanks! I had already seen that one at http://stackoverflow.com/questions/1135679/does-using-xor-reg-reg-give-advantage-over-mov-reg-0 but it hadn't entered my habit :-) Fixed. – Ciro Santilli OurBigBook.com Sep 11 '15 at 08:19
  • 2
    BIOS routines don't require FS and GS to be set. ES doesn't need to be set unless a particular BIOS routine requires it as part of a parameter being passed in. – Michael Petch Oct 30 '15 at 08:37
  • `mov 0x0000, %bp` seems suspect. This would move the 16bit value at DS;0000 into BP. Maybe you meant `mov $0x0000, %bp` ? (Of course you could use XOR to zero out BP) – Michael Petch Oct 30 '15 at 16:09
  • @MichaelPetch thanks for all the info! It as definitely meant to be `mov $0x0000, %bp`, that's what you get from copy pasting from Intel syntax :-) – Ciro Santilli OurBigBook.com Oct 30 '15 at 16:20
  • No problem. As well since ax already is zero you could just mov ax to bp. – Michael Petch Oct 30 '15 at 16:21
  • @MichaelPetch true, saves one byte. – Ciro Santilli OurBigBook.com Oct 30 '15 at 16:24
  • 2
    As for BP it can be used for anything. It doesn't need to be initialized unless you have a reason to use it. BIOS calls don't use stack frames, and it isn't required that one uses stack frames in your own code unless you are trying to be compatible with some type of convention. The BIOS doesn't care. If a BIOS call requires BP as a parameter the documentation will say list that requirement and only needs to be set before the call. SS:SP is a different story. BIOS routines and interrupts need a usable SS:SP. – Michael Petch Oct 31 '15 at 00:43
  • Back when I was doing DOS asm programming, I just used BP as an extra value register for data that I wanted quick access to. – Brian Knoblauch Nov 04 '15 at 21:07
  • I assume that your link to webarchive is in the event that Intel moves/removes the manual? – Michael Petch Nov 05 '15 at 00:33
  • @MichaelPetch yup! And it also contains the original URL as a subset of the URL. This is how you use to take your own screenshot: http://softwarerecs.stackexchange.com/questions/18651/web-service-that-serves-as-public-proof-that-a-given-url-contains-something-at-a – Ciro Santilli OurBigBook.com Nov 05 '15 at 07:00
  • 1
    I had the exact same issue and got resolved by your solution: zeroing-out DS and ES registers at the beginning of my boot-sector code: `xor ax, ax` and `mov ds, ax` and `mov es, ax`. – Megidd Nov 30 '17 at 17:38