Win32汇编中,每个程序都可以用4GB的内存吗?
Win32汇编源代码中,为什么看不到CS,DS,ES,SS等段寄存器?
一 DOS操作系统的内存安排
Win32编程与DOS编程最大的区别之一就是内存的使用.先看下DOS操作系统的内存使用:
如图,DOS操作系统运行于实模式中,由于8086处理器的寻址范围只有1MB,当时系统硬件使用的存储器地址(如图形模式视频缓冲区)被安排在高端,
地址是从A0000h(即640KB)开始的384KB中. 在内存低端,安排了中断向量表和BIOS数据区,剩下的500h到A0000h总共不到640KB的内存是给操作系统
和应用程序使用的. 其中DOS操作系统还占有一部分内存,剩下600KB左右内存才是应用程序真正可用的.
当80386处理器推出后,可寻址的内存范围达到了4GB(32根地址总线).但16位的段寻址方式限制了DOS程序,所有高于1MB的扩展内存只能通过
XMS驱动程序当作数据交换使用,程序的执行空间并没有什么增加.
二 80386的内存寻址机制
Windows的内存管理和和DOS的内存管理有很大不同,在了解Windows的内存管理模式之前,需要对80386保护模式下的内存分页机制有了解.
为了做个对比,先来看看实模式下的内存寻址方式,即DOS下的寻址方式:
如上图,在实模式下,一个完成的地址由段地址x16+偏移地址组成. 如xxxx:yyyy格式的虚拟地址在内存中的实际位置是xxxx10h+yyyy
当80386处理器工作在保护模式和虚拟8086模式的时候,可用使用全部32根地址线访问4GB大的内存.段地址x16+偏移地址的方式无法覆盖
这么大的范围.但是80386中所有的通用寄存器都是32位的,2的32次方=4G,可用寻址4G个内存单元,每个内存单元是1B,即可用寻址4GB.所
以使用通用寄存器来寻址即可访问所有的内存地址.
既然在保护模式下寻址时不再使用段寄存器,那保护模式下段寄存器没用了吗?不是的.虽然在寻址上不再有分段的限制问题,但在保护模式
下,一个地址空间是否可用被写入,可以被多少优先级的代码写入,是不是允许执行等涉及保护的问题就出来了.必须定义一些安全属性.段寄存器
就排上用场了.但是设计安全属性的参数要64位长,80386的段寄存器只有16位,怎么办? 解决办法是把所有段的段描述符顺序放在内存中的指定
位置,组成一个段描述符表,而段寄存器中的16位用来做索引信息,指定这个段的属性用段描述符表中的第一个段描述符.
段描述符表放在内存什么位置?80386中引入了2个新的寄存器来管理段描述符表.一个是48位的全局描述符表寄存器GDTR,一个是16位的局部
描述符表寄存器LDTR.
GDTR指向的描述符表位全局描述符表GDT,GDT包含系统中所有任务都可用的段描述符,通常包含描述操作系统所使用的代码段,数据段和堆
栈段的描述符及各任务的LDT段等,全局描述符表只有一个.
LDTR则指向局部描述符表LDT. 80386处理器设计成每个任务都有一个独立的LDT.它包含有每个任务私有的代码段,数据段和堆栈段的描述符,
也包含该任务所使用的一些门描述符,如任务门和调用门描述符等.
不同任务的局部描述符表分别组成不同的内存段,描述这些局部描述符表的内存段的描述符放在全局描述符表中.和GDTR直接指向内存地址不同,
LDTR和CS.DS等段选择器一样只放索引值,这个索引指向描述符在全局描述符表中的位置(局部描述符表内存段对应的描述符).随着任务的切换,只要
改变LDTR的值,系统当前的局部描述符表LDT也随之切换,这样便于各任务之间数据的隔离.但GDT并不随着任务的切换而切换.
上面只说了全局描述符表和局部描述符表二个表,并没有说段寄存器中的索引值对应哪个表中的描述符啊.实际上,16位的段寄存器只有高13位表示
索引值.剩下的3个数据位中,第0,1位表示程序当前优先级RPL,第2位TI位用来表示段描述符的位置: TI=0表示在GDT中, TI=1表示在LDT中.
如上图,在保护模式下,以xxxx:yyyyyyyy格式表示一个虚拟地址.怎么找到最后的线性地址? 段基址+偏移地址yyyyyyyy得到
要得到段基址先看段选择器中的数据xxxx的TI位:
1 TI位是0的话,先从GDTR寄存器中获取GDT的基址(图中步骤①),然后在GDT中以段选择器xxxx的高13位当作位置索引得到段描述符(步骤②).
段描述符包含段的基址,限长,优先级等各种属性,这就得到段的起始地址(步骤③)
2 TI位是1的话,段描述符在LDT中, 先从GDTR中获取GDT的基址(步骤①),并且要从LDTR中获取LDT所在段的地址索引(步骤②),然后以这个位置
索引在GDT中得到LDT段的位置(步骤③),然后用xxxx做索引从LDT段中获得段描述符(步骤④),再以这个段描述符得到段的基址等信息(步骤⑤)
分这二种情况得到段基址后,段基址+偏移地址yyyyyyyy得到最后的线性地址.
三 80386的内存分页机制
实模式下段地址x10h+偏移地址得到物理地址.而保护模式下,段选择器+偏移地址转换后的地址被称为线性地址.
线性地址是物理地址吗? 也是也不是.若80386的内存分页机制被使用,则线性地址不是物理地址,若内存分页机制不使用,线性地址就是物理地址.
在单任务的DOS系统中,程序退出后,操作系统会回收所有的碎片内存成一个大块内存供下一个程序使用.有个极端情况是当系统中有多个TSR
程序时,后装入的TSR会留在内存中,把空闲内存分割成二个区域.这时应用程序可使用的最大内存只能是二块内存中较大的一块,无法合并二块内存.
对于一个多任务的操作系统,内存的碎片化是不能容忍的.否则,经过一段时间后,即使空闲内存的总和很大,也可能出现任何一片内存都小到无法
装入执行程序的地步.所以多任务操作系统中碎片内存的合并是个大问题.
80386的内存分页机制可用解决这个问题.80386处理器把4KB大小的内存当作一页内存,每页物理内存可用根据页目录和页表,随意映射到不同
的线性地址上.这样,就可以将物理地址不连续的内存映射为连续的线性地址. 在80386处理器中,除了和CR3寄存器相关的指令使用的是物理地址外,
其他指令都是用线性地址寻址的.
四 Windows的内存安排
Windows系统可以在硬盘上建立交换文件(WIndows NT下为PageFile.sys)用做虚拟内存.利用80386处理器的内存分页机制,交换文件在寻址上
可以很方便地作为物理内存使用.只需在真正访问到的时候将硬盘文件的内容读入物理内存,然后重新将线性地址映射到这块物理内存就可以了.
同理,被执行的可执行文件也不必真正全部装入内存,只要在页表中建立映射关系,以后真正运行到某处代码的时候再将它调入物理内存.
如上图左上角,如果把虚拟内存暂时视为物理内存的一部分,从物理内存的层次看,Windows操作系统和DOS一样,也是所有的内容共享内存,
比如操作系统使用的代码和数据(GDT,LDT与页表等),当前执行中的所有程序的代码和数据以及这些程序调用的DLL的代码和数据等.
但是从应用程序代码的层次看,也就是从分页映射后线性地址的层次看,内存的安排却不是这样.Windows是一个分时的多任务操作系统,CPU
时间被分成一个个时间片分配给不同程序轮流使用,在一个程序的时间片中,和这个程序执行无关的部分并不需要映射到线性地址中去.
如上图,WIndows通过切换不同的页表内容让线性地址在不同的时间片映射不同的内容.图中的右边是Windows98操作系统在单个时间片中
线性地址的安排(Windows NT的地址安排稍微不同).
在物理内存中:
①操作系统和系统DLL的代码需要供每个应用程序调用,所以在所有的时间片中都必须被映射.
②用户程序只在自己所属的时间片内被映射.
③用户DLL则有选择地被映射,假设程序A和程序C都要用到xxx.dll,那么物理内存中xxx.dll的代码在图中的时间片1和n中被映射,其它时间片不映射.物理内存只含一份xxx.dll的代码.
综上:
1 每个应用程序都有自己4GB的寻址空间,该空间可存放操作系统,系统DLL和用户DLL的代码,再除去其他的一些空间,余下的是应用程序的代码,数据和
可以分配的地址空间.
2 不同应用程序的线性空间是隔离的.虽然它们在物理内存中同时存在,但在某个程序所属的时间片中,其他应用程序的代码和数据没有被映射到可
寻址的线性地址中,所以是不可访问的. 所以从编程的角度看,程序可以使用4GB寻址空间,而且这个空间是私有的.
3 DLL程序没有自己私有的空间,它们总是被映射到其他应用程序的地址空间中,当作其他应用程序的一部分运行.
五 从Win32汇编的角度看内存寻址
在Win32汇编中访问内存是不是需要先在描述符表中构造正确的描述符,然后再构造页表把物理内存映射到线性地址?不是,Windows安排好了.
Windows为用户程序的代码段,数据段和堆栈段全部预定义好了段描述符.这些段的起始地址为0,限长为ffffffff,所以用它们可以直接寻址全部4GB
地址空间.程序开始执行的时候,CS,DS,ES,SS都指向了正确的描述符,在整个程序的生命周期内,程序员不必改动这些段寄存器,也不必关心它们的值.
所以,在Win32汇编中,汇编代码中可以不出现段寄存器.
所以前面的问题答案是: 并不是Win32源代码用不到段寄存器,而是用户在使用中不必关心段寄存器.