Chinaunix首页 | 论坛 | 博客
  • 博客访问: 519367
  • 博文数量: 184
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1172
  • 用 户 组: 普通用户
  • 注册时间: 2016-06-21 13:40
个人简介

技术改变命运

文章分类

全部博文(184)

文章存档

2020年(16)

2017年(12)

2016年(156)

我的朋友

分类: LINUX

2016-08-24 10:14:54

原文地址:linux的swapper_pg_dir的初始化 作者:雾琰

Linux的启动涉及到一个解压与定位的过程,对于x86体系结构而言,系统被加载到0x100000的地方,那么swapper_pg_dir的值是什么呢?我们知道swapper_pg_dir是一个很重要的东西,它是所有进程内核空间的页表的模板,而且在涉及到896M以上的内存分配时,swapper_pg_dir也是一个同步的根,这些内存分配包括vmalloc区,高端永久区,高端临时区等。这里需要说明的是,swapper_pg_dir这个东西其实就是一个页目录的指针,页目录指针在x86中是要被加载到cr3寄存器的,每个进程都有一个页目录指针,这个指针指示这个进程的内存映射信息,每当切换到一个进程时,该进程的页目录指针就被加载到了cr3,然后直到切换到别的进程的时候才更改,既然swapper_pg_dir是一个页目录指针,那么这个指针是被哪个进程用的呢?现代操作系统的含义指示了进程间内存隔离,那么一个页目录指针只能被一个进程使用,那么到底是哪个特定的进程使用了swapper_pg_dir指针呢?遗憾的是,答案是没有任何用户进程使用swapper_pg_dir作为页目录指针,swapper_pg_dir只是在内核初始化的时候被载入到cr3指示内存映射信息,之后在init进程启动后就成了idle内核线程的页目录指针了,/sbin/init由一个叫做init的内核线程exec而成,而init内核线程是原始的内核也就是后来的idle线程do_fork而成的,而在do_fork中会为新生的进程重启分配一个页目录指针,由此可见swapper_pg_dir只是在idle和内核线程中被使用,可是它的作用却不只是为idle进程指示内存映射信息,更多的,它作为一个内核空间的内存映射模板而存在,在linux中,任何进程在内核空间就不分彼此了,所有的进程都会公用一份内核空间的内存映射,因此,内核空间是所有进程共享的,每当一个新的进程建立的时候,都会将swapper_pg_dir的768项以后的信息全部复制到新进程页目录的768项以后,代表内核空间。另外在操作3G+896M以上的虚拟内存时,只会更改swapper_pg_dir的映射信息,当别的进程访问到这些页面的时候会发生缺页,在缺页处理中会与swapper_pg_dir同步。


了解到swapper_pg_dir的意义与实际作用,我们来看一下它的初始化吧,首先要看它的定义,在arch/i386/kernel/head.S中:

.org 0x1000

ENTRY(swapper_pg_dir)

.long 0x00102007//第一个页目录项,指示前4M的页面映射信息,0x00102000是前4M页表所在的物理页,而0x00000007是访问控制权限,二者相加构成一个页表,以下同义

.long 0x00103007 //第二个页目录项,指示4-8M的页面映射信息

.fill BOOT_USER_PGD_PTRS-2,4,0 //填充到内核边界

.long 0x00102007 //第768个页目录项,指示前4M的页面映射信息

.long 0x00103007 //第769个页目录项,指示第4-8M的页面映射信息

...

org0x1000说明了让系统将swapper_pg_dir加载到地址0x1000处,可是内核最终搬到了0x100000处,那么swapper_pg_dir也就到了0x101000处,现在物理地址已经搞定了,那么最终进入保护模式并且启动分页时,swapper_pg_dir的虚拟地址会在哪里呢?我们看一下内核加载到了哪里然后加上0x1000就是swapper_pg_dir加载到的虚拟地址了,在vmlinux.lds中可以看出内核被加载到了0xc0100000处,于是swapper_pg_dir加载的虚拟地址就是0xc0101000,在初始化的时候,内核将0到8M的物理内存分别映射到了虚拟地址的0到8M和3G到3G+8M两个地方,而且过了初始化阶段到了最终的稳定页表,也同样有前XM的物理内存一一映射到虚拟内存的3G+XM的地方,于是swapper_pg_dir的虚拟地址就是0xc0101000,我们可以打印出swapper_pg_dir看看到底是多少,不幸的是,swapper_pg_dir并不从内核导出,那么怎么办呢?难道非要将打印信息加入内核启动函数然后从新编译一遍内核吗?其实不用,虽然swapper_pg_dir没有被导出,可是init_mm被导出了啊,我们知道init_mm就是内核启动时也就是idle进程的mm_struct,其中一个字段是pgd,就是swapper_pg_dir,我们可以打印init_mm->pgd的值,看看是多少。

事情到此还没有结束,如果你真的写了一个模块,并且打印init_mm.pdg的话,发现可能它并不是期望的0xc0101000,怎么回事呢?不要急,遇到这种情况比遇到内核莫名其妙的down掉要好处理的多,再说这并不影响我们的生活,即使你最终没有弄清楚这是怎么一回事,那么也不会有什么损失的。这种情况好解决的原因还有就是事情发生在内核初始化的阶段,也就是说swapper_pg_dir的初始化在内核初始化阶段,并且以后也不会变化,可能它的内容会变,但是其本身的位置是不会变化的,因此,几乎不用调试,光看代码就可以解决问题,我们搜索一下近来的Changelog,发现在2.6.6中将swapper_pg_dir从原来的固定的.org0x1000的位置移到了BSS段当中了,因为bss段仅仅拥有占位符而不占用映像静态空间,它的真正数据并没有初始化,因此交给操作系统初始化就可以了,既然没有初始化的数据,那么就没有必要在静态的映像中占据空间,而是让操作系统将其载入内存时将bss清零即可,linux内核本身就是操作系统内核,因此它自己负责在启动的时候将bss段清零,因为初始化的时候,临时页表也就需要两个页目录用来映射物理内存的0到8M,这两个页目录很简单,一点不复杂,没有必要写死到内核映像从而占据着3个页面的空间,因此放到bss段中就可以节省3页面的空间,然后在内核启动过程中再手动初始化那两个页目录的值,这样做十分有意义。那么bss被加载到哪里就决定了swapper_pg_dir被加载到了哪里,那么bss到底在哪呢?从arch/i386/kernel/vmlinux.lds.S中可以大致知道答案,如果想知道更加确切的,可以参考/boot/System.map文件,然后可以再打印一下init_mm.pgd的值,看看是不是在bss里面,其实都不用打印,在System.map里面就有swapper_pg_dir的值,该值在2.6.6之后的内核肯定在bss中,之前的肯定是0xc0101000。最后我们看一下2.6.6以后的swapper_pg_dir的定义: 
.section ".bss.page_aligned","w" 
ENTRY(swapper_pg_dir) 
       .fill 1024,4,0



什么是保护模式

自从1969年推出第一个微处理器以来,Intel处理器就在不断地更新换代,从8086、8088、80286,到80386、80486、奔腾、奔腾Ⅱ、奔腾4等,其体系结构也在不断变化。80386以后,提供了一些新的功能,弥补了8086的一些缺陷。这其中包括内存保护、多任务及使用640KB以上的内存等,并仍然保持和8086家族的兼容性。也就是说80386仍然具备了8086和80286的所有功能,但是在功能上有了很大的增强。早期的处理器是工作在实模式之下的,80286以后引入了保护模式,而在80386以后保护模式又进行了很大的改进。在80386中,保护模式为程序员提供了更好的保护,提供了更多的内存。事实上,保护模式的目的不是为了保护程序,而是要保护程序以外的所有程序(包括操作系统)。 

简言之,保护模式是处理器的一种最自然的模式。在这种模式下,处理器的所有指令及体系结构的所有特色都是可用的,并且能够达到最高的性能。 

保护模式和实模式

从表面上看,保护模式和实模式并没有太大的区别,二者都使用了内存段、中断和设备驱动来处理硬件,但二者有很多不同之处。我们知道,在实模式中内存被划分成段,每个段的大小为64KB,而这样的段地址可以用16位来表示。内存段的处理是通过和段寄存器相关联的内部机制来处理的,这些段寄存器(CS、DS、SS和ES)的内容形成了物理地址的一部分。具体来说,最终的物理地址是由16位的段地址和16位的段内偏移地址组成的。用公式表示为: 

物理地址=左移4位的段地址+偏移地址。


在保护模式下,段是通过一系列被称之为“描述符表”的表所定义的。段寄存器存储的是指向这些表的指针。用于定义内存段的表有两种:全局描述符表(GDT)和局部描述符表(LDT)。GDT是一个段描述符数组,其中包含所有应用程序都可以使用的基本描述符。在实模式中,段长是固定的(为64KB),而在保护模式中,段长是可变的,其最大可达4GB。LDT也是段描述符的一个数组。与GDT不同,LDT是一个段,其中存放的是局部的、不需要全局共享的段描述符。每一个操作系统都必须定义一个GDT,而每一个正在运行的任务都会有一个相应的LDT。每一个描述符的长度是8个字节,格式如图3所示。当段寄存器被加载的时候,段基地址就会从相应的表入口获得。描述符的内容会被存储在一个程序员不可见的影像寄存器(shadow register)之中,以便下一次同一个段可以使用该信息而不用每次都到表中提取。物理地址由16位或者32位的偏移加上影像寄存器中的基址组成。实模式和保护模式的不同可以从图1和图2中很清楚地看出来。 




图1 实模式的寻址





图2 保护模式下的寻址





图3 段描述俯的格式

此外,还有一个中断描述符表(IDT)。这些中断描述符会告诉处理器到那里可以找到中断处理程序。和实模式一样,每一个中断都有一个入口,但是这些入口的格式却完全不同。因为在切换到保护模式的过程中没有使用到IDT,所以在此就不多做介绍了。 

进入保护模式

80386有4个32位控制寄存器,名字分别为CR0、CR1、CR2和CR3。CR1是保留在未来处理器中使用的,在80386中没有定义。CR0包含系统的控制标志,用于控制处理器的操作模式和状态。CR2和CR3是用于控制分页机制的。在此,我们关注的是CR0寄存器的PE位控制,它负责实模式和保护模式之间的切换。当PE=1时,说明处理器运行于保护模式之下,其采用的段机制和前面所述的相应内容对应。如果PE=0,那么处理器就工作在实模式之下。 

切换到保护模式,实际就是把PE位置为1。为了把系统切换到保护模式,还要做一些其它的事情。程序必须要对系统的段寄存器和控制寄存器进行初始化。把PE位置1后,还要执行跳转指令。过程简述如下: 

1.创建GDT表; 

2.通过置PE位为1进入保护模式; 

3.执行跳转以清除在实模式下读取的任何指令。 

下面使用代码来实现这个切换过程。 

需要的东西

◆ 一张空白软盘 

◆ NASM编译器 

下面是整个程序的源代码: 

org 0x07c00; 起始地址是0000:7c00 
jmp short begin_boot ; 跳过其它的数据,跳转到引导程序的开始处
bootmesg db "Our OS boot sector loading ......"
pm_mesg db "Switching to protected mode ...."
dw 512 ; 每一扇区的字节数
db 1 ; 每一簇的扇区数
dw 1 ; 保留的扇区号
db 2
dw 0x00e0 
dw 0x0b40 
db 0x0f0 
dw 9 
dw 18 
dw 2 ; 读写扇区号
dw 0 ; 隐藏扇区号
print_mesg :
mov ah,0x13 ; 使用中断10h的功能13,在屏幕上写一个字符串
mov al,0x00 ; 决定调用函  

阅读(1198) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~