INITSEG = 0x9000 ! we move boot here - out of the way
SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
SETUPSEG = 0x9020 ! this is the current segment
start:
mov ax,#INITSEG
mov ds,ax
mov ah,#0x03
xor bh,bh
int 0x10 !
mov [0],dx !
mov ah,#0x88
int 0x15
mov [2],ax !
mov ah,#0x0f
int 0x10
mov [4],bx !
mov [6],ax !
mov ah,#0x12
mov bl,#0x10
int 0x10
mov [8],ax
mov [10],bx
mov [12],cx
上面的代码主要对硬件进行检测,为了保证所有的段寄存器都指向正确的内存地址,代码起始位置首先重新设置一下段寄存器。由于bootsect已经不再有使用价值,所以可以重新覆盖用于保存检测到的一些硬件信息。首先利用int 0x10得到当前光标的信息,并将相应的信息存放到INITSEG段的起始位置。http://www.ctyme.com/intr/rb-0088.htm
int 0x15用于获取系统的扩展内存大小,也就是绝对地址超过1M的内存的大小。大小按照KB为单位保存在AX中,也就是扩展内存最大位64MB。http://www.ctyme.com/intr/rb-1529.htm
int 0x10用于返回当前显示模式,AX返回显示的显示的列数墓,BX则是当前显示的活动页。http://www.ctyme.com/intr/rb-0108.htm。接下来的int 0x10则用于返回显示内存,以后的显示打印可以通过这一部分内存进行直接的显示。
mov ax,#0x0000
mov ds,ax
lds si,[4*0x41]
mov ax,#INITSEG
mov es,ax
mov di,#0x0080
mov cx,#0x10
rep
movsb
mov ax,#0x0000
mov ds,ax
lds si,[4*0x46]
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
rep
movsb
mov ax,#0x01500
mov dl,#0x81
int 0x13
jc no_disk1
cmp ah,#3
je is_disk1
no_disk1:
mov ax,#INITSEG
mov es,ax
mov di,#0x0090
mov cx,#0x10
mov ax,#0x00
rep
stosb
is_disk1:
cli ! no interrupts allowed !
mov ax,#0x0000
cld ! 'direction'=0, movs moves forward
do_move:
mov es,ax ! destination segment
add ax,#0x1000
cmp ax,#0x9000
jz end_move
mov ds,ax ! source segment
sub di,di
sub si,si
mov cx,#0x8000
rep
movsw
jmp do_move
end_move:
代码开始首先测试是否存在硬盘,两个硬盘参数存放在中断向量0x41和0x46中。因为英特尔体系架构下总共可以有256个中断。其中系统保留使用的有0x0-0x31则用于保留给异常使用,而剩下的0x32-0x47则用于系统中的可编程中断,剩下的可以由用户任意分配。因此这里的0x41和0x46号中断向量存放着找到硬盘信息的地址应该是一个默认的惯例。不过这也是因为Linux中的MBR部分不规范所导致的,按照规范的可引导MBR中,应该存放BPB参数表,参数表中包含相应的磁盘信息。但是这样在安装的时候需要收集磁盘的相关信息。int 0x13用于检测磁盘的格式(这里的第二个磁盘是否是除软盘外的第二个磁盘),如果第二个盘不存在则会将它的磁盘格式表进行清零操作。在检测得到所有的硬件信息后,将system中复制到0x0000段位置处。
end_move:
mov ax,#SETUPSEG ! right, forgot this at first. didn't work :-)
mov ds,ax
lidt idt_48 ! load idt with 0,0
lgdt gdt_48 ! load gdt with whatever appropriate
call empty_8042
mov al,#0xD1 ! command write
out #0x64,al
call empty_8042
mov al,#0xDF ! A20 on
out #0x60,al
call empty_8042
mov al,#0x11 ! initialization sequence
out #0x20,al ! send it to 8259A-1
.word 0x00eb,0x00eb ! jmp $+2, jmp $+2
out #0xA0,al ! and to 8259A-2
.word 0x00eb,0x00eb
mov al,#0x20 ! start of hardware int's (0x20)
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x28 ! start of hardware int's 2 (0x28)
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x04 ! 8259-1 is master
out #0x21,al
.word 0x00eb,0x00eb
mov al,#0x02 ! 8259-2 is slave
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0x01 ! 8086 mode for both
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
.word 0x00eb,0x00eb
mov al,#0xFF ! mask off all interrupts for now
out #0x21,al
.word 0x00eb,0x00eb
out #0xA1,al
mov ax,#0x0001 ! protected mode (PE) bit
lmsw ax ! This is it!
jmpi 0,8 ! jmp offset 0 of segment 8 (cs)
empty_8042:
.word 0x00eb,0x00eb
in al,#0x64 ! 8042 status port
test al,#2 ! is input buffer full?
jnz empty_8042 ! yes - loop
ret
gdt:
对8042的设置可以防止系统内存的回卷,为了兼容以前的系统,在后续的英特尔架构下同样可以访问1M以及1M以上的内存。然而,这就不好区分是在实模式下访问1M以上内存还是在保护模式下访问1M以上内存。因此需要设置一个标志,防止系统在保护模式下访问1M以上内存出现回卷。这个标志就是设置键盘中的8042中控制寄存器的第二位为1。至于8042的操作就不具体介绍了,需要注意的是在进行这些处理时需要在关中断情况下进行处理。还有另外一个地方的0x00eb,起始是一个指令码。这个指令码表示向前跳转两个字节,主要用于同步。为了初始化8259中断控制器,总共发出了四个ICW命令控制字,除了ICW3之外用于区分8259的主从之分外,其他的都是一样的。最后将两个8259中的中断给屏蔽掉。然后跳转到32位代码处开始执行。在英特尔架构下CR0是一个控制寄存器,用于控制当前处理器的状态,比较重要的有两个位,一个是PE开启保护模式,另一个是PG开启页式内存管理。
gdt:
.word 0,0,0,0 ! dummy
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9A00 ! code read/exec
.word 0x00C0 ! granularity=4096, 386
.word 0x07FF ! 8Mb - limit=2047 (2048*4096=8Mb)
.word 0x0000 ! base address=0
.word 0x9200 ! data read/write
.word 0x00C0 ! granularity=4096, 386
idt_48:
.word 0 ! idt limit=0
.word 0,0 ! idt base=0L
gdt_48:
.word 0x800 ! gdt limit=2048, 256 GDT entries
.word 512+gdt,0x9 ! gdt base = 0X9xxxx
进入到下一个话题之前,首先需要对上面这些结构进行分析。需要注意的第一点是英特尔是小端模式,因此高位在内存的高位地址。而GDTR中包含48位数据,32位为基地址找到对应的全局段寄存器,而16位作为GDTR的上限;IDTR也是类似的。因此GDTR给定的基地址是0x90512+gdt(也就是上面定义的gdt表在内存中的位置)。由于此时中断被禁止所以是一个无用的数值。gdt中第一个表项被设置为全部为0,是英特尔默认这一项无用。
左图是每一个段的表项的每一个位的解释,整个结构体单元被划分的稀烂。重点看三个,基地址,上限以及DPL。gdt的第二项的基地址是0x0,而上限时0x07ff;dpl则为0。而这正好和之前的长跳转相符,因此下一步的system代码将会在段基地址位0x0,上限为0x07ff处运行,正好和复制时0x8000一致。另外,需要注意的一点是第三个gdt表项,这个表项在接下来的处理中有着隐含的含义。另外需要注意的是,在jmpi指令之前,只需要直接按照原来的存取方式进行读取就可以了。