上一节的最后jmpi把cs:ip设置为0x9020:0000。于是CPU开始执行setup,它的作用是获取机器系统数据至内存,关中断并挪动system,为32位模式转换做准备。
加载系统信息至内存
同样是调用BISO中断,寄存器作为入参和返回值,分别取得内存信息、显卡显示模式、显示参数、第一第二块的硬盘信息等并置于内存中。详细参见: flash-linux0.11-talk/setup.s at main · dibingfa/flash-linux0.11-talk · GitHub
; Get memory size (extended mem, kB)
mov ah,#0x88
int 0x15
mov [2],ax
; Get video-card data:
mov ah,#0x0f
int 0x10
mov [4],bx ; bh = display page
mov [6],ax ; al = video mode, ah = window width
; check for EGA/VGA and some config parameters
...
; Get hd0 data
...
; Get hd1 data
...
最后存储在内存中的信息及其位置如下,其覆盖了bootsect的大部分区域(0x9000~0x901FD,只有2字节未被覆盖),bootsect使命已完成:
关中断并挪动system
为了将第一节中的BIOS中断替换为操作系统的中断,以让后续main函数适应保护模式下的中断。在此之前不对任何中断进行相应,将标志寄存器(EFLAGS,32bit,含状态/控制/系统标志)中的IF置0。
接着通过ds:si和es:di指定源地址和目的地址,将位于0x10000直到0x90000的内容复制到0x00000处。显然本次内存复制有3个意义:
1)将位于0x10000的system挪到了最开始的0地址。
2)由于第一节所知BIOS的中断向量表也在0地址,故BIOS的中断向量也被擦出了。
3)将位于0x7c00的boostsec、位于0x1000的system所占有的内存现在均已不需要(bootsec已执行完,system搬到0地址),等于空间被回收利用了。
洗牌后内存是这个样子:
为32位模式建立全局描述符表和中断描述符表
实模式下,ds存的是段基址(16位),段基址×16+偏移地址=物理地址(20位)
保护模式下,ds存的是段描述符索引/段选择子。该索引指示出在全局描述符表gdt中,我们所用的段描述符(64位)是哪一项。该段描述符里面存着段地址。
段地址+偏移地址=线性地址,线性地址经分页转换后是物理地址。
cpu是如何知道gdt存储位置的呢?32位cpu为了兼容16位系统,同时有16/32/48位寄存器。
lgdt gdt_48 ;lgdt表示把后面的值放在gdtr寄存器
gdtr就是个48位cpu寄存器,存储了个gdt_48标签,该标签处内容的高32位是gdt的真正的内存地址,低12位是gdt的容量。
gdt_48:
.word 0x800 ; 每个gdt项占8byte, 一共256个gdt, 总量2048byte
.word 512+gdt,0x9 ; gdt base = 0X90200+gdt
......
gdt:
.word 0,0,0,0 ; .word 16bit一组, 多个.word表示由低到高排列
.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
上述代码里的gdt标签处内容便是全局描述符在内存中的真正数据: 现在咱们程序都属于setup,setup的位置是0x90200,所以0X90200+gdt就是实际数据的内存位置。实际上gdt可以存放在内存任何位置,因此通过这种方法来让cpu进行定位。
此外能看出目前全局描述符表有三个段描述符,第一个为空,第二个是代码段描述符,第三个是数据段描述符。由于第二/第三个段描述符的段基址都是 0,因此在保护模式下,段选择子查找到无论是代码段还是数据段,取出的段基址都是 0,那么线性将直接等于程序员给出的逻辑地址(准确说是逻辑地址中的偏移地址)。
中断描述符表也与此类似。只不过现在已关中断无需中断服务程序,于是基地址是0,中断描述符表也是空的:
lidt idt_48 ; load idt with 0,0
...
idt_48:
.word 0 ; idt limit=0
.word 0,0 ; idt base=0L
最后给一张gdtr和idtr的示意图:
为32位模式打开A20地址线
最大寻址空间为4GB需要32根地址线,而原本16位CPU支持的最大1MB空间需要20根地址线,分别对应0~19号地址线。现在32位CPU即便有32根地址线,为了兼容老的20根地址线的模式,于是设计成只能使用其中的20位,因此第21位(20号地址线)需要被打开:
mov al,#0xD1 ; command write
out #0x64,al ; CPU对外设的操作通过端口读写指令来完成;读端口IN,写端口OUT。
mov al,#0xDF ; A20 on
out #0x60,al
这段代码就向i8042发送命令:先把命令发送到0x64端口,然后紧接着把这个命令的参数发送到0x60端口。然后我们可以通过读0x60端口,获得i8042返回给我们的数据。这样,就通过i8042控制芯片的输出引脚和A20构成的一个与门电路,缺省A20是on,再写入一个on等于打开了A20。
为保护模式对中断控制器重编程
因为中断号是不能冲突的, 在保护模式下Intel 把 0 到 0x1F 号中断都作为保留中断,比如 0 号中断就规定为除零异常,软件自定义的中断都理应放在这之后。但是 IBM 在原 PC 机中搞砸了,跟保留中断号发生了冲突,以后也没有纠正过来。所以,我们不得不重新对其进行编程。代码就省略了。反正重新编程之后,8259 这个芯片的引脚与中断号的对应关系如下:
进入保护模式
将cr0这个寄存器的位 0 置 1,模式就从实模式切换到保护模式了。
mov ax,#0x0001 ; protected mode (PE) bit
lmsw ax ; Load Machine Status Word,但只有操作数的低4位(PE,MP,EM,TS)被存入CR0,其他位不受影响。
jmpi 0,8 ; jmp offset 0 of segment 8 (cs)
jmpi则将cs:ip设置为8:0,注意现在已是保护模式,那么cs内容便是段选择子:
于是8(00000,0000,0000,1000)意味着描述符索引值是 1,也就是cpu要去全局描述符表中找索引 1 的描述符。回顾上面提到的gdt表,第1项为代码段描述符,他的段基址是 0。所以,这里cs:ip 为8:0的查表的结果即位:段基址0+偏移地址0=线性地址0 。由于我们还未分页,线性地址即是物理地址,于是最终跳转到内存的0地址处,开始执行。
现在setup终于执行完毕了。