Chinaunix首页 | 论坛 | 博客
  • 博客访问: 315558
  • 博文数量: 174
  • 博客积分: 3061
  • 博客等级: 中校
  • 技术积分: 1740
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-04 22:43
文章分类

全部博文(174)

文章存档

2011年(54)

2010年(14)

2009年(30)

2008年(26)

2007年(27)

2006年(23)

我的朋友

分类: WINDOWS

2009-11-03 13:34:56

这两天开始看<<软件调试>> 同时也是为了一个分享做准备,结合以前看linux kernel时候的一些经验,从新书写了自己对保护模式的一些了解。
 
首先保护模式,为什么叫做保护呢?
就是它提供了任务隔离的能力,那么如何实现呢?
 
intel定义了一个运行级别,从0-3,其中windows仅仅使用了0,3,前者为内核模式,后则为用户模式;常见的用vs系列开发的引用程序都运行在3级别,而0保留给了内核态代码或者数据使用,常见的是驱动程序以及内核。
 
那么我们知道,一个windows程序实际上可以使用的地址范围是 0x00000000-0xffffffff , 或者所任何一个windows程序都可以使用这么多地址空间,那么windows是如何进行任务隔离或者所如何实现保护模式的呢?
 
一般来说windows 将低2GB空间即0x00000000-0x7ffffffff 定义为用户态空间,将0x80000000-0xffffffff定义为内核态,或者所将前者的级别定义为3,后者的级别定义为0。那么也就是说想访问或者读取低地址的空间,则需要代码的级别 《= 3即可,而访问高地址的则需要=0。 这样九使得任何一个用户态空间内的引用程序代码是无法访问内核态地址空间的。
 
另外windows使用了段以及页的地址管理模式,将0x00000000-0x7fffffff 的地址空间进行映射,即达到的效果是多个引用程序的用户态空间的相同用户态地址所指向的真正物理地址是不通的。
 
且0x00000000-0x7fffffff 是私有空间,即不公共的;
0x80000000-0xffffffff 是公共空间,即对所有进程是公共的。
 
 
 
综合上述:
1. 应用程序和内核程序是在地址空间上完全格式的,前者是无法直接访问后者的。
2. 应用程序之间其地址访问是相同的,但是其真正指向的物理地址是一般是不同的。
 
这样就实现了应用程序间不会彼此干扰,应用程序不会破坏内核。
 
特别需要说明一下的是:
内核态地址空间对于任何一个应用程序都是真正相同的,即任何一个内核态的地址在任何程序中映射后的地址都相同。 
 
上面说了这么多,其实有个问题是很关键的,就是说我们经常看到的程序中的地址是什么地址?它是如何被转化成物理地址的? 为什么说高地址就是所有程序相同的?
 
我们先说说最后一个问题。
首先从理论上来说,很多书本上都如此说;另外从证实来看页很简单;
因为你可以在任意一个进程中通过windbg 输入 dd
只要地址是高地址那么你会发现内容实际上都一样。
 
下面我们继续分析地址转换....
首先我们程序看到的地址一般成为逻辑地址;而它在最终变成物理地址访问前,windows需要将其转换为一个叫做线性地址,所谓线性就是0-4GB-1的地址空间内的地址 ;最后将线性地址转换为物理地址;
 
我们想来分析windows下逻辑地址是如何转化为线性地址的;
我们可以回忆一下,intel的cpu有很多寄存器,比如cs,ds,ss,分别表示代码段寄存器,数据段寄存器,堆栈段寄存器; 而早些年在实模式下地址的计算方式是cs:ip == cs << 4 + ip .
 
那么如今的windows下呢?
其实所谓逻辑地址页可以认为还是cs:eip ,只是绝大部分情况下cs我们可以不用关心,但是要回答如何将逻辑地址转换为线性地址则就要关心这个寄存器。
 
在保护模式先,段寄存器中存储的成为段选择符,segment selector.
 
那么选择子的格式我们想来看看。
其中RPI 表示需要的运行级别。
TI =0 表示是全局表,=1 表示局部表。
 
其余13位 表示索引。
 
这个时候会发现,有几个新概念被引入了,RPL 0 -3 这个之前讲过了; 但是什么是全局表,什么是局部 表? 索引又是什么么?
 
要知道我们要做的是一个地址转换的工作,那么又联想到一个表,那么貌似这个表中存放的是表的转换地址表?而索引又恰好是指定了用哪一项进行转换? 
 
非常接近,或许说正确答案就是如此。
 
那么什么是全局表,什么是局部表呢? 
windows中GDT 成为全局描述符表,局部表成为LDT 局部描述符表。
其中LDTR 寄存器存放的是LDT在GDT中的index , 结合段寄存器的index,就可以定位LDT中的描述符。
GDTR 存放的是GDT表的地址,GDTR 结合段寄存器就可以定位在GDT中的描述符。
 
从目前的windows来看LDT几乎 不用,先你就知道只有一张表DGT,它存放了将逻辑地址转换为虚拟地址的规则。
 
 
 
GDT是一张表,而表中存储了很多项,这些项被成为描述符,这些描述符,这儿我们就先认为只有段描述符。
我们来看看它的格式:
 
 
所谓系统描述符,指除代码段,数据段描述符外的:
1。 LDT表入口描述符
2. TSS段描述符
3. 中断门描述符
4. 陷阱门
5. 调用门
 
这些LDT一般不用,其他几个以后再讲。
 
这儿会发现又引入了很多概念,我们先将几个最重要的概念介绍一下:
1. 基地址,它说明这个段的开始地址是什么,其共有32bit组成,说明其合法的范围是4GB。
2. 段限长共计有20bit,即1M,不过它结合上另外一个G即粒度标记0表示直接,=1表示4KB,
  因此一个段的限长最大也可以达到4GB.
3. P 表示这个段是否已经在内存中
4. DPL 级别
5. Type , 类型,具体结合实例来看。
 
那么回答问题,逻辑地址如何转换为线性地址,cs:eip 如何转到到线性地址?
规则如下:
1. cs 指定了GDT,以及访问级别,一般来说cs,ds,ss DPL=3,因为往往表示应用程序的代码,数据,堆栈段。
2. cs 的高13bit指明在GDT中属于哪一项,具体是Addr(GDT) + index * sizeof(segment descriptor) ,描述符=8Bytes长度。
3. 将段的baesaddr + 逻辑地址的eip 就得到了线性地址
 
这个如果有可能讲述的时候可以插入一个windbg的实例
1. 任意在windows启动一个目标程序,比如写字板,notepad
2. 启动windbg attach上去
3. 随意输入一些内容,比如123456,然后用s -u 0 L?7fffffff "123456" 搜索确定一个地址比如0x0e789,同时假设cs=23
4. 23:0e789 就是逻辑地址, 输入dg 23 展示出23号段描述符的内容。
5. 将段基地址+0x0e789 得到线性地址 比如0x0e789
 
讲述到了这儿我们基本说清楚了,将一个逻辑地址转换位线性地址,但是在目前的windows中由于启用了分页paging,因此这时候拿到的地址并不是物理地址。 还需要一个线性地址到物理地址的转换。
 
复杂吧,发现竟然还要进行一次地址转换,不过这次和上次差不多,也是查表,呵呵,只是这次查的表更复杂些。 windows定义了页目录,页表. 什么意思呢? 刚才得到的32bit的线性地址,其高10位,成为页目录表索引,中间10位成为页表索引,低12bit成为页内偏移。
 
windows的实现中CR3这个寄存器存储也目录表PDT的入口地址,不过貌似每个windows进程的这个表都不同,且用户太windbg无法访问这个地址,因此拿到某个进程的这个入口地址,是通过在内核态windbg使用
!process pid 0 发现的,DirBase,而为什么内核态下不直接访问CR3,则是由于你不知道当前的进程上下文是谁。
 
 
 
所以DirBase + HI(10b) * sizeof(entry) = PTE entry,即页表的入口,假设称其为addr1.
 
addr1 + mid(10bit) * sizeof(entry) = 页地址 假设称其为addr2,
 
最后addr2 + low(12bit) 就是页内地址,页就是物理地址了。
 
对于那些非4KB的页,比如4MB页,则没有页表,也目录表即addr1的高10位直接作为物理地址的高10位,而线性地址低22位作为offset。
 
这儿要做一下说明:
 
cr3 在进程切换的时候会自动切换KPROCESS 中的dirbase,不过这个记录的是物理地址。
 
对于启用了PAE之后的分析,参看:
 
讲到这儿大部分东西都讲了,或许这儿讲述一个windbg的实例比较好.
阅读(1077) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~