内存映射的初始化

内存映射的初始化,第1张

内核在进入保护模式前,还没有启用分页功能,在这之前内核要先建立一个临时内核页表,因为在进入保护模式后,内核继续初始化直到建

立完整的内存映射机制之前,仍然需要用到页表来映射相应的内存地址。临时页表的初始化是在arch/i386/kernel/head.S中进行的:

swapper_pg_dir是临时页全局目录表,它是在内核编译过程中静态初始化的.

pg0是第一个页表开始的地方,它也是内核编译过程中静态初始化的.

内核通过以下代码建立临时页表:

ENTRY(startup_32)

…………

/*得到开始目录项的索引,从这可以看出内核是在swapper_pg_dir的768个表项开始进行建立的,其对应的线性地址就是0xc0000000以上的地

址,也就是内核在初始化它自己的页表*/

page_pde_offset=(__PAGE_OFFSET>>20)

/*pg0地址在内核编译的时候,已经是加上0xc0000000了,减去0xc00000000得到对应的物理地址*/

movl$(pg0-__PAGE_OFFSET),%edi

/*将目录表的地址传给edx,表明内核也要从0x00000000开始建立页表,这样可以保证从以物理地址取指令到以线性地址在系统空间取指令

的平稳过渡,下面会详细解释*/

movl$(swapper_pg_dir-__PAGE_OFFSET),%edx

movl$0x007,%eax

leal0x007(%edi),%ecx

Movl%ecx,(%edx)

movl%ecx,page_pde_offset(%edx)

addl$4,%edx

movl$1024,%ecx

11:

stosladdl$0x1000,%eax

loop11b

/*内核到底要建立多少页表,也就是要映射多少内存空间,取决于这个判断条件。在内核初始化程中内核只要保证能映射到包括内

核的代码段,数据段,初始页表和用于存放动态数据结构的128k大小的空间就行*/

leal(INIT_MAP_BEYOND_END+0x007)(%edi),%ebp

cmpl%ebp,%eax

jb10b

movl%edi,(init_pg_tables_end-__PAGE_OFFSET)

在上述代码中,内核为什么要把用户空间和内核空间的前几个目录项映射到相同的页表中去呢,虽然在head.S中内核已经进入保护模式,但是

内核现在是处于保护模式的段式寻址方式下,因为内核还没有启用分页映射机制,现在都是以物理地址来取指令,如果代码中遇到了符号地址

,只能减去0xc0000000才行,当开启了映射机制后就不用了现在cpu中的取指令指针eip仍指向低区,如果只建立内核空间中的映射,那么当

内核开启映射机制后,低区中的地址就没办法寻址了,应为没有对应的页表,除非遇到某个符号地址作为绝对转移或调用子程序为止。因此

要尽快开启CPU的页式映射机制.

movl$swapper_pg_dir-__PAGE_OFFSET,%eax

movl%eax,%cr3/*cr3控制寄存器保存的是目录表地址*/

movl%cr0,%eax/*向cr0的最高位置1来开启映射机制*/

orl$0x80000000,%eax

movl%eax,%cr0

ljmp$__BOOT_CS,$1f/*Clearprefetchandnormalize%eip*/

1:

lssstack_start,%esp

通过ljmp$__BOOT_CS,$1f这条指令使CPU进入了系统空间继续执行因为__BOOT_CS是个符号地址,地址在0xc0000000以上。

在head.S完成了内核临时页表的建立后,它继续进行初始化,包括初始化INIT_TASK,也就是系统开启后的第一个进程建立完整的中断处理程

序,然后重新加载GDT描述符,最后跳转到init/main.c中的start_kernel函数继续初始化.

3.3内核页表的完整建立

内核在start_kernel()中继续做第二阶段的初始化,因为在这个阶段中,内核已经处于保护模式下,前面只是简单的设置了内核页表,内核

必须首先要建立一个完整的页表才能继续运行,因为内存寻址是内核继续运行的前提。

pagetable_init()的代码在mm/init.c中:

[start_kernel()>setup_arch()>paging_init()>pagetable_init()]

为了简单起见,我忽略了对PAE选项的支持。

staticvoid__initpagetable_init(void)

{

……

pgd_t*pgd_base=swapper_pg_dir

……

kernel_physical_mapping_init(pgd_base)

……

}

在这个函数中pgd_base变量指向了swapper_pg_dir,这正是内核目录表的开始地址,pagetable_init()函数在通过

kernel_physical_mapping_init()函数完成内核页表的完整建立。

kernel_physical_mapping_init函数同样在mm/init.c中,我略去了与PAE模式相关的代码:

staticvoid__initkernel_physical_mapping_init(pgd_t*pgd_base)

{

unsignedlongpfn

pgd_t*pgd

pmd_t*pmd

pte_t*pte

intpgd_idx,pmd_idx,pte_ofs

pgd_idx=pgd_index(PAGE_OFFSET)

pgd=pgd_base+pgd_idx

pfn=0

for(pgd_idx<PTRS_PER_PGDpgd++,pgd_idx++){

pmd=one_md_table_init(pgd)

if(pfn>=max_low_pfn)

continue

for(pmd_idx=0pmd_idx<PTRS_PER_PMD&&pfn<max_low_pfnpmd++,pmd_idx++){

unsignedintaddress=pfn*PAGE_SIZE+PAGE_OFFSET

……

pte=one_page_table_init(pmd)

for(pte_ofs=0pte_ofs<PTRS_PER_PTE&&pfn<max_low_pfnpte++,pfn++,pte_ofs++){

if(is_kernel_text(address))

set_pte(pte,pfn_pte(pfn,PAGE_KERNEL_EXEC))

else

set_pte(pte,pfn_pte(pfn,PAGE_KERNEL))

……

}

}

通过作者的注释,可以了解到这个函数的作用是把整个物理内存地址都映射到从内核空间的开始地址,即从0xc0000000的整个内核空间中,

直到物理内存映射完毕为止。这个函数比较长,而且用到很多关于内存管理方面的宏定义,理解了这个函数,就能大概理解内核是如何建立

页表的,将这个抽象的模型完全的理解。下面将详细分析这个函数:

函数开始定义了4个变量pgd_t*pgd,pmd_t*pmd,pte_t*pte,pfn

pgd指向一个目录项开始的地址,pmd指向一个中间目录开始的地址,pte指向一个页表开始的地址pfn是页框号被初始为0.pgd_idx根据

pgd_index宏计算结果为768,也是内核要从目录表中第768个表项开始进行设置。从768到1024这个256个表项被linux内核设置成内核目录项,

低768个目录项被用户空间使用.pgd=pgd_base+pgd_idxpgd便指向了第768个表项。

然后函数开始一个循环即开始填充从768到1024这256个目录项的内容。

one_md_table_init()函数根据pgd找到指向的pmd表。

它同样在mm/init.c中定义:

staticpmd_t*__initone_md_table_init(pgd_t*pgd)

{

pmd_t*pmd_table

#ifdefCONFIG_X86_PAE

pmd_table=(pmd_t*)alloc_bootmem_low_pages(PAGE_SIZE)

set_pgd(pgd,__pgd(__pa(pmd_table)|_PAGE_PRESENT))

if(pmd_table!=pmd_offset(pgd,0))

BUG()

#else

pmd_table=pmd_offset(pgd,0)

#endif

returnpmd_table

}

可以看出,如果内核不启用PAE选项,函数将通过pmd_offset返回pgd的地址。因为linux的二级映射模型,本来就是忽略pmd中间目录表的。

接着又个判断语句:

>>if(pfn>=max_low_pfn)

>>continue

这个很关键,max_low_pfn代表着整个物理内存一共有多少页框。当pfn大于max_low_pfn的时候,表明内核已经把整个物理内存都映射到了系

统空间中,所以剩下有没被填充的表项就直接忽略了。因为内核已经可以映射整个物理空间了,没必要继续填充剩下的表项。

紧接着的第2个for循环,在linux的3级映射模型中,是要设置pmd表的,但在2级映射中忽略,只循环一次,直接进行页表pte的设置。

>>address=pfn*PAGE_SIZE+PAGE_OFFSET

address是个线性地址,根据上面的语句可以看出address是从0xc000000开始的,也就是从内核空间开始,后面在设置页表项属性的时候会用

到它.

>>pte=one_page_table_init(pmd)

根据pmd分配一个页表,代码同样在mm/init.c中:

staticpte_t*__initone_page_table_init(pmd_t*pmd)

{

if(pmd_none(*pmd)){

pte_t*page_table=(pte_t*)alloc_bootmem_low_pages(PAGE_SIZE)

set_pmd(pmd,__pmd(__pa(page_table)|_PAGE_TABLE))

if(page_table!=pte_offset_kernel(pmd,0))

BUG()

returnpage_table

}

returnpte_offset_kernel(pmd,0)

}

pmd_none宏判断pmd表是否为空,如果为空则要利用alloc_bootmem_low_pages分配一个4k大小的物理页面。然后通过set_pmd(pmd,__pmd

(__pa(page_table)|_PAGE_TABLE))来设置pmd表项。page_table显然属于线性地址,先通过__pa宏转化为物理地址,在与上_PAGE_TABLE宏,

此时它们还是无符号整数,在通过__pmd把无符号整数转化为pmd类型,经过这些转换,就得到了一个具有属性的表项,然后通过set_pmd宏设

置pmd表项.

接着又是一个循环,设置1024个页表项。

is_kernel_text函数根据前面提到的address来判断address线性地址是否属于内核代码段,它同样在mm/init.c中定义:

staticinlineintis_kernel_text(unsignedlongaddr)

{

if(addr>=(unsignedlong)_stext&&addr<=(unsignedlong)__init_end)

return1

return0

}

_stext,__init_end是个内核符号,在内核链接的时候生成的,分别表示内核代码段的开始和终止地址.

如果address属于内核代码段,那么在设置页表项的时候就要加个PAGE_KERNEL_EXEC属性,如果不是,则加个PAGE_KERNEL属性.

#define_PAGE_KERNEL_EXEC\

(_PAGE_PRESENT|_PAGE_RW|_PAGE_DIRTY|_PAGE_ACCESSED)

#define_PAGE_KERNEL\

(_PAGE_PRESENT|_PAGE_RW|_PAGE_DIRTY|_PAGE_ACCESSED|_PAGE_NX)

最后通过set_pte(pte,pfn_pte(pfn,PAGE_KERNEL))来设置页表项,先通过pfn_pte宏根据页框号和页表项的属性值合并成一个页表项值,

然户在用set_pte宏把页表项值写到页表项里。

当pagetable_init()函数返回后,内核已经设置好了内核页表,紧着调用load_cr3(swapper_pg_dir)

#defineload_cr3(pgdir)\

asmvolatile(movl%0,%%cr3::r(__pa(pgdir)))

将控制swapper_pg_dir送入控制寄存器cr3.每当重新设置cr3时,CPU就会将页面映射目录所在的页面装入CPU内部高速缓存中的TLB部分.现

在内存中(实际上是高速缓存中)的映射目录变了,就要再让CPU装入一次。由于页面映射机制本来就是开启着的,所以从这条指令以后就扩大

了系统空间中有映射区域的大小,使整个映射覆盖到整个物理内存(高端内存)除外.实际上此时swapper_pg_dir中已经改变的目录项很可能还

在高速缓存中,所以还要通过__flush_tlb_all()将高速缓存中的内容冲刷到内存中,这样才能保证内存中映射目录内容的一致性。

3.4对如何构建页表的总结

通过上述对pagetable_init()的剖析,我们可以清晰的看到,构建内核页表,无非就是向相应的表项写入下一级地址和属性。在内核空间

保留着一部分内存专门用来存放内核页表.当cpu要进行寻址的时候,无论在内核空间,还是在用户空间,都会通过这个页表来进行映射。对于

这个函数,内核把整个物理内存空间都映射完了,当用户空间的进程要使用物理内存时,岂不是不能做相应的映射了?其实不会的,内核

只是做了映射,映射不代表使用,这样做是内核为了方便管理内存而已。

保护模式

(Protected Mode,或有时简写为 pmode) 是一种 80286 系列和之后的 x86 兼容 CPU *** 作模式。保护模式有一些新的特色,设计用来增强 多工 和系统稳定度,像是 内存保护,分页 系统,以及硬件支援的 虚拟内存。大部分的现今 x86 *** 作系统 都在保护模式下运行,包含 Linux、FreeBSD、以及 微软 Windows 2.0 和之后版本。

另外一种 286 和其之后 CPU 的 *** 作模式是 真实模式,一种向前兼容且关闭这些特色的模式。设计用来让新的芯片可以执行旧的软件。依照设计的规格,所有的 x86 CPU 都是在真实模式下开机来确保传统 *** 作系统的向前兼容性。在任何保护模式的特色可用前,他们必须要由某些程序手动地切换到保护模式。在现今的电脑,这种切换通常是由 *** 作系统 在开机时候必须完成的第一件工作的一个。它也可能当 CPU 在保护模式下运行时,使用 虚拟86模式 来执行设计给真实模式的程序码。

尽管用软件的方式也有某些可能在真实模式的系统下使用多工,但保护模式下内存保护的特色,可以避免有问题的程序破坏其他工作或是 *** 作系统 核心所拥有的内存。保护模式也有中断正在执行程序的硬件支援,可以把 execution content 交给其他工作,得以实现 先占式多工。

大部分可以使用保护模式的 CPU 也拥有 32 位元暂存器 的特色 (例如 80386 系列和其后任何的芯片),导入了融合保护模式而成为 32 位元处理的概念。80286 芯片虽有支援保护模式,但是仍然只有 16 位元暂存器。Windows 2.0 和之后版本中的保护模式增强称为 "386 增强模式",是因为他们除了保护模式外,还需要 32 位元的暂存器,并且无法在 286 上面执行 (即使 286 支援保护模式)。

即使在 32 位元芯片上已经打开了保护模式,但是 1 MB 以上的内存并无法存取,是由于一种仿照 IBM XT 系统设计特性的 memory wrap-around(内存连续) 的因素。这种限制可以由打开 A20 line 来回避。

在保护模式下,前面 32 个中断都是保留给 CPU 例外处理用。举个例子,中断 0D (十进制 13) 是 一般保护模式错物 和 中断 00 是 除以零。

在8086/8088时代,处理器只存在一种 *** 作模式(Operation Mode),当时由于不存在其它 *** 作模式,因此这种模式也没有被命名。自从80286到80386开始,处理器增加了另外两种 *** 作模式——保护模式PM (Protected Mode)和系统管理模式SMM(System Management Mode),因此,8086/8088的模式被命名为实地址模式RM(Real-address Mode)。

PM是处理器的native模式,在这种模式下,处理器支持所有的指令和所有的体系结构特性,提供最高的性能和兼容性。对于所有的新型应用程序和 *** 作系统来说,建议都使用这种模式。为了保证PM的兼容性,处理器允许在受保护的,多任务的环境下执行RM程序。这个特性被称做虚拟8086模式(Virtual -8086 Mode),尽管它并不是一个真正的处理器模式。Virtual-8086模式实际上是一个PM的属性,任何任务都可以使用它。

RM提供了Intel 8086处理器的编程环境,另外有一些扩展(比如切换到PM或SMM的能力)。当主机被Power-up或Reset后,处理器处于RM下。

SMM是一个对所有Intel处理器都统一的标准体系结构特性。出现于Intel386 SL芯片。这个模式为OS实现平台指定的功能(比如电源管理或系统安全)提供了一种透明的机制。当外部的SMM interrupt pin(SMI#)被激活或者从APIC(Advanced Programming Interrupt Controller)收到一个SMI,处理器将进入SMM。在SMM下,当保存当前正在运行程序的整个上下文(Context)时,处理器切换到一个分离的地址空间。然后SMM指定的代码或许被透明的执行。当从SMM返回时,处理器将回到被系统管理中断之前的状态。

由于机器在Power-up或Reset之后,处理器处于RM状态,而对于Intel 80386以及其后的芯片,只有使用PM才能发挥出最大的作用。所以我们就面临着一个从RM切换到PM的问题。

本文不讨论SMM,本节的重点集中于在Booting阶段如何从RM切换到PM,这里不会过多的讨论PM的细节,因为《Intel Architecture Software Developer’s Manual Volume 3: System Programming》中有非常详尽和准确的介绍。

1. What is GDT

在Protected Mode下,一个重要的必不可少的数据结构就是GDT(Global Descriptor Table)。

为什么要有GDT?我们首先考虑一下在Real Mode下的编程模型:

在Real Mode下,我们对一个内存地址的访问是通过Segment:Offset的方式来进行的,其中Segment是一个段的Base Address,一个Segment的最大长度是64 KB,这是16-bit系统所能表示的最大长度。而Offset则是相对于此Segment Base Address的偏移量。Base Address+Offset就是一个内存绝对地址。由此,我们可以看出,一个段具备两个因素:Base Address和Limit(段的最大长度),而对一个内存地址的访问,则是需要指出:使用哪个段?以及相对于这个段Base Address的Offset,这个Offset应该小于此段的Limit。当然对于16-bit系统,Limit不要指定,默认为最大长度64KB,而 16-bit的Offset也永远不可能大于此Limit。我们在实际编程的时候,使用16-bit段寄存器CS(Code Segment),DS(Data Segment),SS(Stack Segment)来指定Segment,CPU将段积存器中的数值向左偏移4-bit,放到20-bit的地址线上就成为20-bit的Base Address。

到了Protected Mode,内存的管理模式分为两种,段模式和页模式,其中页模式也是基于段模式的。也就是说,Protected Mode的内存管理模式事实上是:纯段模式和段页式。进一步说,段模式是必不可少的,而页模式则是可选的——如果使用页模式,则是段页式;否则这是纯段模式。

既然是这样,我们就先不去考虑页模式。对于段模式来讲,访问一个内存地址仍然使用Segment:Offset的方式,这是很自然的。由于 Protected Mode运行在32-bit系统上,那么Segment的两个因素:Base Address和Limit也都是32位的。IA-32允许将一个段的Base Address设为32-bit所能表示的任何值(Limit则可以被设为32-bit所能表示的,以2^12为倍数的任何指),而不象Real Mode下,一个段的Base Address只能是16的倍数(因为其低4-bit是通过左移运算得来的,只能为0,从而达到使用16-bit段寄存器表示20-bit Base Address的目的),而一个段的Limit只能为固定值64 KB。另外,Protected Mode,顾名思义,又为段模式提供了保护机制,也就说一个段的描述符需要规定对自身的访问权限(Access)。所以,在Protected Mode下,对一个段的描述则包括3方面因素:[Base Address, Limit, Access],它们加在一起被放在一个64-bit长的数据结构中,被称为段描述符。这种情况下,如果我们直接通过一个64-bit段描述符来引用一个段的时候,就必须使用一个64-bit长的段积存器装入这个段描述符。但Intel为了保持向后兼容,将段积存器仍然规定为16-bit(尽管每个段积存器事实上有一个64-bit长的不可见部分,但对于程序员来说,段积存器就是16-bit的),那么很明显,我们无法通过16-bit长度的段积存器来直接引用64-bit的段描述符。

怎么办?解决的方法就是把这些长度为64-bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上,是将段寄存器中的高13 -bit的内容作为索引)。这个全局的数组就是GDT。事实上,在GDT中存放的不仅仅是段描述符,还有其它描述符,它们都是64-bit长,我们随后再讨论。

GDT可以被放在内存的任何位置,那么当程序员通过段寄存器来引用一个段描述符时,CPU必须知道GDT的入口,也就是基地址放在哪里,所以Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此积存器,从此以后,CPU就根据此积存器中的内容作为GDT的入口来访问GDT了。

GDT是Protected Mode所必须的数据结构,也是唯一的——不应该,也不可能有多个。另外,正象它的名字(Global Descriptor Table)所揭示的,它是全局可见的,对任何一个任务而言都是这样。

除了GDT之外,IA-32还允许程序员构建与GDT类似的数据结构,它们被称作LDT(Local Descriptor Table),但与GDT不同的是,LDT在系统中可以存在多个,并且从LDT的名字可以得知,LDT不是全局可见的,它们只对引用它们的任务可见,每个任务最多可以拥有一个LDT。另外,每一个LDT自身作为一个段存在,它们的段描述符被放在GDT中。

IA-32为LDT的入口地址也提供了一个寄存器LDTR,因为在任何时刻只能有一个任务在运行,所以LDT寄存器全局也只需要有一个。如果一个任务拥有自身的LDT,那么当它需要引用自身的LDT时,它需要通过LLDT将其LDT的段描述符装入此寄存器。LLDT指令与LGDT指令不同的时,LGDT指令的 *** 作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而LLDT指令的 *** 作数是一个16-bit的选择子,这个选择子主要内容是:被装入的LDT的段描述符在GDT中的索引值——这一点和刚才所讨论的通过段积存器引用段的模式是一样的。

LDT只是一个可选的数据结构,你完全可以不用它。使用它或许可以带来一些方便性,但同时也带来复杂性,如果你想让你的OS内核保持简洁性,以及可移植性,则最好不要使用它。

引用GDT和LDT中的段描述符所描述的段,是通过一个16-bit的数据结构来实现的,这个数据结构叫做Segment Selector——段选择子。它的高13位作为被引用的段描述符在GDT/LDT中的下标索引,bit 2用来指定被引用段描述符被放在GDT中还是到LDT中,bit 0和bit 1是RPL——请求特权等级,被用来做保护目的,我们这里不详细讨论它。

前面所讨论的装入段寄存器中作为GDT/LDT索引的就是Segment Selector,当需要引用一个内存地址时,使用的仍然是Segment:Offset模式,具体 *** 作是:在相应的段寄存器装入Segment Selector,按照这个Segment Selector可以到GDT或LDT中找到相应的Segment Descriptor,这个Segment Descriptor中记录了此段的Base Address,然后加上Offset,就得到了最后的内存地址。如下图所示:

2. Setup GDT

由上一节的讨论得知,GDT是Protected Mode所必须的数据结构,那么我们在进入Protected Mode之前,必须设定好GDT,并通过LGDT将其装入相应的寄存器。

尽管GDT允许被放在内存的任何位置,但由于GDT中的元素——描述符——都是64-bit长,也就是说都是8个字节,所以为了让CPU对GDT的访问速度达到最快,我们应该将GDT的入口地址放在以8个字节对齐,也就是说是8的倍数的地址位置。

GDT中第一个描述符必须是一个空描述符,也就是它的内容应该全部为0。如果引用这个描述符进行内存访问,则是产生General Protection异常。

如果一个OS不使用虚拟内存,段模式会是一个不错的选择。但现代OS没有不使用虚拟内存的,而实现虚拟内存的比较方便和有效的内存管理方式是页式管理。但是在IA-32上如果我们想使用页式管理,我们只能使用段页式——没有方法可以完全禁止段模式。但我们可以尽力让段的效果降低的最小。

IA-32提供了一种被称作“Basic Flat Model”的分段模式可以达到这种效果。这种模式要求在GDT中至少要定义两个段描述符,一个用来引用Data Segment,另一个用来引用Code Segment。这2个Segment都包含整个线性空间,即Segment Limit = 4 GB,即使实际的物理内存远没有那么多,但这个空间定义是为了将来由页式管理来实现虚拟内存。

在这里,我们只是处于Booting阶段,所以我们只需要初步设置一下GDT,等真正进入Protected Mode,启动了OS Kernel之后,具体OS打算如何设置GDT,使用何种内存管理模式,由Kernel自身来设置,Booting只需要给Kernel的数据段和代码段设置全部线性空间就可以了。

段描述符的格式如下图所示:

具体到代码段和数据段,它们的格式如下图所示:

下面就是在Booting阶段为进入Protected Mode而设置的临时的gdt。这里定义了3个段描述符:第一个是系统规定的空描述符,第2个是引用4 GB线性空间的代码段,第3个是引用4 GB线性空间的数据段。这是"Basic Flat Model"所要求的最下GDT设置,但就booting阶段,只是为了进入Protected Mode,并为内核提供一个连续的,最大的线性空间这个目的而言,已经足够了。

# Descriptor tables

gdt:

.word 0, 0, 0, 0 # dummy

.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)

3. Load GDT

设置好GDT之后,我们需要通过LGDT指令将设定的gdt的入口地址和gdt表的大小装入GDTR寄存器。

GDTR寄存器包括两部分:32-bit的线性基地址,以及16-bit的GDT大小(以字节为单位)。需要注意的是,对于32-bit线性基地址,必须是32-bit绝对物理地址,而不是相对于某个段的偏移量。而我们在Booting阶段,在进入Protected Mode之前,我们CS和DS设置很可能不是0,所以我们必须计算出gdt的绝对物理地址。

为了执行LGDT指令,你需要把这两部分内容放在内存的某个位置,然后将这个位置的内存地址作为 *** 作数传递给LGDT指令。然后LGDT指令会自动将保存在这个位置的这两部分值装入GDTR寄存器。

# 这是存放GDTR所需的两部分内容的位置

gdt_48:

.word 0x8000 # gdt limit=2048,

# 256 GDT entries

.word 0, 0 # gdt base (filled in later)

# 下面这段代码用来计算GDT的32-bit线性地址,并将其装入GDTR寄存器。

xorl %eax, %eax # Compute gdt_base

movw %ds, %ax # (Convert %ds:gdt to a linear ptr)

shll , %eax

addl $gdt, %eax

movl %eax, (gdt_48+2)

lgdt gdt_48 # load gdt with whatever is appropriate

4. Other Preparing Stuff

在进入Protected Mode之前,除了需要设置和装入GDT之外,还需要做如下一些事情:

屏蔽所有可屏蔽中断;

装入IDTR;

所有协处理器被正确的Reset。

由于在Real Mode和Protected Mode下的中断处理机制有一些不同,所以在进入Protected Mode之前,务必禁止所有可屏蔽中断,这可以通过下面两种方法之一:

使用CLI指令;

对8259A可编程中断控制器编程以屏蔽所有中断。

即使当我们进入Protected Mode之后,也不能马上将中断打开,这时因为我们必须在OS Kernel中对相关的Protected Mode中断处理所需的数据结构正确的初始化之后,才能打开中断,否则会产生处理器异常。

在Real Mode下,中断处理使用IVT(Interrupt Vector Table),在Protected Mode下,中断处理使用IDT(Interrupt Descriptor Table),所以,我们必须在进入Protected Mode之前设置IDTR。

IDTR的格式和GDTR相同,IDTR的装入方式和GDTR也相同。由于IDT中相关的中断处理程序需要让OS Kernel来设定,所以在Booting阶段,我们只需要将IDTR中IDT的基地址和Size都设为0就可以了,随后,等进入Protected Mode之后,由OS Kernel来真正设置它。

关于中断机制和中断处理,请参考 Interrupt &Exception ,这里不再赘述。

#

# 这是存放IDTR所需的两部分内容的位置

#

idt_48:

.word 0 # idt limit = 0

.word 0, 0 # idt base = 0L

# 对于IDTR的处理,只需要这一条指令即可

lidt idt_48 # load idt with 0,0

#

# 通过设置8259A PIC,屏蔽所有可屏蔽中断

#

movb xFF, %al # mask all interrupts for now

outb %al, xA1

call delay

movb xFB, %al # mask all irq's but irq2 which

outb %al, x21 # is cascaded

# 保证所有的协处理都被正确的Reset

xorw %ax, %ax

outb %al, xf0

call delay

outb %al, xf1

call delay

# Delay is needed after doing I/O

delay:

outb %al,x80

ret

5. Let's Go

好了,一切准备就绪,Fire!:)

进入Protected Mode,还是进入Real Mode,完全靠CR0寄存器的PE标志位来控制:如果PE=1,则CPU切换到PM,否则,则进入RM。

设置CR0-PE位的方法有两种:

第一种是80286所使用的LMSW指令,后来的80386及更高型号的CPU为了保持向后兼容,都保留了这个指令。这个指令只能影响最低的4 bit,即PE,MP,EM和TS,对其它的没有影响。

#

#通过LMSW指令进入Protected Mode

#

movw , %ax # protected mode (PE) bit

lmsw %ax # This is it!

第二种是Intel所建议的在80386以后的CPU上使用的进入PM的方式,即通过MOV指令。MOV指令可以设置CR0寄存器的所有域的值。

#

#通过MOV指令进入Protected Mode

#

movl %cr0, %eax

xorb , %al # set PE = 1

movl %eax, %cr0 # go!!

OK,现在已经进入Protected Mode了。

很简单,right?But It's not over yet!

6. Start Kernel

我们已经从Real Mode进入Protected Mode,现在我们马上就要启动OS Kernel了。

OS Kernel运行在32-bit段模式,而当前我们却仍然处于16-bit段模式。这是怎么回事?为了了解这个问题,我们需要仔细探讨一下IA-32的段模式的实现方法。

IA-32共提供了6个16-bit段寄存器:CS,DS,SS,ES,FS,GS。但事实上,这16-bit只是对程序员可见的部分,但每个寄存器仍然包括64-bit的不可见部分。

可见部分是为了供程序员装载段寄存器,但一旦装载完成,CPU真正使用的就只是不可见部分,可见部分就完全没有用了。

不可见部分存放的内容是什么?具体格式我没有看到相关资料,但可以确定的是隐藏部分的内容和段描述符的内容是一致的(请参考段描述的格式),只不过格式可能不完全相同。但格式对我们理解这一点并不重要,因为程序员不可能能够直接 *** 作它。

我们以CS寄存器为例,对于其它寄存器也是一样的:

在Real Mode下,当我们执行一个装载CS寄存器的指令的时候(jmp,call,ret等),相关的值会被装入CS寄存器的可见部分,但同时CPU也会根据可见部分的内容来设置不可见部分。比如我们执行"ljmp x1234, $go "之后,CS寄存器的可见部分的内容就是1234h,同时,不可见部分的32-bit Base Address域被设置为00001234h,20-bit的Limit域被设置为固定值10000h,也就是64 KB,Access Information部分的其它值我们不去考虑,只考虑其D/B位,由于执行此指令时处于Real Mode模式,所以D/B被设置为0,表示此段是一个16-bit段。当对CS寄存器的可见部分和不可见部分的内容都被设置之后,CS寄存器的装载工作完成。随后当CPU需要通过CS的内容进行地址运算的时候,则仅仅引用不可见部分。

在Protected Mode下,当我们执行一个装载CS寄存器的指令的时候,段选择子(Segment Selector)被装入CS寄存器的可见部分,同时CPU根据此选择子到相应的描述符表中(GDT或LDT)找到相应的段描述符并将其内容装载入CS寄存器的不可见部分。随后CPU当需要通过CS的内容进行地址运算的时候,也仅仅引用不可见部分。

从上面的描述可以看出,事实上CPU在引用段寄存器的内容进行地址运算时,Real Mode和Protected Mode是一致的。另外,也明白了为什么我们在Real Mode下设置的段寄存器的内容到了Protected Mode下仍然引用的是16-bit段。

那么我们如何将CS设置为引用32-bit段?方法就像我们前面所讨论的,使用jmp或call指令,引用一个段选择子,到GDT中装载一个引用32-bit段的段描述符。

需要注意的是,如果CS寄存器的内容指出当前是一个16-bit段,那么当前的地址模式也就是16-bit地址模式,这与你当前是出于Real Mode还是Protected Mode无关。而我们装载32-bit段的jmp指令或call指令必须使用的是32-bit地址模式。而我们当前的boot部分代码是16-bit代码,所以我们必须在此jmp/call指令前加上地址转换前缀代码66h。

下面的例子就是使用jmp指令装入32-bit段。Jmpi指令的含义是段间跳转,其Opcode为Eah,其格式为:jmpi Offset, Segment Selector。

# 由于当前的代码是16-bit代码,而我们要执行32-bit地址模式的指令,指令前

# 需要有地址模式切换前缀66h,如果我们直接写jmp指令,由编译器来生成代码

# 的话,是无法作到这一点的,所以我们直接写相关数据。

.byte 0x66, 0xea # prefix + jmpi-opcode

.long 0x1000 # Offset

.word __KERNEL_CS # CS segment selector

上面的代码相当于32-bit指令:

jmpi 0x1000,__KERNEL_CS

如果__KERNEL_CS段选择子所引用的段描述符设置的段空间为线形地址[0,4 GB],而我们将OS Kernel放在物理地址1000h,那么此jmpi指令就跳转到OS Kernel的入口处,并开始执行它。

此时,Booting阶段结束,OS正式开始运行!

使用notifier_call_chain向其它部分发出重启的消息,然后调用machine_restart函数完成重启。

machine_restart函数的开始部分有一段SMP相关的代码,主要完成多CPU时由一个CPU完成重启 *** 作,其它CPU处于等待状态。之后系统根据一个变量reboot_thru_bios的内容判断重启方式,通过阅读reboot_setup我们可以得知,这个参数的内容是在系统启动时指定的,决定了是否利用bios,事实上是系统复位后的入口(FFFF:0000)地址的程序进行重启。在不通过bios进行重启的情况下,系统首先设定了重启标志,然后向端口0xfe写入数字0x64,这种重启的具体原理我还不大清楚,似乎是模拟了一次reset键的按下,希望大家和我讨论。在通过bios重启的情况下,系统同样先设定了重启模式,然后切换到了实模式,通过一条ljmp $0xffff,$0x0完成了重启。


欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/yw/7163976.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-02
下一篇 2023-04-02

发表评论

登录后才能评论

评论列表(0条)

保存