|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
boothead.s |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
| These are the same values that were passed into bootblock.s. | ||
|
|
| 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. | ||
|
|
|
|
|
|
| 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. | ||
|
|
| ||
|
|
|
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. | ||
|
|
|
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
concepts involved.
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(). | ||
|
|
| If the K_I386 flag is set for the kernel, this code must switch to protected mode. | ||
|
|
| 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 |
|
|
| 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. | ||
|
3034 |
|
|
|
|
|
|
|
|
|
|
|
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. | ||
|
3040 |
|
|
| 0x1B is the ascii representation of ESC. | ||
|
3042 |
|
|
| 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. | ||
|
|
| _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
3049 3050 .text 3051 begtext: |
|
|
| These functions are defined in boot.c. boot is called on line 3180. | ||
| 3053 |
|
|
|
|
|
|
|
|
| ||
|
3058
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[]. | ||
|
|
|
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. | ||
|
3065
3066 movb al, a_flags |
|
|
| 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. | ||
|
|
| This instruction zeroes the ax register (any number xor'ed
with itself is zero). This is a pretty common practice. The
instruction
mov ax, #0 is slower and is 3 bytes compared with xor's 2 bytes. | ||
|
3070
xchg ax, a_text
! No text
|
|
|
| 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 |
|
|
|
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
sp
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. | ||
|
3077
mov sp, ax
! Set sp at the top of all that
3078 |
|
|
|
|
|
|
|
|
|
|
| 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). | ||
|
3085
mov ss, ax
3086 sti ! Stack ok now |
|
|
| This value is popped into the upper 2 bytes of _rem_part on line 3105. | ||
|
3088
mov es, ax
3089 cld ! C compiler wants UP 3090 3091 ! Clear bss 3092 xor ax, ax ! Zero |
|
|
|
|
| _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. | ||
|
3095
sub cx, di
! Number of bss bytes
|
|
|
|
|
| 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). | ||
|
|
|
|
| Since _device and _rem_part are uninitialized global variables, they are stored in the bss. | ||
|
3102
xorb dh, dh
|
|
|
| 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. |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
3110
andb al, #0x7F !
Mask off bit 7 (no blanking)
3111 movb old_vid_mode, al 3112 movb cur_vid_mode, al 3113 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). | ||
|
3119
mov _caddr+0,
ax
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. | ||
|
|
|
|
|
|
| The memory (base, size) pairs will look something like this:
mem[0]=(0x00000000, size of low memory) mem[1]=(0x00100000, size of memory between 1M and 16M) mem[2]=(0x01000000, size of memory greater than 16M) If the mem[1] and mem[2] memory areas are continugous, then mem[1] and mem[2] 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[1] and mem[2] 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.
| ||
|
3140
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. | ||
|
3143 mov
4(di), ax ! mem[0].size = low memory
size in bytes
3144 mov 6(di), dx |
|
|
| 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 feedback@swartzbaugh.net or submit a comment to the site which will be displayed below.. | |||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||
|
3146
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) 3150 big_ext: 3151 mov ax, #0xE801 ! Code for get memory size for >64M |
|
|
| int 0x15 sets the carry flag if the value in ax (or ah) is not a supported input. | ||
|
3153
jnc got_ext
3154 small_ext: 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 3162 got_ext: 3163 mov cx, ax ! cx = copy of ext mem at 1M 3164 mov 10(di), #0x0010 ! mem[1].base = 0x00100000 (1M) 3165 mul c1024 3166 mov 12(di), ax ! mem[1].size = "ext mem at 1M" * 1024 3167 mov 14(di), dx |
|
|
| 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? |
|
|
|
|
| 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[1] by jumping to adj_ext. | ||
| 3172 mov 18(di), #0x0100 ! mem[2].base = 0x01000000 (16M) |
| 3173 mov 22(di), bx ! mem[2].size = "ext mem at 16M" * 64K |
| 3174 jmp no_ext |
| 3175 adj_ext: |
| 3176 add 14(di), bx ! Add ext mem above 16M to mem below 16M |
| 3177 no_ext: |
| 3178 |
|
|
| Now that we've taken care of a little housekeeping, we can call boot. | ||
| 3180 call _boot |
|
|
| 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. | ||
|
|
|
|
|
|
|
|
| _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. |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
|
|
|
|
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 |
|
|
|
|
|
|
| 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 |
|
|
| 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. | ||
|
3199 any_key:
|
|
|
|
|
|
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
3206 _mon2abs: 3207 mov bx, sp 3208 mov ax, 2(bx) ! ptr 3209 mov dx, ds ! Monitor data segment 3210 jmp seg2abs 3211 |
|
|
|
|
| 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
|
|
|
| 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. | ||
|
3223
push cx
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 3233 ret 3234 |
|
|
| 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. | ||
|
3236
push cx
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 3245 ret 3246 |
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
| 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.) | ||
|
3273
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 |
|
|
|
|
| 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 |
|
|
|
|
|
|
|
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. | ||
|
3286
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. | ||
|
3291
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 |
|
|
| 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). | ||
|
3302
pop cx
! 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. | ||
|
3311
pop di
3312 pop si ! Restore C variable registers 3313 pop bp 3314 ret 3315 |
|
|
|
|
|
|
| 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
3320 _get_word: 3321 mov bx, sp 3322 call gp_getaddr 3323 mov ax, (bx) ! Word to get from addr 3324 jmp gp_ret 3325 _put_word: 3326 mov bx, sp |
|
|
|
|
|
|
| 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. | ||
|
3332
mov ax, 2(bx)
3333 mov dx, 4(bx) 3334 call abs2seg 3335 mov bx, ax 3336 mov ds, dx ! ds:bx = addr 3337 ret 3338 gp_ret: 3339 push es |
|
|
| The value of ds was changed on line 3336. es and ds (before line 3336) were the same value. | ||
|
3341
ret
3342 |
|
|
|
|
|
|
|
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
3347 _relocate: 3348 pop bx ! Return address 3349 mov ax, _caddr+0 3350 mov dx, _caddr+2 3351 call abs2seg |
|
|
|
|
|
|
|
|
|
|
| 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. | ||
|
|
|
|
|
|
| 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. | ||
|
3360
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 3367 |
|
|
|
|
|
|
|
|
|
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
malloc().
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. |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
3372 .data
3373 .align 2 |
|
|
| 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. | ||
|
3375 .text
3376 .define _brk, __brk, _sbrk, __sbrk 3377 _brk: 3378 __brk: ! __brk is for the standard C compiler 3379 xor ax, ax 3380 jmp sbrk ! break= 0; return sbrk(addr); 3381 _sbrk: 3382 __sbrk: 3383 mov ax, break ! ax= current break 3384 sbrk: push ax ! save it as future return value |
|
|
|
|
|
|
| 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. | ||
|
|
| 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. | ||
|
|
| 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. | ||
|
3390
jb heaperr
! 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)
3400 ret 3401 heaperr:mov ax, #chmem 3402 push ax 3403 mov ax, #nomem 3404 push ax 3405 call _printk 3406 jmp quit 3407 .data |
|
|
|
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" 3411 .text 3412 |
|
|
|
|
|
|
|
|
![]() 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
3418 _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 3424 floppy: 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 |
|
|
|
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 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 unsuccessful.
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? |
|
|
| 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 |
|
|
| 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. | ||
|
3437
jz geoerr
3438 xorb ah, ah ! Reset drive |
|
|
| int 0x13, ah=0x00 resets the drive. | ||
|
3440
jc geoerr
3441 jmp finit0 ! Retry once more, it may need to spin up 3442 finit0ok: |
| 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 |
|
|
|
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 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 unsuccessful.
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. | ||
|
3454
jnc ftestok
! 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 3460 ftestok: |
|
|
| In other words, floppies have two heads. | ||
|
3462
jmp geoboth
3463 winchester: 3464 movb ah, #0x08 ! Code for drive parameters |
|
|
| 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. | ||
|
3466
jc geoerr
! 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. | ||
|
3470
movb sectors, cl
! Sectors per track
3471 movb al, cl ! al = sectors per track |
|
|
| mulb multiples al by the operand (in this case dh) and places the result in ax. | ||
|
3473
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. | ||
|
3476
pop di
3477 pop es ! Restore di and es registers 3478 ret 3479 geoerr: movb al, ah ! ax = BIOS error code 3480 xorb ah, ah 3481 jmp geodone 3482 .data 3483 seclist: 3484 .data1 18, 15, 9 ! 1.44M, 1.2M, and 360K/720K floppy sec/track 3485 .text 3486 3487 ! int dev_close(void); 3488 ! Close the current device. Under the BIOS this does nothing. 3489 .define _dev_close 3490 _dev_close: 3491 xor ax, ax 3492 ret 3493 |
|
|
|
|
|
|
|
|
|
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. | ||
|
3498
mov bx, sp
3499 xor dx, dx |
|
|
|
Example 1:
ax=0x0151, dx=0x0000
Example 2:
| ||
|
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.
Look on line 4279 for sectors. Example 1:
Example 2:
| ||
|
|
|
Example 1:
ax=0xFF00 dx=0x0001 Example 2:
| ||
|
Example 1:
ax=0x0AA5 dx=0x0010 Example 2:
| ||
|
|
|
Example 1:
dx=0x000F carry flag (CF) =0 Example 2:
| ||
|
|
|
sbb ax, ax is the same as ax=ax-ax-CF=-CF
Example 1:
Example 2:
| ||
|
|
|
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. Example 1:
Example 2:
| ||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
into bufaddr.
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
3517 _writesectors: 3518 push bp 3519 mov bp, sp |
|
|
| int 0x13, ah=0x03 writes sectors from memory to disk. | ||
|
3521
jmp rwsec
3522 _readsectors: 3523 push bp 3524 mov bp, sp |
|
|
| 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). | ||
|
3531
mov bx, ax
3532 mov es, dx ! es:bx = bufaddr |
|
|
| 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. | ||
|
3535
jb nohd
3536 mov di, #1 ! But only 1 reset on hard disk error 3537 nohd: cmpb 12(bp), #0 ! count equals zero? 3538 jz done |
|
|
|
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. | ||
|
3540
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. | ||
|
3545
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 |
|
|
| ah contains the first sector that is copied to/from (see line 3544). | ||
|
3555
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 |
|
|
| 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 |
|
|
| 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 |
|
|
|
Lines 3564-3568 update the addresses (memory and hard drive sector) and count. | ||
|
3565
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 |
|
|
| 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. | ||
|
3573
je finish
3574 cmpb ah, #0x03 ! Disk write protected? 3575 je finish |
|
|
|
|
| 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. | ||
|
3578
xorb ah, ah
! Code for a reset (0)
3579 int 0x13 3580 jnc more ! Succesful reset, try request again |
|
|
| At this point, ah contains either an error code or 0x00 if there was no error. | ||
| 3582 xorb ah, ah ! ax = error number |
|
|
|
|
|
|
| bp, di and es were saved by pushing them on the stack (lines 3518, 3526 and 3527). They must now be popped back. | ||
|
3586
ret
3587 |
|
|
|
|
|
|
|
This function is called in a couple of places in boot.c.
If the timer expires, getch() returns the ascii code for
ESC
(the escape key) in ax. If an escape key is pressed, the
escape
flag (line 4283) is set and the ascii code for ESC is returned
in ax.
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
3592 _getch: 3593 movb ah, #0x01 ! Keyboard status |
|
|
| 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. | ||
|
3597
test ax, ax
3598 jz _getch |
|
|
| ESC is a macro #defined on line 3041. | ||
|
3600
ret
3601 press: 3602 xorb ah, ah ! Read character from keyboard |
|
|
| 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. | ||
|
|
| 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 |
|
|
| 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. | ||
|
|
| 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. | ||
|
3611
ret
3612 |
|
|
|
|
| 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
3616 _escape: 3617 movb ah, #0x01 ! Keyboard status 3612 |
|
|
| 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). | ||
|
3619
jz escflg
! Keypress?
3620 cmpb al, #ESC ! Escape typed? 3621 jne escflg 3622 xorb ah, ah ! Discard the escape |
|
|
| 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). | ||
|
3624
inc escape
! Set flag
3625 escflg: xor ax, ax 3626 xchg ax, escape ! Escape typed flag 3627 ret 3628 |
|
|
|
|
|
|
|
|
|
|
|
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
3635 _putch: 3636 _putk: mov bx, sp 3637 movb al, 2(bx) ! al = character to be printed |
|
|
|
|
| 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. | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
3640
cmpb al, #0x0A !
al = newline?
3641 jnz putc 3642 movb al, #0x0D |
|
|
| 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
mode 3646 mov bx, #0x0001 ! Page 0, foreground color |
|
|
| 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
3649 |
|
|
|
|
|
|
|
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
3654 _set_mode: 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 3660 _clear_screen: 3661 mov ax, cur_vid_mode |