1. 这里的代码有 linux/boot下的 bootsect.s ; head.s ; setup.s三个文件,想要研读代码,需要具备的知识如下:
8086 汇编语言 ;Intel 80X86 微处理器 的PC 机的体系结构以及80386 32 位保护模式下的编程原理
2. Linux 操作系统启动部分的主要执行流程
当PC 的电源打开后,80x86 结构的CPU 将自动进入实模式,并从地址0xFFFF0 开始自动执行程序代码,这个地址通常是ROM-BIOS 中的地址。PC 机的BIOS 将执行某些系统的检测,并在物理地址0 处开始初始化中断向量。此后,它将可启动设备的第一个扇区(磁盘引导扇区,512 字节)读入内存绝对地址0x7C00 处,并跳转到这个地方运行代码。启动设备通常是软驱或是硬盘。
3. 启动引导时内核在内存中的位置和移动后的位置情况(我看的书是1.2.2版,书上此图有误,在此修正)
1) Linux 的最最前面部分是用8086 汇编语言编写的(boot/bootsect.s),它将由BIOS 读入到内存绝对地址0x7C00(31KB)处。此时bootsect.s开始执行。
2) bootsect.s执行时就会先把自己移到绝对地址0x90000(576KB)处。
3) bootsect.s把启动设备中后2kB 字节代码(boot/setup.s)读入到内存0x90200 处。然后利用BIOS 中断0x13 取磁盘参数表中当前启动引导盘的参数,接着在屏幕上显示“Loading system...”字符串。然后将内核的其它部分(system 模块)读入到从地址0x10000开始处(因为当时system 模块的长度不会超过0x80000 字节大小(即512KB),所以它不会覆盖在0x90000处开始的bootsect 和setup 模块)。随后确定根文件系统的设备号,若没有指定,则根据所保存的引导盘的每磁道扇区数判别出盘的类型和种类(是1.44M A 盘?)并保存其设备号于root_dev(引导块的0x508 地址处),最后长跳转到setup 程序的开始处(0x90200)执行setup 程序。
4)setup.s开始执行。它先利用ROM BIOS 中断读取机器系统数据(这些参数将被内核中相关程序使用,例如字符设备驱动程序集中的ttyio.c 程序等),并将这些数据保存到0x90000 开始的位置(覆盖掉了bootsect 程序所在的地方)。
5) setup.s 将system模块从0x10000-0x8fff(当时认为内核系统模块system的长度不会超过此值:512KB)整块向下移动到内存绝对地址0x00000 处(这样system 模块中代码的地址也即等于实际的物理地址,便于对内核代码和数据的操作。)。接下来加载中断描述符表寄存器(idtr)和全局描述符表寄存器(gdtr)(为了能让head.s 在32 位保护模式下运行),开启A20 地址线,重新设置两个中断控制芯片8259A,将硬件中断号重新设置为0x20 - 0x2f。最后设置CPU 的控制寄存器CR0(也称机器状态字),从而进入32 位保护模式运行(在保护模式下运行cpu可以支持多任务;支持4G 的物理内存;支持虚拟内存;支持内存的页式管理和段式管理;支持特权级。),并跳转到位于system模块最前面部分的head.s 程序继续运行。此时内存结构如下图:
此时临时全局表中有三个描述符,第一个是(NULL)不用,另外两个分别是代码段描述符和数据段描述符。它们都指向系统模块的起始处,也即物理地址0x0000 处。这样当setup.s 中执行最后一条指令 'jmp0,8 '(第193 行)时,就会跳到head.s 程序开始处继续执行下去。这条指令中的'8'是段选择符,用来指定所需使用的描述符项,此处是指gdt 中的代码段描述符。'0'是描述符项指定的代码段中的偏移值。
6)head.s开始执行,此时的内核完全都是在保护模式下运行了!首先是加载各个数据段寄存器,重新设置中断描述符表idt,共256 项,并使各个表项均指向一个只报错误的哑中断程序。然后重新设置全局描述符表gdt。接着使用物理地址0 与1M 开始处的内容相比较的方法,检测A20 地址线是否已真的开启(如果没有开启,则在访问高于1Mb 物理内存地址时CPU 实际只会访问(IP MOD 1Mb)地址处的内容),如果检测下来发现没有开启,则进入死循环。然后程序测试PC 机是否含有数学协处理器芯片(80287、80387 或其兼容芯片),并在控制寄存器CR0 中设置相应的标志位。接着设置管理内存的分页处理机制,将页目录表放在绝对物理地址0 开始处(也是本程序所处的物理内存位置,因此这段程序将被覆盖掉),紧随后面放置共可寻址16MB 内存的4 个页表,并分别设置它们的表项。最后利用返回指令将预先放置在堆栈中的/init/main.c 程序的入口地址弹出,去运行main()程序!!head.s刚执行完时 system模块在内存中的映像如图:
4. 内存管理寄存器
Intel 80386 CPU 有4 个寄存器用来定位控制分段内存管理的数据结构:
GDTR (Global Descriptor Table Register)全局描述符表寄存器。
LDTR (Local Descriptor Table Register)局部描述符表寄存器。
这两个寄存器用于指向段描述符表GDT 和LDT。这两个表是用于内存的分页管理。
IDTR (Interrupt Descriptor Table Register)中断描述符表寄存器。这个寄存器指向中断处理向量(句柄)表(IDT)的入口点。所有中断处理过程的入口地址信息均存放在该表中的描述符表项中。
TR (Task Register)任务寄存器。该寄存器指向处理器定义当前任务(进程)所需的信息,也即任务数据结构task{}。
5. 控制寄存器
Intel 80386 的控制寄存器共有4 个,分别命名为CR0、CR1、CR2、CR3。这些寄存器仅能够由系统程序通过MOV 指令访问。
控制寄存器CR0 含有系统整体的控制标志,它控制或指示出整个系统的运行状态或条件。其中:
PE – 保护模式开启位(Protection Enable,比特位0)。如果设置了该比特位,就会使处理器开始在保护模式下运行。
MP – 协处理器存在标志(Math Present,比特位1)。用于控制WAIT 指令的功能,以配合协处理的运行。
EM – 仿真控制(Emulation,比特位2)。指示是否需要仿真协处理器的功能。
TS – 任务切换(Task Switch,比特位3)。每当任务切换时处理器就会设置该比特位,并且在解释协处理器指令之前测试该位。
ET – 扩展类型(Extention Type,比特位4)。该位指出了系统中所含有的协处理器类型(是80287 还是80387)。
PG – 分页操作(Paging,比特位31)。该位指示出是否使用页表将线性地址变换成物理地址。
CR2 用于PG 置位时处理页异常操作。CPU 会将引起错误的线性地址保存在该寄存器中。
CR3 同样也是在PG 标志置位时起作用。该寄存器为CPU 指定当前运行的任务所使用的页表目录。
6. Intel 32 位保护运行机制
当CPU 运行在保护模式下时,它就将实模式下的段地址当作保护模式下段描述符的指针使用,此时段寄存器中存放的是一个描述符在描述符
表中的偏移地址值。而当前描述符表的基地址则保存在描述符表寄存器中,如全局描述符表寄存器gdtr、中断门描述符表寄存器idtr,加载这些表寄存器须使用专用指令lgdt 或lidt。CPU 在实模式运行方式时,段寄存器用来放置一个内存段地址(比如0x9000),而此时在该段内可以寻址64KB 的内存。但当进入保护模式运行方式时,此时段寄存器中放置的并不是内存中的某个地址值,而是指定描述符表中某个描述符项相对于该描述符表基址的一个偏移量。在这个8 字节的描述符中含有该段线性地址的‘段’基址和段的长度,以及其它一些描述该段特征的比特位。因此此时所寻址的内存位置是这个段基址加上当前执行代码指针eip 的值。当然,此时所寻址的实际物理内存地址,还需要经过内存页面处理管理机制进行变换后才能得到。简而言之,32 位保护模式下的内存寻址需要拐个弯,经过描述符表中的描述符和内存页管理来确定。
针对不同的使用方面,描述符表分为三种:全局描述符表(GDT)、中断描述符表(IDT)和局部描述符表(LDT)。当CPU 运行在保护模式下,某一时刻GDT 和IDT 分别只能有一个,分别由寄存器GDTR和IDTR 指定它们的表基址。局部表可以有0-8191 个,其基址由当前LDTR 寄存器的内容指定,是使用GDT 中某个描述符来加载的,也即LDT 也是由GDT 中的描述符来指定。但是在某一时刻同样也只有其中的一个被认为是活动的。一般对于每个任务(进程)使用一个LDT。在运行时,程序可以使用GDT 中的描述符以及当前任务的LDT 中的描述符。中断描述符表IDT 的结构与GDT 类似,在Linux 内核中它正好位于GDT 表的后面。共含有256 项8字节的描述符。但每个描述符项的格式与GDT 的不同,其中存放着相应中断过程的偏移值(0-1,6-7 字节)、所处段的选择符值(2-3 字节)和一些标志(4-5 字节)。下图是Linux 内核中所使用的描述符表在内存中的示意图。
图中,每个任务在GDT 中占有两个描述符项。GDT 表中的LDT0 描述符项是第一个任务(进程)的局部描述符表的描述符,TSS0 是第一个任务的任务状态段(TSS)的描述符。每个LDT 中含有三个描述符,其中第一个不用,第二个是任务代码段的描述符,第三个是任务数据段和堆栈段的描述符。当DS 段寄存器中是第一个任务的数据段选择符时,DS:ESI即指向该任务数据段中的某个数据。