Chinaunix首页 | 论坛 | 博客
  • 博客访问: 265212
  • 博文数量: 42
  • 博客积分: 1445
  • 博客等级: 上尉
  • 技术积分: 485
  • 用 户 组: 普通用户
  • 注册时间: 2005-05-05 16:21
文章分类

全部博文(42)

文章存档

2011年(1)

2009年(5)

2008年(1)

2007年(1)

2006年(28)

2005年(6)

我的朋友

分类: LINUX

2005-06-15 08:56:41

奉普慈特慈真主之名
一切赞颂全归真主,全世界的主
普慈特慈的主
掌管报应日的主
我们只崇拜你,只求你佑助
求你指引我们正路
受你施恩者的路,而不是你所谴怒者的路,也不是迷误者的路
阿敏!

目录
目录 3
前言 4
第一部分 系统初始化 4
第一章 IA32处理器的初始化 4
1.1 处理器初始化概述 4
1.1.1 复位后的处理器状态 5
1.1.2 软件初始化代码 6
1.2 高速缓存及模式专用寄存器(MSR)的初始化 7
1.2.1 开放高速缓存(cache) 7
1.2.2 设置模式专用寄存器(MSR) 7
1.3 软件初始化的任务 7
1.4 工作模式的切换 8
第二章 BIOS引导过程 9
2.1 BIOS的基本概念 9
2.1.1 ROM BIOS 9
2.1.2 硬盘 BIOS 9
2.2 硬盘寻址参数和分区表 10
2.2.1 硬盘寻址参数 10
2.2.2 分区表 11
2.3 BIOS的引导流程 13
第三章 Linux引导过程 14
3.1 IA32的段页式存储管理机制 14
3.2 bootsect.S的执行分析 16
3.3 setup.S的执行分析(实模式下的初始化) 17
3.3.1 代码签名检查 17
3.3.2 参数设置 20
3.3.3 切换进入保护模式 24
3.4 head.S的执行分析(保护模式下的初始化) 30
第四章 Linux内核初始化总体流程 43
4.1主要流程 44
4.1.1 与体系结构相关的初始化——setup_arch()函数 44
4.1.2 解析内核选项 44
4.1.3 一系列的初始化 44
4.2 init进程 46
4.2.1 init进程的创建 46
4.2.2 init进程的执行 48
4.2.3 do_basic_setup()函数 48


前言
 本书基于IA32体系结构来分析内核,但是,并没有专门开设一章来讲IA32体系结构,而是分散到各个章节中,当需要时进行描述。这样可以更好地体会软件和硬件之间的交互技术。
 本书以Linux 2.4.20内核作为分析对象。
第一部分 系统初始化
 本部分首先描述IA32 处理器本身的初始化规程,然后讲述计算机加电后,操作系统自举的过程。其中,对系统引导部分详细描述,对内核初始化部分,仅描述各个子系统的初始化总体流程,各个子系统的详细初始化过程,在分析该子系统的相应的章节中再分别详细分析。
IA32处理器的初始化
本章暂时抛开操作系统,仅就IA32处理器本身的初始化功能和作用进行较为详细的描述。内容包括处理器初始化、处理器配置、工作模式切换等。
1.1 处理器初始化概述
 处理器的初始化过程总的分为硬件初始化和软件初始化两大部分。
 当系统加电或者RESET引脚有效后,处理器进入硬件初始化过程。硬件初始化过程的第一步是硬件复位。硬件复位将处理器的寄存器设置为复位初始值,并将处理器设置为实模式工作状态。同时,硬件复位还将处理器内部高速缓存(cache)、转换后援缓冲器(TLB)和分支目标缓冲器(BTB)设置为无效的状态。复位操作按照处理器系列不同而有所不同:
P6系列处理器——总是经先进可编程中断控制器(APIC)执行多处理器(MP)初始化操作,然后,MP初始化规程所选择的引导处理器(BSP)开始执行初始化代码(即进入软件初始化过程)。初始化代码的起始地址,由当前代码段中的段基地址和EIP寄存器中的偏移量给定。其余非主处理器(AP)在BSP执行初始化代码时,进入停止状态。
Pentium处理器——无论是在单处理器系统还是多处理器系统中,总有一个处理器被设置为主处理器(注意,这里的主处理器只是在初始化过程中起作用,当操作系统被加载后,系统工作在主从状态还是SMP状态,由操作系统决定)。主处理器按照预定的双处理器(DP)初始化规程执行软件初始化代码,代码的起始地址由当前代码段中的段基地址和EIP寄存器中的偏移量确定。辅助处理器进入停止状态。
Intel 486处理器——主处理器立刻从当前代码段中,由EIP寄存器中偏移量确定的起始地址开始执行软件初始化代码。486处理器不会自动按照DP或者MP规程进行初始化。
硬件复位期间,浮点单元(FPU)也被初始化为复位状态,然后可以执行FPU软件初始化代码。由于无论处理器工作在哪种模式下,FPU的操作总是相同的,所以FPU的初始化不需要进行工作模式切换操作。
1.1.1 复位后的处理器状态
 表1-1是加电后各个处理器系列中各个寄存器地状态。其中,控制寄存器CR0的状态为60000010H,该状态将处理器设置为实地址工作方式,见图1-1。


表1-1 复位后,IA32处理器的状态
 
寄存器 P6处理器 Pentium处理器 Intel486处理器   
EFLAGS 00000002H 00000002H 00000002H   
EIP 0000FFF0H 0000FFF0H 0000FFF0H   
CR0 60000010H 60000010H 60000010H   
CR2、CR3、CR4 00000000H 00000000H 00000000H   
CS 选择符=F000H
基址=FFFF0000H
段限=FFFFH
AR=存在、R/W、已访问 选择符=F000H
基址=FFFF0000H
段限=FFFFH
AR=存在、R/W、已访问 选择符=F000H
基址=FFFF0000H
段限=FFFFH
AR=存在、R/W、已访问   
SS、DS、ES、FS、GS 选择符=0000H
基址=00000000H
段限=FFFFH
AR=存在、R/W、已访问 选择符=0000H
基址=00000000H
段限=FFFFH
AR=存在、R/W、已访问 选择符=0000H
基址=00000000H
段限=FFFFH
AR=存在、R/W、已访问   
EDX 000006xxH 000005xxH 000004xxH   
EAX、EBX、ECX、ESI、EDI、EBP、ESP 00000000H 00000000H 00000000H   
GDTR、IDTR 基址=00000000H
段限=FFFFH
AR=存在、R/W 基址=00000000H
段限=FFFFH
AR=存在、R/W 基址=00000000H
段限=FFFFH
AR=存在、R/W   
LDTR、TR 选择符=0000H
基址=00000000H
段限=FFFFH
AR=存在、R/W 选择符=0000H
基址=00000000H
段限=FFFFH
AR=存在、R/W 选择符=0000H
基址=00000000H
段限=FFFFH
AR=存在、R/W   
DR0、DR1、DR2、DR3 00000000H 00000000H 00000000H   
DR6 FFFF0FF0H FFFF0FF0H FFFF1FF0H   
DR7 00000400H 00000400H 00000000H   
时间戳寄存器 0H 0H 未实现   
性能计数器及事件选择寄存器 0H 0H 未实现   
其他模式专用寄存器(MSR) 未定义 未定义 未定义   
数据和代码cache、TLB 无效 无效 无效   
固定的和可变的MTRR 禁止 未实现 未实现   
机器校验结构 未定义 未实现 未实现   
APIC 使能 使能 未实现 

1.1.2 软件初始化代码
 软件初始化作为处理器初始化的一部分,在处理器复位后,初始化代码必须在引导处理器或者主处理器上完成系统专用初始化以及系统逻辑初始化。对于多处理器(MP)或者双处理器(DP)系统,BSP处理器在完成初始化后,唤醒各个AP处理器,由各个AP处理器执行初始化代码,完成各自的初始化。
 软件初始化代码必须构造必要的运行环境,如在内存中初始化中断描述符表、全局描述符表等系统数据结构。
 硬件复位后,处理器中CS寄存器中可见的段选择符部分的值为F000H,EIP的值为0000FFF0H,由于处理器运行于实模式,并不使用CS中隐含的描述符部分,故处理器执行的第一条指令的地址遵循实模式下的物理地址生成规则而得:CS*16+EIP,即FFFF0000H+FFF0H=FFFFFFF0H。所以,硬件复位后,处理器从物理地址为FFFFFFF0H的存储单元取指执行。因此,要求含有初始化代码(一般就是所谓的BIOS)的EPROM必须映射到该地址。
 为确保EPROM中的软件初始化代码的正常执行,CS寄存器选择符部分的值必须维持不变,在EPROM软件初始化完成以前,代码中不能有包含远程跳转或者远程调用的指令,也不能允许中断,因为这会导致改变CS选择符的值。只有在EPROM软件完成初始化之后,才用一条远程跳转指令将程序控制转移到后续的引导初始化代码上去。
1.2 高速缓存及模式专用寄存器(MSR)的初始化
 本部分的初始化主要由软件初始化代码完成两项任务:开放处理器高速缓存(cache),设置模式专用寄存器(MSR)
1.2.1 开放高速缓存(cache)
 IA32体系结构内部具有指令cache和数据cache。通过设置控制寄存器CR0重的CD和NW标志位,可以实现禁止或者开放cache的目的。当处理器加电复位后,CD和NW被置位,所有内部cache行均处于无效状态,因此初始化代码应当通过清除CR0中的CD和NW标志来开放cache。
 处理器外部具有所谓的二级cache。对二级cache的初始化需要使用系统专用的代码序列来初始化。
1.2.2 设置模式专用寄存器(MSR)
 IA32体系结构内部具有一组模式专用寄存器MSR。MSR与处理器工作模式和硬件、软件的有关特性相关。这些特性对于支持多任务操作系统的开发是非常重要的。下面对它们作一简要介绍。
性能监视计数器及相应的性能监视控制器。P6系列处理器有两个40位的性能监视计数器,一个用来事件计数,一个用于时间间隔测量。
时间戳计数器。一个64位的计数器,以处理器时钟进行增量计数。
调试扩展寄存器。P6系列处理器除了使用8各调试寄存器(DR0~DR7)以外,还设置了两个模式专用的调试扩展寄存器,进一步完善其调试功能。调试扩展寄存器用以记录最近分支、中断和异常,于是可以用来在分支、中断和异常处设置断点。
存储器类型范围寄存器(MTRR)。对于现代计算机而言,其系统存储器的组成成分比较复杂,包括ROM、RAM、帧缓冲存储器以及存储器映射I/O。为了能对不同类型的存储器进行优化操作,P6系列处理器使用MTRR来指定存储器类型以及包含的地址范围。所谓固定的MTRR是指这一组MTRR所指定的存储空间是在00000000H~000F8000H范围内;而可变MTRR则可以指定任意的存储空间。
在初始化代码中,必须对MSR进行设置。使用的指令是运行于0级的特权指令RDMSR和WRMSR。
1.3 软件初始化的任务
 本节分别阐述实模式和保护模式下软件初始化所要完成的工作。两种处理器工作模式的切换在1.4节中叙述。
 前面已经指出,CPU加电复位后,处理器处于实模式工作方式。CPU将从物理地址FFFFFFF0H开始取指执行,进入实模式的软件初始化。实模式的软件初始化的主要任务是设置处理器的基本运行环境,包括处理中断和异常的IDT数据结构。
实模式下的IDT
在实模式工作方式下,唯一需要加载进入内存的系统数据结构是IDT(中断向量表)。初始化代码使用LIDT指令来改变处理器中的中断向量表基地址寄存器IDTR。在开放中断之前,初始化代码必须在IDT中正确设置中断和异常处理程序指针。
实际的中断和异常处理程序代码一般也加载到RAM中,并且必须位于处理器在实模式下可寻址的1M地址空间内。
NMI中断处理
在IDT和实际的中断和异常处理程序尚未加载到RAM中之前,必须禁止NMI。分两种情况:
l 一种情况是简单的IDT和NMI中断处理程序可以放在EPROM中(如BIOS系统),l 这样在复l 位初始化之后,l 可以立即处理NMI中断。
l 另一种情况是由系统硬件提供一个机构来开放和禁止NMI中断。通常的方法是,l 使处理器的NMI信号通过一个与门,l 该与门受控于一位I/O端口。这样初始代码可以通过操纵该I/O端口实现对NMI的控制。
如果处理器还要进一步切换到保护模式下,则在实模式下需要做更多的工作来支持模式切换。这些任务是:
并初始化保护模式下的IDT表
初始化GDT表
初始化一个TSS段
初始化一个LDT表
如果要使用分页机制,则需要初始化页目录和至少一个页表
初始化GDTR寄存器
初始化IDTR寄存器
初始化控制寄存器CR1~CR4
初始化MTRR(仅限于P6系列处理器)
进行了上述必要的初始化后,处理器就可以修改控制寄存器CR0,使PE置位,切换到保护模式。具体的切换细节在1.4节中叙述。
1.4 工作模式的切换
在完成必要的准备后,初始化代码可以通过下述步骤完成从实模式到保护模式的切换。
禁止中断。CLI指令可以禁止可屏蔽中断(INTR),NMI中断可以通过外部电路禁止。
执行LGDT指令,将GDT的基地址加载到GDTR寄存器中。
执行MOV CR0指令,将控制寄存器CR0的PE标志置位,如果要使用分页机制的话,同时把PG标志置位。
紧跟在MOV CR0指令之后,执行一条远程跳转或者远程过程调用指令。典型方式是,远程跳转或者远程过程调用到指令流的下一条指令(JMP或者CALL指令的下一条 指令)。紧跟在MOV CR0后的指令JMP或者CALL指令既改变了指令执行流程,又对处理器的指令预取队列进行了清除。需要注意的是,如果启用了分页,则MOV CR0指令和JMP或者CALL指令的代码必须在映射同一个页面上,转移目标指令的代码可以不在同一个页面上。
如果要使用局部描述符表,执行LLDT指令,将LDT的段选择符加载到LDTR寄存器中。
实行LTR指令,将段选择符加载到任务寄存器,以初始化保护方式的任务。
进入保护模式后,段寄存器继续保持在实模式方式下的值。步骤4中的JMP指令或者CALL指令重置了CS寄存器。使用下列方法之一完成其余段寄存器值的更新:
l 重新显式加载DS、SS、ES、FS和GS的值。
l 执行一条JMP或者CALL指l 令,l 转到一个新的任务上去,l 这样可以根据新任务的TSS段中的内容自动重置所有段寄存器的值。
执行LIDT指令,将保护模式的IDT基地址和界限加载到IDTR寄存器中。
执行STI指令,开放可屏蔽中断(INTR),并执行必要的硬件操作以开放NMI中断。
第二章 BIOS引导过程
2.1 BIOS的基本概念
 BIOS(Basic Input/Output System)即基本输入输出系统。与系统引导有关的BIOS主要有两种。一种是ROM BIOS,固化在系统主板上;另一种为硬盘BIOS,固化在硬盘适配器上的一片2732 EPROM中。对于系统引导而言,ROM BIOS起主要作用。本小节对这两种BIOS分别作一简要介绍。
2.1.1 ROM BIOS
 ROM BIOS存放在1MB内存的高地址区F0000h~FFFFFh。
ROM BIOS包括四方面的程序:
 1.BIOS中断例程
 即BIOS中断服务程序。操作系统对软盘、硬盘等物理硬件设备接口的管理即建立在BIOS中断例程的基础之上。与系统引导有关的例程包括INT 13h和INT 19h。
 2.系统设置程序
 实现对CMOS RAM中的各种参数的设定,如启动介质的顺序。
 3.POST上电自检
 POST(Power On Self Test)包括:CPU,640KB基本内存,1MB以上的扩展内存,ROM,主板,CMOS RAM,串口和并口,显卡,软盘和硬盘子系统以及键盘测试等。
 4.系统自举程序
 按照CMOS中设置的引导介质顺序搜寻并加载MBR到RAM,然后将系统控制权交给MBR,又MBR完成操作系统的引导。
2.1.2 硬盘 BIOS
 硬盘BIOS的绝对地址为C8000h~C9000h。
2.2 硬盘寻址参数和分区表
2.2.1 硬盘寻址参数
 老式硬盘寻址参数由磁头(head)、柱面(cylinder)和扇区(sector)确定,即所谓的CHS参数。其中:   
磁头数(Heads) 表示硬盘总共有几个磁头,也就是有几面盘片, 最大为 255 (用 8 个二进制位存储);
柱面数(Cylinders) 表示硬盘每一面盘片上有几条磁道, 最大为 1023(用 10 个二进制位存储);
扇区数(Sectors) 表示每一条磁道上有几个扇区, 最大为 63 (用 6个二进制位存储);
每个扇区一般是 512个字节, 理论上讲这不是必须的
    所以磁盘最大容量为:heads*cylinders*sectors*512 Byte,即
    255 * 1023 * 63 * 512 / 1048576 = 8024 GB (1MB = 1048576 Bytes)
或硬盘厂商常用的单位:
    255 * 1023 * 63 * 512 / 1000000 = 8414 GB (1MB = 1000000 Bytes)
    在 CHS 寻址方式中, 磁头, 柱面, 扇区的取值范围分别为 0 到 Heads – 1,
0 到 Cylinders - 1, 1 到 Sectors (注意扇区计数是从 1 开始)。
BIOS INT 13h 调用是 BIOS 提供的磁盘基本输入输出中断调用,它可以完成磁盘(包括硬盘和软盘)的复位、读写、校验、 定位、 诊断、格式化等功能。它使用的就是 CHS 寻址方式, 因此最大只能能访问8GB 左右的硬盘。
在老式硬盘中,由于每个磁道的扇区数相等,所以外道的记录密度要远低于内道,因此会浪费很多磁盘空间(与软盘一样)。为了解决这一问题,进一步提高硬盘容量,新式硬盘改用等密度结构生产,也就是说,外圈磁道的扇区比内圈磁道多。采用这种结构后,寻址方式也改为以扇区为单位线性寻址,硬盘不再具有实际的物理的CHS参数。但是现在软件还是使用CHS参数进行硬盘寻址,为了与使用CHS参数寻址的软件兼容(如使用BIOS INT 13h接口的软件),在硬盘控制器内部安装了一个地址翻译器,由它负责将老式CHS参数映射成新的线性参数。 这也是为什么现在硬盘的CHS参数可以有多种选择的原因(不同的工作模式,对应不同的CHS参数,如 LBA, LARGE, NORMAL),因为可能存在多种映射模式。
虽然现代硬盘都已经采用了线性寻址,但是由于基本INT 13h的制约, 使用 BIOS INT 13h 接口的程序,如 DOS 等还是只能访问8GB以内的硬盘空间。为了打破这一限制, Microsoft 等几家公司制定了增强BIOS——EBIOS,并扩展了INT 13h 功能(Extended INT 13h),采用线性寻址方式存取硬盘,可以突破了8GB的限制。目前EBIOS支持的访问的EIDE接口使用16bit表示柱面数,4bit表示磁头数,8bit表示扇区数(这里的CHS参数只是逻辑上的参数,已经不是硬盘中的物理参数,再次强调,现代硬盘以扇区为单位线性寻址),从而达到128GB的容量。EBIOS支持最大2048GB的硬盘容量。
所以今后我们谈到磁道、柱面、扇区等参数时,一定要意识到这只是逻辑上的,实际寻址方式是将CHS参数映射以扇区为单位线性的线性地址后在对物理硬盘寻址的。
2.2.2 分区表
1.引导扇区(Boot Sector) 的组成
    Boot Sector 也就是硬盘的第一个扇区(0柱面、0磁道、1扇区),它由主引导记录 MBR(Master Boot Record),磁盘主分区表DPT(Disk Partition Table)和(Boot Record ID)三部分组成。
    MBR 又称作主引导记录占用 Boot Sector 的前 446 个字节( 0 ~ 0x01BD ),
存放系统主引导程序(它负责从活动分区中装载并运行系统引导程序)。
    DPT 即主分区表占用 64 个字节 (0x01BE ~ 0x01FD),记录了磁盘的基本分区信息。主分区表分为四个分区项,每项 16 字节,分别记录了每个主分区的信息(因此最多可以有四个主分区)。
    Boot Record ID 即引导记录标识号,占用两个字节( 0x01FE ~ 0x1FF),对于合法引导区,它等于 0xAA55,这是判别引导区是否合法的标志。
Boot Sector 的具体结构如下图所示:


2.分区表结构简介
分区表由四个分区项构成, 每一项的结构如下:
BYTE State        : 分区状态,0 = 未激活,0x80 = 激活
BYTE StartHead    : 分区起始磁头号
WORD StartSC      : 分区起始扇区和柱面号,低字节的低6位为扇区号,高2位为柱面号的第 9、10 位,高字节为柱面号的低 8 位
BYTE Type         : 分区类型,如 0x0B = FAT32,0x83 = Linux 等,                     00 表示此项未用
BYTE EndHead      : 分区结束磁头号
WORD EndSC        : 分区结束扇区和柱面号, 定义同前
DWORD Relative    : 在线性寻址方式下的分区首扇区的逻辑扇区号(等于此分区前的扇区数,即相对扇区数)
DWORD Sectors     : 分区大小(总扇区数)
例如,某分区表项为:
80 01 01 00 0B 3F FF 00 3F 00 00 00 81 4F 2F 00
Byte 1 (80)引导标志,80代表可引导,00代表不可引导,一般必须且只能有一个分区表项的引导标志为80,除非你自己修改MBR
Byte 2 (01)分区开始磁头
Byte 3-4 (01 00)=(1,0)分区开始柱面和扇区)
Byte 5 (0B)分区类型
Byte 6 (3F)=(63)分区结束磁头
Byte 7-8 (FF 00)=(768,63)分区结束柱面和扇区
Byte 9-12 (3F 00 00 00)=(63)此分区前扇区总数,即相对扇区数
Byte 13-16 (81 4F 2F 00)=(002F4F81H=3100545)此分区扇区总数
关于分区类型,常见的有:
00      未用,Unused
01      DOS-12(FAT 12)
02      XENIX
04      DOS-16(FAT 16)(分区<32M时用)
05      EXTEND(DOS扩展分区)
06      BIGDOS(>32M)(这个是现在常说的FAT 16)
07      HPFS(OS/2)(NTFS也是这个标记)
0B      FAT 32
0F  Windows Extended
50      DM
63      386/ix(unix)
64      NET286(Novell)
65      NET386(Novell)
82  Linux Swap
83  Linux
85  Linux Extended
FF      BBT(UNIX Bad Block Table)

注意: 在 DOS/Windows 系统下,基本分区必须以柱面为单位划分(Sectors * Heads 个扇区),如对于 CHS 为 764/255/63 的硬盘,分区的最小尺寸为 255 * 63 * 512 / 1048576 = 7.844 MB。

3. 扩展分区简介
    由于主分区表中只能分四个分区,无法满足需求,因此设计了一种扩展分区格式。扩展分区的信息是以链表形式存放的。
首先,主分区表中要有一个主扩展分区项。所有扩展分区的空间都必须包括在这个主扩展分区项所指示的分区空间中。对于DOS/Windows 来说,主扩展分区的类型为 0x05。
除主扩展分区以外的其他所有扩展分区则以链表的形式级联存放,后一个扩展分区的数据项记录在前一个扩展分区的分区表中,但两个扩展分区的空间并不重叠。
    扩展分区类似于一个完整的硬盘,必须进一步分区才能使用。 但每个扩展分区中只能存在一个其他分区。此分区在DOS/Windows 环境中即为逻辑盘,因此每一个扩展分区的分区表(同样存储在扩展分区的第一个扇区中)中最多只能有两个分区数据项(包括下一个扩展分区的数据项)。
扩展分区和逻辑盘的示意图如下:


 


 
2.3 BIOS的引导流程
 BIOS引导程序的流程图如下:


 由于支持多操作系统,如果是从硬盘启动,则引导扇区中存放MBR的地方存放的可能是LILO或者GRUB等多操作系统引导程序的引导代码。鉴于最终还是要执行MBR,本书不考虑多重引导的情况,只是简单的认为引导扇区中前446字节存放的就是MBR,对于Linux来说,这段代码为bootsect.S。
第三章 Linux引导过程
3.1 IA32的段页式存储管理机制
 在引导过程中,需要建立必要的保护模式下的、启动了分页机制的运行环境。这正是系统结构和操作系统实施交互的地方。所以,我们在这里首先对IA32体系结构下的存储管理机制进行简要的介绍。至于操作系统的存储管理部分将在第四部分详细描述。
 就IA32结构本身而言,其支持的存储管理措施分为两部分:分段和分页。在保护模式下,分段是必须的,而分页则是可选的。整个结构如图3-1所示。


 

系统内所有的段都包括在处理器的线性地址空间中。在保护模式下,要在一特定的段内定位一个字节,必须提供逻辑地址(即远程指针)。逻辑地址包括段选择符和偏移量。段选择符是段的唯一标识符。每一个段有一个段描述符,指示段的基地址、段的大小、段的类型、段的访问权限和特级权。段基址加上偏移量形成线性地址,由该线性地址寻址线性地址空间中的一个字节。
 如果不使用分页,处理器的线性地址空间就直接映射到物理地址空间。物理地址空间定义为处理器在其地址总线上所能产生的地址空间。
 由于多任务(多进程)计算机系统中所定义的线性地址空间要比实际的物理存储器空间大,因此,需要“虚拟”线性地址空间的方法,这是通过分页机制实现的。页面可以存放在物理内存中,也可以存放在磁盘上。操作系统为每一个任务或者进程维护一个页目录和页表,用以实现虚实的映射。此映射输入为线性地址,输出为物理地址。
 操作系统要充分利用体系结构提供的分页设施,为应用程序提供透明的分页服务。
 Linux操作系统使用了IA32的flat分段模式,并启用了分页机制。因为从本质上说,有分页,则分段是无必要的。
 从图3-1应当明确操作系统和CPU的分工,CPU定义了接口并执行一系列转换和映射工作,操作系统则需要根据CPU的要求提供相应的数据,如页表,设置相应的CPU寄存器,从而实现与CPU的交互。
 图3-1中涉及的数据结构主要是段选择符、段描述符、页目录项和页表项。对这些数据结构的具体应用,在代码分析中再行描述,而关于这些数据结构本身的定义,则请参考有关Intel CPU体系结构的书籍。
3.2 bootsect.S的执行分析
  bootsect.S位于arch/i386/boot/目录下,其包含的boot.h位于include/asm-i386/目录下。bootsect.S主要完成以下任务:
将自己(512字节)从0x7C00:0000处搬到0x9000:0000处,并跳转到该处继续执行。
movw $BOOTSEG, %ax
 movw %ax, %ds  # %ds = BOOTSEG
 movw $INITSEG, %ax
 movw %ax, %es  # %ax = %es = INITSEG
 movw $256, %cx
 subw %si, %si
 subw %di, %di
 cld
 rep
 movsw
 ljmp $INITSEG, $go
 ...
go: movw $0x4000-12, %di
 ...
其中,BOOTSEG=0x07C0,而INITSEG=DEF_INITSEG=0x9000。
设置堆栈指针SS:SP = 0x9000:3FF4。
go: movw $0x4000-12, %di  
  movw %ax, %ds  
  movw %ax, %ss
  movw %di, %sp
为什么是0x4000?这个值是任选的,但是必须满足该值>=(bootsect的长度+setup程序的长度+堆栈的空间),12字节是磁盘参数表的大小。
将磁盘参数表复制到0x9000:3FF4处,并修改磁盘参数表,第4字节内容改为36,并把中断向量号为0x1E(=78)处的中断向量改为0x9000:3FF4。
movw %cx, %fs #%fs = 0
 movw $0x78, %bx # %fs:%bx is parameter table address
 pushw %ds
 ldsw %fs:(%bx), %si  # %ds:%si is source
 movb $6, %cl   # copy 12 bytes
 pushw %di   # %di = 0x4000-12.
 rep    # don't worry about cld
 movsw    # already done above
 popw %di
 popw %ds
 movb $36, 0x4(%di)  # patch sector count
 movw %di, %fs:(%bx)
 movw %es, %fs:2(%bx)

加载setup.S至0x9000:0200。
获取磁道扇区数,并存入标号为sectors的内存单元。
调用子程序read_it加载操作系统内核(zImage)至0x10000处,如果是__BIG_KERNEL__,则调用bootsect_kludge子程序(位于setup.S中)将内核(bzImage)搬到高端0x100000处。注意,内核模块里面包含了head.S的代码。
调用kill_moto子程序关闭软驱电机。
读光标位置,回车、换行,显示“Loading”。
检查根设备类型,存入标号为root_dev的内存单元。
跳转到setup.S执行实模式下的系统初始化。
3.3 setup.S的执行分析(实模式下的初始化)
 setup.S位于arch/i386/boot/目录下。由上文可知,此时,setup.S的起始代码位于物理地址0x00090200,cpu继续从这个地址取指执行。
 setup程序执行实模式下的初始化工作,主要完成:
BIOS中断调用,获取关于硬件设备的各种参数,并存放到内存INITSEG段(0x9000:0000)。
如果内核代码被装载到低端的内存中(物理地址0x00010000),就把它搬移到物理地址0x00001000处;如果内核是bzImage,则已经被装到高端内存处(见上文),则省去本次搬移。
对video适配器和8259中断控制器 以及协处理器进行初始化。
建立临时的中断描述符表和全局描述符表。
通过把CR0状态寄存器的PE位置1,使CPU从实模式切换到保护模式。
使用操作码前缀0x66的方法使指针由32位变为48位,进入保护模式的线性地址空间(注意此时还没有开启分页功能),并跳转到arch/i386/boot/compress/head.S中的startup_32()函数执行(物理地址0x00010000或者0x00100000)。
下面各小节具体描述上述任务的完成过程。
3.3.1 代码签名检查
 实际执行开始于setup.S中的start_of_setup:
应用BIOS调用INT 13h,取磁盘类型并复位磁盘。
movw $0x01500, %ax
 movb $0x81, %dl
 int $0x13
 movw $0x0000, %ax
 movb $0x80, %dl
 int $0x13
检查位于setup.S代码末尾的签名标志“AA55”或者“5A5A”,以确定setup.S代码是否全部装入SETUPSEG段。如果没有找到签名标志,说明setup.S并未完全装入,程序控制转移至bad_sig处,此时程序将setup.S的剩余部分和内核代码一起装入内存的SYSSEG段(0x1000),重新计算内核的段址为0x1020,并存入start_sys_seg内存单元;然后将setup.S的剩余部分移至0x9020:0x0800处,重复签名检查,如果还找不到签名标志,调用prtptr子程序显示“No setup signature found”后进入死循环。
(1) 签名(2) 检查
movw %cs, %ax
 movw %ax, %ds
 cmpw $SIG1, setup_sig1
 jne bad_sig
 cmpw $SIG2, setup_sig2
 jne bad_sig

 jmp good_sig1
(3) bad_sig处的程序
bad_sig:
 movw %cs, %ax   # SETUPSEG
 subw $DELTA_INITSEG, %ax  
# DELTA_INITSEG = SETUPSEG – INITSEG = 0x0020
 movw %ax, %ds
 xorb %bh, %bh
 movb (497), %bl   # get setup sect from bootsect
 subw $4, %bx    # LILO loads 4 sectors of setup
 shlw $8, %bx    # convert to words (1sect=2^8 words)
 movw %bx, %cx
 shrw $3, %bx    # convert to segment
 addw $SYSSEG, %bx
 movw %bx, %cs:start_sys_seg
# Move rest of setup code/data to here
 movw $2048, %di   # four sectors loaded by LILO
 subw %si, %si
 pushw %cs
 popw %es
 movw $SYSSEG, %ax
 movw %ax, %ds
 rep
 movsw
 movw %cs, %ax   # aka SETUPSEG
 movw %ax, %ds
 cmpw $SIG1, setup_sig1
 jne no_sig

 cmpw $SIG2, setup_sig2
 jne no_sig

 jmp good_sig
(4) no_sig处的程序
no_sig_mess: .string "No setup signature found ..."
...
lea no_sig_mess, %si
 call prtstr
no_sig_loop:
 hlt
 jmp no_sig_loop

# Routine to print asciiz string at ds:si
prtstr:
 lodsb
 andb %al, %al
 jz fin

 call prtchr
 jmp prtstr

fin: ret
找到签名标志后,检查内核是否为big kernel,如果是,则再检查是否由new loader装入,如果不是,则显示“Wrong loader, giving up...”后进入死循环。
good_sig1:
 jmp good_sig
good_sig:
movw %cs, %ax   # aka SETUPSEG
 subw $DELTA_INITSEG, %ax   # aka INITSEG
 movw %ax, %ds
# Check if an old loader tries to load a big-kernel
 testb $LOADED_HIGH, %cs:loadflags # Do we have a big kernel?
 jz loader_ok   # No, no danger for old loaders.
# Do we have a loader that can deal with us?
 cmpb $0, %cs:type_of_loader   
 jnz loader_ok   # Yes, continue.

 pushw %cs    # No, we have an old loader,
 popw %ds    # die.
 lea loader_panic_mess, %si
 call prtstr

 jmp no_sig_loop

loader_panic_mess: .string "Wrong loader, giving up..."
3.3.2 参数设置
获取扩展内存的长度。调用BIOS int 15H,功能号为ah=0x88,返回ax=从0x100000(1MB)处开始的扩展内存的长度(KB),并存入0x90002单元。注意,此时已经开始覆盖掉bootsect.S的代码。
loader_ok:
# Get memory size (extended mem, kB)

 xorl %eax, %eax
 movl %eax, (0x1e0)
#ifndef STANDARD_MEMORY_BIOS_CALL
;E820.h: #define E820NR 0x1e8  /* # entries in E820MAP */
 movb %al, (E820NR)
# Try three different memory detection schemes.  First, try
# e820h, which lets us assemble a memory map, then try e801h,
# which returns a 32-bit memory size, and finally 88h, which
# returns 0-64m

# method E820H:
# the memory map from hell.  e820h returns memory classified # into a whole bunch of different types, and allows memory holes # and everything.  We scan through this memory map and build  # a list of the first 32 memory areas, which we return at
# [E820MAP].
# This is documented at
#

#define SMAP  0x534d4150

meme820:
 xorl %ebx, %ebx   # continuation counter
 movw $E820MAP, %di   # point into the whitelist
      # so we can have the bios
      # directly write into it.

jmpe820:
 movl $0x0000e820, %eax  # e820, upper word zeroed
 movl $SMAP, %edx   # ascii 'SMAP'
 movl $20, %ecx   # size of the e820rec
 pushw %ds    # data record.
 popw %es
 int $0x15    # make the call
 jc bail820    # fall to e801 if it fails

 cmpl $SMAP, %eax   # check the return is `SMAP'
 jne bail820    # fall to e801 if it fails

# cmpl $1, 16(%di)   # is this usable memory?
# jne again820

 # If this is usable memory, we save it by simply advancing %di by
 # sizeof(e820rec).
 #
good820:
 movb (E820NR), %al   # up to 32 entries
 cmpb $E820MAX, %al
 jnl bail820

 incb (E820NR)
 movw %di, %ax
 addw $20, %ax
 movw %ax, %di
again820:
 cmpl $0, %ebx   # check to see if
 jne jmpe820    # %ebx is set to EOF
bail820:


# method E801H:
# memory size is in 1k chunksizes, to avoid confusing loadlin.
# we store the 0xe801 memory size in a completely different place,
# because it will most likely be longer than 16 bits.
# (use 1e0 because that's what Larry Augustine uses in his
# alternative new memory detection scheme, and it's sensible
# to write everything into the same place.)

meme801:
 stc     # fix to work around buggy
 xorw %cx,%cx    # BIOSes which dont clear/set
 xorw %dx,%dx    # carry on pass/error of
      # e801h memory size call
      # or merely pass cx,dx though
      # without changing them.
 movw $0xe801, %ax
 int $0x15
 jc mem88

 cmpw $0x0, %cx   # Kludge to handle BIOSes
 jne e801usecxdx   # which report their extended
 cmpw $0x0, %dx   # memory in AX/BX rather than
 jne e801usecxdx   # CX/DX.  The spec I have read
 movw %ax, %cx   # seems to indicate AX/BX
 movw %bx, %dx   # are more reasonable anyway...

e801usecxdx:
 andl $0xffff, %edx   # clear sign extend
 shll $6, %edx   # and go from 64k to 1k chunks
 movl %edx, (0x1e0)   # store extended memory size
 andl $0xffff, %ecx   # clear sign extend
  addl %ecx, (0x1e0)   # and add lower memory into
      # total size.

# Ye Olde Traditional Methode.  Returns the memory size (up to 16mb or
# 64mb, depending on the bios) in ax.
mem88:

#endif
 movb $0x88, %ah
 int $0x15
 movw %ax, (2)
设置键盘重复率为最大。调用BIOS int 16H。
# Set the keyboard repeat rate to the max
 movw $0x0305, %ax
 xorw %bx, %bx
 int $0x16
调用arch/i386/boot目录下的文件video.S,设置视频参数,存入0x9000段中的相应单元中。
# Check for video adapter and its parameters and allow the
# user to browse video modes.
 call video    # NOTE: we need %ds pointing
      # to bootsector
把BIOS ROM中的hd0的参数表拷贝到从0x90080开始的16个字节中。接着把hd1的参数表拷贝到0x90090开始的16个字节中。检查hd1磁盘类型,如果不存在,则将从0x90090开始的16个字节清零。
# Get hd0 data...
 xorw %ax, %ax
 movw %ax, %ds
 ldsw (4 * 0x41), %si
 movw %cs, %ax   # aka SETUPSEG
 subw $DELTA_INITSEG, %ax  # aka INITSEG
 pushw %ax
 movw %ax, %es
 movw $0x0080, %di
 movw $0x10, %cx
 pushw %cx
 cld
 rep
  movsb
# Get hd1 data...
 xorw %ax, %ax
 movw %ax, %ds
 ldsw (4 * 0x46), %si
 popw %cx
 popw %es
 movw $0x0090, %di
 rep
 movsb
# Check that there IS a hd1 :-)
 movw $0x01500, %ax
 movb $0x81, %dl
 int $0x13
 jc no_disk1
 
 cmpb $3, %ah
 je is_disk1

no_disk1:
 movw %cs, %ax   # aka SETUPSEG
 subw $DELTA_INITSEG, %ax   # aka INITSEG
 movw %ax, %es
 movw $0x0090, %di
 movw $0x10, %cx
 xorw %ax, %ax
 cld
 rep
 stosb
BIOS功能调用,获取系统环境,如果采用微通道总线,则把MCA参数放到从0x900A0开始的16个字节中。
BIOS功能调用,检查鼠标器类型是否是PS/2。如果是,则把参数0xAA送入0x901FF单元,否则,送参数0到0x901FF单元。
APM(高级电源管理)BIOS检查。如果安装了APM BIOS,则APM程序的代码,数据等各种段址和偏移量送0x9000段各个相应单元保存。
3.3.3 切换进入保护模式
禁止8259硬件中断,但是允许NMI。
# This is the default real mode switch routine.
# to be called just before protected mode transition
default_switch:
 cli     # no interrupts allowed !
 movb $0x80, %al   # disable NMI for bootup
      # sequence
 outb %al, $0x70
 lret
再次检查是否是BIG_KERNEL,确定32位内核代码在内存中的准确位置,以便正确执行内核代码。如果是zImage,则将32位内核代码从0x10200处移至0x10000处,每次4KB;如果是bzImage,即为BIG_KERNEL,则引导程序(bootsect.S)已经将其装入到0x100000处,本次移动可以省略。
检查setup.S本身是否位于 SETUPSEG(0x9020)段上,如果不是,把自己搬运到该段起始处。
 选通。激活 线,并测试直到确认。
(1)  地址线问题
IBM推出的PC最初使用的CPU是Intel 8088。在该PC中,地址总线只有20根( ),寻址空间1MB,其所能寻址的最高地址位0xFFFF:0xFFFF,对于超出0x100000(1MB)的地址将默认环绕到0x0FFEF。当1985年引入AT机时,使用的Intel 80286CPU具有24根地址总线,最高可寻址16MB。然而,当寻址地址超过1MB时,它不能象8088那样实现地址的环绕。考虑到当时已经有一些软件是利用这种地址环绕机制工作的,故为了实现兼容,IBM公司使用了一个开关来开启或者禁止0x100000地址比特位。由于当时的8042键盘控制器上恰好有空闲的端口引脚(输出端口P2,引脚P21),于是便使用该引脚作为与门控制这个地址比特位。该信号即被称为 。如果它为零,则bit20及其以上地址都被清除。
发展到现在,除了使用8042键盘控制器来控制 外,还有两种技术。一个是使用0x92端口,一个是通过BIOS调用 int 15h,功能号AH=24,子功能AL=0,禁止 ,AL=1,使能 ,AL=2读取 状态。
上述三种技术在setup.S中都被采用了。
(2) 使用0x92端口的代码:
#if defined(CONFIG_MELAN)
 movb $0x02, %al   # alternate A20 gate
 outb %al, $0x92   # this works on SC410/SC520
a20_elan_wait:
        call a20_test
        jz a20_elan_wait
 jmp a20_done
#endif
...
 # Final attempt: use "configuration port A"
a20_fast:
 inb $0x92, %al   # Configuration Port A
 orb $0x02, %al   # "fast A20" version
 andb $0xFE, %al   # don't accidentally reset
 outb %al, $0x92

 # Wait for configuration port A to take effect
a20_fast_wait:
 xorw %cx, %cx
a20_fast_wait_loop:
 call a20_test
 jnz a20_done
 loop a20_fast_wait_loop
(3) 使用BIOS功能调用的代码:
 # Next, try the BIOS (INT 0x15, AX=0x2401)
a20_bios:
 movw $0x2401, %ax
 pushfl     # Be paranoid about flags
 int $0x15
 popfl

 call a20_test
 jnz a20_done
(4) 使用键盘控制器的代码:
 # Try enabling A20 through the keyboard controller
a20_kbc:
 call empty_8042

 call a20_test   # Just in case the BIOS worked
 jnz a20_done   # but had a delayed reaction.

 movb $0xD1, %al   # command write
 outb %al, $0x64
 call empty_8042

 movb $0xDF, %al   # A20 on
 outb %al, $0x60
 call empty_8042

 # Wait until a20 really *is* enabled; it can take a fair amount of
 # time on certain systems; Toshiba Tecras are known to have this
 # problem.
a20_kbc_wait:
 xorw %cx, %cx
a20_kbc_wait_loop:
 call a20_test
 jnz a20_done
 loop a20_kbc_wait_loop
(5) 测试 是否选通的代码:
# This routine tests whether or not A20 is enabled.  If so, it
# exits with zf = 0.
#
# The memory address used, 0x200, is the int $0x80 vector, which
# should be safe.

A20_TEST_ADDR = 4*0x80

a20_test:
 pushw %cx
 pushw %ax
 xorw %cx, %cx
 movw %cx, %fs   # Low memory
 decw %cx
 movw %cx, %gs   # High memory area
 movw $A20_TEST_LOOPS, %cx
 movw %fs:(A20_TEST_ADDR), %ax
 pushw %ax
a20_test_wait:
 incw %ax
 movw %ax, %fs:(A20_TEST_ADDR)
 call delay    # Serialize and make delay constant
 cmpw %gs:(A20_TEST_ADDR+0x10), %ax
 loope a20_test_wait

 popw %fs:(A20_TEST_ADDR)
 popw %ax
 popw %cx
 ret 
(6) 如果在所有技术都使用后,(7)  仍然未能选通,(8) 则,(9) 打印出错信息,(10) 进入死循环。
 # A20 is still not responding.  Try frobbing it again.
 #
 decb (a20_tries)
 jnz a20_try_loop
 
 movw $a20_err_msg, %si
 call prtstr

a20_die:
 hlt
 jmp a20_die

a20_tries:
 .byte A20_ENABLE_LOOPS

a20_err_msg:
 .ascii "linux: fatal error: A20 gate not responding!"
 .byte 13, 10, 0
装填临时中断描述符表和IDTR,置为空;装填全局描述符表和GDTR,限长2048B=256个入口×8字节。
 # If we get here, all is good
a20_done:

# set up gdt and idt
 lidt idt_48    # load idt with 0,0
 xorl %eax, %eax   # Compute gdt_base
 movw %ds, %ax   # (Convert %ds:gdt to a linear ptr)
 shll $4, %eax
 addl $gdt, %eax
 movl %eax, (gdt_48+2)
 lgdt gdt_48    # load gdt with whatever is
      # appropriate
...
idt_48:
 .word 0    # idt limit = 0
 .word 0, 0    # idt base = 0L
gdt_48:
 .word 0x8000    # gdt limit=2048,
      #  256 GDT entries

 .word 0, 0    # gdt base (filled in later)
检查协处理器是否正常。
屏蔽所有硬件中断。对两片8259中断控制器的重新初始化留待arch/i386/kernel/I8259.c中的init_IRQ()函数完成。这里先解释一下为什么需要重新初始化8259中断控制器。IBM PC使用两片级连的8259控制硬件中断,每片处理8个中断源,两片可管理16个中断源。i386处理器共有256个中断向量,其中,向量号0~31保留给异常事件处理程序,所以硬件中断只能使用的向量号范围为32~255。但是,IBM PC上的BIOS在上电自检POST时,将8259硬中断号初始化为0x08~0x0F,所以需要对两片8259进行重新初始化,把8259处理的中断向量号设置为0x20~0x2F。
将控制寄存器cr0的PE位置1,从而使系统进入保护模式。但是要注意,此时仍然处于段寻址方式,尚未开启分页机制,故只使用选择符和描述符(前面已经设置)。完成这个过程的代码如下:
# Well, now's the time to actually move into protected mode. To make
# things as simple as possible, we do no register set-up or anything,
# we let the gnu-compiled 32-bit programs do that. We just jump to
# absolute address 0x1000 (or the loader supplied one),
# in 32-bit protected mode.
#
# Note that the short jump isn't strictly needed, although there are
# reasons why it might be a good idea. It won't hurt in any case.
 movw $1, %ax    # protected mode (PE) bit
 lmsw %ax    # This is it!
 jmp flush_instr

flush_instr:
 xorw %bx, %bx   # Flag to indicate a boot
 xorl %esi, %esi   # Pointer to real-mode code
 movw %cs, %si
 subw $DELTA_INITSEG, %si
 shll $4, %esi   # Convert to 32-bit pointer
# NOTE: For high loaded big kernels we need a
# jmpi    0x100000,__KERNEL_CS
#
# but we yet haven't reloaded the CS register, so the default size
# of the target offset still is 16 bit.
#       However, using an operand prefix (0x66), the CPU will properly
# take our 48 bit far pointer. (INTeL 80386 Programmer's Reference
# Manual, Mixing 16-bit and 32-bit code, page 16-6)

 .byte 0x66, 0xea   # prefix + jmpi-opcode
code32: .long 0x1000    # will be set to 0x100000
      # for big kernels
 .word __KERNEL_CS
(11) lmsw reg指(12) 令将寄存器reg中的值加载到CR0寄存器(机器状态字)中。
(13) 采用硬编码的方式实现模式转换和程序跳转,(14) 最后三条编码相当于执行指(15) 令:
jmpi 0x00100000, KERNEL_CS
其结果是跳转到物理地址0x00100000处,此处是arch/i386/boot/compressed/head.S的startup_32()函数入口。
(16) 关于__KERNEL_CS的定义在include/asm-i386/segment.h中:
#define GDT_ENTRY_KERNEL_BASE 12

#define GDT_ENTRY_KERNEL_CS  (GDT_ENTRY_KERNEL_BASE + 0)
#define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)
(17) 由于有操作码前缀66H,(18) 则从code32开始的6字节内容被转换承保护模式下的长指(19) 针(16位选择符,(20) 32位偏移量),(21) 选择符=0000 0000 0001 0000B,(22) 
可见,RPL = 00请求特权级为最高级,T1 = 0该段描述符在全局描述符表中,选择符3~15位=2,选择全局描述符表中第3个描述符,后面将看到,此为代码段描述符。
setup.S执行完毕后的内存映象如下表:
 
地址 内容 备注   
1000:0000H

1000:EFFFH zImage
system模块 包含
arch/i386/boot/kernel/head.S   
     
9000:0000H

9000:01FFH 原bootsect.S被系统参数覆盖    
9000:0200H

9000:0BFFH setup.S    
     
0x9xxxxH
~
... gdt表 只有四个表项,第一个为NULL,第二个未用,第三个为代码段描述符,第四个为数据段描述符   
...     
100000H
... BIG_KERNEL(bzImage) System  
setup.S中的全局描述符分析
前面已经指出,在切换至保护模式之前,程序已经将中断描述符表的首地址和全局描述符表的首地址分别加载到IDTR和GDTR寄存器中。setup.S将中断描述符表置为空,基址和限长置为0。全局描述符表设置为256个入口,共2048字节,基址为0x9XXXXH。实际只填入了4个描述符。第一个描述符为NULL,这是CPU规范所要求的。第二个描述符未使用。第三个描述符为代码段描述符,第四个为数据段描述符。定义如下:
gdt:
 .fill GDT_ENTRY_KERNEL_CS,8,0

 .word 0xFFFF    # 4Gb - (0x100000*0x1000 = 4Gb)
 .word 0    # base address = 0
 .word 0x9A00    # code read/exec
 .word 0x00CF    # granularity = 4096, 386
      #  (+5th nibble of limit)

 .word 0xFFFF    # 4Gb - (0x100000*0x1000 = 4Gb)
 .word 0    # base address = 0
 .word 0x9200    # data read/write
 .word 0x00CF    # granularity = 4096, 386
      #  (+5th nibble of limit)
关于段描述符数据结构的定义,请参考《Pentium II/III体系结构及扩展技术》。
(23) 数据段描述符分析
0、1、6字节的低半字节定义段的限长limit = 4GB;2、3、4和7字节定义段基址base = 00000000H,字节5为访问权限=92H=10010010B,有读和写的权限。
(24) 代码段描述符分析
与数据段描述符基本一致,只有访问权限不同,其访问权限=10011010B,只有读和执行权限。
3.4 head.S的执行分析(保护模式下的初始化)
 前面已经指出,setup.S将系统切换到保护模式后,首先跳转到物理地址0x00100000处执行 arch/i386/boot/compressed/head.S的startup_32()函数完成内核的解压缩,对此过程我们就不分析了。现在,解完压缩后,仍然从物理地址0x00100000处开始执行,但是现在执行的是arch/i386/kernel/head.S中的startup_32()函数。该函数继续完成保护模式的初始化,启动分页机制,为进程0建立执行环境。
 首先作几点说明:
对于SMP系统而言,这时运行的是引导处理器。其他处理器处于停等状态,等待引导处理器的启动。任何处理器都从startup_32函数开始执行,但是有些操作仅由引导处理器执行,有些仅由其他处理器执行。程序通过BX寄存器的值区分处理器的身份,引导处理器在进入startup_32时其寄存器BX的值为0,其他处理器的BX的值为1。
进入startup_32时,系统运行于保护模式下的段寻址方式。由于内核空间的地址映射是线性的、连续的,虚拟地址与物理地址之间有个固定的偏移,即0xC0000000=3GB,在链接内核映象时,链接器已经在所有符号地址上加了一个偏移量0xC0000000(__PAGE_OFFSET),这样startup_32的虚拟地址是0xC0100000。但是,在转入这个入口时使用的指令是ljmp 0x100000,而不是ljmp startup_32,所以装入CPU IP寄存器的地址是物理地址0x100000,而不是0xC0100000。这样,CPU在进入startup_32以后,就会继续以物理地址取指执行。只要不在代码段中引用某个地址,就可以一直这样运行下去,而与CS的内容无关。同样的道理,如果以内存地址作为操作数,则在开启页面映射之前都需要减去__PAGE_OFFSET。
进入startup_32时,CPU的中断已经关闭了。

下面分析head.S的主要代码。

初始化除CS以外的所有段寄存器,设置为__KERNEL_DS,表示采用GDT中描述符3。
startup_32:
/*
 * Set segments to known values
 */
 cld
 movl $(__KERNEL_DS),%eax
 movl %eax,%ds
 movl %eax,%es
 movl %eax,%fs
 movl %eax,%gs
建立临时页表
/*
 * Initialize page tables
 */
 movl $pg0-__PAGE_OFFSET,%edi /* initialize page tables */
 movl $007,%eax  /* "007" doesn't mean with right to kill, but
       PRESENT+RW+USER */
2: stosl
 add $0x1000,%eax
 cmp $empty_zero_page-__PAGE_OFFSET,%edi
 jne 2b
这段代码将从pg0开始,直到empty_zero_page之间的8K字节设置成一个临时页表,占用了2个页面=2K个页表项,共映射2K × 4KB=8MB内存空间。各个表项依次为0x7、0x1007、0x2007等等。其中最低三位为1,表示页面为用户页面,可写,页面在内存中。映射的物理页面的基地址,则分别为0x0、0x1000、0x2000等等,即物理内存页框0、1、2等等。由此可见,8MB是Linux内核对内存大小的最低要求。
关于页表项数据结构的定义,请参阅《Pentium II/III体系结构及扩展技术》。
以下是pg0的定义:
/*
 * The page tables are initialized to only 8MB here - the final page
 * tables are set up later depending on memory size.
 */
.org 0x2000
ENTRY(pg0)

.org 0x3000
ENTRY(pg1)

/*
 * empty_zero_page must immediately follow the page tables ! (The
 * initialization loop counts until empty_zero_page)
 */

.org 0x4000
ENTRY(empty_zero_page)
常数__PAGE_OFFSET定义为内核空间的虚拟地址与所映射的物理地址的固定的偏移,定义于include/asm-i386/page.h:
#define __PAGE_OFFSET  (0xC0000000)
页目录。
由图3-1知道,光有页表还不行,还需要页目录。页目录是直接编码定义的。
/*
 * This is initialized to create an identity-mapping at 0-8M (for bootup
 * purposes) and another mapping of the 0-8M area at virtual address
 * PAGE_OFFSET.
 */
.org 0x1000
ENTRY(swapper_pg_dir)
 .long 0x00102007
 .long 0x00103007
 .fill BOOT_USER_PGD_PTRS-2,4,0
 /* default: 766 entries */
 .long 0x00102007
 .long 0x00103007
 /* default: 254 entries */
 .fill BOOT_KERNEL_PGD_PTRS-2,4,0
从swapper_pg_dir开始,定义了1024个页目录项。几个常数的定义在include/asm-i386/pgtable.h中:
extern pgd_t swapper_pg_dir[1024];
#define TWOLEVEL_PGDIR_SHIFT 22
#define BOOT_USER_PGD_PTRS (__PAGE_OFFSET >> TWOLEVEL_PGDIR_SHIFT)
#define BOOT_KERNEL_PGD_PTRS (1024-BOOT_USER_PGD_PTRS)
易知,BOOT_KERNEL_PGD_PTRS为256,BOOT_USER_PGD_PTRS为768。
一个页面目录的大小为4KB,共有1024个目录项,每个目录项指示一个4KB大小的页表,一个4KB大小的页表有1024项页表项,每个页表项映射4KB大小的物理页框,所以这里共映射了4KB × 1024 ×1024=4GB的空间。该空间又分为两部分,页目录中的低768个页目录项映射了用户空间3GB,高256个页目录项映射了内核空间。
代码.fill BOOT_USER_PGD_PTRS-2,4,0的功能在其所在位置上填入768-2=766个页目录项,均设置为0;代码.fill BOOT_KERNEL_PGD_PTRS-2,4,0的功能在其所在位置填入256-2=254个页目录项,均设置为0。
从代码可以看到,在初始的页目录swapper_pg_dir中,用户空间和内核空间都只使用了开头的两个页目录项,即8MB空间,而且有着相同的映射,即指向相同的页表。映射情况如下图所示。


现在需要思考,为什么在虚存低区(用户空间)和高区(内核空间)要同时映射0~8MB的物理地址空间呢?这是因为,CPU在进入startup_32后按照物理地址取指执行,即CPU的IP寄存器指向物理空间的低区。在这种情况下,一旦开启页面映射机制,则CPU将IP寄存器中的内容解释为虚拟地址,如果不在虚存低区加以映射,则将无法继续执行。
开启页面映射机制。
/*
 * Enable paging
 */
3:
 movl $swapper_pg_dir-__PAGE_OFFSET,%eax
 movl %eax,%cr3  /* set the page table pointer.. */
 movl %cr0,%eax
 orl $0x80000000,%eax
 movl %eax,%cr0  /* ..and set paging (PG) bit */
 jmp 1f   /* flush the prefetch-queue */
1:
 movl $1f,%eax
 jmp *%eax  /* make sure eip is relocated */
1:
 /* Set up the stack pointer */
 lss stack_start,%esp
上述代码分两步完成页面机制的启动。首先,设置CR3寄存器为页目录基地址(由于要求是物理地址,所以要减去__PAGE_OFFSET),将CR0寄存器的PG位置1,这时,系统已经开启页面机制,尽管CPU中的IP寄存器的内容还是物理地址低区部分(但是系统已经解释为虚拟地址,需要进行页面转换后寻址内存),但是由于我们在虚存低区提供了映射,所以CPU还能取指执行。同时我们注意到在开启了页面映射机制后,有一条相对转移指令,它起的作用是清空CPU的预取指令队列,这是Intel所建议的,参见1.4小节。第二步,以一个符号地址为目标执行一条绝对转移指令。由于符号地址已经是内核空间的虚拟地址,所以,CPU在执行绝对转移指令时,把该虚拟地址装入IP,从此就改为以虚拟地址在内核空间取指执行了。
当系统中所有的CPU都完成这两步后,低区的映射将被清除。
设置进程0的内核堆栈。
1:
 /* Set up the stack pointer */
 lss stack_start,%esp
...
ENTRY(stack_start)
 .long SYMBOL_NAME(init_task_union)+8192
 .long __KERNEL_DS
init_task_union的定义在arch/i386/kernel/init_task.c中:
union task_union init_task_union
 __attribute__((__section__(".data.init_task"))) =
  { INIT_TASK(init_task_union.task) };
联合体task_union的定义在include/linux/sched.h中:
typedef struct task_struct task_t;
...
# define INIT_TASK_SIZE 2048*sizeof(long)
union task_union {
 task_t task;
 unsigned long stack[INIT_TASK_SIZE/sizeof(long)];
};
我们将在第二部分看到,每个进程的task_struct和进程的内核堆栈共用两个页面,下部是task_struct,上部是内核堆栈。
宏INIT_TASK的定义在include/linux/sched.h中:
/*
 *  INIT_TASK is used to set up the first task table, touch at
 * your own risk!. Base=0, limit=0x1fffff (=2MB)
 */
#define INIT_TASK(tsk) 
{         
    state:  0,      
    flags:  0,      
    sigpending:  0,      
    addr_limit:  KERNEL_DS,     
    exec_domain: &default_exec_domain,    
    lock_depth:  -1,      
    prio:  MAX_PRIO-20,     
    static_prio: MAX_PRIO-20,     
    policy:  SCHED_NORMAL,     
    cpus_allowed: -1,      
    mm:   NULL,      
    active_mm:  &init_mm,     
    run_list:  LIST_HEAD_INIT(tsk.run_list),   
    time_slice:  HZ,      
    tasks:              LIST_HEAD_INIT(tsk.tasks),                     
    ptrace_children: LIST_HEAD_INIT(tsk.ptrace_children),  
    ptrace_list: LIST_HEAD_INIT(tsk.ptrace_list),  
    real_parent:        &tsk,                                          
    parent:             &tsk,                                          
    children:           LIST_HEAD_INIT(tsk.children),                  
    sibling:            LIST_HEAD_INIT(tsk.sibling),                   
    group_leader: &tsk,      
    wait_chldexit: __WAIT_QUEUE_HEAD_INITIALIZER(tsk.wait_chldexit),
    real_timer:  {      
 function:  it_real_fn    
    },         
    cap_effective: CAP_INIT_EFF_SET,    
    cap_inheritable: CAP_INIT_INH_SET,    
    cap_permitted: CAP_FULL_SET,     
    keep_capabilities: 0,      
    rlim:  INIT_RLIMITS,     
    user:  INIT_USER,     
    comm:  "swapper",     
    thread:  INIT_THREAD,     
    fs:   &init_fs,     
    files:  &init_files,     
    signal:  &init_signals,     
    sighand:  &init_sighand,     
    pending:  { NULL, &tsk.pending.head, {{0}}},  
    blocked:  {{0}},      
    alloc_lock:  SPIN_LOCK_UNLOCKED,    
    switch_lock:        SPIN_LOCK_UNLOCKED,                            
    journal_info: NULL,      
}
宏INIT_TASK初始化了进程0的进程描述,可以看到该进程名为swapper。
初始化内核BSS段
BSS段是没有初始化值的变量存在的段。BSS是“ Block Started by Symbol(由符号开始的块)”的缩写,它是IBM 704汇编程序的一个伪指令,UNIX借用了这个名字。由于BSS段保存的是未经初始化的变量,所以事实上它也不需要保存这些变量的映象。运行时所需要的BSS段的大小记录在目标文件中,但是BSS段本身并不占据目标文件的空间。
/*
 * Clear BSS first so that there are no surprises...
 * No need to cld as DF is already clear from cld above...
 */
 xorl %eax,%eax
 movl $ SYMBOL_NAME(__bss_start),%edi
 movl $ SYMBOL_NAME(_end),%ecx
 subl %edi,%ecx
 rep
 stosb
这段代码将从__bss_start开始到_end结束的BSS段全部清零。
调用setup_id()子程序设置中断描述符表。
/*
 * start system 32-bit setup. We need to re-do some of the things done
 * in 16-bit mode for the "real" operations.
 */
 call setup_idt
...
/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It doesn't actually load
 *  idt - that can be done only after paging has been enabled
 *  and the kernel moved to PAGE_OFFSET. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok.
 */
setup_idt:
 lea ignore_int,%edx
 movl $(__KERNEL_CS << 16),%eax
 movw %dx,%ax  /* selector = 0x0010 = cs */
 movw $0x8E00,%dx /* interrupt gate - dpl=0, present */

 lea SYMBOL_NAME(idt_table),%edi
 mov $256,%ecx
rp_sidt:
 movl %eax,(%edi)
 movl %edx,4(%edi)
 addl $8,%edi
 dec %ecx
 jne rp_sidt
 ret
中断描述符表中用到的描述符类型称为中断门,每个8字节,其中含有访问权限字段、代码段选择符以及代码偏移量等信息,这实际上可看成是中断处理程序的入口,但不是直接的,而是需要段机制转换的。关于中断描述符的数据结构,请参阅《Pentium II/III体系结构及扩展技术》。
从代码可以看到,中断描述符表的基地址为idt_table,限长800H=2kB,共有256项,定义于arch/i386/kernel/traps.c中:
/*
 * The IDT has to be page-aligned to simplify the Pentium
 * F0 0F bug workaround.. We have a special link segment
 * for this.
 */
struct desc_struct idt_table[256]
__attribute__((__section__(".data.idt"))) = { {0, 0}, };
Linux一旦进入保护模式,就不再访问BIOS ROM中的中断服务程序,所以BIOS的中断向量表在此被覆盖。同时,因为系统执行到此时,所有的驱动程序尚未加载,故还不能正确处理中断,且中断自在setup.S中关闭以来也还没有打开,所以将所有的中断响应程序都设置成了ignore_int(),代码如下:
/* This is the default interrupt "handler" :-) */
int_msg:
 .asciz "Unknown interrupt "
 ALIGN
ignore_int:
 cld
 pushl %eax
 pushl %ecx
 pushl %edx
 pushl %es
 pushl %ds
 movl $(__KERNEL_DS),%eax
 movl %eax,%ds
 movl %eax,%es
 pushl $int_msg
 call SYMBOL_NAME(printk)
 popl %eax
 popl %ds
 popl %es
 popl %edx
 popl %ecx
 popl %eax
 iret
这是一个标准的中断处理程序,它调用printk打印信息“Unknown interrupt”。
此外要注意,IDTR还没有被重新设置(参考3.3.3小节中对IDTR的设置的描述)。另外,这里主要对硬件中断进行了设置,所有的内部中断,其描述符虽在同一张表中,但是使用的是陷阱门描述符。对中断描述符表的重新设置要等到start_kernel中的中断初始化部分才最后得以完成。

参数收集。
参数主要包括两部分,引导器如LILO传入的参数和setup.S从BIOS收集的放在0x90000处的参数。现在将这些参数复制到empty_zero_page页(对应第一个页框)中。该页前2KB放启动参数,后2KB放命令行参数。
/*
 * Initialize eflags.  Some BIOS's leave bits like NT set.  This would
 * confuse the debugger if this code is traced.
 * XXX - best to initialize before switching to protected mode.
 */
 pushl $0
 popfl
/*
 * Copy bootup parameters out of the way. First 2kB of
 * _empty_zero_page is for boot parameters, second 2kB
 * is for the command line.
 *
 * Note: %esi still has the pointer to the real-mode data.
 */
 movl $ SYMBOL_NAME(empty_zero_page),%edi
 movl $512,%ecx
 cld
 rep
 movsl
 xorl %eax,%eax
 movl $512,%ecx
 rep
 stosl
 movl SYMBOL_NAME(empty_zero_page)+NEW_CL

阅读(4433) | 评论(3) | 转发(0) |
0

上一篇:IPv4&&IPv6

下一篇:bios中断调用

给主人留下些什么吧!~~