此次实验的任务是:通过调试来查找Linux0.11内核启动后第一次引发页异常中断Page Fault的代码,并根据Linux0.11内核代码和intel386手册分析出现中断的原因。
有关调试环境的建立请参考:从linux0.11引导代码小窥内存分段机制。一般中断的处理过程可参考:实例分析Linux0.11内核中断机制。
查找引发第一次缺页中断的代码位置很简单,依据理论:发生中断后,cpu将代码段选择符和返回地址的偏移值压入堆栈(进程内核态堆栈)。这个返回地址就是引发中断的代码地址。
设置页异常中断处理函数入口代码如下:
/*摘自kernel/traps.c,trap_init函数*/ set_trap_gate(14,&page_fault); |
通过查看System.map文件可知page_fault函数地址为0xb858。启动bochsdgb进行调试,命令行如下:
<bochs:1> b 0xb858
<bochs:2> c
(0) Breakpoint 1, 0xb858 in ?? ()
Next at t=16886891
(0) [0x0000b858] 0008:0000b858 (unk. ctxt): xchg dword ptr ss:[esp], eax ; 870424
<bochs:3> dump_cpu
……
esp:0xffffe8 // 任务1内核态堆栈位置
……
tr:s=0x30, dl=0xf2e80068, dh=0x89ff, valid=1 // 由此可知任务1代码引发页异常中断
……
cr2:0x401f244 // 引发页异常中断的线性地址
……
<bochs:4> print-stack
00ffffe8 [00ffffe8] 0007
00ffffec [00ffffec] 6790
00fffff0 [00fffff0] 000f
00fffff4 [00fffff4] 10646
00fffff8 [00fffff8] 1f248
00fffffc [00fffffc] 0017
01000000 <could not translate>
……
按照堆栈向下增长方向整理调试信息,如下表所示:
0x0000 |
原SS |
0x00000017 |
原ESP |
0x0001f248 |
|
EFLAGS |
0x00010646 |
|
0x0000 |
CS |
0x0000000f |
EIP |
0x00006790 |
因此,第一次引发页异常中断的代码地址为0x6790。
接下来确定处于该位置的代码内容,重新启动bochsdgb,在0x6790处设置断点,命令行如下:
<bochs:1> b 0x6790
<bochs:2> c
(0) Breakpoint 1, 0x4006790 in ?? ()
Next at t=16886891
(0) [0x00006790] 000f:00006790 (unk. ctxt): call .+0x6884 ; e8ef000000
在0x6790处的代码是 call 0x6884,查看System.map文件可知init函数的入口地址是0x6884,该函数仅在Init/main.c中的main函数中得到调用:
void main(void) { // 省略部分代码 sti(); // 打开中断标志 move_to_user_mode(); //0号任务移到用户态执行 if (!fork()) { init(); // 此处引发第一次页异常中断 } for(;;) pause(); } |
这个问题很有意思,牵涉很多方面的知识,感兴趣的同志可以先考虑一下,然后再参考下面的答案。
1) CR2寄存器保存引发页异常中断的线性地址。
第一次引发页异常中断的线性地址为0x401f244,这个线性地址位于任务1的地址空间。
2) 任务0调用fork函数创建任务1,在创建结束后,任务1和任务0共享代码和数据。
线性地址0x401f244对应任务0的线性地址0x1f244,而任务0的线性地址可以认为是实模式下的物理地址。
查看System.map文件,在启动的Linux界面输入以下命令:
[/usr/root]# cat System.map |grep 1e
Allocating common __ctmp: 4 at 1e258
Allocating common _user_stack: 1000 at 1e25c // 0x1f244在user_stack的地址范围0x1e25c~0x1f25c之内
……
3) 任务0的用户态堆栈即user_stack,任务1也共享任务0的用户态堆栈。
4) 进程间共享代码和数据的同时,它们相应的页目录和页目录表项都设置成只读的页面,当有写操作时就利用页异常中断调用,执行写时复制。
5) 当任务1执行函数调用init的时候,任务1将对与任务0共享的用户态堆栈执行写操作,因此引发页异常中断。
调试验证:1. 引发页异常中断之前,0x401f244对应的物理页地址与0x1f244对应的物理页地址一致;2. 0x401f244对应的页表项读写位标志为只读;3. 页异常中断处理完毕后,0x401f244对应的物理页地址与0x1f244对应的物理页地址不一致;4. 0x401f244对应的页表项读写位标志为读写。
计算线性地址对应的页表项地址,计算公式为(unsigned long *) (((address>>10) & 0xffc) + (0xfffff000 & *((unsigned long *) ((address>>20) &0xffc))))。(注:address>>12&0x3ff获得页表项索引,每条页表项4字节,要获得页表偏移值还需要将页表项索引值左移2位,同理可得页目录项偏移值,而页目录基址为0,页目录项偏移值即页目录项的实际物理地址。)
启动bochdgb进行调试,命令行如下:
<bochs:1> b 0x6790
<bochs:2> c
(0) Breakpoint 1, 0x4006790 in ?? ()
Next at t=16886891
(0) [0x00006790] 000f:00006790 (unk. ctxt): call .+0x6884 ; e8ef000000
(0x401f244>>20) & 0xffc = 0x40,(0x1f244>>20) & 0xffc = 0x0,查看0x40和0x0处的4字节内容,即相应的页表地址:
<bochs:3> x /1 0x40
[bochs]:
0x00000040 <bogus+ 0>: 0x00ffe027
<bochs:4> x /1 0x0
[bochs]:
0x00000000 <bogus+ 0>: 0x00001027
(0x401f244>>10) & 0xffc = 0x7c,(0x1f244>>10) & 0xffc = 0x7c,0x7c + (0xfffff000 & 0xffe027) = 0xffe07c,0x7c + (0xfffff000 & 0x1027) = 0x107c,查看0xffe07c和0x107c的4字节内容,即相应的物理地址:
<bochs:5> x /1 0xffe07c
[bochs]:
0x00ffe07c <bogus+ 0>: 0x0001f065 //页基址0x1f000
<bochs:6> x /1 0x107c
[bochs]:
0x0000107c <bogus+ 0>: 0x0001f067 //页基址0x1f000
可以看到0x401f244的页面地址为0x1f000,第0位置1标志可用,第1位置0标志只读;0x1f244的页面地址也为0x1f000,第0位置1标志可用,第1位置1标志可读写。查看页中断处理之后的页表项内容:
<bochs:7> n // 简单执行一条next语句即可,中断处理完毕将重启0x6790处的指令
(0) Breakpoint 1, 0x4006790 in ?? ()
Next at t=16889019
(0) [0x00006790] 000f:00006790 (unk. ctxt): call .+0x6884 ; e8ef0000
00
<bochs:8> x /1 0xffe07c
[bochs]:
0x00ffe07c <bogus+ 0>: 0x00ffd007 // 重新申请到物理页,基址0xffd000,可读写
<bochs:9> x /1 0x107c
[bochs]:
0x0000107c <bogus+ 0>: 0x0001f067
下面的内容摘自Intel80386程序员手册9.8.14节。
This exception occurs when paging is enabled (PG=1) and the processor detects one of the following conditions while translating a linear address to a physical address:
● The page-directory or page-table entry needed for the address
translation has zero in its present bit.
● The current procedure does not have sufficient privilege to access the
indicated page.
The processor makes available to the page fault handler two items of information that aid in diagnosing the exception and recovering from it:
● An error code on the stack. The error code for a page fault has a format different from that for other exceptions (see Figure 9-8). The error code tells the exception handler three things:
1. Whether the exception was due to a not present page or to an access rights violation.
2. Whether the processor was executing at user or supervisor level at the time of the exception.
3. Whether the memory access that caused the exception was a read or write.
● CR2 (control register two). The processor stores in CR2 the linear address used in the access that caused the exception (see Figure 9-9). The exception handler can use this address to locate the corresponding page directory and page table entries. If another page fault can occur during execution of the page fault handler, the handler should push CR2 onto the stack.