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 aprint
label - loads the next sector right after the boot sector in memory (to
0x7c00
+0x200
) - prints
b
from the second sector using theprint
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 0ドルx0e, %ah
int 0ドルx10
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 0ドルx02, %ah
// Sectors To Read Count
mov 0ドルx01, %al
// Cylinder/Track number 1 (zero-based)
mov 0ドルx00, %ch
// Sector number 2 (one-based)
mov 0ドルx02, %cl
// Head 1 (zero-based)
mov 0ドルx00, %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 0ドルx00
pop %es
mov 0ドルx7e00, %bx
int 0ドルx13
// 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:
1 Answer 1
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 0ドルx7C00, %sp /
The BIOS.Teletype function 0Eh expects to find the desired DisplayPage in the
%bh
register.print: mov 0,ドル %bh mov 0ドルx0E, %ah int 0ドルx10 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 0ドルx0201, %ax // AH=02h ReadSectors, AL=1 SectorCount
mov 0ドルx0002, %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).
-
\$\begingroup\$ Thanks! Clearing
%es
was done explicitly because I wanted to set the offsetES:BX
to0x0000:0x7e00
, see the0 * 16 + 0x7e00
part. \$\endgroup\$Peter Badida– Peter Badida2021年09月08日 07:23:04 +00:00Commented Sep 8, 2021 at 7:23 -
2\$\begingroup\$ Is there any reason of using
mov
instead ofxor
for zeroing out the registers? I thoughtxor
is faster/more efficient (in general, CPU-specific obviously) due to less operators. \$\endgroup\$Peter Badida– Peter Badida2021年09月08日 07:24:06 +00:00Commented Sep 8, 2021 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\$Peter Badida– Peter Badida2021年09月08日 07:30:07 +00:00Commented Sep 8, 2021 at 7:30 -
\$\begingroup\$ Also re
diskNum
, I don't need to retrieve the0xff
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\$Peter Badida– Peter Badida2021年09月08日 07:34:05 +00:00Commented Sep 8, 2021 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– JDługosz2021年09月08日 14:19:30 +00:00Commented Sep 8, 2021 at 14:19
0x7c00
+0x100
. In other words, you got a typo here: should read0x200
. \$\endgroup\$256
(0x100
,0x7d00
) instead of512
(0x200
,0x7e00
), then I forgot about it and left it in the text. Thanks, editing! \$\endgroup\$