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()的执行代码就存储在这里。