📄 jiurl玩玩win2k内存篇 内存共享(二) copyonwrite.htm
字号:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<!-- saved from url=(0074)http://jiurl.cosoft.org.cn/jiurl/document/JiurlPlayWin2k/MmCopyOnWrite.htm -->
<HTML><HEAD><TITLE>JIURL玩玩Win2k内存篇 内存共享(二) CopyOnWrite</TITLE>
<META content="text/html; charset=gb2312" http-equiv=Content-Type>
<STYLE type=text/css>.title {
FONT-FAMILY: "黑体", Arial, sans-serif; FONT-SIZE: 21px; FONT-WEIGHT: bold; LINE-HEIGHT: 48px; TEXT-DECORATION: none
}
.author {
FONT-FAMILY: "宋体"; FONT-SIZE: 12px; LINE-HEIGHT: 16px
}
.content {
FONT-SIZE: 14px; LINE-HEIGHT: 20px
}
</STYLE>
<META content="MSHTML 5.00.2614.3500" name=GENERATOR></HEAD>
<BODY bgColor=#f7f7f7 topMargin=5>
<DIV align=center>
<CENTER>
<TABLE border=0 cellPadding=0 cellSpacing=0 height=29 width="96%">
<TBODY>
<TR>
<TD class=title height=41 width="100%">
<P align=center><FONT face=宋体>JIURL玩玩Win2k内存篇 内存共享(二) </FONT><FONT
face=宋体>CopyOnWrite</FONT></P></TD></TR></CENTER>
<TR>
<TD class=author height=9 width="100%">
<P align=center><FONT face=宋体>作者: <A
href="mailto:jiurl@mail.china.com">JIURL</A> </FONT></P></TD></TR>
<TR>
<TD class=author height=6 width="100%">
<P align=center><FONT
face=宋体>
主页: <A href="http://jiurl.yeah.net/">http://jiurl.yeah.net/</A>
</FONT></P></TD></TR>
<TR>
<TD class=author height=2 width="100%">
<P align=center><FONT face=宋体> 日期: 2003-7-30</FONT>
</P></TD></TR></TBODY></TABLE></DIV>
<DIV align=center>
<CENTER>
<TABLE border=0 cellPadding=0 cellSpacing=0 height=1 width="96%">
<TBODY>
<TR>
<TD height=1 width="100%">
<HR color=#396da5 SIZE=3>
</TD></TR></TBODY></TABLE></CENTER></DIV>
<DIV align=center>
<TABLE border=0 cellPadding=0 cellSpacing=0 class=content height=10000
width="96%">
<TBODY>
<TR>
<TD height=1041 vAlign=top width="131%">
<P><FONT face=宋体><B>CopyOnWrite</B></FONT>
<P><FONT face=宋体>
对于同一个可执行文件运行的两个进程,或者被多个进程共享的动态链接库,只读共享代码部分的物理页是没有问题的。而对于向代码页中写(调试器就可能向代码页中写),这将会影响其他共享这个代码页的进程的正确执行。或者数据页比如说初始化了的全局变量所在的页,每个进程可能会写入不同的数据,也将影响其他共享这个页的进程的正常运行。为了使程序正常运行,应该使每个进程的可能被写的页映射到自己的物理页上,这样就不会影响别的进程了。如果为每个进程每个可能会被写入的页都直接分配新的物理页的话,很有可能会造成不必要的浪费,如果这个一个进程自始至终都没有向某页写入数据的话,那么分配该页所花的执行时间,该页所占的物理内存,都是没有用的。Win2k
为了避免这种浪费,提高效率,使用叫做 Copy On Write 的机制来处理这种情况。这是叫做 Lazy Evaluation
技术的一部分。</FONT>
<P><FONT face=宋体> Copy On Write
就是对于一页,多个使用进程共享,直到一个进程要向该页写入数据的时候,系统会给该进程一个新的物理页,并把原来的页的数据复制过来,更新该进程的页表项,使该进程映射新的物理页,数据会写入该进程新的自己的物理页中。这样就不会影响原来的数据页。而其他共享该页的进程仍然可以继续共享,直到他们也试图写入数据。</FONT>
<P><FONT face=宋体> Win2k 中 Copy On Write
机制的主要应用是,可以方便调试器向一个进程的代码页写入东西,而不会影响别的共享这个代码页的进程。某些数据页。</FONT>
<P><FONT face=宋体> Copy On Write 机制的实现。Win2k 把需要 Copy On
Write 的页的页表项(PTE)的标志位中的读写位设为只读,并设置 CopyOnWrite
标志位。这样当一个进程向该页写入数据的时候,因为页表项设为只读,所以会引起 Page-Fault 异常(Exception)。从而使 CPU
转去执行异常处理程序,异常处理程序检查页表项发现设置了 CopyOnWrite
标志,就会完成分配新物理页,更新进程的页表项,把新的页表项的读写标志设为读写等工作。最后 CPU
重新执行引起异常的指令,这时该指令所写的虚拟地址已经是新的物理页了,并且该虚拟地址的页表项的标志也设为了可写,于是就可以顺利执行。下面我们针对
x86 CPU 做更详细的说明。x86 CPU 的页表项定义如下<BR><BR>struct _HARDWARE_PTE_X86
(sizeof=4)<BR>bits0-0 Valid<BR>bits1-1 Write<BR>bits2-2 Owner<BR>bits3-3
WriteThrough<BR>bits4-4 CacheDisable<BR>bits5-5 Accessed<BR>bits6-6
Dirty<BR>bits7-7 LargePage<BR>bits8-8 Global<BR>bits9-11
reserved<BR>bits12-31 PageFrameNumber<BR><BR>
首先要说明的是,这个格式是由 CPU 定义的,CPU 将按照这个定义,对每一位做出解释,然后决定处理方式。其中要注意的是 bits1-1 Write
这将决定该页是否只读,为 0 表示只读,为 1 表示可读可写。bits9-11 reserved 这3位,CPU
没有定义,留给操作系统使用。Win2k 用 bits9-9 CopyOnWrite 来表示是否使用
CopyOnWrite,该位为0表示不使用,为1表示使用。<BR><BR> 当某页的页表项中标志位设置了只读,和
CopyOnWrite 之后,当某条指令向该页中写入的时候,比如指令 MOV AddressInCopyOnWritePage,1
,执行这条指令时,CPU 会自动通过页目录和页表把 虚拟地址AddressInCopyOnWritePage
转换成物理地址,在地址转换过程中,CPU
在从页表项得到物理页地址的同时,会进行页保护检查,比如看该页表项是否有效,是否是只读等等。在这里我们指令中地址的页表项设置了只读标志,于是就会引发异常。异常也是由
CPU 实现的。这里引起的是一个 Page Fault 异常,它的中断号是 0xe (十进制14),需要注意的是 Page Fault 的中断号是
0xe 这是由 CPU 定义的( x86 CPU 的 从 0 - 31 这32个中断是由 CPU 定义的,CPU
将根据这个定义做相应工作)。在发生异常时,CPU
自动把一些寄存器压入堆栈,然后根据中断号,(通过IDTR找到中断描述符表)在中断描述符表中找到相应的中断描述符,根据中断描述符中的地址,转到异常处理程序。中断描述符是由Win2k设置,异常处理程序也是由Win2k决定。对于
Win2k Build 2195 来说,中断 0xe 的处理程序是 ntoskrnl!KiTrap0E 地址在 804648a4 。当转到
KiTrap0E 时,CPU
已经在堆栈中压入了下面的内容<BR><BR>|-------------| <BR>|
EFLAGS |<BR>|-------------|<BR>|
CS
|<BR>|-------------|<BR>| EIP
|<BR>|-------------|<BR>| Error Code |<BR>|-------------|<---- [
ESP ]<BR><BR><BR>page-fault 异常 (#PF) 的 Error Code 定义如下( CPU 定义
)<BR> |
3 | 2 | 1 | 0
|<BR>+---------------------------------------------------+ <BR>|
Reserved |RSVD|U/S|R/W|
P
|<BR>+---------------------------------------------------+ <BR><BR>P
0 错误由无效页引起<BR> 1 错误由违反页保护引起<BR>W/R 0
引起错误的内存访问是读<BR> 1 引起错误的内存访问是写<BR>U/S 0
访问错误时处理器处在管理模式<BR> 1 访问错误时处理器处在用户模式<BR><BR>需要说明的是堆栈中压入的
EIP 就是引发异常的指令地址,将来将根据这个地址重新执行该指令。而寄存器 cr2 中是引发异常时访问的地址。<BR><BR>#PF异常处理程序
KiTrap0E(由Win2k提供)将会调用 ntoskrnl!MmAccessFault ,MmAccessFault
根据是写操作引起的异常,以及CR2中的访问地址,计算出相应的 PDE,PTE地址,检查 PTE 发现设置了CopyOnWrite 标志(这个标志是由
Win2k 定义的,也是由它来处理),最终会调用 ntoskrnl!MiCopyOnWrite 来完成 Copy On Write
的相关工作。比如分配新的物理页,改变页表项中的物理地址,改变页表项的标志,把只读改为可读可写。<BR><BR>执行完异常处理程序之后,CPU
重新执行引起异常的指令,这时指令可以正常执行了。至此 Copy On Write 已经被实现了。<BR><BR>下面我们通过一个例子来观察 Copy
On Write
的实际情况<BR><BR>//----------------------------------------------------------------------<BR><BR>#include
<windows.h><BR>#include <stdio.h><BR>#include
<conio.h><BR><BR>#pragma data_seg(".jiurl")<BR><BR>char
JiurlSegData[32]="aaaaaaaaaaaaaaaaaaaaaaaa";<BR><BR>#pragma
data_seg()<BR><BR>void main()<BR>{<BR>printf("JiurlSegData Address:
0x%08x\n",JiurlSegData);<BR>printf("JiurlSegData Pte Address:
0x%08x\n",<BR>((ULONG)(JiurlSegData)>>12)*4+0xC0000000);<BR>printf("JiurlSegData:
%s\n",JiurlSegData);<BR>printf("\n");<BR><BR>printf("Input New String to
JiurlSegData\n:");<BR>scanf("%s",JiurlSegData);<BR><BR>printf("JiurlSegData:
%s\n",JiurlSegData);<BR><BR>getch();<BR>}<BR><BR>//----------------------------------------------------------------------<BR><BR>这个程序一个叫
".jiurl" 的节中,有一个初始化了的全局变量(把这个全局变量放在单独的一个节中,是为了避免受其他变量的影响)。分析编译生成的 PE
可执行文件,就可以看到这个节,以及这个节的属性,读,写,初始化数据。程序运行时,将输出这个全局变量的地址,以及根据这个地址计算出的该地址的PTE的地址,并
printf 全局变量中的内容。然后等待输入一个新的字符串写入全局变量中。我们将运行这个程序的两个实例,使用 SoftICE 来观察 Copy On
Write 。需要强调一点,运行程序之后,Ctrl+D 动作快点,因为时间长了系统很有可能把该页换出物理内存。<BR><BR>这个程序的名字叫做
CopyOnWrite-Er<BR><BR>1. 运行一个 CopyOnWrite-Er ,再运行一个 CopyOnWrite-Er<BR>首先用
SoftICE 的 addr 命令列出当前运行的进程<BR><BR>:addr<BR>CR3 LDT Base:Limit KPEB Addr
PID Name<BR>00030000 8141E020 0008 System<BR>04E2B000 810F75C0 008C
smss<BR>06562000 810E8C40 00A8 csrss<BR>07547000 810CC0C0 00BC
winlogon<BR>078E9000 810C14E0 00D8 services<BR>078FA000 810BFD60 00E4
lsass<BR>00ABD000 8109F200 0170 svchost<BR>00324000 810924C0 0190
svchost<BR>00564000 81054880 0204 Explorer<BR>024CF000 8108B960 0250
internat<BR>07FEC000 82F873C0 01F4 conime<BR>036C1000 8331C180 034C
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -