|
||||||||||||||
|
||||||||||||||
bootblock.s |
||||||||||||||
Skip to line: 2025 - 2050 - 2075 - 2100 - 2125 - 2150 - 2175 - 2206 | ||||||||||||||
If you have a comment for bootblock.s, please click here. | ||||||||||||||
|
||||||||||||||
2017 ! |
It's also important to know that dl contains the drive that
was booted (in other words, the drive from which this code originated).
This is 0x00 or 0x01 for the first or second floppy drives or 0x80, 0x81,
0x82, or 0x83 for the first through fourth hard drives.
If this code originated from a hard drive and the master boot code loaded this code, then the partition table entry that corresponds to the booted partition is passed in es:si. | ||
2018
2019 LOADOFF = 0x7C00 ! 0x0000:LOADOFF is where this code is loaded 2020 BOOTSEG = 0x1000 ! Secondary boot code segment. 2021 BOOTOFF = 0x0030 ! Offset into secondary boot above header |
2022 BUFFER = 0x0600 ! First free memory |
2023 LOWSEC = 8 ! Offset of logical first sector in partition |
2024
! table
2025 |
2026 ! Variables addressed using bp register |
2027 device = 0 ! The boot device |
2028 lowsec = 2 ! Offset of boot partition within drive |
2029 secpcyl = 6 ! Sectors per cylinder = heads * sectors |
This is fairly common practice in assembler. Several values are
pushed on the stack (lines 2043, 2044, and 2046) and then the stack pointer,
sp ,
is saved in bp (mov bp, sp - line 2047). If 2 byte
values are pushed on the stack, then the last 2 bytes are accessed by 0(bp) ,
the next-to-last 2 bytes are accessed by 2(bp) , and so on (see
line 2095).
While these 3 variables take up 8 bytes, only 6 bytes are pushed onto the stack. lowsec (lines 2043 and 2044) and device (line 2046) are pushed, but what about secpcyl ? When a value is finally stored in secpcyl (line 2110), the first 2 bytes of the bootblock code are overwritten - the first xor ax, ax instruction (line 2036) is overwritten. This doesn't matter since the code never jumps back to the beginning. This saves not only the 2 bytes needed for the variable but also the memory required for a push instruction. | ||
2036 xor ax, ax ! ax = 0x0000, the vector segment |
2037 mov ds, ax |
The first instruction zeroes the ax register (any number xor'ed
with itself is zero). This is a pretty common practice. The
instruction
mov ax, #0 is slower and is 3 bytes compared with xor's 2 bytes. One thing that initially confuses people with assembler is the order of the operands. The syntax of the mov instruction is: mov destination, source This seems a little strange to me but most assemblers use this syntax. | ||
2038 cli ! Ignore interrupts while setting stack |
2039 mov ss, ax ! ss = ds = vector segment |
2041 sti |
Whenever a value is moved to the stack register (ss)
or the stack pointer (sp), the interrupts must be disabled first.
The stack holds the address to which an interrupt returns after its completion.
If the ss and
sp registers are in flux and an interrupt
occurs, it's impossible to predict where the code will return.
The interrupts are disabled with the cli (clear interrupts) instruction and reenabled with the sti (set interrupts) instruction. What's the pound (#) sign all about? The pound sign indicates that the value of LOADOFF (0x7C00 - see line 2019) is moved to the register rather than the contents of the memory location LOADOFF. | ||
2042
2043 push ax 2044 push ax ! Push a zero lowsec(bp) 2045 2046 push dx ! Boot device in dl will be device(bp) |
2047 mov bp, sp ! Using var(bp) is one byte cheaper then var. |
This is a little technical, but I believe the idea is that an immediate instruction is one byte larger than a special address mode instruction (ss:disp[bp]). For example, the instruction on line 2095 is one byte smaller than it would have been if 2 bytes had been set aside (using .data2) for lowsec and the mov es, LOADOFF+lowsec instruction were used. Since the bootblock code and sector addresses (see lines 2204-2206) must fit in a single block (1 block = 2 sectors = 2 * 512 bytes = 1024 bytes), space needs to be conserved. Since a stack doesn't take up any hard drive space, hard drive space is saved by not allocating space within the code (using .data1 or .data2). | ||
2048
2049 push es 2050 push si ! es:si = partition table entry if hard disk 2051 |
di now points to sectors (line 2200). | ||
2053 |
2054 testb dl, dl ! Winchester disks if dl >= 0x80 |
2055 jge floppy |
testb sets the sign flag if the value in dl is negative. If dl is not negative, jge jumps to floppy. When is the value negative? In two's complement notation, if the rightmost bit is a 1 then the value is negative. So for a one byte value, anything greater than 0x7F (=0111111) is negative. Remember that 0x00 and 0x01 correspond to the first and second floppy drives and 0x80, 0x81, 0x82, and 0x83 correspond to hard drives 1-4. | ||
2056
2057 winchester: 2058 2059 ! Get the offset of the first sector of the boot partition from the partition 2060 ! table. The table is found at es:si, the lowsec parameter at offset LOWSEC. 2061 |
2062 eseg |
An instruction prepended by eseg uses es (instead of ds) as the segment register. The les instruction loads the register specified by the first operand (in this case ax) with an offset address and es with a segment register. The value at es:LOWSEC(si) is loaded in ax and the value at es:LOWSEC+2(si) is loaded in es. The eseg prefix is needed since es:LOWSEC(si), not ds:LOWSEC(si), is the memory address that holds the lowest sector (see comment for line 2017). Note that the es and ax registers are overwritten by the eseg les ax, LOWSEC(si) instruction. For this reason, the es and ax registers were pushed onto the stack on lines 2049-2050. | ||
2066
2067 ! Get the drive parameters, the number of sectors is bluntly written into the 2068 ! floppy disk sectors/track array. 2069 2070 movb ah, #0x08 ! Code for drive parameters |
2071 int 0x13 ! dl still contains drive |
int 0x13, ah=0x08 returns the device geometry of the drive specified by dl. dl will be 0x80 for the first drive, 0x81 for the second, 0x82 for the third and 0x83 for the fourth. int 0x13, ah=0x08 returns the maximum sector number in bits 0-6 of cl and the maximum head number in dh. Adding to the confusion, the value that int 0x13, ah=0x08 returns for the maximum head number has a 0-origin. This means that if int 0x13, ah=0x08 returns a 15 for the maximum head number, there are actually 16 heads; dh is incremented on line 2074 to move to a 1-origin. | ||
2072
andb cl, #0x3F !
cl = max sector number (1-origin)
2073 movb (di), cl ! Number of sectors per track 2074 incb dh ! dh = 1 + max head number (0-origin) 2075 jmp loadboot 2076 |
2077 ! Floppy: |
2078 ! Execute three read tests to determine the drive type. Test for each floppy |
2079 ! type by reading the last sector on the first track. If it fails, try a type |
2080 ! that has less sectors. Therefore we start with 1.44M (18 sectors) then 1.2M |
2081 ! (15 sectors) ending with 720K/360K (both 9 sectors). |
An attempt to read the 18th sector on the first track of the floppy is first made. If that attempt fails, the drive specified by dl (either 0x00 or 0x01) is not a functioning 1.44M floppy drive. An attempt to read the 15th sector on the first track is next made. If that fails, the drive specified by dl is not a functioning 1.2M floppy drive. An attempt to read the 9th sector is the final attempt (for 720K/360K floppy drives). | ||
2082 |
2083 next: inc di ! Next number of sectors per track |
di points to sectors (see lines 2052 and 2200). | ||
2084 |
2085 floppy: xorb ah, ah ! Reset drive |
int 0x13, ah=0x00 resets the drive specified by dl. | ||
2086
int 0x13
2087
2088 movb cl, (di) ! cl = number of last sector on track 2089 2090 cmpb cl, #9 ! No need to do the last 720K/360K test 2091 je success 2092 2093 ! Try to read the last sector on track 0 2094 2095 mov es, lowsec(bp) ! es = vector segment (lowsec = 0) |
The memory region beginning at address BUFFER is not being used for anything now, so this region can be used as scratch area and hard drive sectors can be copied there to test out the floppy drives. | ||
2097
mov ax, #0x0201 ! Read
sector, #sectors = 1
2098 xorb ch, ch ! Track 0, last sector 2099 xorb dh, dh ! Drive dl, head 0 |
2100 int 0x13 |
int 0x13, ah=0x02 copies sectors from a hard drive or floppy to memory. al specifies the number of sectors to copy, bits 0-5 of cl specify the sector number, dh specifies the head number and ch specifies the low 8 bits of the cylinder number and bits 6-7 of cl specify the high bits of the cylinder number. es:bx specifies where in memory to load the sectors. If the int 0x13, ah=0x02 call fails, the carry (C) flag is set. If the carry flag is set, the jc instruction (line 2101) jumps to memory address next (line 2083). | ||
2103 success:movb dh, #2 ! Load number of heads for multiply |
A floppy is a single disk with 2 heads, one for each side. | ||
2104 |
2105 loadboot: |
Lines 2105-2153 are the heart of bootblock. The sectors listed
at memory address addresses (line 2204) are loaded; these sectors
make up the secondary boot. The sector numbers were patched at addresses
by the installboot
utility program.
Grinding through this code isn't fun. Everything until line 2141 sets things up for the int 0x13 bios call. | ||
2106 ! Load the secondary boot code from the boot device
2107 2108 movb al, (di) ! al = (di) = sectors per track |
2109 mulb dh ! dh = heads, ax = heads * sectors |
mulb multiplies al by the operand (in this case dh). The result is placed in ax. | ||
2110
mov secpcyl(bp), ax ! Sectors
per cylinder = heads * sectors
2111 2112 mov ax, #BOOTSEG ! Segment to load secondary boot code into 2113 mov es, ax 2114 xor bx, bx ! Load first sector at es:bx = BOOTSEG:0x0000 2115 mov si, #LOADOFF+addresses ! Start of the boot code addresses 2116 load: |
2117 mov ax, 1(si) ! Get next sector number: low 16 bits |
2118 movb dl, 3(si) ! Bits 16-23 for your 8GB disk |
2119 xorb dh, dh ! dx:ax = sector within partition |
The installboot utility patches in 2 values for each 4 byte entry at addresses. It uses the 1st byte of the entry for the count and the 2nd, 3rd and 4th bytes of the entry for the sector number (actually, it's the offset of the sector within the partition - see comments for 2120-2121). The best way to describe the count is with an example. If the secondary boot code is found at sectors 500, 501, 502, 509, and 510, there would be two entries at addresses - the first entry would be count=3, sector number=500 and the second entry would be count=2, sector number=509. | ||
The value patched at addresses is the offset of the sector within the partition, not the absolute sector number. The int 0x13, ah=0x02 bios call needs the absolute sector number so lowsec is added to the offset (lowsec is the first sector of the partition). | ||
2122
div secpcyl(bp)
! ax = cylinder, dx = sector within cylinder
2123 xchg ax, dx ! ax = sector within cylinder, dx = cylinder 2124 movb ch, dl ! ch = low 8 bits of cylinder |
2125 divb (di) ! al = head, ah = sector (0-origin) |
The divb instruction divides ax by the operand (in our example (di) - di points to the memory address that holds the number of sectors per track) and puts the quotient into al and the remainder into ah. | ||
2126
xorb dl, dl
! About to shift bits 8-9 of cylinder into dl
2127 shr dx, #1 2128 shr dx, #1 ! dl[6..7] = high cylinder 2129 orb dl, ah ! dl[0..5] = sector (0-origin) 2130 movb cl, dl ! cl[0..5] = sector, cl[6..7] = high cyl 2131 incb cl ! cl[0..5] = sector (1-origin) 2132 movb dh, al ! dh = al = head 2133 movb dl, device(bp) ! dl = device to read |
2134 movb al, (di) ! Sectors per track - Sector number (0-origin) |
2135 subb al, ah ! = Sectors left on this track |
2136 cmpb al, (si) ! Compare with # sectors to read |
2137 jbe read ! Can't read past the end of a cylinder? |
2138 movb al, (si) ! (si) < sectors left on this track |
The int 0x13, ah=0x02 bios call can only read sectors from
a single track at a time. Lines 2134-2138 calculate the maximum numbers
of sectors that can be read without crossing a track boundary.
cmp is a subtraction that only affects the flag register. jbe (line 2137) jumps if the first operand in cmp is below or equal to the second operand. | ||
2139 read: push ax
! Save al = sectors to read
2140 movb ah, #2 ! Code for disk read (all registers in use now!) |
2141 int 0x13 ! Call the BIOS for a read |
int 0x13, ah=0x02 copies sectors from a hard drive or floppy to memory. al specifies the number of sectors to copy, bits 0-5 of cl specify the sector number, dh specifies the head number and ch specifies the low 8 bits of the cylinder number and bits 6-7 of cl specify the high bits of the cylinder number. es:bx specifies the memory address where the sectors are loaded. If the int 0x13, ah=0x02 call fails, the carry (C) flag is set and an error code is placed in ah. If the carry flag is set, the jc instruction jumps to memory address error (line 2170). | ||
2142
pop cx
! Restore al in cl
2143 jc error ! Jump on disk read error 2144 movb al, cl ! Restore al = sectors read |
2145 addb bh, al ! bx += 2 * al * 256 (add bytes read) |
2146 addb bh, al ! es:bx = where next sector must be read |
int 0x13, ah=0x02 writes the sectors to es:bx.
What's done here is somewhat tricky. If 1 is added to bh, it is equivalent to adding 256 to bx. If 2 is added to bh, it is equivalent to adding 512 to bx (remember that 1 sector = 512 bytes). | ||
2147 add 1(si), ax ! Update address by sectors read |
2148 adcb 3(si), ah ! Don't forget bits 16-23 (add ah = 0) |
2149 subb (si), al ! Decrement sector count by sectors read |
Lines 2147-2149 modify the count and sector number in case the consecutive sectors crossed a track boundary and all the sectors could not be read. | ||
2150 jnz load ! Not all sectors have been read |
2151 add si, #4 ! Next (address, count) pair |
si points to the current entry. To move to the next entry, 4 is added to si (each entry is 4 bytes). | ||
2152
cmpb ah, (si)
! Done when no sectors to read
2153 jnz load ! Read next chunk of secondary boot code 2154 |
2155 done: |
When the entire secondary boot has been loaded, the code falls through to done . | ||
2156
2157 ! Call secondary boot, assuming a long a.out header (48 bytes). The a.out 2158 ! header is usually short (32 bytes), but secondary boot has two entry points: 2159 ! One at offset 0 for the long, and one at offset 16 for the short header. |
2160 ! Parameters passed in registers are: |
2161 ! |
2162 ! dl = Boot-device. |
2163 ! es:si = Partition table entry if hard disk. |
2164 ! |
Click here
to look at boothead.s.
If the secondary boot has either a short header or a long header, the instruction at BOOTSEG:BOOTOFF jumps to the boot address. These are the same parameters that were passed to the bootstrap (this code). Keep three things in mind: 1) BOOTSEG (defined in this file) and LOADSEG (defined in boothead.s) are both defined as 0x1000. 2) Adding 2 to the segment register adds 32 to the address formed by the segment:offset registers and adding 3 to the segment adds 48 to the address formed by the segment:offset registers. 3) boot is the offset from the end of the header. | ||
2165
pop si
! Restore es:si = partition table entry
2166 pop es ! dl is still loaded 2167 jmpf BOOTOFF, BOOTSEG ! jmp to sec. boot (skipping header). 2168 2169 ! Read error: print message, hang forever |
2170 error: |
If something goes wrong, the code ends up here. After a failed
int0x13, ah=0x02 bios call, ah holds the error code.
It helps to look at an ascii chart before trying to understand the following code. The ascii string at errno (line 2196) is modified. The *second* digit is first incremented to reflect (in ascii representation) the value in the left-most 4 bits of ah. If the right-most 4 bits of ah are nonzero, the code loops back to prnum and increments the *first* digit to reflect (in ascii representation) the value in the right-most 4 bits of ah . | ||
2171
mov si, #LOADOFF+errno+1
2172 prnum: movb al, ah ! Error number in ah 2173 andb al, #0x0F ! Low 4 bits 2174 cmpb al, #10 ! A-F? 2175 jb digit ! 0-9! 2176 addb al, #7 ! 'A' - ':' 2177 digit: addb (si), al ! Modify '0' in string 2178 dec si 2179 movb cl, #4 ! Next 4 bits 2180 shrb ah, cl 2181 jnz prnum ! Again if digit > 0 2182 |
Note that the strings on lines 2195-2196 occupy consecutive memory addresses and end at errend. One character at a time is printed until errend is reached. | ||
2184 print: lodsb
! al = *si++ is char to be printed
2185 movb ah, #0x0E ! Print character in teletype mode 2186 mov bx, #0x0001 ! Page 0, foreground color |
2187 int 0x10 ! Call BIOS VIDEO_IO |
int 0x10, ah=0x10 prints the character in al to the current cursor position and advances the cursor. bh holds the active page and bl holds the foreground color. | ||
2189 jb print |
Note that after reaching errend, the code falls through to hang. | ||
2190
2191 ! Hang forever waiting for CTRL-ALT-DEL 2192 hang: jmp hang 2193 2194 .data 2195 rderr: .ascii "Read error " 2196 errno: .ascii "00 " 2197 errend: 2198 2199 ! Floppy disk sectors per track for the 1.44M, 1.2M and 360K/720K types: 2200 sectors: 2201 .data1 18, 15, 9 2202 2203 .align 2 |
2204 addresses: |
2205 ! The space below this is for disk addresses for a 66K secondary boot |
2206 ! program (worst case, i.e. file is fragmented). It should be enough. |
The installboot utility patches
(count, sector address) pairs here. Look at line 2105 and lines 2117-2119
for a detailed explanation.
This code and the the secondary boot addresses have to fit into a single block (hence the name "boot block"). 1 block = 2 sectors = 2 * 512 bytes = 1024 bytes. If there is space for a 66K secondary boot when the secondary boot is fragmented (meaning that no 2 sectors are consecutive) then that means that there is space for (66K * 2 sectors/K=) 132 (count, sector address) entries. Each entry takes up 4 bytes so the total space taken up by the entries is 528 bytes. So the actual code (everything but the entries) takes up the remaining (1024-528=) 496 bytes. | ||