linux 内核源代码情景分析——地址映射的全过程

linux 内核采用页式存储管理。虚拟地址空间划分成固定大小的“页面”,由MMU在运行时将虚拟地址映射成某个物理内存页面中的地址。页式内存管理比段式内存管理有很多好处,但是由于Intel是先使用段式管理的,然后才发明了页式管理,为了兼容,i386 CPU 一律对程序中使用的地址先进行段式映射,然后才能进行页式映射,既然CPU的硬件结构是这样,linux内核也只好服从intel的选择。通过一个例子看看linux内核是怎样在i386 CPU 上进行地址映射的。

    假设我们写了这么一个程序:

 1 #include <stdio.h>
 2  
 3 greeting()
 4 {
 5     printf("hello, world!\n");
 6 }
 7  
 8 main()
 9 {
10     greeting();
11 }

经过编译后,我们得到可执行代码hello。用命令:objdump -d hello来反汇编这一段二进制代码

从上面可以看出,ld给greeting()分配的地址为0x80483e4,在elf格式代码中,ld 总是从0x80000000开始安排程序的“代码段”,对每个程序都是这样,至于程序在执行时在物理内存中的实际位置则就要由内核在为其建立内存映射时临时作出安排。

    假设该程序已经在运行,整个映射机制都已经建立好,并且CPU 正在执行main()中的“call 80483e4”这条指令,要转移到虚拟地址0x080483e4上去,接下来就一步一步的走过这个地址映射过程。

    首先是段式映射,由于地址0x080483e4是一个程序的入口,更重要的是在执行的过程中是由CPU 中的“指令计数器”EIP所指向的,所以在代码段中,因此,i386 CPU使用代码段寄存器CS的当前值来作为段式映射的“选择码”,也就是用它作为在段描述表中的下标。

    接下来看看CS的内容,内核建立一个进程时都要将其段寄存器设置好(?),代码在include/asm-i386/processor.h 中:

    可以看到,除了CS被设置成USER_CS外,其他的所有段寄存器都被设置成USER_DS。就是说,虽然Intel 的意图是将一个进程的映像分成代码段、数据段和堆栈段,linux 内核中堆栈段和数据段式不分的。

    再来看看USER_CS和USER_DS到底是什么

也就是说,linux 内核中只是用四种不同的段寄存器值,两种用于内核本身,两种用于所有的进程。现在我们将这四种数值用二进制展开并与段寄存器的格式对照:

                               Index               TI   RPL                       

————————————————————————————————————————————

__KERNEL_CS    0x10        0000  0000  0001  0  |  0  | 0  0

__KERNEL_DS    0x18        0000  0000  0001  1  |  0  | 0  0

__USER_CS      0x23        0000  0000  0010  0  |  0  | 1  1

__USER_DS      0x2B        0000  0000  0010  1  |  0  | 1  1

 

经过和段寄存器的格式对照:

__KERNEL_CS       index = 2              TI = 0              RPL = 0

__KERNEL_DS       index = 3              TI = 0              RPL = 0

__USER_CS         index = 4              TI = 0              RPL = 3

__USER_DS         index = 5              TI = 0              RPL = 3

 

TI 都是0说明用的都是GDT,RPL只用了0和3,内核为0级,用户为3级

我们上面写的程序显然不属于内核,所以在进程的用户空间中运行,内核在调度该进程进入运行时,把CS设置成__USER_CS,即0x23,所以,CPU 以4为下标在全局描述表GDT中招对应的段描述项。

    GDT内容如下:

把4个段描述项的内容按二进制展开,和段描述项的定义对照,可以得出以下结论:

每个段都是从0地址开始的整个4GB空间,虚地址到线性地址的映射保持不变。所以,linux 内核设计的段式映射机制把地址0x080483e4 映射到了自身,现在作为线性地址出现了,下面进入页式映射过程:

    每个进程都有其自身的页面目录PGD,指向这个目录的指针保持在每个进程的mm_struct数据结构中,每当调度一个进程进入运行的时候,内核都要为即将运行的进程设置好控制寄存器CR3,而MMU的硬件则总是从CR3中取得指向当前页面目录的指针。不过,CPU 在执行程序时使用的虚存地址,而MMU硬件在进行映射时所用的则是物理地址,这是在switch_mm()函数中完成的

1 static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu)
2 {
3     ……
4       
5     asm volatile("movl %0, %%cr3" : : "r" (__pa(next->pgd)));
6     ……
7 }

当我们程序中要转移到地址0x80483e4去的时候,进程正在运行中,CR3早已设置好,指向我们这个进程的页面目录了,先将线性地址展开:

        0000 1000 0000 0100 1000 0011 1110 0100

        0000100000  0001001000   0011 1110 0100

         32                    72                 996

已CR3中的内容指向的地址为基地址,以32为下标就找到了目录项,取得到的目录项中的高20位然后加上12个0就得到了该页面表的指针。同理再以刚刚找到的页面表指针为基地址,以72为下标,找到页表项,然后取高20位,再加上996即是greeting()函数的物理地址。

posted @ 2013-10-18 11:05  linux_rookie  阅读(1631)  评论(0编辑  收藏  举报