免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 8180 | 回复: 3
打印 上一主题 下一主题

[FreeBSD] freebsd9.2-内核如何实现IA32内存管理 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2014-07-04 00:50 |只看该作者 |倒序浏览
本帖最后由 71v5 于 2014-07-05 16:30 编辑

注:
IA-32的内存管理详细信息大家可以参考"Intel 64 and IA-32 ArchitecturesSoftware Developer’s Manual Volume 3A"第二章和第三章,下面关于一些术语的描述
参考了这个Manual,因为这是最权威的定义,为了做一个最普遍的说明,前提如下:
1:一般情况下,很少使用LDT,这里略去对LDT的说明。
2:使用32-Bit分页,不使用大尺寸页。。
3:因为每个cpu都有自己的GDT,下面只分析BSP是如何建立自己的GDT,其它AP也是用同样的方法建立的。

下面将简单分析一下freebsd9.2如何实现IA-32内存管理,争取用最简单的描述把问题说清楚,使大家尽量不用去参考"Intel 64 and IA-32 ArchitecturesSoftware Developer’s Manual Volume 3A",
就能大概了解IA-32的内存管理方式,以及freebsd9.2是如何实现的,可能不会涵盖全部内容。

这里所说的"实现IA-32内存管理"--IA-32在硬件级别对内存管理提供最基本的支持,这种支持包括IA-32提供的一些寄存器,IA-32要求的一些数据结构等,
在能正确进行一个存储器访问之前,freebsd9.2必须要建立并初始化这些数据结构,并用有意义的值加载这些寄存器。

这里将简单分析一下"建立并初始化这些数据结构"的简单过程;对于用来加载寄存器的某些"有意义的值",将简单的说明其代表的是什么含义,而不去分析其具体的
实怎么初始化(但是会指出这个有意义的值具体是在哪个位置初始化,大家有兴趣就可以直接参考相应的代码),现阶段只需要知道其具体代表的是什么就够了,在
后续中,将会回过头看看这些"有意义的值是如何初始化的",相信这样会更容易理解。


[IA-32: Segment Registers]:

IA-32有6个段寄存器,分别是CS,DS,SS,ES,FS,GS,这些段寄存器中保存的是16bit长的segment selectors,一个segment selector标识了内存中的一个段,不是
直接标识,而是间接标识。

一个segment selector的格式如下:
  1. bit15                                                                 bit0
  2. ***************************************************************************
  3. *                                            *              *             *
  4. *   Index   13个bit                          *  TI 1个bit   *  RPL 2个bit *
  5. ***************************************************************************

  6. Index字段:
  7. bit3-15,用来选择全局描述表(GDT)或者局部描述表(LDT)中8192个描述符中的一个,每个描述符是GDT或者LDT中的一个表项,每个表项的长度
  8. 是8字节,cpu这样来计算某一个表项的地址,即(index * 8)+(GDT/LDT的基地址)。

  9. TI:
  10. bit2,,标识当前使用的是GDT还是LDT;为0,使用GDT;为1,使用LDT。

  11. RPL:
  12. bit0-1,指定了一个segment selector,取值为0,1,2,3。0为最高特权级,3为最低特权级;结合freebsd9.2可以这么理解,当cpu处于内核态时,
  13. segment selector中的RPL字段的值为0;当cpu处于用户态时,segment selector中的RPL字段的值为3;在试图进行越权访问时,IA-32硬件会执行
  14. 严格的权限检查,关于权限检查的详细信息大家可以参考"Intel 64 and IA-32 ArchitecturesSoftware Developer’s Manual Volume 3A"第五章。
复制代码
[freebsd9.2:使用Segment Registers]:

在freebsd9.2中,使用CS,DS,SS,ES,FS这5个段寄存器,其中CS,DS分cpu处于内核态和cpu处于用户态两种情况,这几段寄存器包含的segment selector
如下所示:

KCSEL-内核代码段的segment selector,当cpu处于内核态时,将用KCSEL加载CS寄存器:
  1. #define GCODE_SEL       4      
  2. #define SEL_KPL         0
  3. 内核代码段的Segment Selector,值为GSEL(GCODE_SEL, SEL_KPL),二进制表示为0000000000100000.
  4. 图示如下,Index字段的值为4;TI字段的值为0,表示使用GDT;RPL字段的值为0,表示处于最高特权级:
  5. bit15                                                                 bit0
  6. ***************************************************************************
  7. *                                            *              *             *
  8. *   GCODE_SEL                                *  0           *   0         *
  9. ***************************************************************************
复制代码
KDSEL-内核数据段的segment selector,当cpu处于内核态时,将用KDSEL加载DS寄存器:
  1. #define        GDATA_SEL        5      
  2. #define   SEL_KPL         0
  3. 内核数据段的Segment Selector,值为GSEL(GDATA_SEL, SEL_KPL),二进制表示为0000000000101000.
  4. 图示如下,Index字段的值为5;TI字段的值为0,表示使用GDT;RPL字段的值为0,表示处于最高特权级:
  5. bit15                                                                 bit0
  6. ***************************************************************************
  7. *                                            *              *             *
  8. *   GDATA_SEL                                *  0           *   0         *
  9. ***************************************************************************
复制代码
KPSEL-一个特殊的segment selector,在cpu处于内核态时,将用KPSEL加载FS寄存器,用来访问每cpu数据对象struct pcpu __pcpu[MAXCPU]
这里将用来访问__pcpu[0],使用readelf读取kernel的符号表,通过地址c130f680访问__pcpu[0],所以由KPSEL确定的段描述符的
Base address fields成员将被设置为c130f680:
  1. #define        GPRIV_SEL        1
  2. #define  SEL_KPL         0
  3. Per-Processor Private Data Descriptor的Segment Selector,值为GSEL(GPRIV_SEL, SEL_KPL),二进制表示为0000000000001000.  
  4. 图示如下,Index字段的值为1;TI字段的值为0,表示使用GDT;RPL字段的值为0,表示处于最高特权级:
  5. bit15                                                                 bit0
  6. ***************************************************************************
  7. *                                            *              *             *
  8. *   GPRIV_SEL                                *  0           *   0         *
  9. ***************************************************************************
复制代码
_ucodesel-用户代码段的segment selector,当cpu处于用户态时,将用_ucodesel加载CS寄存器:
  1. #define        GUCODE_SEL        6
  2. #define       SEL_UPL         3
  3. 用户代码段的Segment Selector,值为GSEL(GUCODE_SEL, SEL_UPL),二进制表示为0000000000110011.
  4. 图示如下,Index字段的值为6;TI字段的值为0,表示使用GDT;RPL字段的值为3,表示处于最低特权级:
  5. bit15                                                                 bit0
  6. ***************************************************************************
  7. *                                            *              *             *
  8. *   GUCODE_SEL                               *  0           *   3         *
  9. ***************************************************************************
复制代码
_udatasel-用户数据段的segment selector,当cpu处于用户态时,将用_udatasel加载DS寄存器:
  1. #define        GUDATA_SEL        7
  2. #define       SEL_UPL         3
  3. 用户数据段的Segment Selector,值为GSEL(GUDATA_SEL, SEL_UPL),二进制表示为0000000000111011.
  4. 图示如下,Index字段的值为7;TI字段的值为0,表示使用GDT;RPL字段的值为3,表示处于最低特权级:
  5. bit15                                                                 bit0
  6. ***************************************************************************
  7. *                                            *              *             *
  8. *   GUDATA_SEL                               *  0           *   3         *
  9. ***************************************************************************
复制代码
这里简要描述一下TSS,IA-32要求系统中至少包含一个TSS,freebsd9.2定义了一个类型为struct i386tss的数据对象来描述
TSS,现阶段只需要明白struct i386tss对象的tss_esp0和tss_ss0成员;tss_esp0成员动态更新,典型的是在线程切换时,
会用新替换线程的内核栈的栈顶重新设置tss_esp0成员;tss_ss0成员在初始化函数init386中被设置为KDSEL,没有特殊
操作时,将保持不变。

在描述了上面的段寄存器后,那么到底是什么时候加载的,这里简单描述一下:
假设此时cpu处于用户态,当一个中断到来,此时cpu陷入内核态时,cpu硬件将用KCSEL加载CS寄存器;使用TSS中的tss_ss成员加载SS寄存器,即KDSEL;
当内核接管工作后,将用KDSEL加载ES寄存器和DS寄存器,使用KPSEL加载FS寄存器。

当一个线程执行系统调用execve,内核在execve的最后阶段会调用exec_setregs函数重新设置保存在线程内核栈上的硬件上下文,将保存在内核栈
上SS寄存器的值更改为_udatasel,将保存在内核栈上的DS寄存器的值更改为_udatasel,将保存在内核栈上的ES寄存器的值更改为_udatasel,将保存在内核栈上
FS寄存器的值更改为_udatasel,将保存在内核栈上CS寄存器的值更改为_ucodesel,这样当cpu返回到用户态时,相应的寄存器都被设置为正确的
segment selector。


[IA-32:全局描述表(GDT)]:
在IA-32中,所有的存储器访问都是通过GDT来进行的,GDT中包含的表项被称作段描述符,段描述符提供了关于段的基地址,
访问权限,类型等信息,如何在GDT或者LDT选择相应的段描述符,就是通过上面描述的segment selector来实现的,并且
CPU不使用GDT中的第一个表项,即表项0,不过可以将表项0初始化为一个Null Descriptor,好了,关于GDT了解这些就足够了。

Global Descriptor Table Register(GDTR)-用来访问GDT,lgdt指令用来加载GDTR,格式如下:
  1. bit47                                                                 bit0
  2. ***************************************************************************
  3. *                                            *                            *
  4. *   32-bit Linear Base Address               *   16-Bit Table Limit       *
  5. ***************************************************************************
  6. Base Address指定了GDT的基地址,Table Limit指定了GDT的大小字(节数)。
复制代码
Segment Descriptors-即GDT中每个表项的name,段描述符提供了段的基地址,访问权限,类型等信息,段描述的大小是8字节,
一个段描述符的标准格式如下图所示:

段描述符格式如上所示,这里只需要了解下面的字段就够了:
Segment limit field:
处理器将上面两个segment limit fields组合起来以产生一个20bit的segment limit value,这个字段和G位联合起来解释:
如果G位设置为0:segment的范围在1Byte到1MByte之间。
如果G为被设置为1:segment的范围在1KByte到4GByte之间。

Base address fields:
处理器将上面三个Base address字段组合起来产生一个32bit的线程地址,该线性地址为Segment的基地址。

Type field:
段描述符的类型,这个大家得参考一下"Intel 64 and IA-32 ArchitecturesSoftware Developer’s Manual Volume 3A"第3.4节的定义。

S flag:
S为1时,该段描述符描述的段是一个代码段或者数据段。
S为0时,该段描述符描述的段是一个系统段。

DPL filed:
指定了段的特权级,可取值为0,1,2,3,和上面segment selector中的RPL字段类似,用来控制对段的访问。


[freebsd9.2:使用GDT]:

相关数据结构:
[union   descriptor]:
  1. /*******************************************************************************************************
  2. * Generic descriptor。
  3.    因为IA-32定义的段描述符大小都为8字节,只是在格式上不同,所以通过类型为union descriptor
  4.    的联合给予统一的访问,如果使用LDT的话,就会建立一个门描述符,此时就会使用gd成员。
  5.    一般情况下,只使用sd成员。

  6.    在freebsd9.2中,GDT有19个表项,系统中每个cpu都有自己的GDT,所以freebsd9.2定义了一个数组gdt,
  7.    这里讨论的是BSP上的初始化,所以BSP要将gdt[0].sd,gdt[1].sd,gdt[2].sd。。。。。。gdt[18].sd初始
  8.    化为有意义的值。
  9.    #define NGDT            19
  10.    union descriptor gdt[NGDT * MAXCPU];
  11. *************************************************/
  12.    102  union   descriptor      {
  13.    103          struct  segment_descriptor sd;
  14.    104          struct  gate_descriptor gd;
  15.    105  };
复制代码
[struct region_descriptor]:
  1. /**************************************************************************************
  2. * struct region_descriptor r_gdt;
  3.   
  4.    struct region_descriptor类型的数据对象只在使用lgdt指令加载GDTR时使用:
  5.    rd_limit:GDT的大小。
  6.    rd_base:GDT的线性基地址。

  7.    在初始化函数init386中,会对r_gdt的成员做如下设置,在这之前已经调用函数ssdtosd正确
  8.    初始化了gdt[0].sd,gdt[1].sd,gdt[2].sd等:
  9.    r_gdt.rd_limit = NGDT * sizeof(gdt[0]) - 1;
  10.    r_gdt.rd_base =  (int) gdt;
  11.    lgdt(&r_gdt);
  12.    lgdt指令的参数r_gdt已经成功构建,此时加载GDTR。
  13. *****************************************/
  14.    164        struct region_descriptor {
  15.    165                unsigned rd_limit:16;                /* segment extent */
  16.    166                unsigned rd_base:32 __packed;        /* base address  */
  17.    167        };
复制代码
[数组gdt_segs]-数组元素的类型为struct soft_segment_descriptor,元素数目为NGDT。在这里可以不用关心这个数据类型,
因为freebsd9.2定义的数组gdt_segs只是为了方便函数ssdtosd使用这些数组元素建立正确的GDT中的段描述符,可以在内核源代码文件
"$FreeBSD: release/9.2.0/sys/i386/i386/machdep.c"中找到数组gdt_segs的定义。函数ssdtosd在初始化函数init386中
被调用,使用数组gdt_segs中的元素建立正确的段描述符,该函数是用汇编语言编写的,这里也不对其进行分析,分析这个函数
没有实际意义,可以在"$FreeBSD: release/9.2.0/sys/i386/i386/support.s"文件中找到这个函数。

这里只列出几个重要的由其建立的标准段描述符的详细格式,字段的值都以二进制表示,只将重要字段名在上面
标注,其余字段名请大家参看上面段描述标准格式那张图:


从上面图中建立的段描述符可以明显看出,5个段的Base address都为00000000,Segment limit为fffff,G flag为1,这表示通过
这5个段,可以访问整个4GB的线程地址空间,而且偏移地址和线性地址相等,貌似跟linux中一样。

在没有介绍分页之前,看看使用上面建立的数据结构,怎样将一个逻辑地址转换为线性地址,根据IA-32的官方描述,在使用一个
物理地址之前,cpu要使用两个步骤来产生这个物理地址:逻辑地址翻译和线性地址空间分页,下面简单的看看逻辑地址翻译过程。

假设现在要访问内核数据段中的一个数据对象__pcpu[0],偏移地址为c130f680,此时cpu处于内核态:
指令ljmp $KDSEL, $c130f680
上面指令的参数就指定了一个逻辑地址,指定的segment selector为内核数据段,segment selector中Index字段的值为5;
TI字段的值为0,表示使用GDT;RPL字段的值为0,表示处于最高特权级。

cpu硬件此时依次做:
1:在GDT中使用Index确定相应的段描述符,这里为内核数据段的描述符,即上面gdt数组的第5个元素。
2:对访问权限进行检查,并检查偏移地址是否越界。
3:将偏移地址加到内核数据段描述符中的Base address上,这样就产生了一个线性地址,具体到这里
   就为00000000 + c130f680 = c130f680;可以看出偏移地址和线性地址相等。



[freebsd9.2:32-Bit分页]:

对于32-Bit分页,下面通过一张图和将上面的线性地址c130f680翻译成物理地址的过程简单说明一下,参考下图:


在能正常使用32-Bit分页之前,CR3,Page Directory,Page Table都要被正确的初始化。

对于内核:
在初始化阶段,CR3,Page Directory,Page Table在"$FreeBSD: release/9.2.0/sys/i386/i386/locore.s"中建立,IdlePTD变量
为Page Directory的物理基地址,在系统初始化阶段和运行内核线程时,CR3中包含了IdlePTD。

对于普通进程:
在创建进程的时候,pmap_pinit函数会为进程创建一个Page Directory,同时将映射内核的PDE复制到普通进程Page Directory中相应的PDE中,
这样当cpu运行在内核态时,就可以直接访问内核代码和数据,而不需要重新使用IdlePTD加载CR3。
在进行线程切换时,该Page Directory的物理基地址将被加载到CR3中,PDE和PTE根据情况由缺页异常处理程序来建立。
pmap_pinit函数可以在"$FreeBSD: release/9.2.0/sys/i386/i386/pmap.c"中找到。


CR3:
即控制寄存器3,包含了Page Directory的物理基地址,这个物理基地址低12bit为0,按4KB对齐。
Page Directory中的PDE:包含了Page Table的物理基地址,按4KB对齐。
Page Table中的PTE:包含了4-KByte Page的物理基地址,按4KB对齐。

线性地址c130f680的二进制表示为:1100000100 1100001111 011010000000。
因为每个PDE和PTE大小为4字节,所以线性地址中用来选择PDE或者PTE的索引字段要左移2位。
为分析方便,下面的字母都假设为20位物理地址的二进制表示。
线性地址翻译过程:
1:此时CR3中包含了IdlePTD,即Page Directory的物理基地址,这里假设为A,使用地址A110000010000确定了Page Directory中的一个PDE。
2:假设上一步确定的PDE中包含的Page Table的物理基地址为B,使用地址B110000111100确定了Page Table中的一个PTE。
3:假设上一步确定的PTE包含的4-KByte Page的物理基地址为C,那么经过地址翻译后最终的物理地址为C011010000000。
其实在上面3步的地址翻译过程中,cpu硬件单元要进行很多的权限检查,这里都忽略了这些检查。

论坛徽章:
89
水瓶座
日期:2014-04-01 08:53:31天蝎座
日期:2014-04-01 08:53:53天秤座
日期:2014-04-01 08:54:02射手座
日期:2014-04-01 08:54:15子鼠
日期:2014-04-01 08:55:35辰龙
日期:2014-04-01 08:56:36未羊
日期:2014-04-01 08:56:27戌狗
日期:2014-04-01 08:56:13亥猪
日期:2014-04-01 08:56:02亥猪
日期:2014-04-08 08:38:58程序设计版块每日发帖之星
日期:2016-01-05 06:20:00程序设计版块每日发帖之星
日期:2016-01-07 06:20:00
2 [报告]
发表于 2014-07-04 08:51 |只看该作者
lz加油!

论坛徽章:
0
3 [报告]
发表于 2014-07-05 01:08 |只看该作者
回复 2# fender0107401




   

论坛徽章:
0
4 [报告]
发表于 2014-07-13 16:06 |只看该作者
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP