3
\$\begingroup\$

I'm playing with the Real mode and was trying to code something when suddenly I exceeded the 510 boundary (.org 510) of the 512 boot sector size for BIOS and as warning popped out:

code.s: Assembler messages:
code.s:<line num>: Error: attempt to move .org backwards

So I started reading about how to split the code into multiple sectors and load them from the disk using the floppy for simplicity.

I'm probably missing a lot of stuff one of which would be manual stack init, though I'm not sure if that would affect the loading as it's coded here. I'm using -monitor stdio to check the values in registers if necessary with info registers. Seems sufficient enough for now, but feel free to suggest better debugging tools that work with Qemu.

Feel free not to hold back, I'd like to learn more. :)

The code is simple:

  • stores the disk number the boot sector was loaded from
  • prints a from the first sector via a print label
  • loads the next sector right after the boot sector in memory (to 0x7c00 + 0x200)
  • prints b from the second sector using the print label from the first sector, so should prove that it's not overlapping in or something weird like that
  • jumps to infinite loop to keep it running

Building:

#!/bin/sh -xe
as -o code.o code.s -g --statistics --warn --fatal-warnings
ld -o code.bin --oformat=binary -Ttext=0x7c00 --build-id=none code.o
qemu-system-i386 -monitor stdio -drive file=code.bin,index=0,if=floppy,format=raw

Code:

.att_syntax
.code16
.global _start

.text
// sector 1 begin
_start:
    // store booted from disk number for sector loading
    mov %dl, diskNum

    mov $'a', %al
    call print

    jmp load_next_sector

print:
    // expects char to print in %al
    mov $0x0e, %ah
    int $0x10
    ret

load_next_sector:
    // load the code to memory directly after the boot sector
    // boot: [0x7c00, 0x7c00 + 512); after: 0x7e00 = 0x7c00 + 512

    // service 02h: Read Sectors From Drive
    mov $0x02, %ah
    // Sectors To Read Count
    mov $0x01, %al
    // Cylinder/Track number 1 (zero-based)
    mov $0x00, %ch
    // Sector number 2 (one-based)
    mov $0x02, %cl
    // Head 1 (zero-based)
    mov $0x00, %dh
    // Disk Number
    mov diskNum, %dl

    // set Buffer Address Pointer to after boot sector (0x7e00)
    // es:bx = 0x7e00 -> es * 16 + bx = 0x7e00 -> 0 * 16 + 0x7e00
    push $0x00
    pop %es
    mov $0x7e00, %bx
    int $0x13

    // fail, AL codes: http://www.oocities.org/wangxuancong/int13h.html
    jc load_next_sector

    // success
    jmp sector_2

.org 510
.word 0xAA55
// sector 1 end

// sector 2 begin
sector_2:
    mov $'b', %al
    call print
    jmp pause

pause:
    jmp pause

.data
diskNum:
    .byte 0xff

.org 1024
// sector 2 end
.end

Binary:

0000000 1688 8000 61b0 02e8 eb00 b405 cd0e c310
0000010 02b4 01b0 00b5 02b1 00b6 168a 8000 006a
0000020 bb07 7e00 13cd e872 d5e9 0001 0000 0000
0000030 0000 0000 0000 0000 0000 0000 0000 0000
*
00001f0 0000 0000 0000 0000 0000 0000 0000 aa55
0000200 62b0 06e8 ebfe eb00 00fe 0000 0000 0000
0000210 0000 0000 0000 0000 0000 0000 0000 0000
*
0000400 00ff 0000 0000 0000 0000 0000 0000 0000
0000410 0000 0000 0000 0000 0000 0000 0000 0000
*
0000800

Output:

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Funny that you should talk about "overlapping in memory", because that's exactly what would happen if you loaded the 2nd sector at 0x7c00 + 0x100. In other words, you got a typo here: should read 0x200. \$\endgroup\$
    – Sep Roland
    Sep 8 at 2:00
  • \$\begingroup\$ Hahaha, yeah, it bit me once when I used 256 (0x100, 0x7d00) instead of 512 (0x200, 0x7e00), then I forgot about it and left it in the text. Thanks, editing! \$\endgroup\$ Sep 8 at 7:13
3
\$\begingroup\$

Improvements

  • When BIOS starts your bootloader (after it got loaded at linear address 7C00h), the only register that has a defined meaning is the %dl register. It contains the bootdrive code.
    From this follows that it is up to you to setup the segment registers. You really should not trust any BIOS, be it real or emulated, to have initialized the segment registers to 0, like you seem to expect. Although then, I'm a bit surprised to see that you have cleared the %es segment register manually in the load_next_sector routine.
    Given that you will load the following sector(s) above the bootsector, I would suggest you setup the stack beneath the bootsector:

    xor %ax, %ax
    mov %ax, %ds
    mov %ax, %es
    mov %ax, %ss       \ Always keep these 2 together and in this order
    mov $0x7C00, %sp   /
    
  • The BIOS.Teletype function 0Eh expects to find the desired DisplayPage in the %bh register.

    print:
      mov $0, %bh
      mov $0x0E, %ah
      int $0x10
      ret
    
  • It's fine to repeat the LoadSector operation in case it failed, but you should not allow this to continue forever. You should limit the number of retries to, say 5 times. And in between you can use the BIOS.ResetDisk function 00h in case that helps.

Optimization

A bootsector is limited to just 512 bytes. That's one reason to write compact code. Instead of loading related byte-sized registers separately, you could load these together in a word-sized operation:

mov $0x0201, %ax      // AH=02h ReadSectors, AL=1 SectorCount
mov $0x0002, %cx      // CH=0 Cylinder, CL=2 SectorNumber

Eventhough you mention "(zero-based)", I find it confusing to see that you comment about "Cylinder number 1" and "Head 1" and then load 0 in the relevant registers.
Better phrasing is "Cylinder 0" or "First cylinder", and "Head 0" or "First head".

Observation

0000400 00ff 0000 0000 0000 0000 0000 0000 0000

The diskNum variable is not part of any of the 2 sectors that get loaded into memory. In this extremely simple code, I see no harm, but once the project becomes bigger and possibly located elsewhere, you will have to make sure that that memory is available and remains available (depending on what your OS does).

\$\endgroup\$
5
  • \$\begingroup\$ Thanks! Clearing %es was done explicitly because I wanted to set the offset ES:BX to 0x0000:0x7e00, see the 0 * 16 + 0x7e00 part. \$\endgroup\$ Sep 8 at 7:23
  • 2
    \$\begingroup\$ Is there any reason of using mov instead of xor for zeroing out the registers? I thought xor is faster/more efficient (in general, CPU-specific obviously) due to less operators. \$\endgroup\$ Sep 8 at 7:24
  • \$\begingroup\$ The diskNum part, that I haven't noticed and in a longer code I just moved it to the top because it annoyed me to have it at the end without noticing the impact. I wonder, if I leave it like that and exceed the 512 bytes for the boot sector, is it considered an undefined behavior, CPU-specific behavior (as in the emulator does X and the real hw Y) or perhaps an error that went of silent (thus a possible bug in the assembler?). Because in general terms, it "just works" when I move after 2kB but that might be just some magic of Qemu and would break on a real hw. \$\endgroup\$ Sep 8 at 7:30
  • \$\begingroup\$ Also re diskNum, I don't need to retrieve the 0xff value, I just need a place to store %dl to, so perhaps that's the reason it "works" - it just gave me "some" memory from somewhere with a garbage value and then I have just overwritten it. \$\endgroup\$ Sep 8 at 7:34
  • 1
    \$\begingroup\$ @PeterBadida Indeed; besides not needing an immediate value (so shorter in original 8086 CPUs) modern pipelined CPUs understand both XOR and SUB of a register with itself as zeroing the value with no dependency on the old value, and handle it in the register remapping unit directly. \$\endgroup\$
    – JDługosz
    Sep 8 at 14:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.