`
memorymyann
  • 浏览: 266237 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

linux学习一 对于intel i386的2层内存寻址模式详细过程

阅读更多

linux内存寻址详细过程:
下面我们看这条语句call 0x0804837c
整个内存寻址分为2个阶段,段式和页式。处理器是intel i386系列.操作系统会先将虚地址经过段式映射后变成线性地址,再通过页式变成物理地址。从而获取内存上的数据或者是代码.
这里执行的是call语句,转移指令。首先会找到cs寄存器,intel i386是32位处理器,CS仍然是16位。高13位存贮的是段描述位置(注意不是地址,读到后面就知道了)。低3位中,倒数第3位是指使用GDT寄存器还是LDT(寄存器)。后2位表示权限。(在linux中只用了2种权限,最高级别的系统权限00和用户权限最低11)。该寄存器内容是在建立进程的时候就被设置好的,其代码可以在include/asm-i386/processor.h中找到:
define start_thread(regs, new_eip, new_esp) do {        \
    __asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));    \
    set_fs(USER_DS);                    \
    regs->xds = __USER_DS;                    \
    regs->xes = __USER_DS;                    \
    regs->xss = __USER_DS;                    \
    regs->xcs = __USER_CS;                    \
    regs->eip = new_eip;                    \
    regs->esp = new_esp;                    \
} while (0)
这里我们可以看到除了CS寄存器,其他的寄存器值都被设置成了__USER_DS,这与intel当初设计有出入。linux并没有买intel的帐。当然我们想知道具体的值,至于__USER_DS和__USER_CS的值我们可以在include/asm-i386/segment.h中找到:
#define __KERNEL_CS 0X10    //相应的2进制表示 0000 0000 0001 0000
#define __KERNEL_DS 0X18    //相应的2进制表示 0000 0000 0001 1000
#define __USER_CS 0X23      //相应的2进制表示 0000 0000 0010 0011
#define __USER_DS 0X2B      //相应的2进制表示 0000 0000 0010 1011
注意不同内核版本的值可能不一样,但这并不影响。看到后面你们会发现这些值是有自己的规则的。对应着上面说到的CS,DS等等寄存器的意义(DS和CS16位值意义是一样的),我们回忆可得知,最高13位表示段描述结构的位置,倒数第3位表示用GDT还是LDT,最后表示权限,我们很容易得到下面的值:
                段描述结构的位置    是GDT还是LDT(0表示用GDT,1表示LDT)    权限
__KERNEL_CS            2                0用GDT                                    00最高系统权限
__KERNEL_DS            3                0用GDT                                    00最高系统权限
__USER_CS             4                0用GDT                                    11用户权限
__USER_DS            5                0用GDT                                    11用户权限
这里我们看到linux只用了GDT寄存器,这违背了intel初衷(GDT表示全局段描述表,LDT是局部段描述表)。名字也反映了GDT和LDT的作用,他们是用来存贮段描述结构的地址起点的。看到这里可能有些人不明白,内存寻址不是段地址加上偏移吗,直接把CS的值加上程序中给出的偏移就可以找到了物理地址了吗?那是intel 8086和8088的虚地址模式,在intel 80286确切的说是80386更多的采用的是保护模式寻址,这种寻址方式改变了寄存器内部值的意义,至于什么是保护模式,大家GOOGLE下就知道了。
既然用到了GDT寄存器,那么在装入线程的时候肯定是要给这个寄存器所指向的表赋值了,这段代码是在arch/i386/kernel/head.S中:
ENTRY(cpu_gdt_table)    //定义了GDT表值
    .quad 0x00cf9a000000ffff    /* 0x60 kernel 4GB code at 0x00000000 */
    .quad 0x00cf92000000ffff    /* 0x68 kernel 4GB data at 0x00000000 */
    .quad 0x00cffa000000ffff    /* 0x73 user 4GB code at 0x00000000 */
    .quad 0x00cff2000000ffff    /* 0x7b user 4GB data at 0x00000000 */
我只贴出了关键的代码,其实在赋值时候,GDT指向的表的第一项没有被使用,原因是防止加电后段寄存器未经初始化就进入保护模式并使用GDT,第2项也没有被使用,那么我们找到我们程序要用的第4项,既是.quad 0x00cffa000000ffff    /* 0x73 user 4GB code at 0x00000000 */,这是个8字节64位的数。我们转换成相应的2进制就可以看到它的值是:0000 0000 1100 1111 1111 1010 0000 0000  0000 0000 0000 0000 1111 1111 1111 1111,我们看下它的意义。从低位向高位,从第16位(起始第0位),到第31位是0,从第32位到第39位是0,最高的8位还是0,这个就是我们以前所说的段址,也就是段的基地址(有人会问为什么地址搞这么复杂32位基地址连在一起不好吗,干嘛分成3段。16位16位分想信大家可以理解,至于后面又把16位分成了2个8位,是因为intel设计初衷是用于24位处理器,但很快意识到应该用于32位,于是修修补补就变成了现在这个样子了),在linux中他被设置成了0,也许有人会说这不会出问题吗,后面还有页式内存管理,这里不是最终的物理地址。接下来每个描述段都有自己的意义了,其实在linux中对这些2进制单元的内存设置是为了应付intelCPU基于段式内存管理的保护模式,最低16位加上,倒数第12位到15位都是1,他们合起来16进制值便是0xffffff表示段的上限,一旦用户越过了这个上限就会报非法越界了。我们看到倒数第8位是1表示4KB,0表示段长度单位是1B,1表示段长度单位为4KB,倒数第9为1表示当前指令为32,否则为16位。倒数第16位为1表示在内存,否则则不在内存。后面紧接着为11表示要求的权限,这里为11和段寄存器中相符,表示可以使用,没有越权。剩下的几个数位就不一一介绍了,读者可以自己去查相关的资料。到这里读者可能会想到,我们可以通过设置CS或者DS寄存器从而可以达到非法访问的目的,但linux内核会在页式映射里面继续执行一次检查,之所以在这里做一次是因为例行i386的公事,不得已而为之。大家也会看到由于段寄存器被指定了值,任何一个地址访问到的最后的段描述结构的值都将会是代码中列出的值。linux这样做只是由于i386的设计,必需先完成段式印射。
这样我们得到了段的基地址0,加上偏移就是线性地址了,很明显在linux中线性地址数值是等于虚地址的,因为加的是0.接下来要做的是把线性地址转换成物理地址。这里介绍下i386是如何把线性地址转换成物理地址的。线性地址是32位,I386将其看成10位,10位,12位。在i386中有个寄存器叫CR3。它指向一个地址,内存管理单元(mmu)找到这里,根据最高10位,得到下标,从而获取页面表位置,找到对应的页面表,在从第2个10位作为下标找到页面位置,加上最后12为偏移就是我们要找的物理地址了。CR3的设置是在include/asm-i386/mmu_context.h里面,代码为:
static inline void switch_mm(struct mm_struct *prev,
                 struct mm_struct *next,
                 struct task_struct *tsk){
                 ...
                 asm voliatile("movl %0, %%cr3"::"r" (__pa(next->pgd));
                 ...
                 }
大家也许没有看到有什么权限判断的地方,权限判断是在找页面表的时候完成的,页面表的大小占一个页面,在32位的i386中,一个页面为4K,那么页面的起始位置一定是4K的倍数,存贮页面表其实地址的表单里面,每个值(表示页面表的起始位置)最后的12必然是0,因此,抛弃这12个零,找到值后直接在后面加上12个0就是页面表的地址,同理,在页面表中低12位也是0,因此可以将这些必定是0的数位用到其它方面,比如权限判断。因此从线性地址到物理地址的过程就是到CR3指向地址,以最高10位为下标找到页面表地址,再在页面表中以第2个10位为下标找到页面位置,加上最后12位的位移便是物理地址了。

也许大家会觉得为了一个内存寻址,会访问内存到3次,效率是不是太低了。但是由于告诉缓存的作用,加上大部分操作是由硬件完成的,所以速度很快。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics