[FreeBSD]x86地址映射实例

http://www.chinaunix.net 作者:qiuhanty 

 x86地址映射实例
qiuhan
2007.8.15

今天我们通过qemu来探讨freeBSD下x86地址映射。
用户地址空间的映射:
我们以调试auditd为例
# qgdb auditd
(gdb) b main
Breakpoint 1 at 0x804b594: file /kerndebug/umbrella/usr.sbin/auditd/../../contrib/audit_supt/auditd/auditd.c, line 1140.
(gdb) target remote :1234
Remote debugging using :1234
0xc08daf1c in .rtld_start () from /libexec/ld-elf.so.1
(gdb) c
Continuing.
Breakpoint 1, main (argc=-1077940752, argv=0x0)
    at /kerndebug/umbrella/usr.sbin/auditd/../../contrib/audit_supt/auditd/auditd.c:1138
1138    {
(gdb) cpu_dump
ldtr:s=0x0050, bs=0xc0a73ae0, lm=0x00000087, flag=0x0000e200
tr:s=0x0048, bs=0xc0a73dc0, lm=0x00000067, flag=0xc00089a7
gdtr:base=0xc0a73a40, limit=0x97
idtr:base=0xc0a73f20, limit=0x7ff
dr0:0x00000000, dr1:0x00000000, dr2:0x00000000
dr3:0x00000000, dr6:0x00000000, dr7:0x00000000
cr0:0xe005003b, cr1:0x00000000, cr2:0x281d9028
cr3:0x079b6000, cr4:0x00000690
(gdb) p $eip
$1 = (void (*)()) 0x804b578 <main>
(gdb) x/x $eip
0x804b578 <main>:       0x83e58955

这里,0x804b578是一个虚拟地址,经过段地址映射出的线性地址不变;我们主要来看线性地址到物理地址的转换。
该地址的前10位左移2位得到0x80,加上作为页目录(Page-directory)基址的cr3,得到对应的页目录项地址
0x079b6080(physical),查看该地址内容:
(gdb) xp/x 0x079b6080
0x79b6080:      0x04180067
这里得到的是页表(Page-table)的基址0x04180000.再取0x804b578的中间10位左移2位得到0x12c,相加得到
对应的页表项地址0x0418012c,查看该地址内容:
(gdb) xp/x 0x0418012c
0x418012c:      0x04115425
这里得到的是页的基址0x04115000,加上0x804b578的最后12位,得到最终的物理地址0x04115578
(gdb) xp/x 0x04115578
0x4115578:      0x83e58955
哈哈,内容一样吧!

内核地址空间的映射:
用户程序(通过系统调用或中断)进入内核空间时,是不需要切换cr3的,很神奇吧!我们来看为什么。
继续在刚才的环境,我们随便查看一个内核地址0xc0a39628
(gdb) x/x 0xc0a39628
0xc0a39628:     0xc1c910b4
内核地址到物理地址相差一个3G,我们直接减去0xc00000000就可以了
(gdb) xp/x 0xa39628
0xa39628:       0xc1c910b4
但是这个地址MMU是怎么得到的呢?仍然要通过cr3,我们来手动完成这个过程。
0xc0a39628的前10位左移2位得到0xc08(这里有个技巧,直接取前3位(16进制),第3位取4的整数倍即可),加上
cr3得到0x079b6c08,查看该地址内容:
(gdb) xp/x 0x079b6c08
0x79b6c08:      0x008001e3
注意,这里和刚才有所不同。刚才我们没有讨论最后12位的含义,这里必须说一下.当倒数第8位(Page size)的值
为1(即16进制的倒数第2位的值大于8 )时,以为着页面大小为4M,这时就从先前的两级映射退化为一级映射,页的基址
就是0x00800000,而地址的后22位(0x239628 )均为偏移,相加就得到物理地址0xa39628.
那么有多少个这样页面大小为4M的映射呢?
(gdb) xp/16x 0x079b6c00
0x79b6c00:      0x01000063      0x004001a3      0x008001e3      0x00c001e3
0x79b6c10:      0x01004023      0x01005063      0x01006003      0x01007063
0x79b6c20:      0x01008063      0x01009023      0x0100a023      0x0100b003
0x79b6c30:      0x0100c003      0x0100d003      0x0100e003      0x0100f003
原来只有3个(0x004001a3, 0x008001e3以及0x00c001e3),它们对应的地址空间为0xc0400000到0xc1000000,
即物理地址的4M--16M,共12M.还记得我们在《loader分析》中说到,kernel加载的基址就是0xc0400000,这不
是一个巧合。
为什么要用这样页面大小为4M的映射呢?
Intel <System Programming Guide> 3.7.3解释到,把操作系统或者可执行的内核放在大的页面中可以减少
TLB(Translation Lookaside Buffer, 用于缓存页目录项和页表项) misses, 从而可以提高系统整体性能。
而且,4M和4K的页项使用不同的TLB.
为什么用户程序进入内核空间时,不需要切换cr3?
应为所有的可以赋给cr3的基址在0xc00偏移上的内容几乎都是一致的。包括专门标志内核空间的IdlePTD(其值一般
为0x101e000).IdlePTD是cr3的第一次,在内核初始化化过程中一直是该值,直到启动第一个用户程序。在cpu_switch
会通过比较当前cr3是否等于该值来判断是否需要切换cr3.

为了更清楚地址映射过程,我们来看一下qemu中的一段代码:
    if (!(env->cr[0] & CR0_PG_MASK)) {//页表映射没有使能
            pte = addr;
            page_size = 4096;
        } else {
            /* page directory entry */
            //页目录项地址,a20_mask的值一般均为0xffffffff
            pde_addr = ((env->cr[3] & ~0xfff) + ((addr >> 20) & ~3)) & env->a20_mask;
            pde = ldl_phys(pde_addr);
            if (loglevel & CPU_LOG_QIUHAN) {
                fprintf(logfile, "pde_addr=0x%08x, pde=0x%08x\n", pde_addr, pde);
            }
            if (!(pde & PG_PRESENT_MASK))//页表不存在
                return -1;
            //页面大小是否为4M
            if ((pde & PG_PSE_MASK) && (env->cr[4] & CR4_PSE_MASK)) {
                pte = pde & ~0x003ff000; /* align to 4MB */
                page_size = 4096 * 1024;
            } else {
                /* page directory entry */
                pte_addr = ((pde & ~0xfff) + ((addr >> 10) & 0xffc)) & env->a20_mask;
                pte = ldl_phys(pte_addr);
                if (!(pte & PG_PRESENT_MASK))//页不存在
                    return -1;
                page_size = 4096;
            }
        }
        pte = pte & env->a20_mask;
    }

    page_offset = (addr & TARGET_PAGE_MASK) & (page_size - 1);
    paddr = (pte & TARGET_PAGE_MASK) + page_offset;
    if (loglevel & CPU_LOG_QIUHAN) {
        fprintf(logfile, "pte_addr=0x%08x, pte=0x%08x, page_offset=0x%08x, paddr=0x%08x\n",
                pte_addr, pte, page_offset, paddr);
    }
    return paddr;

读懂这段代码,就比较清楚了。下面是刚才寻址时打印出的调试信息,能看懂吧:
addr=0x0804b578, cr3=0x079b6000, a20_mask=0xffffffff
pde_addr=0x079b6080, pde=0x04180067
pte_addr=0x0418012c, pte=0x04115425, page_offset=0x00000000, paddr=0x04115000
paddr=0x04115578

addr=0xc0a39628, cr3=0x079b6000, a20_mask=0xffffffff
pde_addr=0x079b6c08, pde=0x008001e3
pte_addr=0xbfbf8b98, pte=0x008001e3, page_offset=0x00239000, paddr=0x00a39000
paddr=0x00a39628
这就是qemu的好处:如果你对硬件不熟悉,可以通过阅读它的代码或者打印调试信息来理解。我正是先读了这段代码才
能写下此文的。

这样我们就对freeBSD下x86地址映射有了一个比较清楚的认识,至于这种映射是如何建立起来的,我们下次再说吧。

你可能感兴趣的:(c,FreeBSD,X86)