8048380: 55 push %ebp
8048381: 89 e5 mov %esp,%ebp
8048383: 83 ec 08 sub $0x8,%esp
8048386: 83 e4 f0 and $0xfffffff0,%esp
8048389: b8 00 00 00 00 mov $0x0,%eax
804838e: 83 c0 0f add $0xf,%eax
8048391: 83 c0 0f add $0xf,%eax
8048394: c1 e8 04 shr $0x4,%eax
8048397: c1 e0 04 shl $0x4,%eax
804839a: 29 c4 sub %eax,%esp
804839c: e8 c7 ff ff ff call 8048368
80483a1: c9 leave
80483a2: c3 ret
80483a3: 90
nop
从上述结果可以看到, ld给test()函数分配的地址为0x08048368.在elf格式的可执行文件代码中,ld的实际位置总是从0x8000000开始安排程序
的代码段, 对每个程序都是这样。至于程序在执行时在物理内存中的实际位置就要由内核在为其建立内存映射时临时做出安排, 具体地址则
取决于当时所分配到的物理内存页面。假设该程序已经运行, 整个映射机制都已经建立好, 并且CPU正在执行main()中的call 8048368这条指
令, 要转移到虚拟地址0x08048368去运行. 下面将详细介绍这个虚拟地址转换为物理地址的映射过程.
首先是段式映射阶段。由于0x08048368是一个程序的入口,更重要的是在执行的过程中是由CPU中的指令计数器EIP所指向的, 所以在代码段中
。 因此, i386CPU使用代码段寄存器CS的当前值作为段式映射的选择子, 也就是用它作为在段描述表的下标.那么CS的值是多少呢?
用GDB调试下test:
(gdb) info reg
eax 0x10 16
ecx 0x1 1
edx 0x9d915c 10326364
ebx 0x9d6ff4 10317812
esp 0xbfedb480 0xbfedb480
ebp 0xbfedb488 0xbfedb488
esi 0xbfedb534 -1074940620
edi 0xbfedb4c0 -1074940736
eip 0x804836e 0x804836e
eflags 0x282 642
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
可以看到CS的值为0x73, 我们把它分解成二进制:
0000 0000 0111 0011
最低2位为3, 说明RPL的值为3, 应为我们这个程序本省就是在用户空间,RPL的值自然为3.
第3位为0表示这个下标在GDT中。
高13位为14, 所以段描述符在GDT表的第14个表项中, 我们可以到内核代码中去验证下:
在i386/asm/segment.h中:
#define GDT_ENTRY_DEFAULT_USER_CS 14
#define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
可以看到段描述符的确就是GDT表的第14个表项中。
我们去GDT表看看具体的表项值是什么, GDT的内容在arch/i386/kernel/head.S中定义:
ENTRY(cpu_gdt_table)
.quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* 0x0b reserved */
.quad 0x0000000000000000 /* 0x13 reserved */
.quad 0x0000000000000000 /* 0x1b reserved */
.quad 0x0000000000000000 /* 0x20 unused */
.quad 0x0000000000000000 /* 0x28 unused */
.quad 0x0000000000000000 /* 0x33 TLS entry 1 */
.quad 0x0000000000000000 /* 0x3b TLS entry 2 */
.quad 0x0000000000000000 /* 0x43 TLS entry 3 */
.quad 0x0000000000000000 /* 0x4b reserved */
.quad 0x0000000000000000 /* 0x53 reserved */
.quad 0x0000000000000000 /* 0x5b reserved */
.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 */
.quad 0x0000000000000000 /* 0x80 TSS descriptor */
.quad 0x0000000000000000 /* 0x88 LDT descriptor */
/* Segments used for calling PnP BIOS */
.quad 0x00c09a0000000000 /* 0x90 32-bit code */
.quad 0x00809a0000000000 /* 0x98 16-bit code */
.quad 0x0080920000000000 /* 0xa0 16-bit data */
.quad 0x0080920000000000 /* 0xa8 16-bit data */
.quad 0x0080920000000000 /* 0xb0 16-bit data */
/*
* The APM segments have byte granularity and their bases
* and limits are set at run time.
*/
.quad 0x00409a0000000000 /* 0xb8 APM CS code */
.quad 0x00009a0000000000 /* 0xc0 APM CS 16 code (16 bit) */
.quad 0x0040920000000000 /* 0xc8 APM DS data */
.quad 0x0000000000000000 /* 0xd0 - unused */
.quad 0x0000000000000000 /* 0xd8 - unused */
.quad 0x0000000000000000 /* 0xe0 - unused */
.quad 0x0000000000000000 /* 0xe8 - unused */
.quad 0x0000000000000000 /* 0xf0 - unused */
.quad 0x0000000000000000 /* 0xf8 - GDT entry 31: double-fault TSS */
.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
我们把这个值展开成二进制:
0000 0000 1100 1111 1111 1010 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111
根据上述对段描述符表项值的描述, 可以得出如下结论:
B0-B15, B16-B31是0, 表示基地址全为0.
L0-L15, L16-L19是1, 表示段的上限全是0xffff.
G位是1 表示段长度单位均为4KB。
D位是1 表示对段的访问都是32位指令
P位是1 表示段在内存中。
DPL是3 表示特权级是3级
S位是1 表示为代码段或数据段
type为1010 表示代码段, 可读, 可执行, 尚未收到访问
这个描述符指示了段从0地址开始的整个4G虚存空间,逻辑地址直接转换为线性地址。
所以在经过段式映射后就把逻辑地址转换成了线性地址, 这也是在linux中, 为什么逻辑地址等同于线性地址的原因了。
4.3 页式映射分析
现在进入页式映射的过程了, Linux系统中的每个进程都有其自身的页面目录PGD, 指向这个目录的指针保存在每个进程的mm_struct数据结构中。 每当调度一个进程进入运行的时候,内核都要为即将运行的进程设置好控制寄存器cr3, 而MMU的硬件则总是从cr3中取得指向当前页面目录的指针。当我们在程序中要转移到地址0x08048368去的时候, 进程正在运行,cr3早以设置好,指向我们这个进程的页面目录了。 先将线性地址0x08048368展开成二进制:
0000 1000 0000 0100 1000 0011 0110 1000
对照线性地址的格式,可见最高10位为二进制的0000 1000 00, 也就是十进制的32,所以MMU就以32为下标在其页面目录中找到其目录项。这个目录项的高20位指向一个页面表,CPU在这20位后添上12个0就得到页面表的指针。找到页面表以后, CPU再来看线性地址中的中间10位,0001001000,即十进制的72.于是CPU就以此为下标在页表中找相应的表项。表项值的高20位指向一个物理内存页面,在后边添上12个0就得到物理页面的开始地址。假设物理地址在0x620000的,线性地址的最低12位为0x368. 那么test()函数的入口地址就为0x620000+0x368 = 0x620368
。