Search (using Google):  Web Karig

 

9 January 2004

Reading loader sectors

I've been stuffing more and more code into the boot sector. Eventually I will run out of room, and I'll have to start putting code onto other sectors. When that happens, I will have to put into the boot sector some code to load one or more other sectors. This new code obviously has to go into the boot sector, because that's the only sector that the BIOS loads at startup.

I've also been thinking about the role of the boot sector. In some operating systems, the boot sector contains only temporary code — the boot sector is loaded and run and then discarded, and the memory it took up is reused. However, in colorForth, for example, the creed is to avoid waste and to squeeze as much functionality as possible into as little space as possible, so the boot sector is kept around; the code in the boot sector is used by code that is loaded later.

I had an idea I find appealing: The boot sector is simply the first 512 bytes of the system loader. The boot sector's job is simply to load in the rest of the system loader (to address 0000:7E00). If there is room left in the boot sector for normal system-loader code, then such code should be put into the boot sector. The system loader as a whole would be a real-mode chunk of code starting at address 0000:7C00 and extending no further than 0000:FFFF — that is, the system loader would be no larger than 33KB and would occupy no more than sixty-six sectors on the floppy disk. That doesn't sound like a lot of space, but I suspect that I can squeeze a lot into 33KB if I try.

BIOS routines

I need to use two BIOS services here:

  • INT 13, AH=00h resets the disk drive. Pass the drive number (0 for the floppy drive) in DL. This service returns with the carry set if there was an error.
  • INT 13, AH=02h reads sectors into memory. You'd pass AL = number of sectors to read, CH = cylinder number*, CL = starting sector, DH = head number, DL = drive number (again, 0 for the floppy), ES:BX = buffer into which the sectors are copied. This service returns with the carry set if there was an error.

*(Actually CH holds only the low eight bits of the cylinder number, which has ten bits. The top two bits of this number are stored in the top two bits of CL. However, the code I present here doesn't deal with cylinder numbers higher than one, so this is just a technicality — ignore it for now.)

Code

The full code is here.

Code — boot sector

The code to read in other sectors works like this:

I reset the drive (INT 13h, function 00h) before trying to read from it. If this fails, I keep trying until the drive is reset. (Note that when the BIOS passes control to the boot sector, DL will contain the number of the drive from which the boot sector was read. I save this on the stack here because I'll need it later to read more sectors from the same drive.)

		push	dx
	.0:	xor	ax, ax
		int	0x13
		jc	.0
		pop	dx

I then prepare to read the sectors into memory.

The ES register must point to the segment into which the sectors are to be read. My boot-sector code starts out by pointing all data-segment registers at segment zero, which is indeed where I want to load the sectors, so I don't need to do anything. If not for that, I'd include these lines to do the job:

		; these lines aren't in the boot-sector code
		xor	ax, ax
		mov	es, ax

I then load registers with starting values. These values will change as the loop runs.

The loop is here to call the sector-reading service more than once. The reason is that the service cannot read sectors from more than one track at once. If the sectors you want to load are spread out over four tracks, then you must call the service four times.

Each track contains eighteen sectors. If the number of sectors I have to load here is eighteen or less, then I call the service once, to load the sectors from the first track. If the number of sectors is greater than eighteen, then I load the sectors from the last track first, followed by one or more entire tracks. The last track loaded is thus the first one on the disk — the one that begins with the boot sector.

(Yes I reload the boot sector. This should cause no harm, as I am overwriting the boot-sector code with a copy of the same code. I did it this way to simplify the code I had to write.)

AL contains the number of sectors to load. Here, I load it with the number of sectors to load from the last track to be loaded here.

		mov	al, initial_count

DH contains the number of the head to use (0 or 1). CH contains the number of the cylinder to read from. (The head and the cylinder together determine the track. Two cylinders times two heads equals four tracks. [Because this code will cause an error if it tries to read more than 66 sectors total, it will not read more than four tracks.])

		mov	dh, initial_head
		mov	ch, initial_cylinder

BX contains the offset into the segment to the buffer into which the sectors are to be read.

		mov	bx, initial_address

CL contains the number of the first sector to read from the track. Here, this will always be one. (Note that the first sector is sector one. There is no sector zero.)

		mov	cl, initial_sector

Here is the loop code. Note how all registers are preserved on the stack so they don't have to be reloaded before calling the BIOS service again.

		mov	ah, 2
	.1:	pusha
		int	0x13
		popa
		jc	.1

Now I prepare to read the previous track, if there is one. Because I preserved all the parameters I need to pass, I can simply modify the ones that need to be changed.

If there is another track to read, I want to read in the entire track.

		mov	al, sectors_per_track

The address of the buffer must be adjusted down by the size of the track (512 bytes per sector times 18 sectors per track).

		sub	bx, bytes_per_track

I decrement the head number. If the result is not zero, I set the head number to one and decrement the cylinder number. If that result is positive or zero, then I must go back and read in another track.

		dec	dh
		jz	.9
		mov	dh, 1
		dec	ch
		jns	.1

Code — test code

The test code simply shows the contents of the last thirty-two bytes of memory at the end of the "system loader" just loaded. The first sixteen bytes should be a line of NOP instructions (hex value 0x90), and the second sixteen bytes should be the characters "Last sector read".

Note that I don't bother to load FS here because the code at the start of the boot sector already set it to segment 0000.

		mov	bx, 0xFFE0
		call	dump_16
		call	dump_16

Code — end of boot sector

I decided to change the code that expands the boot sector to 512 bytes:

; ------ (Required to make this a boot sector.)
		times	508 - ($-$$) db 0x90 ; nop
		jmp	short $+4
		db	0x55, 0xAA

Here I am reserving the last four bytes of the boot sector. The last two bytes are always reserved for the magic numbers 0x55 and 0xAA, so that the BIOS recognizes the boot sector as a boot sector. I'm reserving the two bytes before this for a JMP SHORT instruction that hops over the last two bytes.

I'm doing this because I want the sectors following the boot sector to be an extension of the boot sector. Code in the boot sector should simply continue beyond the end of the boot sector, so I padded the boot sector with NOP instructions and ended it with that JMP.

Code — other sectors

This code creates an ersatz "system loader" for testing. Actually it appends 65 sectors' worth of NOP instructions to the end of the boot sector, followed by a sixteen-character message. If the sector-loading code above works correctly, the message below will appear on the screen when the boot sector is run.

		times	(512 * 65) - 16 db 0x90 ; nop
		db "Loader is loaded" ; for testing

Code — NASM directives

These lines come last in the file — after any code intended to be part of the system loader. (They all use NASM directives; they'd need to be changed before they'd work with other assemblers.)

The first three of these lines pad the binary so that it will fill an integral number of sectors.

	%if (($-$$) % 512)
		times 512 - (($-$$) % 512) db 0
	%endif

The rest of these lines are equates — constant values, or the results of calculations performed by NASM during the assembly process. If these calculations can be done by the assembler, then I don't have to burden the boot sector further with the code needed to do these calculations at runtime.

	sector_total        equ ($-$$)/512
	sectors_per_track   equ 18
	heads_per_track     equ 2
	bytes_per_sector    equ 512
	bytes_per_track     equ bytes_per_sector * sectors_per_track

These are initial values to be passed to the sector-reading service the first time it is called. Note that initial_platter is used here only, to make the calculations easier to understand.

	initial_sector      equ 1
	initial_platter     equ (sector_total - 1) / sectors_per_track
	initial_cylinder    equ initial_platter / heads_per_track
	initial_head        equ initial_platter % heads_per_track
	initial_address     equ initial_platter * bytes_per_track + 0x7C00

These lines ensure that initial_count (the number of sectors to be read in the first disk access) cannot be zero and always falls in the range 1..18.

	initial_count_test  equ sector_total % sectors_per_track
	%if initial_count_test
		initial_count equ initial_count_test
	%else
		initial_count equ sectors_per_track
	%endif

Results

I just ran this code on my laptop, and the screen displays just what I expected it would:

0000:FFE0: 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 90 | ................
0000:FFF0: 4C 6F 61 64 65 72 20 69 73 20 6C 6F 61 64 65 64 | Loader is loaded

Check the index for other entries.