1操作系统的启动和操作系统的引导程序的编写
1.linux 0.01中的引导汇编程序的解释
当PC机打开电源后,80x86结构自动的进入实施模式。
--------------------------------------------------------------------------------------------
所谓的实时模式是指的cpu启动时候的模式,这时候就相当于一个速度超快的8086.此时不能够实现多线程,不能够实现权限分级,还不能访问地址在20位以上的内存地址。与实施模式相对应的是保护模式,保护模式是指的是在操作接管cpu之后,会使cpu进入的状态。此时可以发挥cpu的所有的功能。包括权限分级,内存的分页等。保护模式有许多优越性。其中最最直接的好处就是:内存采用了分页和分段的管理方式,从而使应用程序重定位,虚拟内存等成为可能,你的程序可以利用更多的内存了! 分页和分段是由操作系统控制的,所以相对于实模式,应用程序对硬件直接操作的权限小得多,所以系统不容易因为应用程序崩溃而崩溃,所以称为“保护”。 而安全模式是Windows的一种诊断模式,在该模式下,系统只载入最基本的必须的模块和驱动,以便排除和解决问题。
-------------------------------------------------------------------------------------------
继续操作系统的启动过程:并从地址0xFFFF0开始执行代码,这个地址通常是rom bios中的地址。而此时cpu只是执行一个跳转命令来跳到bios真正的启动代码去执行。系统的bios启动代码首先要做的就是进行加电自检,加电自检的主要的功能是检查关键的设备是否能够正常的运行。其次bios的代码开始初始化设备。检查显卡的bios,存放显卡的bios的rom芯片的起始地址通常是0xc0000。系统的bios在查找到显卡的bios后,调用显卡的bios初始化代码。由显卡的bios来初始化显卡。查找玩所有其他设备的bios之后,系统的bios将显示出系统自己的启动画面。其中包括的有系统bios的类型,序列号,版本号等内容,接着系统的bios将检测和显示cpu的类型和工作频率。然后开始检测ram,并且同时显示检测的进度。
内存检查通过之后的话,系统的bios开始检查系统中安装的标准的硬件。之后开始检查和配置系统中即插即用的设备,没找到一个设备的话,bios会在显示器上显示对应的信息,同时为该设备分配中断向量表,dma通道和io设备端口等资源。在所有的硬件设备都检查完之后,系统的bios更新ESCD(拓展系统配置数据)。在ESCD更新完成之后,系统的bios将进行它的最后一项的工作,根据用户的启动的顺序来从软盘或者是硬盘等启动操作系统。以Windows XP 为例,系统BIOS将启动盘(一般是主硬盘)的第一个扇区(Boot Sector,引导扇区)读入到内存的0x7c00处,并检查0x7dfe 地址的内存,如果其内容是0xaa55,跳转到0x7c00 处执行MBR (MasterBoot Record,主引导记录),MBR 接着从分区表(Partition Table)中找到第一个活动分区(Active Partition,一般是C 盘分区),然后按照类似方式读取并执行这个活动分区的引导扇区(Partition Boot Sector),而引导扇区将负责读取并执行NTLDR (NT LoaDeR,Windows NT
的加载程序),然后主动权就移交给了Windows 。
下面是对集中启动方式的简介。
1软盘
软盘是 没有所谓的MBR的,当我们使用软盘启动电脑的时候,系统首先读取的就是第一个扇区,如果这个扇区的最后两个字节是0xaa55的话,那么就简单的叫做boot sector。所以我们所需要做的就是在启动扇区的开始处填入需要执行的机器指令,在启动扇区的最后两个字节填入0xaa55,这样就可以制作一张可启动的软盘。
《深入理解计算机系统》第三章 :Machine-Level Representation of Programs
下面是linx的内核的引导程序的分析和说明。
(写给哪些完全没有汇编基础的同志)
虚拟内存和设计及内存地址。
虚拟内存设计的目的之一就是想要提供更多的内存空间来攻程序使用,为386采用的段和页的机制也是同样的为了实现更大范围的寻址。
物理内存,在应用中,自然是顾名思义,物理上,真实的插在板子上的内存是多大就是多大了。看机器配置的时候,看的就是这个物理内存。
虚拟内存,这个概念就要稍微了解一下CPU了,^_^,只是稍微,毕竟我们现在谈的是应用中的概念。我们应该知道,对于一般的32位CPU,有32根地址线,那么它的寻址空间就是4GB。也就是说,如果没有其他的限制,我们的主板上最大可以安装4GB的物理内存。哈哈,一般的机器是不会装那么多物理内存的,大把的银子啊,性价比可合不上。程序员可不管这个,我们对CPU编程,不能一台机器根据你物理内存的大小我编一个程序吧?那也太原始社会了吧。所以程序员都是直接使用的4GB的奢侈的进程空间(或许,不应该用奢侈这么短视的词。曾几何时,128M的物理内存也是我们不可想象的呢?)。这怎么办?总不能不用那些程序了吧。好吧,这个问题交给OS去解决吧。这样,OS就提出了一个虚拟内存的概念。就是进程、用户、不必考虑实际上物理内存的限制,而直接对4GB的进程空间进行寻址。如果所寻址的数据实际上不在物理内存中,那就从“虚拟内存”中来获取。这个虚拟内存可以是一个专门文件格式的磁盘分区(比如linux下的swap分区),也可以是硬盘上的某个足够大的文件(比如win下的那个i386文件,好像是这个名字)。物理内存中长期不用的数据,也可以转移到虚拟内存中。这样的交换由OS来控制,用户看起来就好像物理内存大了一样。有了虚拟内存的概念,我们就可以自由的使用4GB的进程空间了。但是,前提是你的硬盘由足够的空间,而且你舍得划分出(4GB-物理内存)大的虚拟内存空间来。^_^。一般情况下,虚拟内存的大小,各个OS也进行了限制(比如linux的swap分区的大小,win下也可以调整虚拟内存文件的大小和位置)。所以,我们程序所能使用的存储空间大小就是:物理内存+虚拟内存。
2、CPU中的概念。
物理内存,CPU的地址线可以直接进行寻址的内存空间大小。比如8086只有20根地址线,那它的寻址空间就是1MB。我们就说8086能支持1MB的物理内存。即使我们安装了128M的内存条在板子上,我们也只能说8086拥有1MB的物理内存空间。同理32位的386以上CPU,就可以支持最大4GB的物理内存空间了。
虚拟内存,这便是一个和CPU的寻址方式有关的一个概念了。x86体系结构中,为了更好的管理内存空间,采用分段的方式来对内存进行寻址。比如8086就用两个字节的段基地址和两个字节的偏移地址来寻址整个可以寻址的内存空间,即:0000:0000方式(具体怎么计算出实际的地址,参见各种汇编教材)。这样,对整个1MB的物理内存空间寻址是没有问题了。可是,用这种方式,最大可以寻址到10FFEF这个地址。这超出了20根地址线的地址的FFEF大小的空间,就可以说是8086的虚拟内存了,所以可以说8086的虚拟内存地址空间可以达到10FFEF。^_^,具体怎么使用和看待这段内存,还取决于A20线的选通与否了,这是另外的话题了。同样的道理,386以上的CPU,由于在保护模式下使用了GDT和LDT,将段的定义放到了内存中,从而可以使用16位的段地址和32位的偏移地址。这样算来,386以上的CPU的虚拟内存地址空间就可以达到64TB了。真是大的惊人,看来,这么大的地址空间,一时还不能被软件的发展淘汰。
x86中内存段和段描述符
一、段
保护模式中80x86 提供了4GB的物理地址空间。这是处理器在其地址总线上可以寻址的地址空间。这个地址空间是平坦的,地址范围从0到0xFFFFFFFF。这个物理地址空间可以映射到读写内存、只读内存以及内存映射I/O中。分段机制就是把虚拟地址空间中的虚拟内存组织成一些长度可变的称为段的内存块单元。80386虚拟地址空间中的虚拟地址(逻辑地址)由一个段部分和一个偏移部分构成。段是虚拟地址到线性地址转换机制的基础。每个段由以下几个参数定义:
(1)段基地址(Base Address):指定段在线性地址空间中的开始地址。基地址是线性地址,对应于段中偏移0处。
(2)段限长(Limit):是虚拟地址空间中段内最大可用偏移位置。它定义了段的长度。
(3)段属性(Attributes):指定段的特性。例如该段是否可读、可写或可作为一个程序执行;段的特权级等。
段限长定义了在虚拟地址空间中段的大小。段基址和段限长定义了段所映射的线性地址范围或区域。段内0到limit的地址范围对应线性地址中范围Base到Base+Limit。偏移量大于段限长的虚拟地址是无意义的,如果使用则会导致异常。另外,若访问一个段并没有得到段属性许可则也会导致异常。例如,如果你试图写一个只读的段,那么80386就会产生一个异常。另外,多个段映射到线性地址中的范围可以部分重叠或覆盖,甚至完全重叠,如图4-6所示。在本书介绍的Linux 0.1x系统中,一个任务的代码段和数据段的段限长相同,并被映射到线性地址完全相同而重叠的区域上。
|
图4-6 虚拟(逻辑)地址空间中的段映射到线性地址空间 |
段的基地址、段限长以及段的保护属性存储在一个称为段描述符(Segment Descriptor)的结构项中。在逻辑地址到线性地址的转换映射过程中会使用这个段描述符。段描述符保存在内存中的段描述符表(Descriptor Table)中。段描述符表是包含段描述符项的一个简单数组。前面介绍的段选择符即用于通过指定表中一个段描述符的位置来指定相应的段。
即使利用段的最小功能,使用逻辑地址也能访问处理器地址空间中的每个字节。逻辑地址由16位的段选择符和32位的偏移量组成,如图4-7所示。段选择符指定字节所在的段,而偏移量指定该字节在段中相对于段基地址的位置。处理器会把每个逻辑地址转换成线性地址。线性地址是处理器线性地址空间中的32位地址。与物理地址空间类似,线性地址空间也是平坦的4GB地址空间,地址范围从0到0xFFFFFFFF。线性地址空间中含有为系统定义的所有段和系统表。
|
图4-7 逻辑地址到线性地址的变换过程 |
为了把逻辑地址转换成一个线性地址,处理器会执行以下操作:
(1)使用段选择符中的偏移值(段索引)在GDT或LDT表中定位相应的段描述符(仅当一个新的段选择符加载到段寄存器中时才需要这一步)。
(2)利用段描述符检验段的访问权限和范围,以确保该段是可访问的并且偏移量位于段界限内。
(3)把段描述符中取得的段基地址加到偏移量上,最后形成一个线性地址。
如果没有开启分页,那么处理器直接把线性地址映射到物理地址,即线性地址被送到处理器地址总线上。如果对线性地址空间进行了分页处理,那么就会使用二级地址转换把线性地址转换成物理地址。页转换将在稍后进行说明。
二、段描述符
段描述符表是段描述符的一个数组,如图4-8所示。描述符表的长度可变,最多可以包含8192个8字节描述符。有两种描述符表:全局描述符表GDT(Global Descriptor Table)和局部描述符表LDT(Local Descriptor Table)。
|
图4-8 段描述符表结构 |
描述符表存储在由操作系统维护着的特殊数据结构中,并且由处理器的内存管理硬件来引用。这些特殊结构应该保存在仅由操作系统软件访问的受保护的内存区域中,以防止应用程序修改其中的地址转换信息。虚拟地址空间被分割成大小相等的两半。一半由GDT来映射变换到线性地址,另一半则由LDT来映射。整个虚拟地址空间共含有214个段:一半空间(即213个段)是由GDT映射的全局虚拟地址空间,另一半是由LDT映射的局部虚拟地址空间。通过指定一个描述符表(GDT或LDT)以及表中描述符号,我们就可以定位一个描述符。
当发生任务切换时,LDT会更换成新任务的LDT,但是GDT并不会改变。因此,GDT所映射的一半虚拟地址空间是系统中所有任务共有的,但是LDT所映射的另一半则在任务切换时被改变。系统中所有任务共享的段由GDT来映射。这样的段通常包括含有操作系统的段以及所有任务各自的包含LDT的特殊段。LDT段可以想象成属于操作系统的数据。
图4-9表明一个任务中的段如何能在GDT和LDT之间分开。图中共有6个段,分别用于两个应用程序(A和B)以及操作系统。系统中每个应用程序对应一个任务,并且每个任务有自己的LDT。应用程序A在任务A中运行,拥有LDTA,用来映射段CodeA和DataA。类似地,应用程序B在任务B中运行,使用LDTB来映射CodeB和DataB段。包含操作系统内核的两个段CodeOS和DataOS使用GDT来映射,这样它们可以被两个任务所共享。两个LDT段:LDTA和LDTB也使用GDT来映射。
|
图4-9 任务所用的段类型 |
当任务A在运行时,可访问的段包括LDTA映射的CodeA和DataA段,加上GDT映射的操作系统的段CodeOS和DataOS。当任务B在运行时,可访问的段包括LDTB映射的CodeB和DataB段,加上GDT映射的段。
这个例子通过让每个任务使用不同的LDT,演示了虚拟地址空间如何能够被组织成隔离每个任务。当任务A在运行时,任务B的段不是虚拟地址空间的部分,因此任务A没有办法访问任务B的内存。同样地,当任务B运行时,任务A的段也不能被寻址。这种使用LDT来隔离每个应用程序任务的方法,正是关键保护需求之一。
每个系统必须定义一个GDT,并可用于系统中所有程序或任务。另外,可选定义一个或多个LDT。例如,可以为每个运行任务定义一个LDT,或者某些或所有任务共享一个LDT。
GDT本身并不是一个段,而是线性地址空间中的一个数据结构。GDT的基线性地址和长度值必须加载进GDTR寄存器中。GDT的基地址应该进行内存8字节对齐,以得到最佳处理器性能。GDT的限长以字节为单位。与段类似,限长值加上基地址可得到最后表中最后1字节的有效地址。限长为0表示有1个有效字节。因为段描述符总是8字节长,因此GDT的限长值应该设置成总是8的倍数减1(即8n-1)。
处理器并不使用GDT中的第1个描述符。把这个"空描述符"的段选择符加载进一个数据段寄存器(DS、ES、FS或GS)并不会产生一个异常,但是若使用这些加载了空描述符的段选择符访问内存时就肯定会产生一般保护性异常。通过使用这个段选择符初始化段寄存器,那么意外引用未使用的段寄存器肯定会产生一个异常。
LDT表存放在LDT类型的系统段中。此时GDT必须含有LDT的段描述符。如果系统支持多LDT的话,那么每个LDT都必须在GDT中有一个段描述符和段选择符。一个LDT的段描述符可以存放在GDT表的任何地方。
访问LDT需使用其段选择符。为了在访问LDT时减少地址转换次数,LDT的段选择符、基地址、段限长以及访问权限需要存放在LDTR寄存器中。
当保存GDTR寄存器内容时(使用SGDT指令),一个48位的"伪描述符"被存储在内存中。为了在用户模式(特权级3)避免对齐检查出错,伪描述符应该存放在一个奇字地址处(即 地址 MOD 4 = 2)。这会让处理器先存放一个对齐的字,随后是一个对齐的双字(4字节对齐处)。用户模式程序通常不会保存伪描述符,但是可以通过使用这种对齐方式来避免产生一个对齐检查出错的可能性。当使用SIDT指令保存IDTR寄存器内容时也需要使用同样的对齐方式。然而,当保存LDTR或任务寄存器(分别使用SLTR或STR指令)时,伪描述符应该存放在双字对齐的地址处(即 地址 MOD 4 = 0)。
一、分页机制
分页机制是80x86内存管理机制的第二部分。它在分段机制的基础上完成虚拟(逻辑)地址到物理地址转换的过程。分段机制把逻辑地址转换成线性地址,而分页则把线性地址转换成物理地址。分页可以用于任何一种分段模型。处理器分页机制会把线性地址空间(段已映射到其中)划分成页面,然后这些线性地址空间页面被映射到物理地址空间的页面上。分页机制有几种页面级保护措施,可和分段机制保护机制合用或替代分段机制的保护措施。例如,在基于页面的基础上可以加强读/写保护。另外,在页面单元上,分页机制还提供了用户-超级用户两级保护。
我们通过设置控制寄存器CR0的PG位可以启用分页机制。如果PG=1,则启用分页操作,处理器会使用本节描述的机制将线性地址转换成物理地址。如果PG=0,则禁用分页机制,此时分段机制产生的线性地址被直接用作物理地址。
前面介绍的分段机制在各种可变长度的内存区域上操作。与分段机制不同,分页机制对固定大小的内存块(称为页面)进行操作。分页机制把线性和物理地址空间都划分成页面。线性地址空间中的任何页面可以被映射到物理地址空间的任何页面上。图4-16示出了分页机制如何把线性和物理地址空间都划分成各个页面,并在这两个空间之间提供了任意映射。图中的箭头把线性地址空间中的页面与物理地址空间中的页面对应了起来。
|
图4-16 线性地址空间页面 |
80x86使用4K(212)字节固定大小的页面。每个页面均是4KB,并且对齐于4K地址边界处。这表示分页机制把232B(4GB)的线性地址空间划分成220(1M = 1048576)个页面。分页机制通过把线性地址空间中的页面重新定位到物理地址空间中进行操作。由于4KB大小的页面作为一个单元进行映射,并且对齐于4K边界,因此线性地址的低12位可作为页内偏移量直接作为物理地址的低12位。分页机制执行的重定位功能可看做把线性地址的高20位转换到对应物理地址的高20位。
另外,线性到物理地址的转换功能被扩展成允许一个线性地址被标注为无效的,而非让其产生一个物理地址。在两种情况下一个页面可以被标注为无效的:①操作系统不支持的线性地址;②对应在虚拟内存系统中的页面在磁盘上而非在物理内存中。在第一种情况下,产生无效地址的程序必须被终止。在第二种情况下,该无效地址实际上是请求操作系统虚拟内存管理器把对应页面从磁盘上加载到物理内存中,以供程序访问。因为无效页面通常与虚拟存储系统相关,因此它们被称为不存在的页面,并且由页表中称为存在(present)的属性来确定。
在保护模式中,80x86允许线性地址空间直接映射到大容量的物理内存(如4GB的RAM)上,或者(使用分页)间接地映射到较小容量的物理内存和磁盘存储空间中。这后一种映射线性地址空间的方法被称为虚拟存储或者需求页(Demand-paged)虚拟存储。
当使用分页时,处理器会把线性地址空间划分成固定大小的页面(长度4KB),这些页面可以映射到物理内存中或磁盘存储空间中。当一个程序(或任务)引用内存中的逻辑地址时,处理器会把该逻辑地址转换成一个线性地址,然后使用分页机制把该线性地址转换成对应的物理地址。
如果包含线性地址的页面当前不在物理内存中,处理器就会产生一个页错误异常。页错误异常的处理程序通常就会让操作系统从磁盘中把相应页面加载到物理内存中(操作过程中可能还会把物理内存中不同的页面写到磁盘上)。当页面加载到物理内存中之后,从异常处理过程的返回操作会使得导致异常的指令被重新执行。处理器用于把线性地址转换成物理地址时所需的信息及处理器产生页错误异常(若必要的话)所需的信息都存储于页目录和页表中。
分页与分段最大的不同之处在于分页使用了固定长度的页面。段的长度通常与存放在其中的代码或数据结构具有相同的长度。与段不同,页面有固定的长度。如果仅使用分段地址转换,那么存储在物理内存中的一个数据结构将包含其所有的部分。但如果使用了分页,那么一个数据结构就可以一部分存储于物理内存中,而另一部分保存在磁盘中。
为了减少地址转换所要求的总线周期数量,最近访问的页目录和页表会被存放在处理器的缓冲器件中。该缓冲器件被称为转换查找缓冲区(Translation Lookaside Buffer,TLB)。TLB可以满足大多数读页目录和页表的请求而无需使用总线周期。只有当TLB中不包含要求的页表项时才会使用额外的总线周期从内存中读取页表项,通常在一个页表项很长时间没有访问过时才会出现这种情况。
一、内存管理寄存器
处理器提供了4个内存管理寄存器(GDTR、LDTR、IDTR和TR),用于指定内存分段管理所用系统表的基地址,如图4-2所示。处理器为这些寄存器的加载和保存提供了特定的指令。有关系统表的作用请参见4.2节"保护模式内存管理"中的详细说明。
|
(点击查看大图)图4-2 内存管理寄存器 |
GDTR、LDTR、IDTR和TR都是段基址寄存器,这些段中含有分段机制的重要信息表。GDTR、IDTR和LDTR用于寻址存放描述符表的段。TR用于寻址一个特殊的任务状态段(Task State Segment,TSS)。TSS中包含着当前执行任务的重要信息。
(1)全局描述符表寄存器GDTR
GDTR寄存器中用于存放全局描述符表GDT的32位的线性基地址和16位的表限长值。基地址指定GDT表中字节0在线性地址空间中的地址,表长度指明GDT表的字节长度值。指令LGDT和SGDT分别用于加载和保存GDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。在保护模式初始化过程中必须给GDTR加载一个新值。
(2)中断描述符表寄存器IDTR
与GDTR的作用类似,IDTR寄存器用于存放中断描述符表IDT的32位线性基地址和16位表长度值。指令LIDT和SIDT分别用于加载和保存IDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。
(3)局部描述符表寄存器LDTR
LDTR寄存器中用于存放局部描述符表LDT的32位线性基地址、16位段限长和描述符属性值。指令LLDT和SLDT分别用于加载和保存LDTR寄存器的段描述符部分。包含LDT表的段必须在GDT表中有一个段描述符项。当使用LLDT指令把含有LDT表段的选择符加载进LDTR时,LDT段描述符的段基地址、段限长度以及描述符属性会被自动地加载到LDTR中。当进行任务切换时,处理器会把新任务LDT的段选择符和段描述符自动地加载进LDTR中。在机器加电或处理器复位后,段选择符和基地址被默认地设置为0,而段长度被设置成0xFFFF。
(4)任务寄存器TR
TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。
(沿用c语言的注释风格,汇编中没有相应的注释)
SYSSIZE = 0X3000 //指定编译后system模块大小
//首先boot.s被BIOS自称粗加载到0x7c00处,并将自己移动到地址0x90000的地址处。然后使用BIOS
//的中断功能来将setup.s加载到自己后面,并将system加载到0x10000处。
.global begtext, begdata, begbss, endtext, enddata, endbass //全局标示符
.text //文本段
begtext:
.data //数据段
begdata:
.bss //为初始化数据段
begbss
.text //文本段
SETUPLEN = 4 //setup程序的扇区数
BOOTSEG = 0x07C0 //boot的原始地址,段地址
INITSEG = 0x9000 //将boot移动到这里
SETUPSEG = 0x1000 //setup程序从这里开始
ENDSEG = SYSSEG + SYSSIZE
entry start //告诉连接器程序从这里开始
start:
//---------------------------------------------------------------------
//以下的代码实现的是boot.s将自身移动到0x9000,共256个字节
mov ax, #BOOTSEG //设置ds段寄存器位置0x7c0
mov ds, ax
mov ax, # INITSEG //将es段寄存器设置为0x90000
mov es, ax
mov cx, #256 //设置移动字节数
sub si, si //源地址0x07c0:0x0000
sub di, di //目的地址0x9000:0x0000
rep //重复执行,直到cx = 0
movw //移动一个字
未完待续
linux内核学习 (二)