2.6 Linux系统地址映射举例

Linux采用分页存储管理。虚拟地址空间划分成固定大小的“页”,由MMU在运行时将虚拟地址映射(变换)成某个物理页面中的地址。从80X86系列的历史演变过程可知,分段管理在分页管理之前出现,因此,80X86的MMU对程序中的虚拟地址先进行段式映射(虚拟地址转换为线性地址),然后才能进行页式映射(线性地址转换为物理地址)。既然硬件结构是这样设计的,Linux内核在设计时只好服从这种选择,只不过,Linux巧妙地使段式映射实际上不起什么作用。

本节通过一个程序的执行来说明地址的映射过程。

假定我们有一个简单的C程序Hello.c

 # include <stdio.h>
 greeting ( )
 {
       printf(“Hello,world!\n”);
 }
 main()
  {
        greeting();
  }

之所以把这样简单的程序写成两个函数,是为了说明指令的转移过程。我们用gcc和ld对其进行编译和连接,得到可执行代码hello。然后,用Linux的实用程序objdump对其进行反汇编:

% objdump –d hello

得到的主要片段为:

 08048568 <greeting>:
 8048568:     pushl  %ebp
 8048569:     movl  %esp, %ebp
 804856b:     pushl  $0x809404
 8048570:     call    8048474  <_init+0x84>
 8048575:     addl   $0x4, %esp
 8048578:     leave
 8048579:     ret
 804857a:     movl  %esi, %esi
 0804857c <main>:
 804857c:     pushl  %ebp
 804857d:     movl  %esp, %ebp
 804857f:     call    8048568  <greeting>
 8048584:     leave
 8048585:     ret
 8048586:     nop
 8048587:     nop

最左边的数字是连接程序ld分配给每条指令或标识符的虚拟地址,其中分配给greeting()这个函数的起始地址为0x08048568。Linux最常见的可执行文件格式为elf(Executable and Linkable Format)。在elf格式的可执行代码中,ld总是从0x8000000开始安排程序的“代码段”,对每个程序都是这样。至于程序执行时在物理内存中的实际地址,则由内核为其建立内存映射时临时分配,具体地址取决于当时所分配的物理内存页面。

假定该程序已经开始运行,整个映射机制都已经建立好,并且CPU正在执行main()中的“call 08048568”这条指令,于是转移到虚地址0x08048568。Linux内核设计的段式映射机制把这个地址原封不动地映射为线性地址,接着就进入页式映射过程。

每当调度程序选择一个进程运行时,内核就要为即将运行的进程设置好控制寄存器CR3,而MMU的硬件总是从CR3中取得指向当前页目录的指针。

当我们的程序转移到地址0x08048568的时候,进程正在运行中,CR3指向我们这个进程的页目录。根据线性地址0x08048568最高10位,就可以找到相应的目录项。把08048568按二进制展开:

0000 1000 0000 0100 1000 0101 0110 1000

最高10位为0000 1000 00,即十进制32,这样以32为下标在页目录中找到其目录项。这个目录项中的高20位指向一个页表,CPU在这20位后填12个0就得到该页表的物理地址。

找到页表之后,CPU再来找线性地址的中间10位,为0001001000,即十进制72,于是CPU就以此为下标在页表中找到相应的页表项,取出其高20位,假定为0x840,然后与线性地址的最低12位0x568拼接起来,就得到greeting()函数的入口物理地址为0x840568, greeting()的执行代码就存储在这里。

results matching ""

    No results matching ""