主要讲解了80x86cpu在启动的时候时bios如何工作,以及如何最终转换到保护模式。
1.1 启动bios
80x86作为冯诺依曼结构下的cpu,工作模式也是取指执行,即cpu根据cs:ip寄存器的值‘计算’出一个真正的物理地址,在8086实模式的情况下这个地址是20位的,然后通过20位的地址线去寻址。20位的地址线对应1MB可寻址空间,这里特意使用了可寻址空间而不是我一直以为的内存空间,言下之意1MB的寻址空间包括内存(RAM)也包括一些其他的存储介质,而bios就放在这些其他的存储介质上,具体来说就是主板上的ROM里,ROM里的代码是厂家生产CPU时候就烧进去的,固定的。
现在回到计算机启动的问题上,计算机在加电的时候,没有操作系统,内存里空空如也,谁来设置第一条指令的地址?第一条指令的设定是通过硬件来实现的,为了解决CPU启动的问题Intel工程师把所有80x86系列的cpu设置为在启动的时候工作在实模式,然后cs寄存器的值设置为0xf000,IP寄存器的值设置为0xfff0,这样第一条指令的位置就是0xFFFF0。cpu从这条指令开始,开始了它循环往复的取值执行的一生,直至断电。
这张表是实模式下1MB的内存布局,从0xF0000到0XFFFFF都是rom的空间,开机时候跳转到的地址为0xFFFF0,CPU要从这个地址开始执行指令,然后自增ip取下一条指令。根据这个表可以看出来从0xFFFF0开始到地址空间的末尾只有16位的内容,显然是不够写BIOS的代码的,所以这里的代码是一个无条件跳转指令,跳转到ROM里的另一个地方即0xF000:E05B处,从这里开始才是真正的BIOS开始的地方。
1.2 BIOS干了什么
在召唤出操作系统之前的所有对硬件的管理工作都是由BIOS完成的。
1.2.1 自检、设置中断向量表
首先对硬件进行自检,包括内存、显卡等外设信息,然后开始在0x00000的位置开始用1KB的存储空间建立中断向量表,然后在紧挨着中断向量表后面用256个字节建立BIOS的数据区,然后再接着后面是与中断向量表相关的中断服务程序。结合上面实模式下的内存布局,可以看到这里的内容都是建立在内存里(RAM)而不是ROM里。
中断向量表的空间大小是1KB,所谓中断向量就是一个指向中断服务例程的地址,CS:IP,CSIP都是两个字节,加一起是四个字节,所以一共有256个中断向量,操作系统从指定的存储设备加载到内存里就离不开中断向量表。
1.2.1 真正的加载开始了!
开始了真正的从存储设备上加载操作系统,以从软盘上加载操作系统为例,当然启动顺序要在BIOS里设置好启动顺序。在BIOS完成自检和中断向量表的设置之后,CPU会收到一个int 0x19中断,既然是中断就得有人给cpu发出中断,我猜测这个中断就是BIOS程序发出的。不管如何,CPU在收到这个中断的时候回查中断向量表找到中断向量进一步找到中断服务程序的地址,然后CPU开始执行int 0x19的中断响应程序,该程序的功能是找到软盘的第一扇区并加载到内存的指定的位置。从这里可以看到,这段的执行过程和操作系统无关,无论软盘里放的什么操作系统,到CPU都是简单的把他加载进来,CPU做的就是找到第一扇区,然后读进来。
第一扇区的内容被称为启动扇区(bootsector),它被读到了 0X07C00处,这个扇区的内容就被称为bootsect,到了这里才有了Linux内核的代码,bootsect的内容是和操作系统相关的,不同的操作系统bootsect不一样,bootsect负责后续的linux内核代码的加载。
bootsect在加载后续代码之前,做的第一件事情是对整个内存区域做一个划分,防止后续的内容相互覆盖,然后把bootsect的内容从0x7C000处复制到0x90000处。
然后bootsect开始从软盘里读取后续的操作系统的代码。这次读取代码是通过int 0x13中断完成,该中断的相应例程也是从软盘上读取数据到内存里,相较于读取bootsect的int0x19,int 0x13从软盘读取数据的位置和读到内存的位置是可以设置的,这个设置的动作就是bootsect完成的。总之通过int0x13中断bootsect召唤了setup,setup的在内存的位置是有bootsect设计好的。在加载完setup之后同样适用int0x13中断从软盘上加载system,system在内存的位置同样是设计好的。至此bootsect的历史使命基本完成了,在他离开之前还要完成最后一个任务即确定根设备号,剩下的操作是由setup和system完成。
setup做的第一件事情就是加载机器系统数据,这一加载过程也是通过中断完成的,一如加载bootsect setup system。
1.3 走向保护模式
1.3.1 什么是保护模式
保护模式保护的是内存,在实模式下所有内存空间对所有进程都是敞开怀抱的,操作系统和用户进程属于同一特权级别。因为实模式下访问地址的方式是段基址加上偏移地址,段基址和偏移地址可以所以修改。保护模式是80286以上的CPU才有的概念,8086只有16位的所谓的实模式,实际上只有32位的CPU才区分保护模式和实模式。
32位的CPU里的寄存器在实模式下都是16位的,到了32位的保护模式会对寄存器进行一定的扩展,除了段寄存器之外的寄存器都被扩展到了32位。
保护模式下寻址还是段基址加偏移地址的模式,和实模式和保护模式最大的区别是对段基址的“翻译”不同,不是简单的左移4位和偏移地址相加,而是经过一个相对复杂的映射程序然后再和偏移地址相加。在这种复杂的映射模式下,段寄存器保存的16为的数据不再被称为段基址,因为他不是“内存里一个段的基址”,而被称为段选择符。
1.3.2 GDT LDT 与寻址
实模式下段寄存器里的16位的数据不再是段基址,它可以分为三个部分如下所示。根据TI标志位选择使用GDT还是LDT,然后从寄存器里找到GDT或者LDT的地址,用3~15位的数据在该表里找到段描述符,段描述符的地位类似于实模式下的段基址,段描述符里保存段的起始地址。
段描述符是8个字节,存储的内容不仅仅是段的起始地址,也包括该段基址对应段的控制信息,例如是否可写,代码段还是数据段。至此16位的段寄存器里的内容被翻译成32位的段基址并获得了相应的控制信息,这个复杂的翻译过程就是实模式下的段基址左移4位的过程。
1.3.3 打开保护模式
首先关中断,然后把system内容复制到内存的起始位置,之前内存的起始位置放的是BIOS的中断表,被system覆盖后将无法响应中断,所以这里要关中断。所谓关中断就是执行cli指令,cli指令把CPU的中断允许标志位置0,是一个硬件层面的操作。
然后初始化GDTR和IDTR,前者是用于保护模式下寻址,后者是保护模式下中断服务表。此时内存里的IDT是一张空表因为现在CPU屏蔽了中断也不会响应任何中断。
接着打开A20标志位,允许cpu用32位的地址线寻址。
对IDT表初始化,这样CPU有了再保护模式下响应中断的能力,最后打开CPU相应的标志位开启CPU的保护模式。