总目录
1. WinDbg概述
2. WinDbg主要功能
3. WinDbg程序调试示例
4. CPU寄存器及指令系统
5. CPU保护模式概述
6. 汇编语言不等于CPU指令
7. 用WinDbg观察托管程序架构
8. Windows PE/COFF文件格式简述
9. 让WinDbg自动打开DotNet Runtime源程序
10. WinDbg综合实战
前言
上一篇《4. CPU寄存器与指令系统》中,我们介绍了CPU工作模式、寄存器、指令系统以及如何借助AI学习汇编语言。
本篇,我们继续介绍Intel CPU。主要内容涉及:CPU运行模式、CPU内存模式、段寄存器、地址与操作数长度等。
IA-32和Intel 64架构
Intel按架构特点,将其CPU分为IA-32和Intel 64两种。从80386开始,一直到2004年之前上市的CPU都属于IA-32架构,自2004年开始,Intel 64架构开始占据Intel主流,2008年以后的CPU则全部属于Intel 64架构。
CPU运行模式
Intel 手册用术语operating mode来定义其运行模式。
所谓运行模式,是指同一块CPU,通过给某些控制寄存器赋予不同的值,就可以让其指令系统、寄存器数量与长度以及运行方式都发生重大变化的能力。
为了把抽象的运行模式概念具象化,让我们先花一点儿时间,从Intel第一代大规模应用的CPU 说起。
8086
1978年,Intel推出了8086和8088两款CPU,这是两款功能非常相似的CPU,都是16位寄存器,20位外部地址线,可寻址1M地址空间。两款CPU的差异仅在于:
- 8086有16条外部数据总线;
- 8088有8条外部数据总线;
注意:这两款CPU都不属于IA-32架构,但他们依旧属于x86系列。
8086段寄存器
16位寄存器最多只能表示65536个地址,如何让其实现对1M内存的寻址?Intel想出了一个很巧妙的方法:将内存分段。下面举例说明:
假设物理地址0x7F102存储单元保存着数据0x1A,如何使用CPU指令将此数据读到8位的AL寄存器中?在8086中,可以使用如下两条指令完成此任务:
mov DS, 7F00h ;将十六进制7F00h传给段寄存器DS
mov AL, DS:[102h] ;把DS段偏移102h的数据传给AL寄存器
在MASM汇编语言中,数据后缀h代表16进制数;分号后面是注释。上面的代码中,DS是段寄存器,Intel就是采用了段寄存器和偏移量组合的方式,来实现20位地址空间寻址的。在CPU硬件层面,每次寻址一个物理地址时,CPU会自动将段寄存器内容左移4位(相当于乘以16),然后再加上段内偏移计算出要实际要寻址的20位物理内存地址。针对我们的示例,7F00h左移4位(相当于十六进制左移一位)后变成7F000h,然后加上段内偏移102h,就形成了物理地址7F102h。
以上两条指令执行完毕以后,AL寄存器就会存有从7F102h取得的数据1Ah了。如果接下来想将AL的数据再保存到0x7F002单元,就可以这样写程序:
mov DS:[0002h], AL
注意:这次我们没有再为DS段寄存器赋值,因为只要未重新赋值,DS中就依旧是原来的 7F00h。但是,如果我们又想再次将AL中的数值赋值给00110h,就必须重新给DS寄存器赋值了,否则7F00h左移4位以后得到的基址是7F000h,偏移又只能是正数,两者和无论如何都无法满足00010h。这种情况下,程序员可以有多种选择,比如:
mov DS, 0h
mov DS:[110h], AL
或者:
mov DS, 10h
mov DS:[10h], AL
或者…总之会有很多种不同组合,但效果完全一样。
这引发了一个非常敏感的话题:同一个物理地址,会产生多种不同的[段基址+偏移量]组合。比如给DS赋值008Ah偏移量1025h,与给DS赋值055h偏移量1375h其实对应的是同一个内存物理地址:018C5h,但直观上根本看不出来。
所以,使用段寄存器分段内存的解决方案虽然看似巧妙,但也给程序员带来了不小的麻烦。
实际上,8086 CPU并非只有一个DS段寄存器,而是总计有4个段寄存器,分别是:
- CS:代码段寄存器
- DS:数据段寄存器
- ES:附加数据段寄存器
- SS:堆栈段寄存器
之所以设计多个段寄存器,是为了编程方便。下面举个例子:
假设想把地址00000h~001FFh合计512个字节数据拷贝到从FF000h开始的连续内存中,如果只有一个DS段寄存器,那么每次读数据前必须将DS设置为较小的值,而每次写数据前又必须将DS改为较大的值,就像下面的代码这样:
mov SI, 00 ;用SI寄存器作为偏移量
Start: ;循环开始标记
mov DS,0000
mov AX, word ptr DS:[SI] ;从DS:[SI]指向的内存读两个字节到AX寄存器
mov DS, FF00h ;修改DS以指向目标段
mov word ptr DS:[SI], AX ;AX寄存器内容保存到DS:[SI]
add SI, 2; SI = SI + 2,指向下一个字
cmp SI, 1FFh; 判断SI是否小于1FFh?
jb Start; 如果SI<1FFh,则跳转回Start开始下一个循环
而如果CPU有两个数据段寄存器DS和ES,就可以在初始化阶段让DS指向源数据区,让ES指向目标数据区,循环过程中就可以维持DS和ES不变,程序就可以变成如下这样:
mov DS,0000
mov ES, FF00h
mov SI, 00
Start:
mov AX, word ptr DS:[SI]
mov word ptr ES:[SI], AX
add SI, 2
cmp SI, 1FFh; =>SI是否小于101h
jb Start
备注,有关CPU寄存器,如果搞不清楚AL, AH, AX, EAX, RAX的区别,请查找我以前的文章。
显然,新程序每个循环只需5条指令,而旧程序则需要7条。
备注:可以使用STOSW指令完成相同功能,效率会好很多,但此处我们重点是介绍段寄存器,复杂的汇编指令并不是我们关注的重心。
如果每一条寻址指令都必须在前面写上使用的段寄存器,其实对程序员来说也是不小的负担。所以Intel指令系统在设计时就为不同类型的寻址指令设计了默认使用的段寄存器,除非需要显式变更默认段寄存器,否则在汇编语言中可以省略。比如上面的代码中,mov AX, DS:[SI]就可以简写为
- mov AX, [SI]
通过显式使用非默认段寄存器,其实在CPU指令系统中是采用了段超越前缀来实现的。
指令指针寄存器IP固定必须和CS寄存器配合且是CPU固定搭配,用户不可以使用段超越前缀指定IP使用其他段。
call, ret, int, iret等指令会涉及到堆栈操作,这类指令导致的栈操作自动使用SS:SP寄存器组合,不能更改。
8086的运行模式
8086只有一种运行模式,称为实地址模式(real address mode)。该运行模式有以下特点:
- 内存寻址使用段寄存器值左移4位作为基址,加上偏移量形成20位线性地址(CPU指令认识的地址),CPU硬件会自动将线性地址复制成物理地址提交给CPU地址线引脚;
- 实地址模式不支持多任务,无需考虑进程和线程;
- 任何程序都具有超集权限,可以对整个内存的任何地址数据进行读写,甚至运行期间可以修改任何代码,不能对操作系统提供任何保护。
因为实地址模式下应用程序权限不受限制,很方便在这个模式下学习计算机基础知识,方便进行各种测试。不过硬币总有另一面:这种模式下不支持多任务,比如不能同时打开记事本和CAD,给用户带来非常不好的体验,同时因为程序权限不受控,也给计算机安全带来巨大隐患,编写DOS下的病毒程序简直不能再容易!
IA-32架构的引入
1982年,Intel推出了初步具备IA-32架构的第一款CPU,称为80286,简称286。80286带来的架构变革,在Intel发展史上是最为巨大的,它完全颠覆了8086的单任务/非保护模式,引入了段选择子、段寄存器、保护模式及多任务系统支持。
1985年Intel发布了80386,这款CPU首次将寄存器由16位扩展到32位,新增了指令并行处理能力、Flat地址模式、页式存储和虚拟内存技术,标志着Intel已完美完成了由实地址模式IA-32架构的转型。
此后,Intel推出了大量新款CPU,如486、奔腾、赛扬、P6、志强、酷睿、酷睿i7系列…,应该说每一款新CPU的面世,都给用户带来了新的惊喜。不过后来的CPU在核心架构上的变化并不大,比如增加了128位或256位的寄存器及SIMD及AVX指令,做成了多核形态,增加了多级缓存和预取指机制等等,说到底都只是性能上的优化,整体架构依旧保持了386的结构特点。所以说:
针对程序调试目的来说,基本搞清楚了80386的知识已经足够!
至于Intel 64架构,其实除了新增了对64位操作系统和应用程序的支持以外,也并没有太多新东西,因此Intel才可以用一套《Intel 64 and IA-32 Architectures Manual》把两个架构CPU都说清楚。
CPU运行模式
有了以上的铺垫,我们可以在深入说一说CPU运行模式了。
现在的Intel CPU都支持多种运行模式,可以理解为每一种运行模式下,CPU会有不同的寄存器,不同的指令,不同的寻址方式,不同的外部特性,就如同一个会“变脸”的京剧演员。
IA-32架构和 Intel 64 架构均支持的运行模式包括:
- 8086模式,即实地址模式;
- 保护模式;
- 系统管理模式;
保护模式下,IA-32架构还可以直接运行实地址模式下的应用程序,且依旧可以享受保护模式下的多任务和保护功能,这被Intel称为虚拟8086模式(其实并不是一个单独模式,虚拟8086模式仅仅是保护模式的一个特性而已),但 Intel 64架构CPU已经取消了对此功能的支持。
不同运行模式之间的切换,原理上是使用CPU超级权限指令来修改CPU的不同控制寄存器来完成的。只不过呢,我们作为操作系统的普通用户,开发的程序一般都只有Ring3级权限,不具备执行超级指令权限,所以运行模式的切换虽然十分频繁,但基本都是操作系统直接使用的,普通用户如果想试验这种模式转换,一般只能不使用已有操作系统。
Intel手册第三卷中有一张图,简单描述了运行模式转换的的步骤:
如图所示,系统刚上电或按下reset键时,CPU工作是工作在实地址模式的。当系统程序通过超级指令设置PE位(Protection Enable,即CR0寄存器最低位)为1时,CPU就会切换到保护模式。之后如果使PE为变为0,CPU又会回到实地址模式。比如使用下面的CPU指令可以让CPU进入保护模式:
mov eax, cr0
or eax, 1
mov cr0, eax
备注:对cr0的读写需要代码具备ring0特权级别。
其他各种模式之间的转换图中也都有标注,比如一旦CPU的外部中断引脚SMI#得到信号,或者通过CPU内置APIC发出了SMI,则在任何模式下系统都会自动切换到SMM系统管理模式,系统处理完中断以后,会恢复之前保存的原CPU状态,然后返回。
不过呢,模式切换本身看似简单,但如果想让模式切换以后可以正常工作就没有那么容易了。最难的可能要属实地址模式到保护模式的切换,因为在保护模式下,原实地址模式的中断向量表已经无用,原来的段寄存器是用来装段基址的,在保护模式下则被用于装段选择子,因此必须先准备好段描述符,创建好全局描述符表和局部描述符表,初始化中断描述符对应的中断代码等等,还要开通A20门电路以便CPU可以访问高内存,完成所有这些准备以后,切换才能不至于死机。这些细节现在读起来还比较抽象,继续阅读就能基本明白。
也有一些模式的切换非常简单,比如从保护模式到虚拟8086模式,仅需修改EFLAGs寄存器的第17位(VM)即可。另外保护模式和IA-32e模式之间的切换也并不复杂,主要就是通过EFER寄存器的LME位来实现。
保护模式要点
《Intel 64 and IA-32 Architectures Manual》总计有5000多页,几乎所有的篇幅都是讲的保护模式,所以把保护模式学精深是很飞时间的,不过我们也没必要知道那么多,仅仅了解些我们日常调试最常遇到的知识即可。这也是本文立意所在,下买呢我们就开始。
段寄存器变化
IA-32和Intel 64架构的CPU依旧使用段寄存器,名称依旧是CS, DS, ES, SS,不过又新增了两个段寄存器FS和GS,所有段寄存器均为16位长度。
段寄存器的作用和8086的实地址模式完全不同。我个人以为,这种变化是保护模式与实地址默认的最根本区别。
简单说,CPU一旦进入保护模式运行,段寄存器中装的,就不再是段基址,而是段选择子。
段选择子是用来选择段描述符的索引值。
比如对于指令 mov EAX, dword ptr [03ab1122h] ,在保护模式下,CPU会首先找到默认的段寄存器DS,然后以DS中的段选择子作为索引,去内存中查找一个称为段描述符表的数组结构。段描述符表是一种结构数组,每一个成员都是一个段描述符。CPU通过查表得到对应的段描述符,通过段描述符获得该段的基址和上限。此后,CPU会比较我们的程序指令中的偏移03ab1122h(称为有效地址)和段描述符设定的上限是否冲突,如果超限,则会抛出异常;接下来,CPU会再通过段描述符核实该段是否允许读,如果不允许读也会抛出异常;再然后,CPU会通过段描述符核实该段要求访问代码必须具备的权限级别,如果级别不够,还会抛出异常。
如果以上检查均无问题,则CPU会使用段描述符设定的段基址加上偏移03ab1122h形成线性地址,然后执行读取内存动作,并将结果保存到EAX寄存器。
从以上简单描述可以知道,段描述符是一个非常完善的内存管理数据结构,其中定义了很多实地址模式下根本不存在的权限和检查。所谓保护模式,其核心要义也恰恰在于段描述符对内存提供了十分完善的保护和检查机制。在具体介绍段描述符之前,我们先说一下段选择子。
段选择子
段选择子是一种16位的数据结构,分成三个部分,如下图所示:
如前文所述,保护模式下的段寄存器中装的是段选择子。段选择子最低两位称为RPL,是Requested Privilege Level的缩写,意思是请求权限级别。TI位指示应该到哪个段描述符表中去查找段描述符。操作系统会创建两个段描述符表,一个叫全局描述符表GDT,基址保存在GDTR寄存器中;另外一个叫局部描述符表,基址保存在局部描述符表寄存器LDTR中。其余13位(位3 ~ 15)是索引值(Index)。
下面举一个具体例子,先让大家看看段选择子长什么样。
在WinDbg中,如果输入r命令,就可以列出所有寄存器,其中就包括6个段寄存器:
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b
段寄存器中的每个值都是段选择子。
针对这个列表,我们以ds所装段选择子002b为例进行分析。
将十六进制的002b转成二进制: 0000 0000 0010 1011。
根据上面的数据结构图,可以得出:
- TI = 0,说明需要到GDT中查找段描述符
- Index = 5,说明需要查找GDT中第6个段描述符(段描述符以0起始)
- RPL = 3,说明当前代码的权限级别为3(数字越大,级别越低,0级代表最高级别)
那么,GDT和LDT在哪里?他们都在内存中,是操作系统为我们建立的。GDT首地址保存在GDTR寄存器中,LDT的查找稍稍复杂些,这里不做介绍。如果用WinDbg调试内核模式程序,就可以查到GDTR寄存器的值(实际是通过GDTR得到段基址,通过DDTL得到Limit):
6: kd> r gdtr
gdtr=ffffb800fcbf4fb0
6: kd> r gdtl
gdtl=0057
段描述符
上文中,我们说到,CPU通过段寄存器中的段选择子及GDTR,就可以查到全局描述符表和段描述符。如果找到了段描述符,CPU要如何对其进行解读呢?这又涉及到了段描述符的数据结构,如下图所示:
每个段描述符占64位,其中包括32位基址(4GB,红色框所示)和20位上限(绿色框所示)。图中可以发现,段基址和上限都不是连续的,为什么会这样?原因就在于刚开始段基址和上限都没有现在这么大,结果后来不够用了扩展时,为了和历史版本兼容,就只好将数据分隔了。除了这两个最重要的数据以外,段描述符中还有很多其他字段,比如DPL是允许访问级别,Type是段类型(数据段还是代码段),P位表示该段是否在物理内存(与虚拟内存相关,如果已经被临时转储到了磁盘交换文件中,则此位为0,否则此位为1),其他字段不再介绍,如果需要时查查手册即可。
另外我们发现,刚才列出的 ds, es, ss的段选择子其实是相同的,都是002b,说明数据段、附加数据段和堆栈段其实是公用的。只有 cs = 0033 比较特殊。所以CS可能会存在特殊设置,比如可以执行但不可以写等等。
补充说明1:保护模式下,如果每次访问内存都要进行如此复杂的查表操作,效率是不是会非常低?其实,实际段寄存器长度远超16位,只不过其他位对用户程序不可见,只有CPU自己知道。这些隐藏位保存着当前段选择子对应的段描述符,相当于是一个缓存。实际上应用程序变更段选择子并不频繁,所以缓存可以很好解决效率问题。
补充说明2:目前的应用程序基本被编程成平坦(Flat)内存模式,这种模式下,cs, ds, es, ss的基址都是0,上限都是FFFFFh。所以,此时偏移地址(或有效地址)实际就是线性地址,要比实地址模式的非0段偏移更容易被程序员接受。
补充说明3: 由上面的布局图我们发现,上限一共只有20位,也就是最大FFFFFh,是不是说每个段最大只能是1M字节?其实,在段描述符中的G位(granularity)决定了段上限的单位。如果G位为0,则单位是字节(byte),最大空间确实只有1M;但如果G位是1,则单位是4K字节,所以最大段空间可达4G字节。
补充说明4:fs和gs这两个Intel后引入的段寄存器,一般被操作系统用于特殊用途,比如Windows系统用FS:0指向当前线程的TEB, 其基址即便在Flat模式也并不为0。
最后,通过WinDbg显示一个实际的段描述符:
0:000> dg ds
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
002B 00000000`00000000 00000000`ffffffff Data RW Ac 3 Bg Pg P Nl 00000cf3
该段的段选择子是002B;基址是0;限是FFFFFFFF,亦即4GB;Data:数据段; RW:可读可写;PL:访问级别3;pg:限粒度是4kB;P:在内存中;Nl: Not Long(不是64位代码)…
而fs段的情况如下:
0:000> dg fs
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0053 00000000`003c8000 00000000`00000fff Data RW Ac 3 Bg By P Nl 000004f3
fs段的基址不是0,而是00000000`003c8000。
内存模式
和程序调试密切相关的另一个知识点,就是内存模式。
CPU有三种内存模式可选:
- 实地址模式:如果CPU工作在实地址运行模式,则内存模式也只能是实地址模式。在实地址模式下,线性地址由段寄存器和偏移量决定,且物理地址 = 线性地址。
- 分段地址模式:在保护模式下,如果不同段的段描述符基址不同,就称这种模式为分段模式。分段模式与实地址模式的根本区别在于分段模式下使用的是段描述符,计算有效地址时,基址也无需左移。
- 平坦(Flat)模式:在保护模式下,设置CS,DS,ES和SS基址均为0,边界均为FFFFFFFF并保持程序运行期间不变,就成为平坦模式。在平坦模式下,因为各个段的基址都是0,所以有:偏移地址 = 线性地址。平坦模式是目前应用最广泛的内存模式。
下图展示了三种内存模式之间的差异:尤其要注意最右侧的线性地址,中间的偏移地址或有效地址,以及左边由【段基址和偏移】构成的逻辑地址这几个概念。Intel手册中对这些概念是严格区分的,如果疏忽,会导致阅读苦难。
虚拟内存和分页
在介绍内存模式时,我们说线性地址被复制成了物理地址;但在介绍分段模式和平坦模式时,我们并未提及物理地址,而只说了线性地址。
其实,在后两种内存模式下,线性地址到物理地址之间,还隔着一套地址转换。如果未使用虚拟内存,或者说未采用分页技术,那么物理地址就等于线性地址。否则,物理地址需要以线性地址为基准,再经过查表获得。
什么是虚拟内存呢?我们先做一个测试:
假设我的电脑有16G内存,刚开机时操作系统后台进程有20个,每个占了0.5G内存,那么当时我们还有6G内存可用。
假设我们后来我们开启了两个WinDbg,一个调试程序Core.dll,另一个调试程序Hello.dll。
分别在两个调试器中输入db 402000 L20命令,显示以402000h开始的32个字节,情况如下:
0:000> db 402000 l20 ; $$调试Core.dll
00000000`00402000 b0 5e b1 5e b2 5e b4 5e-ba 5e bb 5e bc 5e bd 5e .^.^.^.^.^.^.^.^
00000000`00402010 bf 5e c0 5e c1 5e c2 5e-c3 5e c4 5e c5 5e 3f 00 .^.^.^.^.^.^.^?.
0:000> db 402000 l20 ; $$调试Hello.dll
00402000 54 27 00 00 00 00 00 00-48 00 00 00 02 00 05 00 T'......H.......
00402010 98 20 00 00 1c 06 00 00-03 00 02 00 02 00 00 06 . ..............
很显然,虽然地址相同,但内容完全不同。这是如何实现的?此为问题1。
接下来,假设我们又开启了浏览器、PDF阅读器、Vidual Studio、AutoCad和记事本,随着同时运行的程序越来越多,最后可能会导致剩余内存空间不足。比如当我们最后打开Execl时,出现了内存不足,此时操作系统发现第一个WinDbg自从输入了db命令以后好久未操作了,于是把该程序占用的内存暂时保存到了磁盘文件中,腾出了空间留给Execl使用。由于此时Excel占用了原WinDbg的空间,显然此时的402000地址数据应该和WinDbg占用时期是不同的。
再经过一段时间,我们又把第一个WinDbg调到了前台,并再次输入db 402000 L20命令。本来我们预计,此时的402000已经被Excel给占用了,显示的内存数据应该与之前不同。但实际情况完全不是这样,无论我们如何倒腾,甚至再加载更多的窗口,任何时候我们在第一个WinDbg中查到的402000地址数据都不会发生变化,第二个WinDbg也是一样不会变化,但第一个和第二个之间永远不相同。Windwos到底如何实现的这种功能?此为问题2。
事实上,这就是操作系统管理下的虚拟内存为我们变的“魔术”。
在启用了虚拟内存分页机制以后,操作系统将实际物理内存人为划分成许多的页,假设每页是100M(实际每页大多是4K),那么我们的16G内存就被分成了160个页。然后操作系统会建立一张包含160项的内存转换表,这个表中每一条都会包含线性地址基址、物理内存页号,于是,即便被调配到磁盘文件中的源WinDbg内存未被释放,而是使用了一个新的页存储,只需操作系统用新的页号更新此转换表,我们在WinDbg中就不会发觉出现了异样,尽管实际物理地址已经不同。
这就是虚拟内存的基本原理。
正因为现在绝大多数电脑都默认使用了虚拟内存,所以我们需要知道:
我们在调试软件中看到的内存地址,或者我们在应用程序中使用的内存地址,实际上都不是真实的物理地址,最常见的是线性地址,也被称为虚拟地址!
比如我们在WinDbg汇编窗口看到的如下图所示的地址是虚拟地址:
我们用汇编语言写的如下程序中的1020h也是虚拟地址:
mov EAX, DS:[1020h]
也就是说,如果不用特殊手段,平时我们很难接触到真实物理地址,见到最多的,其实都是虚拟地址(即线性地址)。至于进程的线性地址到物理地址的具体转换,涉及到需要通过控制寄存器CR3找到页目录(PDT)首址,然后通过线性地址查找页目录得到内存页物理地址,再与线性地址的部分位相加,虽然算法并不困难,但毕竟我们只想说原理,这里不具体展开。
不过,我们除了经常见到线性地址以外,我们在调试程序时也经常见到以逻辑地址或有效地址显示的情况。比如以下WinDbg命令的输出,就是以逻辑地址形式显示的:
0:000> dd fs:0 l4
0053:00000000`00000000 00000000 00000000 00860000 00000000
0:000> dg fs
P Si Gr Pr Lo
Sel Base Limit Type l ze an es ng Flags
---- ----------------- ----------------- ---------- - -- -- -- -- --------
0053 00000000`003c8000 00000000`00000fff Data RW Ac 3 Bg By P Nl 000004f3
0:000> db fs:0 l30
0053:00000000`00000000 00 00 00 00 00 00 00 00-00 00 86 00 00 00 00 00 ................
0053:00000000`00000010 00 e0 85 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0053:00000000`00000020 00 1e 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
上面的例子中,我们看到每一行的地址都是0053:xxxxxxxx xxxxxxxx格式,这就是逻辑地址格式,它表示线性地址基址由0053段选择子决定,后面是偏移量。举例来说,倒数第二行第二个数据e0的逻辑地址是0053:00000000 00000011,因为段选择子0053对应的段描述符的基址是00000000 003c8000,所以其线性地址就应该是00000000 003c8000 + 00000000 00000011 = 00000000 003c8011。这个计算是可以被验证的:
0:000> db 003c8011 l10
00000000`003c8011 e0 85 00 00 00 00 00 00-00 00 00 00 00 00 00 00
这一节,我用最通俗的方式介绍了虚拟内存。本来虚拟内存道理很简单,不过如果使用严谨的教材推理,过程却比较复杂,但因为我个人语言能力有限,加上写作时间太长,真的有点儿吃不消了,写得很乱,还希望谅解。
如何应用内存模式
前面,我们谈到了内存模式有实地址模式、分段模式和平坦模式,且很多应用程序都只能使用平坦模式,不能选择其他了。比如在MASM汇编器中,内存模式指令是.MODEL,针对16位应用程序,可以选择TINY, SMALL, COMPACT, MEDIUM, LARGE, HUGE和FLAT,但针对32位则只能选择FLAT,而针对64位汇编器ML64.EXE,则直接取消了.MODEL指示,也就是64位程序自动就是FLAT的。具体信息请点击微软官网。
至于选择32位抑或64位程序,通常也是通过编译程序为我们来处理的。