|Skip to line: 3100 - 3200 - 3300 - 3400 - 3500 - 3600 - 3700 - 3800 - 3900 - 4000 - 4100 - 4200 - 4284|
|If you have a comment for boothead.s, please click here.|
3000 ! Boothead.s
- BIOS support for boot.c
Author: Kees J. Bot
3003 ! This file contains the startup and low level support for the secondary
3004 ! boot program. It contains functions for disk, tty and keyboard I/O,
3005 ! copying memory to arbitrary locations, etc.
|3007 ! The primary bootstrap code supplies the following parameters in registers:|
|These are the same values that were passed into bootblock.s.|
3008 ! dl
3009 ! es:si = Partition table entry if hard disk.
3012 .define begtext, begdata, begbss
3015 .ascii "(null)\0" ! Just in case someone follows a null pointer
|3019 o32 = 0x66 ! This assembler doesn't know 386 extensions|
|In Makefile, boothead.s is compiled
with the -mi86 option (LD86 contains the mi86 option). This option
uses the machine instructions (mi) of the 8086 system which does not have
32-bit registers (like eax, ebx, etc.). If an instruction is needed
that uses a 32-bit value, the 8086 instruction must be prefixed with 0x66.
Look at line 3933. If the -mi86 option is used and the retf instruction has no prefix, the instruction jumps to the address specified by the last 2 bytes on the stack (the offset) and the next-to-last 2 bytes on the stack (the segment). However, if the last 4 bytes on the stack are the offset and the next-to-last 4 bytes on the stack are the segment and the -mi86 option is used, the instruction must be prefixed with 0x66. On lines 3922-3925, these 8 bytes are pushed on the stack.
|3020 BOOTOFF = 0x7C00 ! 0x0000:BOOTOFF load a bootstrap here|
|3021 LOADSEG = 0x1000 ! Where this code is loaded.|
|3022 BUFFER = 0x0600 ! First free memory|
|The bootstrap (which is bootblock.s) loaded this code (the secondary boot loader) at address 0x1000:0x0000. If the user wishes to boot a different partition, the bootstrap from that partition is loaded at address 0x0000:0x7C00 and the boot process repeats itself (the bootstrap loads the secondary boot loader which loads the kernel). masterboot.s and bootblock.s describe this process in greater detail.|
|3023 PENTRYSIZE = 16 ! Partition table entry size.|
|3024 a_flags = 2 ! From a.out.h, struct exec|
In your book, look at line 01400. This is the header file a.out.h.
The first thing declared in this file is the struct exec.
All minix executables (with a few exceptions like bootblock and masterboot
- these 2 files must begin with executable code) begin with headers.
a_flags is at an offset of 2 bytes, a_text is at an offset of 8 bytes, and so on. a_flags describes the kernel (with the options shown on lines 3029-3033) and a_text, a_data, a_bss, and a_total are sizes.
Note that the A_SEP flag describes this executable (the secondary boot loader) whereas the K_I386, K_RET, K_INT86, and K_MEML flags describe the kernel.
|3029 A_SEP = 0x20 ! Separate I&D flag|
Read section 4.7.1 and the first 10 paragraphs of section 4.7.3 of Operating
Systems and try to understand as much as you can. Some of the
terminology may be unfamiliar so I will give a short description of the
This executable (the secondary boot) is compiled with the -mi86 option and runs in real mode and not in protected mode. For this reason, the secondary boot is not be able to take advantage of the protection features of protected mode. However, since this is the first time we've run into the A_SEP flag, it's a good place to discuss shared vs. separate segments.
In protected mode, the text (code) and the data+bss+heap+stack (I will refer to this as the total data - see the next paragraph for a description of each of these) in an executable with separate text and total data segments are protected from one another. For example, if the code tries to jump to a memory address that's within the total data segment, the hardware triggers a segment violation. If they're not separate (A_SEP in a_flags is not set), chaos results. Another advantage of separating the text and total data is that the text can be shared among multiple instances of the same program. The total data will differ between two instances of the same program but the text will be the same.
Data contains initialized global variables, bss contains uninitialized global variables and must be initialized to zero (see lines 3091-1098), and the heap is the memory that malloc() allocates at run-time.
It's best to also keep the data+bss+heap and the stack separate - although Minix doesn't separate the two for the reasons given in section 4.7.3. This means that if the heap or the stack grows too large, one can overwrite the other. If the stack overwrites the heap and the overwritten data is not accessed immediately, identifying the problem is difficult.
On disk, the a_text field in the header holds the size of the text and the a_data field holds the size of the data. If the kernel doesn't have separate text and total data segments, the variables a_data and a_text are combined into a_data and the variable a_text is set to zero (see lines 3069-3071). Note that even though the values are changed in memory, they do not affect the values on disk. a_bss is the size of the bss. a_total is the size of the data+bss+heap+stack (separate) or the text+data+bss+heap+stack (shared). Unlike a_text, it doesn't need to be modified if the text and total data are shared. a_total determines the top of the stack (see lines 3075-3077) and is also used (with a_text) to determine the global variable _runsize (see lines 3127-3135) which is needed by boot.c in initialize().
|3030 K_I386 = 0x0001 ! Call Minix in 386 mode|
|If the K_I386 flag is set for the kernel, this code must switch to protected mode.|
|3031 K_RET = 0x0020 ! Returns to the monitor on reboot|
|Look at lines 3936 and 3942. The minix kernel returns there on a halt or reboot if the K_RET is set for the kernel. If the K_RET flag is not set, the system simply halts.|
3032 K_INT86 = 0x0040 ! Requires generic INT support
|3033 K_MEML = 0x0080 ! Pass a list of free memory|
|The variable _mem (see line 3048) is used to pass this memory list. The int 0x12 (see line 3141) and int 0x15 (see lines 3152 and 3157) bios calls are used to determine the low memory and high memory size.|
|3035 DS_SELECTOR = 3*8 ! Kernel data selector|
|3036 ES_SELECTOR = 4*8 ! Flat 4 Gb|
|3037 SS_SELECTOR = 5*8 ! Monitor stack|
|3038 CS_SELECTOR = 6*8 ! Kernel code|
|3039 MCS_SELECTOR= 7*8 ! Monitor code|
To support multiprocessing, the 80286 and up use global descriptor
tables (GDT's). p_gdt (line 4242) is the descriptor table.
Anything that is labeled UNSET must be filled in before the global
descriptor table is loaded using the lgdt instruction (see line
4133). These values are filled in on lines 3871-3897.
The following values are the offsets of the entries within the global descriptor table. For example, since the entry for the kernel code is the 7th entry (see line 4267) and the size of each entry is 8 bytes, its offset is 6*8 (remember that the first entry has a 0 offset). The MCS_SELECTOR is pushed onto the stack (if the K_RET flag is set for the kernel) before jumping to the kernel (look at lines 3918-3920) . Also before the jump is made to the kernel, the ds and es registers are loaded with DS_SELECTOR and ES_SELECTOR, respectively.
|3041 ESC = 0x1B ! Escape character|
|0x1B is the ascii representation of ESC.|
|3043 ! Imported variables and functions:|
|Memory for a variable can be allocated in only one file (i.e. the variable
is "defined") but the variable must be declared as extern in every
other file that accesses it. To accomplish this, the macro EXTERN
is #defined as the empty string in boot.c
. This prevents the EXTERN macro from being #defined
as extern in boot.h when boot.h
is #included in boot.c. boot.h is also #included
in bootimage.c. Since EXTERN is not #defined (and
is therefore undefined), EXTERN is replaced by extern
in bootimage.c. This mechanism ensures that memory for a variable
is allocated only once.
A similar trick is used in the kernel. Read the 5th paragraph of section 2.6.3 of Operating Systems for details.
Variables that are shared between assembler and C code are prefixed with an underscore ( _ ) in the assembler code but are not prefixed with an underscore in the C code.
|_caddr is the absolute address of the first byte of the text. _daddr is the absolute address of the first byte of the data. _runsize is the size of the entire executable (text+data+bss+heap+stack). I believe that _edata and _end are variables that are generated by the compiler. _edata is the offset address of the end of the data and _end is the offset address of the end of the bss. These two variables are used on lines 3091-3098. See the comment for line 3145 for further discussion of _edata and _end.|
|3047 .extern _k_flags ! Special kernel flags|
|_k_flags contains the K_I386 , K_RET, K_INT86, and K_MEML flags (lines 3030-3033). _k_flags is set in bootimage.c.|
3048 .extern _mem
! Free memory list
|3052 .extern _boot, _printk ! Boot Minix, kernel printf|
|These functions are defined in boot.c. boot is called on line 3180.|
|3054 ! Set segment registers and stack pointer using the programs own header!|
|3055 ! The header is either 32 bytes (short form) or 48 bytes (long form). The|
|3056 ! bootblock will jump to address 0x10030 in both cases, calling one of the|
|3057 ! two jmpf instructions below.|
3059 jmpf boot, LOADSEG+3 ! Set cs right (skipping long a.out header)
3060 .space 11 ! jmpf + 11 = 16 bytes
3061 jmpf boot, LOADSEG+2 ! Set cs right (skipping short a.out header)
Whether this code has a short header or a long header, the second instruction
executed (after the first jump) is at address boot.
Before boot is called on line 3180, a few things are done. (Don't confuse the two boot's; one's an address (line 3062) and the other's a function defined in boot.c (line 3180).)
Lines 3062-3080: The ds, ss, and sp registers are loaded. The values loaded depend on whether this executable has a separate text and total data (A_SEP in a_flags is set) or this executable has a shared text and total data (A_SEP in a_flags is not set).
Lines 3092-3097: Clear the bss. The bss contains uninitialized global variables and needs to be zeroized.
Lines 3100-3135: Initialize various global variables so that when boot (line 3180) is called, the C code can access their values.
Lines 3137-3177: Initialize the array mem.
|3064 mov ds, ax ! ds = header|
What's the pound (#) sign all about? The pound sign indicates
that the value of LOADOFF is moved into the register
rather than the contents of the memory location LOADOFF.
Why can't the instruction mov ds, #LOADSEG be used instead of using ax as an intermediate register? The 80x86 processors forbids immediate data to segment register transfers. (Immediate data is data that is within the instruction itself, as opposed to data that is at a memory location or data that is in a register.) Memory to segment register transfers are also forbidden. Only register to segment register transfers are allowed. The one exception to this rule is the cs register. The cs register is even more restrictive. The only two instructions that can alter the cs register are jmpf (far jump) and return retf (far return) instructions.
3066 movb al, a_flags
|3068 jnz sepID|
|testb sets the zero flag if A_SEP is not set in a_flags. If the zero flag is not set (A_SEP is set), then jnz jumps to sepID.|
|3069 comID: xor ax, ax|
|This instruction zeroes the ax register (any number xor'ed
with itself is zero). This is a pretty common practice. The
mov ax, #0
is slower and is 3 bytes compared with xor's 2 bytes.
|3074 and ax, #0xFFFE ! Round down to even|
|I'm not sure why we do this. However, since the size of the stack is arbitrary and there should be plenty of room to spare, rounding down to an even value shouldn't be a problem. The efficiency of transferring 2 bytes from an even memory address may be greater than transferring 2 bytes from an odd memory address.|
3075 mov a_total, ax ! total - text = data + bss + heap + stack
|3076 cli ! Ignore interrupts while stack in limbovv|
Whenever a value is moved into the stack register (ss)
or the stack pointer (sp), the interrupts must be first disabled.
The ss and sp registers hold the address to which an
interrupt returns after its completion. If the ss and
register are in flux, one can't predict where the code will return.
Interrupts are disabled with the cli (clear interrupts) instruction and reenabled with the sti (set interrupts) instruction.
mov sp, ax
! Set sp at the top of all that
|3080 movb cl, #4|
|3081 shr ax, cl|
|3082 mov cx, cs|
|3083 add ax, cx|
|3084 mov ds, ax ! ds = cs + text / 16|
|Each segment register (cs , ds, es , ss,etc.) is internally appended with a 0x0 before being added to a non-segment register (like ip or ax ) to form an address. For example, if the cs register holds the value 0x1000 and the ip register holds the value 0x1000, then together these registers point to address 0x11000. So if we wish to add an offset (in bytes) to a segment register, we must first shift the offset 4 bits to the right (line 3081).|
mov ss, ax
3086 sti ! Stack ok now
|3087 push es ! Save es, we need it for the partition table|
|This value is popped into the upper 2 bytes of _rem_part on line 3105.|
mov es, ax
3089 cld ! C compiler wants UP
3091 ! Clear bss
3092 xor ax, ax ! Zero
|3093 mov di, #_edata ! Start of bss is at end of data|
|3094 mov cx, #_end ! End of bss (begin of heap)|
|_edata and _end are variables that are set by the compiler. _edata is the offset address of the end of the data and_end is the offset address of the end of the bss.|
sub cx, di
! Number of bss bytes
|3098 stos ! Clear bss|
|The instruction prefix rep repeats the instruction (in this case stos) cx times. stos stores ax at the memory address es:di. Since stos stores words and not bytes, cx must be shifted to the right by 1 (in other words, divided by 2).|
|3100 ! Copy primary boot parameters to variables. (Can do this now that bss is|
|3101 ! cleared and may be written into).|
|Since _device and _rem_part are uninitialized global variables, they are stored in the bss.|
xorb dh, dh
|3109 int 0x10|
|int 0x10,ah=0x0F returns the current video mode into
al. Some examples of return values are al=0x13 (VGA, 320x2100
resolution, 256 colors), al=0x12 (VGA, 640x480, 16), and al=0x0E
(CGA, 640x240, 16).
I don't know what "blanking" is. If you know, please submit a comment to the site which will be displayed below.
andb al, #0x7F !
Mask off bit 7 (no blanking)
3111 movb old_vid_mode, al
3112 movb cur_vid_mode, al
3114 ! Give C code access to the code segment, data segment and the size of this
3115 ! process.
3116 xor ax, ax
3117 mov dx, cs
|Line 3222 converts a segment:offset address in dx:ax to an absolute address in dx-ax. Note that the notation dx-ax does not mean dx minus ax. It means that the lower 2 bytes are in ax and the upper 2 bytes are in dx. This notation is used in other places in the code (for example, lines 3227-3243).|
3120 mov _caddr+2, dx
3121 xor ax, ax
3122 mov dx, ds
3123 call seg2abs
3124 mov _daddr+0, ax
3125 mov _daddr+2, dx
3126 push ds
3127 mov ax, #LOADSEG
3128 mov ds, ax ! Back to the header once more
3129 mov ax, a_total+0
3130 mov dx, a_total+2 ! dx:ax = data + bss + heap + stack
|If this executable has a separate text and total data segment, a_text must be added to a_total to get the total size of the executable. If it has a shared text and total data segment, a_total is the size of the text and the total data. However, a_text was set to zero on line 3070 and can be added anyway and it won't matter.|
|3137 ! Determine available memory as a list of (base,size) pairs as follows:|
|3138 ! mem = low memory, mem = memory between 1M and 16M, mem = memory|
|3139 ! above 16M. Last two coalesced into mem if adjacent.|
| The memory (base, size) pairs will look something like this:
mem=(0x00000000, size of low memory)
mem=(0x00100000, size of memory between 1M and 16M)
mem=(0x01000000, size of memory greater than 16M)
If the mem and mem memory areas are continugous, then mem and mem are combined.
Since mem is an uninitialized variable, it is found in the bss space, which was zeroized on lines 3091-1098. The following instructions are not needed since these 4 bytes are already zero.
mov 0(di), #0
Also, since the lower 2 bytes of the base of both mem and mem are also zero, the following instructions are also not needed:
mov 8(di), #0
The lower 2 bytes of the lower memory size are stored in 4(di) and the upper 2 bytes are stored in 6(di) (lines 3443-3144). Likewise, 12(di) and 14(di) hold the size of the memory between 1M and 16M. 20(di) and 22(di) hold the size of the memory above 16M. Since int 0x15 , ax=0xE081 returns the number of 64K (not 1K) blocks of memory in bx (see line 3152), 20(di) will equal 0.
mov di, #_mem
! di = memory list
3141 int 0x12 ! Returns low memory size (in K) in ax
|c1024 is a memory address (see line 4207). "c" stands for constant. mul multiples ax by the operand (in this case the value at the address specified by the operand) and puts the lower 2 bytes of the result in ax and the upper 2 bytes in dx.|
4(di), ax ! mem.size = low memory
size in bytes
3144 mov 6(di), dx
|3145 call _getprocessor|
|It's pretty obvious what _getprocessor does, but I can't find where it's defined. It returns 86 into ax for an 8086, 286 for a 80286, 386 for a 80386 and so on. It's possible that _getprocessor is a function that's supplied by the compiler (like I believe that _edata and _end are variables supplied by the compiler) but I'm not sure. What leads me to believe that it's a function supplied by the compilier is that this code calls two other functions that are not defined in this file (boot is defined in boot.c and printk is declared in minix/minlib.h ,which is #included in boot.c, and is part of the standard library) and both of these are declared as .extern on line 3052. _getprocessor, _edata, and _end are neither defined nor declared in this file, suggesting that they are special in some way. If you have any answers to this, please send an e-mail to firstname.lastname@example.org or submit a comment to the site which will be displayed below..|
cmp ax, #286
! Only 286s and above have extended memory
3147 jb no_ext
3148 cmp ax, #486 ! Assume 486s were the first to have >64M
3149 jb small_ext ! (It helps to be paranoid when using the BIOS)
3151 mov ax, #0xE801 ! Code for get memory size for >64M
|3152 int 0x15 ! ax = mem at 1M per 1K, bx = mem at 16M per 64K|
|int 0x15 sets the carry flag if the value in ax (or ah) is not a supported input.|
3155 movb ah, #0x88 ! Code for get extended memory size
3156 clc ! Carry will stay clear if call exists
3157 int 0x15 ! Returns size (in K) in ax for AT's
3158 jc no_ext
3159 test ax, ax ! An AT with no extended memory?
3160 jz no_ext
3161 xor bx, bx ! bx = mem above 16M per 64K = 0
3163 mov cx, ax ! cx = copy of ext mem at 1M
3164 mov 10(di), #0x0010 ! mem.base = 0x00100000 (1M)
3165 mul c1024
3166 mov 12(di), ax ! mem.size = "ext mem at 1M" * 1024
3167 mov 14(di), dx
|3168 test bx, bx|
|If bx has any value other than 0, it was put there by int 0x15 on line 3152.|
3169 jz no_ext ! No more ext mem above 16M?
|3170 cmp cx, #15*1024 ! Chunks adjacent? (precisely 15M at 1M?)|
|3171 je adj_ext|
|If there are 15M between 1M and 16M, then the memory between 1M and 16M and the memory above 16M is contiguous. If the memory is contiguous, the two sizes are combined into mem by jumping to adj_ext.|
|3172 mov 18(di), #0x0100 ! mem.base = 0x01000000 (16M)|
|3173 mov 22(di), bx ! mem.size = "ext mem at 16M" * 64K|
|3174 jmp no_ext|
|3176 add 14(di), bx ! Add ext mem above 16M to mem below 16M|
|3179 ! Time to switch to a higher level language (not much higher)|
|Now that we've taken care of a little housekeeping, we can call boot.|
|3180 call _boot|
|3181 ! Time to switch to a higher level language (not much higher)|
|Until bootstrap (line 3786), the code is a little tedious. Functions are defined that are called by the secondary boot's C code. The one thing that makes it interesting is that it gives a little insight into when you can use C and when you have to use assembler. Most of the assembler functions make a lot of calls to the bios.|
|3184 .define _exit, __exit, ___exit ! Make various compilers happy|
|_exit, __exit, and ___exit are all the same
addresses. I am not entirely sure why we need to match up with specific
compilers, but I'm willing to make a guess. My guess is that every
compiler has a built-in exit function and in order to override
the compiler default, you have to supply your own exit function and
give the function the appropriate compiler-specific name. If anyone
knows for sure, please submit a comment to the site which will be displayed below.
If the _exit function is called, then for one reason or another, the boot code has decided to exit rather than jump to the kernel. If no error occured (status=0), reboot. Otherwise, wait for a key to be pressed and then reboot.
|3188 mov bx, sp|
|3189 cmp 2(bx), #0 ! Good exit status?|
This is something you'll see a lot so make sure you understand it.
When C code calls a function, it pushes its arguments onto the stack.
The C code pushes its last argument first and the first argument last (there's
only one argument for the exit function - status).
After finishing with the arguments, the return address is pushed (since
the function is exit and the system is rebooted, this value is
never used but it's pushed onto the stack anyway). The stack at this
moment looks like this:
|3190 jz reboot|
|3191 quit: mov ax, #any_key|
|3192 push ax|
|3193 call _printk|
|We can now see an example of an assembler function calling a C function that requires parameters. The printk() function (which has the same syntax as printf()) is very flexible - one way that it can be called is by passing it a pointer to a string as an argument. In order to pass the C function a pointer as an argument, the address of the string (in this case, any_key) is pushed onto the stack (line 3192) before making the call (line 3193). Only a single argument is passed to the C function, but if more than one argument were passed, the last argument would be pushed first and the first argument would be pushed last.|
|3194 xorb ah, ah ! Read character from keyboard|
|3195 int 0x16|
|int 0x16, ah=0 waits for a key to be pressed and then puts the ascii code of the pressed key in al. Since we don't care what key is pressed, the contents of al is never examined.|
|I'm not sure why we need to restore the old video settings before rebooting, but I guess it can't hurt. If you know why we do this, please submit a comment to the site which will be displayed below.|
|3197 int 0x19 ! Reboot the system|
|Variables can be interspersed anywhere in the code with the .data declaration. Since the address any_key is used on line 3191, it's convenient to place the .data declaration here.|
|3203 ! u32_t mon2abs(void *ptr)|
|3204 ! Address in monitor data to absolute address.|
mon2abs converts a 2-byte offset address (using ds
as the segment address) to a 4-byte absolute address. ptr
is this 2-byte offset. vec2abs converts a 4-byte segment:offset
address to a 4-byte absolute address. vec points to this
4-byte address. (Note that a segment:offset address is called a vector.)
Let's look in boot.c where both of these functions are called. lowsec holds a 2-byte integer while rem_part holds a 2-byte offset address in its lower 2 bytes and a 2-byte segment address in its upper 2 bytes (see lines 3104-3105). mon2abs converts the 2-byte offset address of lowsec (using ds as its 2-byte segment address) to a 4-byte absolute address and vec2abs converts the 4-byte segment:offset address found at rem_part to a 4-byte absolute address. The figure below shows the relationship between the stack of vec2abs and the variable rem_part.
The comment for vec2abs is a little misleading. As the above example shows, vec2abs can be used for conversions of segment:offset vectors that are not interrupt vectors. This is the only place in the boot sequence where vec2abs is called.
3205 .define _mon2abs
3207 mov bx, sp
3208 mov ax, 2(bx) ! ptr
3209 mov dx, ds ! Monitor data segment
3210 jmp seg2abs
|3212 ! u32_t vec2abs(vector *vec)|
|3213 ! 8086 interrupt vector to absolute address.|
|As discussed in the comments for 3203-3204, vec2abs converts a 4-byte segment:offset address to a 4-byte absolute address. vec points to this 4-byte address.|
3214 .define _vec2abs
|3222 seg2abs: ! Translate dx:ax to the 32 bit address dx-ax|
|The segment address must be shifted to the left 4 bits before being added to an offset address. ch is used to store intermediate values. Note that dx-ax does not mean dx minus ax. It represents a 4-byte value with the upper 2 bytes in dx and the lower 2 bytes in ax. To make sure that you understand the steps below, convert a segment:offset address (for example 0x0100:0x0116 = dx:ax) to its absolute address (0x00001116 = dx-ax) using the instructions below.|
3224 movb ch, dh
3225 movb cl, #4
3226 shl dx, cl
3227 shrb ch, cl ! ch-dx = dx << 4
3228 add ax, dx
3229 adcb ch, #0 ! ch-ax = ch-dx + ax
3230 movb dl, ch
3231 xorb dh, dh ! dx-ax = ch-ax
3232 pop cx
|3235 abs2seg: ! Translate the 32 bit address dx-ax to dx:ax|
|This is the reverse of below. An absolute value in dx-ax is converted to a segment:offset address in dx:ax. This operation would convert dx-ax = 0x00001116 to dx:ax = 0x0111:0x0006. Note that the 2 segment:offset addresses 0x0100:0x0116 and 0x0111:0x0006 are the same absolute address.|
3237 movb ch, dl
3238 mov dx, ax ! ch-dx = dx-ax
3239 and ax, #0x000F ! Offset in ax
3240 movb cl, #4
3241 shr dx, cl
3242 shlb ch, cl
3243 orb dh, ch ! dx = ch-dx >> 4
3244 pop cx
|3247 ! void raw_copy(u32_t dstaddr, u32_t srcaddr, u32_t count)|
|3248 ! Copy count bytes from srcaddr to dstaddr. Don't do overlaps.|
|3249 ! Also handles copying words to or from extended memory.|
The most difficult part of this function is dealing with extended memory.
If the source or destination address is greater than 1MB, extended memory must be accessed using the int 0x15, ah=0x87 bios call.
Keep in mind that the system is still in real mode. When (and if) the system is switched to protected mode, this problem goes away. In protected mode, 4GB of memory can be accessed.
If the absolute address range 0x1000-0x2000 were copied to location 0x1800-0x2800, the source range and the destination range would overlap. Overlaps are not allowed.
After line 3253, the stack will look like this:
3250 .define _raw_copy
|3268 cmp dx, #0x0010 ! Copy to extended memory?|
|3269 jae ext_copy|
|3270 cmp 10(bp), #0x0010 ! Copy from extended memory?|
|3271 jae ext_copy|
|If either the source or destination address is greater than 0x00100000 (1MB), then the int 0x15, ah=0x87 bios call must be used.|
|In order to use the rep movs instruction, the source and destination addresses must be converted from absolute addresses to segment:offset addresses. abs2seg converts the absolute address dx-ax to the segment:offset address dx:ax. (Note that dx-ax does not mean dx minus ax. It represents a 4-byte value whose upper 2 bytes are in dx and whose lower 2 bytes are in ax.)|
mov di, ax
3274 mov es, dx ! es:di = dstaddr
3275 mov ax, 8(bp)
3276 mov dx, 10(bp)
3277 call abs2seg
3278 mov si, ax
3279 mov ds, dx ! ds:si = srcaddr
|3280 shr cx, #1 ! Words to move|
|rep movs copies cx words from ds:si to es:di. Since cx holds the number of bytes and words being copied, cx must be shifted to the right 1 bit (this divides cx by 2).|
|3282 movs ! Do the word copy|
|3283 adc cx, cx ! One more byte?|
|3285 movsb ! Do the byte copy|
The shr instruction on line 3280 shifted the right-most bit
into the carry flag. If cx were previously odd, then the
carry flag will be set and if cx were previously even, the carry
flag will not be set. On line 3283, cx (which was decremented
to 0 by the rep movs instruction) is added to itself (for a total
of 0) and then the carry flag is added. So cx will be 1
if it had been odd before the shr instruction and it will be 0
if it been even before the shr instruction.
Either 1 byte or 0 bytes is then copied from ds:si to es:si.
mov ax, ss
! Restore ds and es from the remaining ss
3287 mov ds, ax
3288 mov es, ax
3289 jmp copyadjust
|Look at line 4214. The UNSET's for both x_src_desc and x_dst_desc must be modified with the lowest addresses (bases) of the source and destination addresses before making the int 0x15, ah=0x87 bios call. None of the other UNSET's in the table matters here.|
mov x_dst_desc+2, ax
3292 movb x_dst_desc+4, dl ! Set base of destination segment
3293 mov ax, 8(bp)
3294 mov dx, 10(bp)
3295 mov x_src_desc+2, ax
3296 movb x_src_desc+4, dl ! Set base of source segment
3297 mov si, #x_gdt ! es:si = global descriptor table
3298 shr cx, #1 ! Words to move
3299 movb ah, #0x87 ! Code for extended memory move
|3300 int 0x15|
|For the int 0x15, ah=0x87 bios call, es:si points to the extended move table (line 4214) and cx holds the number of words to copy.|
|The stack contents are modified in order to advance the current source and destination addresses and keep track of how many more bytes must be copied (see line 3263).|
! Restore count
3303 add 4(bp), cx
3304 adc 6(bp), #0 ! srcaddr += copycount
3305 add 8(bp), cx
3306 adc 10(bp), #0 ! dstaddr += copycount
3307 sub 12(bp), cx
3308 sbb 14(bp), #0 ! count -= copycount
3309 jmp copy ! and repeat
|At this point, the copying is complete. bp, di, and si were pushed onto the stack (lines 3252, 3254, and 3255) and must be popped before returning.|
3312 pop si ! Restore C variable registers
3313 pop bp
|3316 ! u16_t get_word(u32_t addr);|
|3317 ! void put_word(u32_t addr, u16_t word);|
|3318 ! Read or write a 16 bits word at an arbitrary location.|
|u16_t get_word(u32_t addr) returns the 2 byte (16 bit) value at absolute memory address addr.|
3319 .define _get_word, _put_word
3321 mov bx, sp
3322 call gp_getaddr
3323 mov ax, (bx) ! Word to get from addr
3324 jmp gp_ret
3326 mov bx, sp
|3327 push 6(bx) ! Word to store at addr|
|3329 pop (bx) ! Store the word|
|The value pushed on line 3327 is the same value popped on line 3329. This value is word. It is pushed to the location specified by ds:bx, which was set on lines 3335-3336.|
|3330 jmp gp_ret|
|"gp" stands for get/put. gp_getaddr converts addr (found on the stack) to a segment:offset address in ds:bx.|
mov ax, 2(bx)
3333 mov dx, 4(bx)
3334 call abs2seg
3335 mov bx, ax
3336 mov ds, dx ! ds:bx = addr
3339 push es
|3340 pop ds ! Restore ds|
|The value of ds was changed on line 3336. es and ds (before line 3336) were the same value.|
|3343 ! void relocate(void);|
|3344 ! After the program has copied itself to a safer place, it needs to change|
|3345 ! the segment registers. Caddr has already been set to the new location.|
It's slightly more complicated than this, but in the initialize()
function in boot.c the secondary boot (this
program) is copied to the end of the available low memory and a jump is
made to it.
The return address (which is only an offset) of this function is popped into bx to start this function. The last instruction before the final instruction, retf, is to push this offset on the stack. The returning offset will be the same but the segment will be different (the new segment is pushed on the stack on line 3364).
3346 .define _relocate
3348 pop bx ! Return address
3349 mov ax, _caddr+0
3350 mov dx, _caddr+2
3351 call abs2seg
|3352 mov cx, dx ! cx = new code segment|
|3353 mov ax, cs ! Old code segment|
|3354 sub ax, cx ! ax = -(new - old) = -Moving offset|
|3355 mov dx, ds|
|3356 sub dx, ax|
|The difference between the new code segment (cs) and the old code segment will be the same as the difference between the new data segment (ds) and the old data segment.|
|3357 mov ds, dx ! ds += (new - old)|
|3358 mov es, dx|
|3359 mov ss, dx|
|ds, es, and ss are set to the new value using the mov instruction. The code segment (cs) and the instruction pointer (ip) can be changed by a jump instruction or a retf instruction but not by a mov instruction.|
xor ax, ax
3361 call seg2abs
3362 mov _daddr+0, ax
3363 mov _daddr+2, dx ! New data address
3364 push cx ! New text segment
3365 push bx ! Return offset of this function
3366 retf ! Relocate
|3368 ! void *brk(void *addr)|
|3369 ! void *sbrk(size_t incr)|
|3370 ! Cannot fail implementations of brk(2) and sbrk(3), so we can use|
|3371 ! malloc(3). They reboot on stack collision instead of returning -1.|
Neither brk nor sbrk is found in boot.c, bootimage.c,
or rawfs.c. On the other hand, in boot.c and bootimage.c we call
I believe that malloc() calls this function,
brk or sbrk,
to determine if there's enough room to allocate the requested space on
the heap. Remember that the stack and the heap can collide with each
other, as discussed in section 4.7.3 of Operating
Systems . Here's a simple figure of the memory layout.
If you can shed any light on the brk and sbrk calls, please submit a comment to the site which will be displayed below.
brk specifies the address on the heap (addr) to which malloc() wishes to allocate. sbrk specifies the incremental space on the heap (incr) that malloc() wishes to allocate.
3373 .align 2
|3374 break: .data2 _end ! A fake heap pointer|
|This is the top of the heap. As discussed in the comments on 3044, _end is the initial top of the heap and is supplied by the compiler. This value is modified on line 3387.|
3376 .define _brk, __brk, _sbrk, __sbrk
3378 __brk: ! __brk is for the standard C compiler
3379 xor ax, ax
3380 jmp sbrk ! break= 0; return sbrk(addr);
3383 mov ax, break ! ax= current break
3384 sbrk: push ax ! save it as future return value
|3385 mov bx, sp ! Stack is now: (retval, retaddr, incr, ...)|
|3386 add ax, 4(bx) ! ax= break + increment|
|3387 mov break, ax ! Set new break|
|If brk was called, the stack will be: (retval, retaddr, addr, ...) Line 3386 either adds the value at address break and incr (for sbrk) or adds the value of addr and 0 (for brk). Either way, the new top of the heap is stored at address break on line 3387.|
|3388 lea dx, -1024(bx) ! sp minus a bit of breathing space|
|The lea instruction loads a 16-bit register (in this case, dx) with the offset address of the data specified by the second operand (in this case, sp - 1024; sp=bx, see line 3385). The lea instruction and the mov instruction are similar but not the same. The mov instruction would have loaded dx with the data specified by the second operand, not the offset address of the data specified.|
|3389 cmp dx, ax ! Compare with the new break|
|If 1K (1024) doesn't separate the stack from the new top of the heap, there are major problems and the system is rebooted. If less than 4K (but greater than 1K) separate the two, a warning is issued.|
! Suffocating noises
3391 lea dx, -4096(bx) ! A warning when heap+stack goes < 4K
3392 cmp dx, ax
3393 jae plenty ! No reason to complain
3394 mov ax, #memwarn
3395 push ax
3396 call _printk ! Warn about memory running low
3397 pop ax
|The user is warned once but not twice.|
3399 plenty: pop ax
! Return old break (0 for brk)
3401 heaperr:mov ax, #chmem
3402 push ax
3403 mov ax, #nomem
3404 push ax
3405 call _printk
3406 jmp quit
|3408 nomem: .ascii "\nOut of%s\0"|
This call would be written in C as:
printk("\nOut of%s", " memory, use chmem to increase the heap\n\0");
The "%s" is replaced by the second string.
chmem is a utility program that changes the size of the stack. It does this by altering the a_total field in the executable's header (see comment for line 3029).
3409 memwarn:.ascii "\nLow on"
3410 chmem: .ascii " memory, use chmem to increase the heap\n\0"
|3413 ! int dev_open(void);|
|3414 ! Given the device "_device" figure out if it exists and what its number|
|3415 ! of heads and sectors may be. Return the BIOS error code on error,|
|3416 ! otherwise 0.|
dev_open() determines the number of heads and sectors of the device being booted and sets sectors (sectors/track - line 4279) and secspcyl (sectors/cylinder - line 4280) using these values. dev_open() is called from the initialize() function in boot.c and it's also called from boot.c in the event that the user decides to boot another device.
As usual, the greatest difficulty arises if the device is a floppy drive. If _device is a floppy, an attempt is made to read the first sector to make sure the drive is properly functioning. If the drive is properly functioning, an attempt is made to read the 18th sector of the first track, then the 15th sector of the first track, and finally the 9th sector of the first track (see line 3483). If the attempt to read the 18th sector of the first track is successful, then there are 18 sectors on a track and only a 1.44M floppy has 18 sectors on a single track. If the 18th sector can't be read but the 15th sector can be read, we know we have a 1.2M floppy and if only the 9th sector can be read, we know we have a 720K floppy.
3417 .define _dev_open
3419 push es
3420 push di ! Save registers used by BIOS calls
3421 movb dl, _device ! The default device
3422 cmpb dl, #0x80 ! Floppy < 0x80, winchester >= 0x80
3423 jae winchester
3425 mov di, #3 ! Three tries to init drive by reading sector 0
3426 finit0: xor ax, ax
3427 mov es, ax
3428 mov bx, #BUFFER ! es:bx = scratch buffer
3429 mov ax, #0x0201 ! Read sector, #sectors = 1
3430 mov cx, #0x0001 ! Track 0, first sector
3431 xorb dh, dh ! Drive dl, head 0
|3432 int 0x13|
int 0x13, ah=0x02 copies sectors from a hard drive
or floppy (specified by dl) to memory. al specifies
how many sectors to copy, bits 0-5 of cl specify the sector number,
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 we want
to load the sectors. If the int 0x13, ah=0x02 call
fails, the carry (C) flag is set. int 0x13, ah=0x02
returns 0 in ah if successful and an error code in ah
If the carry flag is not set and everything worked, the jnc instruction jumps to memory location finit0ok. The data that is copied to memory is not examined. We are only concerned with whether we are able to perform the copying operation.
|3433 jnc finit0ok ! Sector 0 read ok?|
|3434 cmpb ah, #0x80 ! Disk timed out? (Floppy drive empty)|
|A return value of 0x80 in ah means that the floppy drive is empty. A jump to geoerr (line 3479) is made if that's the case.|
|3435 je geoerr|
|3436 dec di|
|The drive (line 3439) is reset two times and an attempt to read the first sector of the floppy is made again before jumping to geoerr.|
3438 xorb ah, ah ! Reset drive
|3439 int 0x13|
|int 0x13, ah=0x00 resets the drive.|
3441 jmp finit0 ! Retry once more, it may need to spin up
|An attempt is first made to read sector 18. If that doesn't work, di (line 3458) is decremented to point to 15. If that doesn't work, di is decremented again to point to 9.|
3444 flast: movb cl, (di)
! Sectors per track to test
3445 cmpb cl, #9 ! No need to do the last 720K/360K test
3446 je ftestok
3447 xor ax, ax
3448 mov es, ax
3449 mov bx, #BUFFER ! es:bx = scratch buffer
3450 mov ax, #0x0201 ! Read sector, #sectors = 1
3451 xorb ch, ch ! Track 0, last sector
3452 xorb dh, dh ! Drive dl, head 0
|3453 int 0x13|
int 0x13, ah=0x02 copies sectors from a hard drive
or floppy (specified by dl) to memory. al specifies
how many sectors to copy, bits 0-5 of cl specify the sector number,
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 we want
to load the sectors. If the int 0x13, ah=0x02 call
fails, the carry (C) flag is set. int 0x13, ah=0x02
returns 0 in ah if successful and an error code in ah
Again, we won't look at the data that we copy to memory. We are only concerned with whether we are able to perform the copying operation.
! Sector cl read ok?
3455 xorb ah, ah ! Reset drive
3456 int 0x13
3457 jc geoerr
3458 inc di ! Try next sec/track number
3459 jmp flast
|3461 movb dh, #2 ! Floppies have two sides|
|In other words, floppies have two heads.|
3464 movb ah, #0x08 ! Code for drive parameters
|3465 int 0x13 ! dl still contains drive|
|int 0x13, ah=0x08 returns the device geometry of the drive specified by dl. dl is 0x80 for the first drive, 0x81 for the second, 0x82 for the third and 0x83 for the fourth. The call 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. This is why dh is incremented on line 3468.|
! No such drive?
3467 andb cl, #0x3F ! cl = max sector number (1-origin)
3468 incb dh ! dh = 1 + max head number (0-origin)
|At this point, cl holds the number of sectors per track and dh holds the number of heads.|
movb sectors, cl
! Sectors per track
3471 movb al, cl ! al = sectors per track
|3472 mulb dh ! ax = heads * sectors|
|mulb multiples al by the operand (in this case dh) and places the result in ax.|
mov secspcyl, ax
! Sectors per cylinder = heads * sectors
3474 xor ax, ax ! Code for success
|es and di were saved on lines 3419-3420 by pushing them on the stack. Now they need to be popped back.|
3477 pop es ! Restore di and es registers
3479 geoerr: movb al, ah ! ax = BIOS error code
3480 xorb ah, ah
3481 jmp geodone
3484 .data1 18, 15, 9 ! 1.44M, 1.2M, and 360K/720K floppy sec/track
3487 ! int dev_close(void);
3488 ! Close the current device. Under the BIOS this does nothing.
3489 .define _dev_close
3491 xor ax, ax
|3494 ! int dev_boundary(u32_t sector);|
|3495 ! True if a sector is on a boundary, i.e. sector % sectors == 0.|
|3496 .define _dev_boundary|
dev_boundary() is called from bootimage.c.
Since it is often the case that several consecutive sectors are read, the
array buf is used as a buffer
to hold consecutive sectors. get_sector() reads the requested
sector (vsec) plus successive sectors but the successive sectors
must be on the same track. In other words, get_sector()
does not cross track boundaries.
The div instruction divides dx-ax by the operand (in our example, the value at address sectors) and puts the quotient into ax and the remainder into dx.
Two div instructions are used in this function. It's unclear to me why they don't load the top 2 bytes of sector into dx , the bottom 2 bytes into ax and then execute only one div sectors instruction. Since sectors is 2 bytes, dx can hold the largest possible remainder.
If the value in dx-ax is large (for example 0x0FF0FF00) and the operand is small (for example 0x0004) and you are interested in the quotient (note that we are only interested in the remainder in this function), then two div instructions are necessary. Otherwise, the ax register will overflow (i.e. the quotient will exceed 2 bytes).
For this function, let's look at 2 examples. sector=0x0151FF00, sectors=0x30 for the first example and sector=0x0151FEF0, sectors=0x30 for the second example. The first example is on a boundary and the second example is not on a boundary.
mov bx, sp
3499 xor dx, dx
|3500 mov ax, 4(bx) ! divide high half of sector number|
The div instruction divides dx-ax by the operand (in our example,
the value at address sectors) and puts the quotient into ax
and the remainder into
Look on line 4279 for sectors.
|3502 mov ax, 2(bx) ! divide low half of sector number|
|3504 sub dx, #1 ! CF = dx == 0|
carry flag (CF) =0
|3505 sbb ax, ax ! ax = -CF|
sbb ax, ax is the same as ax=ax-ax-CF=-CF
|3506 neg ax ! ax = (sector % sectors) == 0|
The neg instruction negates a number (e.g. +5 becomes -5 and
-10 becomes +10).
This function is called from C code. C expects the return value from called functions to be in the ax register.
|3509 ! int readsectors(u32_t bufaddr, u32_t sector, u8_t count)|
|3510 ! int writesectors(u32_t bufaddr, u32_t sector, u8_t count)|
|3511 ! Read/write several sectors from/to disk or floppy. The buffer must|
|3512 ! be between 64K boundaries! Count must fit in a byte. The external|
|3513 ! variables _device, sectors and secspcyl describe the disk and its|
|3514 ! geometry. Returns 0 for success, otherwise the BIOS error code.|
readsectors() and writesectors() are called from
several places in boot.c and bootimage.c.
bufaddr is a 32-bit absolute memory address. sector
is a 32-bit absolute sector number on drive _device and count
is the number of sectors (starting at sector) that must be read
Look at the get_sector() function in bootimage.php. bufaddr will usually be the absolute memory address of an array (in this case, the array buf).
3516 .define _readsectors, _writesectors
3518 push bp
3519 mov bp, sp
|3520 movb 13(bp), #3 ! Code for a disk write|
|int 0x13, ah=0x03 writes sectors from memory to disk.|
3523 push bp
3524 mov bp, sp
|3525 movb 13(bp), #2 ! Code for a disk read|
|int 0x13, ah=0x02 reads sectors from disk to memory.|
3526 rwsec: push di
3527 push es
3528 mov ax, 4(bp)
3529 mov dx, 6(bp)
|abs2seg converts the absolute address dx-ax to the segment:offset address dx:ax (see line 3235).|
mov bx, ax
3532 mov es, dx ! es:bx = bufaddr
|3533 mov di, #3 ! Execute 3 resets on floppy error|
|An attempt to read from or write to hard drives is made twice before giving up. An attempt to read from or write to floppy drives is made four times before giving up. Floppy drives need to spin up to speed before being read or written to; this creates complications in the code and sometimes necessitates repeated attempts to read from or write to a floppy.|
|_device will be 0x00 or 0x01 for floppy drives 1 or 2, respectively, and 0x80, 0x81, 0x82, or 0x83 for hard drives 1, 2, 3, or 4, respectively.|
3536 mov di, #1 ! But only 1 reset on hard disk error
3537 nohd: cmpb 12(bp), #0 ! count equals zero?
3538 jz done
|3539 more: mov ax, 8(bp)|
As many sectors are copied as can be without crossing a track boundary
or a cylinder boundary. The code then loops back to more
if there are more sectors to copy.
Lines 3539-3557 set up the registers for the int 0x13 bios call on line 3560. It's a pain and not too interesting.
mov dx, 10(bp) !
dx:ax = abs sector. Divide it by sectors/cyl
3541 div secspcyl ! ax = cylinder, dx = sector within cylinder
3542 xchg ax, dx ! ax = sector within cylinder, dx = cylinder
3543 movb ch, dl ! ch = low 8 bits of cylinder
|divb divides ax by the operand and places the quotient in al and the remainder in ah.|
xorb dl, dl
! About to shift bits 8-9 of cylinder into dl
3546 shr dx, #1
3547 shr dx, #1 ! dl[6..7] = high cylinder
3548 orb dl, ah ! dl[0..5] = sector (0-origin)
3549 movb cl, dl ! cl[0..5] = sector, cl[6..7] = high cyl
3550 incb cl ! cl[0..5] = sector (1-origin)
3551 movb dh, al ! dh = head
3552 movb dl, _device ! dl = device to use
|3554 subb al, ah ! = Sectors left on this track|
|ah contains the first sector that is copied to/from (see line 3544).|
cmpb al, 12(bp) ! Compare
with # sectors to transfer
3556 jbe doit ! Can't go past the end of a cylinder?
3557 movb al, 12(bp) ! 12(bp) < sectors left on this track
3558 doit: movb ah, 13(bp) ! Code for disk read (2) or write (3)
3559 push ax ! Save al = sectors to read
|3560 int 0x13 ! call the BIOS to do the transfer|
|int 0x13 , ah=0x02/0x03 copies sectors from/to a hard drive or floppy (specified by dl ) to/from memory. al specifies how many 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/0x03 call fails, the carry (C) flag is set. int 0x13 , ah=0x02/0x03 returns 0 in ah if successful and an error code in ah if unsuccessful.|
|3561 pop cx ! Restore al in cl|
|3562 jc ioerr ! I/O error|
|If something goes wrong with the int 0x13 bios call, the carry flag is set. If the carry flag is set, a jump is made to ioerr.|
|3563 movb al, cl ! Restore al = sectors read|
|3564 addb bh, al ! bx += 2 * al * 256 (add bytes transferred)|
Lines 3564-3568 update the addresses (memory and hard drive sector) and count.
addb bh, al
! es:bx = where next sector is located
3566 add 8(bp), ax ! Update address by sectors transferred
3567 adc 10(bp), #0 ! Don't forget high word
3568 subb 12(bp), al ! Decrement sector count by sectors transferred
3569 jnz more ! Not all sectors have been transferred
3570 done: xorb ah, ah ! No error here!
3571 jmp finish
|3572 ioerr: cmpb ah, #0x80 ! Disk timed out? (Floppy drive empty)|
|int 0x13, ah=0x02/0x03 returns an error code in ah if the bios call is unsuccessful. An error code of 0x80 means that the drive was empty and 0x03 means that the drive is write protected.|
3574 cmpb ah, #0x03 ! Disk write protected?
3575 je finish
|3576 dec di ! Do we allow another reset?|
|3577 jl finish ! No, report the error|
|di was set on lines 3533 (floppy drives) and 3536 (hard drives). It is decremented for each loop back to more (line 3539). When di becomes negative, we give up trying.|
xorb ah, ah
! Code for a reset (0)
3579 int 0x13
3580 jnc more ! Succesful reset, try request again
|3581 finish: movb al, ah|
|At this point, ah contains either an error code or 0x00 if there was no error.|
|3582 xorb ah, ah ! ax = error number|
|3583 pop es|
|3584 pop di|
|3585 pop bp|
|bp, di and es were saved by pushing them on the stack (lines 3518, 3526 and 3527). They must now be popped back.|
|3588 ! int getch(void);|
|3589 ! Read a character from the keyboard, and check for an expired timer.|
|3590 ! A carriage return is changed into a linefeed for UNIX compatibility.|
This function is called in a couple of places in boot.c.
If the timer expires, getch() returns the ascii code for
(the escape key) in ax. If an escape key is pressed, the
flag (line 4283) is set and the ascii code for ESC is returned
In minix, any carriage returns ('\r') read from the keyboard
are converted to newlines ('\n'). Before writing to the
screen, newlines are converted to a carriage return followed by a newline
(see line 3632).
In minix, any carriage returns ('\r') read from the keyboard are converted to newlines ('\n'). Before writing to the screen, newlines are converted to a carriage return followed by a newline (see line 3632).
3591 .define _getch
3593 movb ah, #0x01 ! Keyboard status
|3594 int 0x16|
|int 0x16, ah=0x01 checks whether a key has been pressed and, if a key has indeed been pressed, places the ascii code of the key in al. int 0x16, ah=0x01 does not remove the key from the keyboard's buffer. int 0x16, ah=0x00 (line 3603) places the ascii code of the key pressed into al and removes the key from the keyboard's buffer. If a key has been pressed, int 0x16, ah=0x01 unsets the zero flag (Z=0).|
|3595 jnz press|
|expired() is defined in boot.c.|
test ax, ax
3598 jz _getch
|3599 mov ax, #ESC ! Return ESC|
|ESC is a macro #defined on line 3041.|
3602 xorb ah, ah ! Read character from keyboard
|3603 int 0x16|
|int 0x16 , ah=0x00 (line 3603) places the ascii code of the key pressed into al and removes the key from the keyboard's buffer.|
|3604 cmpb al, #0x0D ! Carriage return?|
|0x0D is the ascii code for a carriage return ('\r'). 0x0A (see line 3606) is the ascii code for a linefeed ('\n').|
|3605 jnz nocr|
|3606 movb al, #0x0A ! Change to linefeed|
|This is the line where carriage returns are converted to linefeeds.|
3607 nocr: cmpb al, #ESC
! Escape typed?
3608 jne noesc
|If the escape key was pressed, the escape flag must be incremented.|
|3610 noesc: xorb ah, ah ! ax = al|
|Since an ascii code is only a single byte and values are returned in ax, the high byte of ax (which is ah) must be zeroized.|
|3613 ! int escape(void);|
|3614 ! True if ESC has been typed.|
|The escape flag (line 4283) is set by getch() (line 3592) when the escape key (ESC) is pressed. If an escape key is waiting in the keyboard's buffer or the escape flag is set, escape() returns true (a nonzero value) in ax. If an escape key is waiting in the keyboard's buffer, escape() also discards the key from the buffer.|
3615 .define _escape
3617 movb ah, #0x01 ! Keyboard status
|3618 int 0x16|
|int 0x16, ah=0x01 checks whether a key has been pressed and places the ascii code of the key in al. int 0x16, ah=0x01 does not remove the key from the keyboard's buffer. If a key has been pressed, int 0x16, ah=0x01 unsets the zero flag (Z=0).|
3620 cmpb al, #ESC ! Escape typed?
3621 jne escflg
3622 xorb ah, ah ! Discard the escape
|3623 int 0x16|
|int 0x16, ah=0x00 (line 3603) places the ascii code of the key pressed into al and removes the key from the keyboard's buffer. It is already known that the pressed key is the escape key (ESC).|
! Set flag
3625 escflg: xor ax, ax
3626 xchg ax, escape ! Escape typed flag
|3629 ! int putch(int c);|
|3630 ! Write a character in teletype mode. The putk synonym is|
|3631 ! for the kernel printk function that uses it.|
|3632 ! Newlines are automatically preceded by a carriage return.|
printk needs putk to print individual characters.
The printk function substitutes the arguments itself. So
for the following C function call:
printk("my name is %s", "Andrew");
printk substitutes "Andrew" for "%s" to form the string "my name is Andrew" before calling putk to print the individual characters.
3634 .define _putch, _putk
3636 _putk: mov bx, sp
3637 movb al, 2(bx) ! al = character to be printed
|3638 testb al, al ! 1.6.* printk adds a trailing null|
|3639 jz nulch|
|It seems a little pointless for printk to call putk with a null (0) argument if putk doesn't print anything and just returns a 0. If you understand why printk does this, please submit a comment to the site which will be displayed below.|
cmpb al, #0x0A !
al = newline?
3641 jnz putc
3642 movb al, #0x0D
|3644 movb al, #0x0A ! Restore the '\n' and print it|
|It's clever how the ret instruction on line 3648 can return to line 3644 if putc is called on line 3643 or it can return to the printk function that called putch or putk.|
3645 putc: movb
ah, #0x0E ! Print character in teletype
3646 mov bx, #0x0001 ! Page 0, foreground color
|3647 int 0x10 ! Call BIOS VIDEO_IO|
|int 0x10, ah=0x0E 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.|
3648 nulch: ret
|3650 ! void set_mode(unsigned mode);|
|3651 ! void clear_screen(void);|
|3652 ! Set video mode / clear the screen.|
cur_vid_mode (line 4278) is a global variable that holds the
mode. Possible values of cur_vid_mode are given on lines
3664 and 3668.
There are a few things that I couldn't find in either the assembler book or the bios book I was using. If you have a book with the answers to the questions below, please submit a comment to the site.
3653 .define _set_mode, _clear_screen
3655 mov bx, sp
3656 mov ax, 2(bx) ! Video mode
3657 cmp ax, cur_vid_mode
3658 je modeok ! Mode already as requested?
3659 mov cur_vid_mode, ax
3661 mov ax, cur_vid_mode
|3662 andb ah, #0x7F ! Test bits 8-14, clear bit 15 (8x8 flag)|
|Modes for xvesa adapters have a nonzero ah. I'm not sure what the 8x8 flag is. If you know, please submit a comment to the site which will be displayed below.|
|3663 jnz xvesa ! VESA extended mode?|
|3664 int 0x10 ! Reset video (ah = 0)|
|int 0x10, ah=0x00 sets the video to the standard video mode (as opposed to extended VESA mode) specified by al. Possible values of al are 0x00 (CGA; text; resolution: 320x200; 2 colors), 0x03 (VGA; text; 720x400; 16), and 0x11 (VGA; graphics; 640x480; 2).|
|3665 jmp mdset|
|3666 xvesa: mov bx, ax ! bx = extended mode|
|Super VGA (SVGA) and extended VGA (EVGA and XVGA) are examples of modes conforming to the extended VESA (Video Electronics Standard Assocation).|
|3667 mov ax, #0x4F02 ! Reset video|
|3668 int 0x10|
|int 0x10, ax=4F02 sets the mode specified by bx for extended VESA adapters. Possible values of bx are 0x0100 (resolution: 640x400; 256 colors), 0x0106 (1,280x1,024; 16), and 0x0108 (80x60 in text mode).|
3669 mdset: testb cur_vid_mode+1,
3670 jz setcur ! 8x8 font requested?
3671 mov ax, #0x1112 ! Load ROM 8 by 8 double-dot patterns
3672 xorb bl, bl ! Load block 0
|3673 int 0x10|
|I cannot find information on int 0x10, ax=0x1112. If your book covers this bios call, please submit a comment to the site which will be displayed below.|
3674 setcur: xor dx, dx
! dl = column = 0, dh = row = 0
3675 xorb bh, bh ! Page 0
3676 movb ah, #0x02 ! Set cursor position
|3677 int 0x10|
|int 0x10, ah=0x02 sets the cursor to the position specified by dl (column), dx (row), and bh (page).|
|old_vid_mode is also a global variable (line 4277). It is set on line 3111 and is the baseline video mode in which the system begins after being powered up.|
3683 call _set_mode
3684 pop ax
|3687 ! u32_t get_tick(void);|
|3688 ! Return the current value of the clock tick counter. This counter|
|3689 ! increments 18.2 times per second. Poll it to do delays. Does not|
|3690 ! work on the original PC, but works on the PC/XT.|
|get_tick() returns the number of ticks since midnight.|
3691 .define _get_tick
3693 xorb ah, ah ! Code for get tick count
3694 int 0x1A
3695 mov ax, dx
3696 mov dx, cx ! dx:ax = cx:dx = tick count
|3700 ! Functions used to obtain info about the hardware. Boot uses this information|
|3701 ! itself, but will also pass them on to a pure 386 kernel, because one can't|
|3702 ! make BIOS calls from protected mode. The video type could probably be|
|3703 ! determined by the kernel too by looking at the hardware, but there is a small|
|3704 ! chance on errors that the monitor allows you to correct by setting variables.|
The environment variable bus is set using this function in
This variable is passed on to the minix kernel.
The comment on lines 3700-3702 doesn't apply to minix-386. On line 3902, the address int86 (line 4045) is pushed on the stack (if the kernel's K_INT86 flag is set) before jumping to the kernel (line 3933). The int86 function allows minix-386 to make 8086 bios interrupt calls.
It's possible that minix-386 isn't a "pure" 386 kernel since it can switch back temporarily to real mode to make a bios call. I'm not sure what a "pure" 386 kernel is.
get_bus() uses the int 0x15, ah=0xC0 bios call and getprocessor() to determine the bus. get_bus() returns 0 for an XT, 1 for an AT, and 2 for an MCA.
The XT bus was introduced with the Intel 8088 processor and has 8 data lines (the 8088 was an 8-bit processor) and 20 address lines. The XT bus operates at 4.77MHz, the same speed as the 8088. The PC bus is another name for the XT bus.
The AT bus was introduced with the 80286 and has 16 data lines (the 80286 is a 16-bit processor) and 24 address lines (the 80286 can address up to 16 megabytes). The most common speed of the AT bus is 8MHz. The ISA bus is another name for the AT bus.
The MCA bus is a proprietary IBM bus that was introduced with the IBM PS/2 (the PS/2 has an 80386 processor). It has 32 data lines (the 80386 is a 32-bit processor) and 32 address lines (the 80386 can address up to 4 gigabytes). The MCA bus is not compatible with the XT and AT buses. Therefore old XT and AT adapter cards cannot be used. Also, IBM charged high royalty fees to use the MCA technology. These two things prevented MCA from becoming as commonplace as the competing EISA standard.
Modern buses include the PCI bus and the SCSI bus.
3706 .define _get_bus ! returns type of system bus
3707 .define _get_video ! returns type of display
3709 ! u16_t get_bus(void)
3710 ! Return type of system bus, in order: XT, AT, MCA.
|3712 call _getprocessor|
|As explained on line 3145, it's pretty obvious what _getprocessor does, but I can't find where it's defined. I believe that the compiler supplies the function. _getprocessor returns 86 into ax for an 8086, 286 for a 80286, 386 for a 80386 and so on.|
xor dx, dx
! Assume XT
3714 cmp ax, #286 ! An AT has at least a 286
3715 jb got_bus
3716 inc dx ! Assume AT
3717 movb ah, #0xC0 ! Code for get configuration
|3718 int 0x15|
|int 0x15 ,ah=0xC0 returns the address of the system configuration table in es:bx. We are only concerned with the 5th byte in the table - the "feature byte #1". The int 0x15 , ah=0xC0 bios call clears the carry flag (CF =0) and places 0x00 in ah if the call was successful. It either sets the carry flag (CF=1) or places 0x86 in ah if the call was unsuccessful. If the call is unsuccessful, the bus must be an AT bus since all MCA buses support the int 0x15 , ah=0xC0 bios call. Some, but not all, AT buses support this bios call.|
! Carry clear and ah = 00 if supported
3720 testb ah, ah
3721 jne got_bus
3723 movb al, 5(bx) ! Load feature byte #1
3724 inc dx ! Assume MCA
3725 testb al, #0x02 ! Test bit 1 - "bus is Micro Channel"
3726 jnz got_bus
3727 dec dx ! Assume AT
|3728 testb al, #0x40 ! Test bit 6 - "2nd 8259 installed"|
|The 8259 is a bus controller. The XT bus has one and the AT has two.|
3730 dec dx ! It is an XT
3732 push ds
3733 pop es ! Restore es
3734 mov ax, dx ! Return bus code
|bus is an uninitialized global variable and (for this reason) is found in the bss (see line 4284).|
|3738 ! u16_t get_video(void)|
|3739 ! u16_t get_video(void)|
|3739 ! Return type of display, in order: MDA, CGA, mono EGA, color EGA,|
|3740 ! mono VGA, color VGA.|
|Go to this link for a short description of different display standards. get_video() returns the video type in ax.|
3742 mov ax, #0x1A00 ! Function 1A returns display code
3743 int 0x10 ! al = 1A if supported
|3744 cmpb al, #0x1A|
|int 0x10, ax=0x1A00 returns 5 in bl if the display type is a monochrome EGA, 4 in bl for a color EGA, 7 in bl for a monochrome VGA, and 8 in bl for a color VGA. If the int 0x10, ax=0x1A00 bios call is not supported, the int 0x10, ah=0x12, bl=0x10 bios call (line 3762) is attempted and if that doesn't work, the int 0x11 bios call (line 3772) is attempted.|
! No display code function supported
3747 mov ax, #2
3748 cmpb bl, #5 ! Is it a monochrome EGA?
3749 jz got_video
3750 inc ax
3751 cmpb bl, #4 ! Is it a color EGA?
3752 jz got_video
3753 inc ax
3754 cmpb bl, #7 ! Is it a monochrome VGA?
3755 jz got_video
3756 inc ax
3757 cmpb bl, #8 ! Is it a color VGA?
3758 jz got_video
3760 no_dc: movb ah, #0x12 ! Get information about the EGA
3761 movb bl, #0x10
|3762 int 0x10|
int 0x10, ax=0x12, bl=0x10 is specifically
for monochrome EGA and color EGA adapters. If the adapter isn't an
EGA, the bios call returns 0x10 in bl. If the adapter is
a monochrome EGA adapter, the bios call returns 1 in bh.
As an aside, I have not found a complete reference for bios calls. In these notes, I have often needed to look at the code to determine what a bios call does. This is obviously not ideal. I have an assembler book (8086/8088, 80286, 80386, and 80486 Assembly Language Programming by Barry B. Brey) and a PC bios book (System BIOS for IBM PCs, Compatibles, and EISA Computers, Second Edition) and both are incomplete. I've also searched the web looking for a good bios reference. If anyone has found the nirvana of bios books, please submit a comment to the site which will be displayed below.
cmpb bl, #0x10 !
Did it come back as 0x10? (No EGA)
3764 jz no_ega
3766 mov ax, #2
3767 cmpb bh, #1 ! Is it monochrome?
3768 jz got_video
3769 inc ax
3770 jmp got_video
|3772 no_ega: int 0x11 ! Get bit pattern for equipment|
|int 0x11 returns a system equipment list in ax. Bits 4-5 of ax specify the video mode. If the adapter is an MDA adapter, both bits are set.|
and ax, #0x30
! Isolate color/mono field
3774 sub ax, #0x30
3775 jz got_video ! Is it an MDA?
3776 mov ax, #1 ! No it's CGA
|3782 ! Functions to leave the boot monitor.|
|Since the next two functions are called from code outside these files, they must be .defined. Names of functions that are called from outside the assembler file in which they are defined must be preceded by an underscore (_).|
3783 .define _bootstrap
! Call another bootstrap
3784 .define _minix ! Call Minix
|3786 ! void bootstrap(int device, struct part_entry *entry)|
|3787 ! Call another bootstrap routine to boot MS-DOS for instance. (No real|
|3788 ! need for that anymore, now that you can format floppies under Minix).|
|3789 ! The bootstrap must have been loaded at BOOTSEG from "device".|
I believe the comment indicates that minix was unable to format floppies
earlier. So if you wanted to format a floppy, you would exit the
minix kernel and return to the boot monitor (this program) and then call
the bootstrap of a partition that contained MS-DOS. You'd then format
the floppy in MS-DOS, reboot the system and then return to the minix kernel.
This sounds like a pain. Since formatting floppies is a common task,
it's good that they gave minix the capability to format floppies.
Now's a good time to give an overview of the boot monitor. Note that the boot monitor is another name for the secondary boot - the code we're studying now.
After going through an initialization routine (part of which we've already seen on lines 3062-3180), the secondary boot enters a command loop on lines 6617-6625 of boot.c. monitor() reads a line that the user types in and parses it into tokens to form a command chain (which we'll discuss ad nauseum in boot.c). execute() takes this command chain and executes the appropriate function.
Since this sequence will be clearer with an example and since we're looking at the bootstrap() function, let's see how a user boots a different partition. Let's say, for example, that this program (the secondary boot) came from hd1 and the user (for whatever reason) wants to boot partition hd4. The user types in:
hd1> boot hd4
monitor() parses this string into two tokens, "boot" and "hd4", and links them together to form a two token command chain. execute() goes through its numerous if clauses looking for a command string with two tokens, the first of which is "boot". It finds a match and calls the appropriate function, boot_device(). boot_device() makes sure that hd4 can be booted and then calls exec_bootstrap().
exec_bootstrap() calls bootstrap()(line 6112) after readsector() (line 6103) reads in the first sector of the partition (the boot sector) specified by entry. readsector() reads the boot sector into memory location 0x0000:0x7C00.
BOOTOFF is a macro defined as 0x7C00 in this file (line 3020)
and BOOTPOS is a macro defined as 0x7C00 in boot.h.
Since BOOTSEG is not defined in this file, the comment referring
to BOOTSEG is confusing and appears to be a mistake or a remnant
of a previous boothead.s that defined the macro BOOTSEG.
|restore_video() is on line 3680. It restores the system to the baseline video mode in which it began after being powered up.|
mov bx, sp
3793 movb dl, 2(bx) ! Device to boot from
3794 mov si, 4(bx) ! ds:si = partition table entry
3795 xor ax, ax
3796 mov es, ax ! Vector segment
3797 mov di, #BUFFER ! es:di = buffer in low core
3798 mov cx, #PENTRYSIZE ! cx = size of partition table entry
|3799 rep movsb ! Copy the entry to low core|
|rep movsb moves cx bytes from ds:si to es:di. In this case, the partition entry specified by entry is copied to 0:BUFFER (defined as 0x0600 on line 3022), where it should be safe from being overwritten by the bootstrap that has just been loaded.|
|The minix bootstrap and MS-DOS's bootstrap expect dl to hold the device being booted and es:si to hold the address of the partition table entry of the partition being booted. Possible values of dl are 0x00 and 0x01 for the first and second floppy drives and 0x80, 0x81, 0x82, and 0x83 for hard drives 1-4.|
|3801 mov ds, ax ! Some bootstraps need zero segment registers|
|The minix bootstrap doesn't need to zeroize the segment registers since the first thing it does it to zeroize the ds and ss registers.|
|The beginning of the bootstrap's code will be the top of the bootstrap's stack.|
0:BOOTOFF is indeed the memory location where the bios loads
the first sector of floppy drives and hard drives when the system starts.
However, the bootstrap to which the code jumps was loaded by readsector()
in boot.c and not by the bios. For
this reason, the comment is a little misleading.
jmpf BOOTOFF, 0 jumps to 0:BOOTOFF.
|3808 ! u32_t minix(u32_t koff, u32_t kcs, u32_t kds,|
|3809 ! char *bootparams, size_t paramsize, u32_t aout);|
|3810 ! Call Minix.|
Please read the comments for lines 3786-3789 before reading these comments.
The user wishes to load the minix kernel. He can follow several paths from the boot monitor; I'll describe two. 1) He can simply load the kernel. 2) He can pass an option to the kernel while loading it. If he wishes to simply load the kernel, he types in:
monitor() reads this in and parses it into a one token command chain ("boot"). execute() finds a match and executes bootminix(). bootminix() in bootimage.c calls exec_image(), which reads the OS image (i.e. the kernel+fs+mm+networking(if enabled)+init) into memory and then finally calls minix() on line 7555.
If the user wishes to pass an option to the kernel, he types in:
hd1> boot -option1
monitor() reads this in and parses it into a two token command chain ("boot" and "-option1"). execute() finds a match and sets the environment variable bootopts to "-option1" before calling bootminix(). This environment variable can be accessed by the kernel.
minix() sets up the stack the way the kernel expects it, sets ds and es to the kernel's ds and es, and then jumps to the kernel. If the kernel is minix-386 (as opposed to minix-86), minix() also switches from real mode to protected mode.
Here's the stack after line 3813:
3812 push bp
3813 mov bp, sp ! Pointer to arguments
3815 mov dx, #0x03F2 ! Floppy motor drive control bits
3816 movb al, #0x0C ! Bits 4-7 for floppy 0-3 are off
|3817 outb dx ! Kill the motors|
outb sends the data in al to the port specified by
the operand (in this case, dx). Note that dx does
not contain an address in memory; it contains an address in the I/O space.
Bits 4-5 of I/O address 0x03F2 are the floppy control bits for floppy drives 1-2. An input of 0x0C (00001100) disables the 2 motors. (Bits 4-5 are the fifth and sixth bits from the right. The bits are 0-indexed.)
Bit 3 of I/O address 0x03F2 is for floppy drive DMA enable and bit 2 is for floppy controller reset. A 0 in bit 2 resets the floppy drive (since bit 2 is high for 0x0C, the controller is not reset). The other bits are unused.
exec_image() loads the OS image before calling minix(). Since the image has already been loaded, there's no reason why any drive (floppy drive or hard drive) should be needed for some time. So the motors are killed.
3819 xor ax, ax ! Vector & BIOS data segments
3820 mov ds, ax
|3821 andb 0x043F, #0xF0 ! Clear diskette motor status bits of BIOS|
Bits 0-1 of memory address 0x043F (the range 0x400-0x4FF in memory is
the bios data area) are the motor status bits for floppy drives 1 and 2,
respectively. They need to be changed to reflect the previous
instruction (line 3817).
As an aside, I'm not sure why the floppy status bits don't automatically reflect the state of the floppy drives. It seems strange that we need to manually change the status bits. If you have any insight into this, please submit a comment to the site which will be displayed below.
|3822 pop ds|
|3823 cli ! No more interruptions|
|The interrupts won't be enabled until the code jumps to the kernel.|
3825 test _k_flags, #K_I386 ! Switch to 386 mode?
3826 jnz minix386
3828 ! Call Minix in real mode.
If minix is called in real mode, everything's a little easier.
There's no need to switch to protected mode.
By the time the code reaches the retf instruction (line 3860), the stack looks like this:
The retf instruction pops the last 4 bytes (kernel cs and kernel offset) off the stack.
|3830 test _k_flags, #K_MEML ! New memory arrangements?|
|3831 jz 0f|
|jz 0f jumps forward (f) to the first 0: label.|
! Address of a.out headers
3833 push 20(bp)
3835 push 18(bp) ! # bytes of boot parameters
3836 push 16(bp) ! Address of boot parameters
3838 test _k_flags, #K_RET ! Can the kernel return?
3839 jz noret86
3840 mov dx, cs ! Monitor far return address
|3841 mov ax, #ret86|
|Look on line 3935 for ret86. The minix-86 kernel returns to the monitor at ret86.|
|The mem array is initialized on lines 3137-3177.|
|3843 jnz 0f|
|3844 xor dx, dx ! If no ext mem then monitor not preserved|
|There isn't room for the boot monitor if there's no extended memory.|
xor ax, ax
3846 0: push dx ! Push monitor far return address or zero
3847 push ax
3850 mov ax, 8(bp)
3851 mov dx, 10(bp)
|abs2seg() (line 3235) converts a 32-bit absolute address in dx-ax to a segment:offset address in dx:ax.|
! Kernel code segment
3854 push 4(bp) ! Kernel code offset
3855 mov ax, 12(bp)
3856 mov dx, 14(bp)
3857 call abs2seg
3858 mov ds, dx ! Kernel data segment
3859 mov es, dx ! Set es to kernel data too
3860 retf ! Make a far call to the kernel
3862 ! Call Minix in 386 mode.
|There's quite a bit more to jumping to the minix-386 kernel. Everything that was done for the minix-86 kernel must be done (setting up the stack the way the kernel expects it and setting ds and es to the kernel's ds and es) plus the switch to protected mode.|
The cseg prefix causes the mov instruction to move
the contents of cs and ds to the memory addresses cs:cs_real-2
and cs_ds_real-2 instead of ds:cs_real-2 and ds:ds_real-2.
These addresses are found on lines 4160 and 4162. 0xDEAD and 0xBEEF are overwritten by the contents of cs and ds, respectively. Did you really think that 0xDEAD and 0xBEEF were meaningful values?
|3866 .data1 0x0F,0x20,0xC0 ! mov eax, cr0|
|3867 orb al, #0x01 ! Set PE (protection enable) bit|
The control registers (one of which is cr0) determine the
operating mode of the processor. One of the flags in cr0
is the Protection Enable (PE) flag. This flag is set on line 3867
but instead of moving the new state to cr0, it is saved in
(line 4281). Later, when the switch to protected mode is made (lines
4138-4140), the new state is moved to cr0.
The control registers are unique to the Intel processors with protected mode capabilities. Since this code was compiled with the -mi86 option (see comment for line 3019), our compiler knows nothing of the control registers. For this reason, we cannot rely on the compiler to translate an assembler instruction containing cr0 to machine language but must use machine language directly.
The o32 prefix (0x66) converts the instruction mov msw, ax to mov msw, eax. See the comment for line 3019.
The only complete source (that I know of) that discusses the control registers (and a lot more) is Volume 3 of the Intel Architecture Software Developer's Manual . It's not an easy read.
We now need an overview of the global descriptor table (gdt) and protected
In real mode, the contents of a segment register (cs, ds, ss, etc.) are shifted to the left 4 bits and then added to an offset register (ip, si, sp, etc.) to form an address. Note that the offset registers are 16-bit registers
For example, if the contents of cs (the code segment) are 0x1000 and the contents of ip (the instruction pointer) are 0x3000, together they form the address 0x13000.
Protected mode is more complex. The segment register points to an entry in the global descriptor table (gdt). Look at lines 4242-4274. p_null_desc, p_gdt_desc, p_ds_desc, p_es_desc, etc. are all gdt entries. Each entry is 8 bytes (2 .data2's + 4 .data1's) and has this layout:
On lines 3871-3897 bytes 2, 3, and 4 of the p_gdt_desc, p_ds_desc, p_ss_desc, p_cs_desc, and p_mcs_desc entries are patched in. These bytes form the base's first three bytes (the base has a total of four bytes). The fourth byte (byte 7 in the figure) is zero for all of the entries shown. Since the kernel is located low in memory, 3 bytes is sufficient for the base of the kernel's entries.
In protected mode, the base of the entry that a segment register (cs, ds, ss, etc.) points to is added to an offset register (eip, esi, esp, etc.) to form an address. Note that the offset registers are 32-bit registers
For example, suppose the contents of cs (the code segment) is CS_SELECTOR (=6*8; see line 3038). CS_SELECTOR points to an entry within the gdt (see line 4255). The base of this entry (p_cs_desc) is added to ip (the instruction pointer) to form an address.
Let's assume that (on lines 3890 and 3891) the base of the entry p_cs_desc is patched with 0x00001100 (the most significant 8 bits are already 0) and that the contents of eip are 0x00000030. If cs holds CS_SELECTOR, the two registers form the address 0x00001130 (not 0x00011030; neither register is shifted).
Look at the figure above. Bytes 0, 1 and the 4 least significant bits of byte 6 (for a total of 20 bits; I will call these bits the "limit bits" from here on) and the granularity bit (discussed at the end of this comment) form the limit. Any offset added to the base must be less than or equal to the limit. If the limit is exceeded, the processor complains by triggering a General Protection Fault (GPF). The processor enforces protected mode with the GPF.
Let's assume that cs contains CS_SELECTOR and the base of the entry p_cs_desc is 0x00001100 and the limit is 0x03000. If the contents of eip become 0x00003001 or greater, a GPF will be triggered.
The result of a GPF depends on what triggered it. If a user process triggers the GPF, the user process is terminated. If the kernel triggers the GPF, you're in trouble. However, if you look at the comments on lines 4256, 4259, and 4267, you'll see that the data segment descriptor, the extra segment descriptor, and the code segment descriptor have 4 GB limits so the kernel can't trigger a GPF by exceeding its limit. These 4 GB limits give the kernel "roaming" ability. The kernel can read from and write to any memory location (of course, it still can't write to any ROM portions of the memory). This roaming ability is vital to the messaging system, which you'll learn about in the Processes chapter (Chapter 2) of Operating Systems.
The last thing we'll discuss before moving on is the "granularity" of the limit. The granularity is a bit (bit "G") in byte 6 (labeled "random stuff" in the figure above). If G=0, the limit bits specify a limit that can be 1 byte to 1M bytes. If G=1, the limit bits are multiplied by 4K bytes (0x1000). If G=1, the limit can be any multiple of 4K bytes. A segment can therefore have a length of 4K bytes to 4G bytes in steps of 4K bytes.
For example, if the limit bits of an entry were 0x00300 and G=0, the limit would be 0x00300. If the limit bits of an entry were 0x00300 and G=1, the limit would be 0x00300000.
|3871 mov dx, ds ! Monitor ds|
|3872 mov ax, #p_gdt ! dx:ax = Global descriptor table|
|3874 mov p_gdt_desc+2, ax|
|3875 movb p_gdt_desc+4, dl ! Set base of global descriptor table|
The first entry of the gdt (p_null_desc; line 4243) must be
an entry of all 0's. It's called the null descriptor. The second
entry must describe the table itself. On lines 3871-3875, the base
of the global descriptor table is patched. ds:p_gdt (line
4242) is the base of the table. Before patching in the base of the
table, the segment:offset address must be converted to an absolute address.
seg2abs() (line 3235) converts an absolute address in dx-ax to the segment:offset address in dx:ax.
|3877 mov ax, 12(bp)|
|3878 mov dx, 14(bp) ! Kernel ds (absolute address)|
|3879 mov p_ds_desc+2, ax|
|3880 movb p_ds_desc+4, dl ! Set base of kernel data segment|
On lines 3877-3880, the base of the data segment is patched.
Here, again, are the arguments passed into minix() (lines 3808-3810):
|3882 mov dx, ss ! Monitor ss|
|3883 xor ax, ax ! dx:ax = Monitor stack segment|
|3883 xor ax, ax ! dx:ax = Monitor stack segment|
|3885 mov p_ss_desc+2, ax|
|3886 movb p_ss_desc+4, dl|
|On lines 3882-3886, the base of the stack segment is patched. The address ss:0x0000 is used since the kernel initially uses the monitor's stack. Note that line 06062 in the book corroborates this.|
|3888 mov ax, 8(bp)|
|3889 mov dx, 10(bp) ! Kernel cs (absolute address)|
|3890 mov p_cs_desc+2, ax|
|3891 movb p_cs_desc+4, dl|
|On lines 3888-3891, the base of the code segment is patched.|
|3893 mov dx, cs ! Monitor cs|
|3894 xor ax, ax ! dx:ax = Monitor code segment|
|3894 xor ax, ax ! dx:ax = Monitor code segment|
|3896 mov p_mcs_desc+2, ax|
|3897 movb p_mcs_desc+4, dl|
|On lines 3893-3897, the base of the monitor code segment is patched. If we wish to return to the boot monitor, we'll need this.|
On lines 3899-3919, the stack is prepared the way the kernel expects
it. Below is the stack after line3925. The retf instruction
on lines 3932-3933 pops off the bottom 8 bytes.
This figure assumes that the K_RET bit in the kernel's k_flags is set. In other words, this figure assumes that the kernel can return. The figure also assumes that a list of available memory is being passed in the mem array (i.e. the K_MEML bit in the kernel's k_flags is set).
If there's generic INT86 support, the address int86 is pushed on the stack instead of the address bios13. We will discuss the difference between generic INT86 support and bios int 13 support on line 4020.
3900 test _k_flags, #K_INT86 ! Generic INT86 support?
3901 jz 0f
3902 push #int86 ! Far address to INT86 support
3903 jmp 1f
3904 0: push #bios13 ! Far address to BIOS int 13 support
3906 test _k_flags, #K_MEML ! New memory arrangements?
3907 jz 0f
3908 .data1 o32
3909 push 20(bp) ! Address of a.out headers
3911 push #0
3912 push 18(bp) ! 32 bit size of parameters on stack
3913 push #0
3914 push 16(bp) ! 32 bit address of parameters (ss relative)
Now's a good time to see how the stack will appear after the jump to
the kernel. Below is the stack after line 06064 in the book.
Note that the code in mpx386.s (look at line 06047 in the book) assumes that MCS_SELECTOR and ret386 are pushed on the stack on lines 3918-3919. If the two values weren't pushed on the stack, 8(ebp) and 12(ebp) on lines 06093 and 06094 wouldn't correspond to the boot parameters offset and length (bootparams and paramsize in the figure).
Since we're looking at the code in the book, look at lines 06053 and 06055. get_clickshift() in bootimage.c extracts click_shift, which we'll discuss later, and k_flags from the kernel image.
3918 push #MCS_SELECTOR
3919 push #ret386 ! Monitor far return address
|3922 push #0|
|3924 push 6(bp)|
|3925 push 4(bp) ! 32 bit far address to kernel entry point|
|These 8 bytes will be popped off the stack by the retf instruction on lines 3932-3933.|
|real2prot() (line 4128) switches from real mode to protected mode.|
|3929 mov ds, ax|
|3931 mov es, ax|
|ds, es, and cs are set in lines 3928-3933. cs is indirectly set by the retf instruction. Look at line 06062 in the book. The kernel sets the stack segment (ss) register itself.|
|3935 ! Minix-86 returns here on a halt or reboot.|
|3937 mov 8(bp), ax|
|3938 mov 10(bp), dx ! Return value|
|3939 jmp return|
|3941 ! Minix-386 returns here on a halt or reboot.|
|3944 mov 8(bp), ax ! Return value|
|3948 mov sp, bp ! Pop parameters|
|3949 sti ! Can take interrupts again|
I need to find out what the stack looks like when the kernel returns.
I need to find out where in thekernel code it returns.
Please submit a comment to the site if you know why.
|get_video() (line 3738) returns 0 for an MDA adapter, 1 for a CGA adapter, 2 for a EGA adapter, etc. If it's a MDA or CGA adapter, there can only be 25 rows. If it's an EGA adapter or better, memory location ds:0x0484 = 0x0000:0x0484 in the bios data area holds the number of rows minus one.|
movb dh, #24
! dh = row 24
3953 cmp ax, #2 ! At least EGA?
3954 jb is25 ! Otherwise 25 rows
3955 push ds
3956 xor ax, ax ! Vector & BIOS data segments
3957 mov ds, ax
3958 movb dh, 0x0484 ! Number of rows on display minus one
3959 pop ds
3961 xorb dl, dl ! dl = column 0
3962 xorb bh, bh ! Page 0
3963 movb ah, #0x02 ! Set cursor position
|3964 int 0x10|
|int 0x10, ah=0x02 sets the cursor position at column dl, page bh.|
3966 xorb ah, ah ! Whack the disk system, Minix may have messed
3967 movb dl, #0x80 ! it up
|3968 int 0x13|
|int 0x13, ah=0x00 resets the disk in dl. Possible values for dl are 0x00 and 0x01 for floppy drives 1 and 2 and 0x80, 0x81, 0x82, and 0x83 for hard drives 1-4.|
3966 xorb ah, ah ! Whack the disk system, Minix may have messed
3967 movb dl, #0x80 ! it up
|3970 call _getprocessor|
|getprocessor() returns 88 for an 8088 processor, 286 for a 80286 processor, 386 for a 80386 processor, etc. As noted in the comment for line 3145, I believe that getprocessor() is supplied by the compiler. If the processor is something less than an 80286 processor, the int 0x1A, ah=0x02 bios call is unavailable and the system clock cannot be reset.|
cmp ax, #286
3972 jb noclock
|3973 xorb al, al|
|3974 tryclk: decb al|
|3975 jz noclock|
|al enters the loop with a value of 0 and is then decremented to a value of 255. The int 0x1A, ah=0x02 bios call (line 3977) is attempted 255 times before giving up.|
|3976 movb ah, #0x02 ! Get real-time clock time (from CMOS clock)|
|3977 int 0x1A|
int 0x1A, ah=0x02 first determines if the real time
clock device is updating its clock value. If it is updating its clock
value, the carry flag is set. If it's not updating its clock value,
and ch holds the hours since midnight, cl holds the minutes,
and dh holds the seconds. Resetting the system clock is
complicated by the fact that int 0x1A, ah=0x02 returns
the values in BCD (Binary-Coded Decimal). Packed BCD stores 2 decimal
numbers in 1 byte. Unpacked BCD stores 1 decimal number in 1 byte.
The values returned in cl, ch, cl, and dh
by int 0x1A, ah=0x02 are packed BCD. Below are
some decimal numbers converted to packed and unpacked BCD.
Decimal Packed BCD Unpacked BCD
The conversion from a BCD value to a regular value is done in bcd (line 4014).
|3978 jc tryclk ! Carry set, not running or being updated|
|3979 movb al, ch ! ch = hour in BCD|
|Lines 3979-3991 convert the hours, minutes, and seconds since midnight that int 0x1A, ah=0x02 returns in BCD format to the number of seconds in dx-bx. Note that dx-bx is a 4 byte value with the upper 2 bytes in dx and the lower 2 bytes in bx. It does not mean dx minus bx.|
|See line 4014.|
|mulb multiplies al by the operand (in this case the value at address c60 (see line 4206), which is 60) and places the result in ax.|
mov bx, ax
! bx = hour * 60
3983 movb al, cl ! cl = minutes in BCD
3984 call bcd
3985 add bx, ax ! bx = hour * 60 + minutes
3986 movb al, dh ! dh = seconds in BCD
3987 call bcd
3988 xchg ax, bx ! ax = hour * 60 + minutes, bx = seconds
3989 mul c60 ! dx-ax = (hour * 60 + minutes) * 60
3990 add bx, ax
3991 adc dx, #0 ! dx-bx = seconds since midnight
|3992 mov ax, dx|
It took me a while to figure this out, but 19663/1080=18.2. There
are 18.2 clock ticks per second. To get the number of clock ticks
since midnight, the number of seconds since midnight is multiplied by 19663
and then divided by 1080.
On lines 3992-4003, the number of seconds in dx-bx is converted to the number of clock ticks in cx-dx. int 0x1A, ah=0x01 sets the system time with the number of ticks in cx-dx.
Let's give an example. Let's assume that the time is 19:25.55. This is 69955 seconds after midnight. So 0x0001 will be in dx and 0x1143 will be in bx. (0x00011143 = 69955)
The mul instruction multiplies ax by the operand (see line 3993). Since the value that is being multiplyied can be greater than 2 bytes and therefore too big for ax (as in our example - 0x11143), a single mul instruction cannot be used but the multiplication must be broken up into 2 mul instructions (lines 3993 and 3995).
I only show the relevant registers.
mul multiplies ax by the operand (in this case the
value at address c19663 (see line 4209), which is 19663=0x4CCF)
and places the result in dx-ax.
|3994 xchg ax, bx|
|3996 add dx, bx ! dx-ax = dx-bx * (0x1800B0 / (2*2*2*2*5))|
dx-ax = seconds * 19663
Now this value must be divided by 1080. The div instruction (line 4000) places the quotient in ax and the remainder in dx. Since the quotient may be greater than 2 bytes (as it is in our example - 0x00136F22) and won't fit in ax, a single div instruction cannot be used but the division must be broken up into 2 div instructions (lines 4000 and 4002).
The value 0x1800B0 does not appear to be relevant to this code.
|3997 mov cx, ax ! (0x1800B0 = ticks per day of BIOS clock)|
|3998 mov ax, dx|
|3999 xor dx, dx|
div divides dx-ax by the operand (in this case the
value at the address c1080 (see line 4208), which is 1080) and
places the quotient in ax and the remainder in dx.
|4001 xchg ax, cx|
|4003 mov dx, ax ! cx-dx = ticks since midnight|
(69955 seconds)*(18.2 clock ticks/second) = 1273181 = 0x00136D5D
0x00136D5D isn't equal to 0x00136F22 but it's close enough. 18.2 is an approximation for the clock ticks per second anyway.
|4005 int 0x1A|
int 0x1A, ah=0x01 sets the timer tick count (the number of ticks since midnight) with the value from cx-dx.
|4008 mov ax, 8(bp)|
|4009 mov dx, 10(bp) ! dx-ax = return value from the kernel|
|4010 pop bp|
|4011 ret ! Return to monitor as if nothing much happened|
I need to find out what the stack looks like when the kernel returns.
I need to find out where in the kernel code it returns.
Please submit a comment to the site if you know why.
|4014 bcd: movb ah, al|
|4015 shrb ah, #4|
|4016 andb al, #0x0F|
|On lines 4014-4016, the upper 4 bits of al are moved into ah. For example, a value of 0x005A in ax would be converted to 0x050A.|
|4017 .data1 0xD5,10 ! aad ! ax = (al >> 4) * 10 + (al & 0x0F)|
|aad converts an unpacked BCD in ax into a regular value. The author was apparently having some problems with the compiler so he used the actual machine instruction instead.|
! (BUG: assembler messes up aad & aam!)
|4020 ! Support function for Minix-386 to make a BIOS int 13 call (disk I/O).|
|Before jumping to the minix-386 kernel (line 3933), either the address int86 (line 3902) or bios13 (line 3904) is pushed onto the stack. This allows minix-386 to switch back to real mode and use any bios function call (int86) or use only bios function call int 0x13 (bios13). int 0x13 allows disk I/O. Obviously, the int86 option is more flexible.|
|4022 mov bp, sp|
|prot2real() (line 4149) switches the system from protected mode to real mode. The system must be in real mode to make bios function calls.|
|4024 sti ! Enable interrupts|
|The kernel disables the interrupts in anticipation of the jump to bios13. The stack is in flux in prot2real()(see line 4165) and necesitates the disabling. After prot2real() returns, the interrupts can be reenabled.|
|4026 mov ax, 8(bp) ! Load parameters|
|An int 0x13 bios call potentially uses the ax, bx, cx, dx, and es registers. Since the calls to prot2real() (line 4023) and real2prot() (line 4039) could alter the contents of these registers, the values in these registers must be loaded and saved immediately before and after the int 0x13 bios call.|
mov bx, 10(bp)
4028 mov cx, 12(bp)
4029 mov dx, 14(bp)
4030 mov es, 16(bp)
|4031 int 0x13 ! Make the BIOS call|
Here are a few examples of int 0x13 calls:
int 0x13, ah=0x00 resets the drive.
int 0x13, ah=0x02 copies sectors from a hard drive or floppy (specified by dl) to memory. al specifies how many 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. int 0x13, ah=0x02 returns 0 in ah if successful and an error code in ah if unsuccessful.
int 0x13, ah=0x08 returns the device geometry of the drive specified by dl. dl is 0x80 for the first drive, 0x81 for the second, 0x82 for the third and 0x83 for the fourth. The call 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 returns for the maximum head number has a 0-origin. This means that if int 0x13 returns a 15 for the maximum head number, there are actually 16 heads. This is the reason that dh is incremented on line 3468.
mov 8(bp), ax
! Save results
4033 mov 10(bp), bx
4034 mov 12(bp), cx
4035 mov 14(bp), dx
4036 mov 16(bp), es
|4038 cli ! Disable interrupts|
|The stack is in flux in real2prot() (see line 4146) so the interrupts must be disabled. The interrupts are reenabled after the return to the kernel (line 4043).|
prot2real() (line 4150) switches the system from protected
mode to real mode. The system must be in real mode to make bios function
real2prot() (line 4128) switches the system from real mode back to protected mode.
4041 mov ds, ax
|Before the code jumps back to the minix-386 kernel, ds must be reloaded with the minix-386 kernel's data segment selector.|
|4043 retf ! Return to the kernel|
|The o32 prefix causes the retf instruction to use the bottom 8 bytes on the stack for the return address rather than the bottom 4 bytes. See line 3019 for details.|
|4045 ! Support function for Minix-386 to make an 8086 interrupt call.|
int86 can handle all of the bios function calls whereas bios13
(line 4020) can only handle int 0x13 (disk I/O) bios function
calls. int86 can also make far calls to drivers not supplied
by the kernel (see line 4065). A few additional complexities are
The 0xFF in the int 0xFF call (line 4087) must be replaced with the desired interrupt. If a jump to a far address is desired, the far address must be pushed on the stack (line 4065) and the entire int 0xFF instruction must be replaced with a retf instruction (line 4066).
The minix-386 kernel counts clock ticks without relying on the bios. When the kernel jumps to the boot monitor to take care of a bios interrupt call, it can no longer count the clock ticks. For this reason, the bios keeps track of the clock ticks until the return to the kernel. I assume that bios13 (line 4020) does not keep track of the lost clock ticks because the function is shorter. If you know the answer to this, please submit a comment to the site which will be displayed below.
|4047 mov bp, sp|
The stack looks like this:
|prot2real() (line 4149) switches from protected mode to real mode. In order to make an 8086 interrupt call, the system must be in real mode.|
|4051 xor ax, ax|
|The o32 prefix (0x66) converts the instruction xor ax, ax to xor eax, eax. See the comment for line 3019.|
|4052 mov es, ax ! Vector & BIOS data segments|
|4054 eseg mov 0x046C, ax ! Clear BIOS clock tick counter|
|This instruction moves the contents of eax (which is 0) into es:0x046C=0x0000:0x046C. 0x0000:0x046C is the memory location that holds the bios clock counter. The number of clock ticks that accrue during the int86 call are returned in cx (see line 4118). See the comment for lines 4045 and 4046.|
4056 sti ! Enable interrupts
|4058 movb al, #0xCD ! INT instruction|
|It is assumed that int86 has been called to invoke a bios function call. However, if int86 has been called to jump to a far address for a driver, the value in al is overwritten on line 4066.|
|4059 movb ah, 8(bp) ! Interrupt number?|
|4060 testb ah, ah|
|4061 jnz 0f ! Nonzero if INT, otherwise far call|
testb sets the zero flag (Z) if the contents of ah
jnz 0f jumps forward to the first 0: label (line 4067) if Z is not set.
|4062 push cs|
|4063 push #intret+2 ! Far return address|
|The driver at the far address uses this segment:offset pair as a return value. intret (line 4087) is the first byte of the retf; nop instructions. intret+2 is the first byte after these two instructions (in other words, the first byte of the instruction on lines 4089-4090).|
4065 push 12(bp) ! Far driver address
|4066 mov ax, #0x90CB ! RETF; NOP|
|If 8(bp) is 0, the int 0xFF instruction on line 4087 is replaced with the retf instruction (see lines 4059-4061). However, since the int 0xFF instruction is 2 bytes and the retf instruction is only a single byte (0xCB), a nop (no operation) instruction (0x90) is added after the retf instruction to make up for the difference in instruction length.|
|4067 0: cseg mov intret, ax ! Patch `INT n' or `RETF; NOP' into code|
The prefix cseg is used since cs:intret (and not ds:intret) is the address that is patched. Note that any address within the code (like intret) is an offset within the code segment (cs) and any address within the data (like c60 on line 4206) is an offset within the data segment (ds).
|4068 jmp .+2 ! Clear instruction queue|
An instruction queue is a mechanism to decrease the time necessary to fetch instructions from RAM. Several instructions are fetched at once instead of fetching one instruction at a time. This works great most of the time. However, after replacing the int 0xFF instruction, there will be a discrepency between the instruction queue and RAM. The current instruction queue needs to be thrown out and replaced by a neue queue that reflects the current contents of the RAM.
The jmp instruction clears the instruction queue and then jumps to the address specified by the operand.
"." is replaced by the address of the first byte of the instruction. Since the jmp instruction is 2 bytes, jmp .+2 jumps to the next instruction.
|4070 mov ds, 16(bp) ! Load parameters|
|An interrupt call or a call to a far address potentially uses the ds, es, ax, bx, cx, dx, si, di and bp registers as inputs and outputs. The registers are loaded with the inputs on lines 4070-4085 and the results of the interrupt or far call are saved on lines 4095-4109.|
|4071 mov es, 18(bp)|
|4073 mov ax, 20(bp)|
|The o32 prefix converts the instruction mov ax, 20(bp) to mov eax, 20(ebp). 4 bytes (instead of 2 bytes) are transferred.|
4075 mov bx, 24(bp)
4076 .data1 o32
4077 mov cx, 28(bp)
4078 .data1 o32
4079 mov dx, 32(bp)
4080 .data1 o32
4081 mov si, 36(bp)
4082 .data1 o32
4083 mov di, 40(bp)
4084 .data1 o32
4085 mov bp, 44(bp)
|4087 intret: int 0xFF ! Do the interrupt or far call|
|This instruction is replaced by either an interrupt call or a retf instruction (see line 4067).|
4089 .data1 o32 ! Save results
4090 push bp
|4093 mov bp, sp|
|This instruction pushes the processor's flags onto the stack. The processor's flags reflect the status of the processor and include the zero flag (Z), the carry flag (C), overflow (O), sign (S), etc. The flags are saved at memory location 8+8(bp) (lines 4093-4094).|
4096 mov 8+16(bp), ds
4097 mov 8+18(bp), es
4098 .data1 o32
4099 mov 8+20(bp), ax
4100 .data1 o32
4101 mov 8+24(bp), bx
4102 .data1 o32
4103 mov 8+28(bp), cx
4104 .data1 o32
4105 mov 8+32(bp), dx
4106 .data1 o32
4107 mov 8+36(bp), si
4108 .data1 o32
4109 mov 8+40(bp), di
4110 .data1 o32
|4111 pop 8+44(bp) ! ebp|
This is the current stack:
|4113 cli ! Disable interrupts|
|The stack is in flux in real2prot() (see line 4146). As I said earlier, I'm not sure if the interrupts need to be disabled to access the bios clock tick counter. If you know the answer, please submit a comment to the site.|
4115 xor ax, ax
4116 mov ds, ax ! Vector & BIOS data segments
4117 .data1 o32
|4118 mov cx, 0x046C ! Collect lost clock ticks in ecx|
|The lost clock ticks are returned to the minix-386 kernel in ecx. The bios clock tick counter is at address ds:0x046C = 0x0000:0x046C.|
4120 mov ax, ss
|4121 mov ds, ax ! Restore monitor ds|
|p_gdt_desc (line 4246) is accessed on line 4133 in real2prot(). Since p_gdt_desc is not located at ds:p_gdt_desc = 0x0000:p_gdt_desc, ds must be set back to its original value before it was zeroized (see lines 4115-4116).|
|real2prot() (line 4129) switches the system from real mode to protected mode.|
|4124 mov ds, ax|
|Before the code jumps back to the minix-386 kernel, ds is reloaded with the minix-386 kernel's data segment selector.|
4126 retf ! Return to the kernel
|4128 ! Switch from real to protected mode.|
The process of switching from real mode to protected mode after the
system has been powered on actually begins on line 3862. Before continuing,
make sure that you understand lines 3862-3927 (especially lines 3862-3897)
and download Volume
3 of the Intel Architecture Software Developer's Manual. I will
refer to figures in this manual.
There are several remaining things that need to be done before the switch from real mode to protected mode is complete:
1) The A20 address line must be enabled (see lines 4131 and 4170).
2) The global descriptor table must be loaded (see line 4133).
3) Control register cr3 must be loaded - cr3 is the page directory base register (see lines 4134-4136).
4) Control register cr0 must be loaded - cr0 is the machine status register (see lines 4137-4140).
5) A far jump (jmpf) must be made to force cs to use MCS_SELECTOR (line 4141).
6) Segment registers ds, es and ss must be loaded with their new values (lines 4143-4146).
7) The interrupt descriptor table must be loaded. This is done in the kernel.
4130 movb ah, #0x02 ! Code for A20 enable
gate_A20, ah=0x02 enables the A20 address lines. See line 4170.
|4133 lgdt p_gdt_desc ! Global descriptor table|
|At this point, the global descriptor table (gdt) beginning at address p_gdt (line 4242) has been set. The system now needs to know where in memory this global descriptor table is. Note that the operand for lgdt is p_gdt_desc and not p_gdt. The p_gdt_desc (line 4247) entry describes where in memory the gdt is and the gdt's length. See lines 3871-3875. The gdt's length is 8*8-1. Note that lengths in the gdt are 0-indexed.|
|4136 .data1 0x0F,0x22,0xD8 ! mov cr3, eax|
|4137 .data1 0x0F,0x20,0xC0 ! mov eax, cr0|
The o32 prefix converts the instruction mov ax, pdbr
to mov eax, pdbr. 4 bytes are transferred. pdbr
is found on line 4282.
Since the control registers are not accessible by the 8086 instruction set (see line 3019), machine language must be used.
Note that pdbr has not been modified to this point (if the system has just been powered on). Since pdbr is in the bss, its value is 0 (the bss is initialized to 0 on lines 3091-3098). I don't believe that the kernel modifies pdbr.
Look at fig. 2-5 in Volume 3 of the Intel Architecture Software Developer's Manual. When the system is powered on, paging is disabled (bit PG of cr0 is 0). If paging is disabled, cr3 is ignored. (See page 2-16 of the manual for the PCD and the PWT bits.) I'm not sure why we bother setting cr3 to 0, since cr3 is ignored. Previous versions of boothead.s did not modify cr3. If you understand why we zeroize cr3, please submit a comment to the site.
|4140 .data1 0x0F,0x22,0xC0 ! mov cr0, eax|
|msw is set on lines 3866-3869. msw's PE (protection mode enable) bit is set. (See page 2-16 of the manual for the PE bit.) Loading cr0 with msw turns on the system's PE bit.|
|This instruction jumps to the next instruction. This seemingly pointless jump forces the processor to load the code segment selector (MCS_SELECTOR) into cs and begin using the global descriptor table for address resolution.|
|4144 mov ds, ax|
|4145 mov es, ax|
|4146 mov ss, ax|
|ds, es, and ss are set with descriptors from the global descriptor table in anticipation of the jump to the kernel. Note that the value of ss is modified on line 4146. Because the value of ss changes, any code that calls real2prot() must first disable the interrupts (with the cli instruction- see line 3823).|
|4149 ! Switch from protected to real mode.|
|The opposite of what was done for real2prot() (line 4128) is done here. prot2real() is called from ret386 (see line 3945), bios13 (see line 4023), and int86 (see line 4048).|
|4151 lidt p_idt_desc ! Real mode interrupt vectors|
|The real mode interrupt vectors are found in memory locations 0x0000-0x03FF. p_idt_desc (line 4251) describes this memory range. This memory range is not touched by the kernel. The first 2K is removed from the mem array on line 5487 of boot.c. The mem array describes the unused memory ranges that the kernel can use.|
|4152 .data1 0x0F,0x20,0xD8 ! mov eax, cr3|
|This is the counterpart to lines 4134-4136.|
|4155 .data1 0x0F,0x20,0xC0 ! mov eax, cr0|
|4158 .data1 0x0F,0x22,0xC0 ! mov cr0, eax|
|This is the counterpart to lines 4137-4140.|
|4159 jmpf cs_real, 0xDEAD ! Reload cs register|
|This is the counterpart to line 4141. 0xDEAD is only a place holder. The value of cs before the monitor jumps to the minix-386 kernel is patched in on line 3864.|
|4161 mov ax, #0xBEEF|
|4163 mov ds, ax ! Reload data segment registers|
|4164 4164 mov es, ax|
|4165 mov ss, ax|
|0xBEEF is also a place holder. The value of ds before the monitor jumps to the minix-386 kernel is patched in on line 3865. This value is used to set ds, es, and ss. Note that the value of ss is modified on line 4165. Because the value of ss changes, any code that calls prot2real() must first disable the interrupts (with the cli instruction). The kernel disables the interrupts before jumping to ret386, bios13, and int86.|
4167 xorb ah, ah ! Code for A20 disable
4168 !jmp gate_A20
|4170 ! Enable (ah = 0x02) or disable (ah = 0x00) the A20 address line.|
Real mode can directly access 1MB of memory (memory above 1MB can be
accessed indirectly with the int 0x15 bios call) through address
lines 0-19. Protected mode (on the 386 and up) can access 4GB through
address lines 0-31.
I could not find a clear explanation of the A20 address line. This is only speculation but I believe that enabling the A20 address line doesn't enable only address line 20 but enables address lines 20-31. This would allow the direct access of 4GB of memory. If you know the function of the A20 address line, please submit a comment to the site.
Enabling and disabling the A20 address line is tricky. The process is simplified on the proprietary MCA bus of the IBM PS/2.
|bus (line 4284) is set in get_bus() (line 3709). bus is 0 for XT, 1 for AT, and 2 for MCA.|
|4173 je gate_PS_A20|
|kb_wait (line 4186) doesn't return until the keyboard input buffer isn't full.|
|4175 movb al, #0xD1 ! Tell keyboard that a command is coming|
The next sequence is tricky. This sequence is for systems that
have XT and AT buses. This link
may help you understand the sequence a little better.
There are two controllers for the keyboard, one on the motherboard and one inside the keyboard itself. Data written to port 0x60 normally goes to the controller on the keyboard. However, if the outb 0x60 instruction is preceded by writing 0xD1 to port 0x64, the data written to port 0x60 goes to the controller on the motherboard. The controller on the motherboard is responsible for enabling and disabling the A20 address line.
|4176 outb 0x64|