80386 汇编-基础知识
9unk Lv5

80x86 系列处理器简史

win32 可以在多种应尽平台上运行,但使用最广泛的硬件平台是基于 Intel 公司 80x86 系列处理器的微型计算机。

1978 年 6 月 Intel 公司推出了它的第一个 16 位微处理器 8086。 直到 1985 年 Intel 公司推出 32 位的 80386 处理器。80386 处理器在设计的时候考虑了多用户及多任务的需要,在芯片中增加了保护模式、优先级、任务切换和片内的存储单元管理等硬件单元。80386 的出现使 Windows 和 Unix 等多任务操作系统可以在 PC 上运行。

1989 年 Intel 公司推出 32 位的 80486 处理器。

1993 年 3 月 Intel 公司推出 80586 处理器。由于无法阻止其他公司把自己的兼容产品叫做 x86,所以把产品取名为 Pentium,并且进行了商标注册,同时启用了中文名 “奔腾”。

1995 年推出代号为 P6的新一代 Pentium Pro 处理器,中文名称为 “高能奔腾”

1997 年推出 Pentium II 芯片,中文名称为 “奔腾II代”

1999 年推出 Pentium III 芯片

2000 年推出 Pentium IIII 芯片

80x86 处理器的工作模式

80386 处理器有 3 种模式:实模式、保护模式和虚拟 86 模式。实模式和虚拟 86 模式是为了兼容 8086 处理器而设置的。保护模式是 80386 处理器的主要工作模式。

80386 处理器的 3 种工作模式各有特点且相互联系。实模式是 80386 处理工作的基础,这时 80386 就作为于一个快速的 8086 处理器工作。实模式下可以通过指令切换到保护模式,也可以从保护模式下退回到实模式。虚拟 86 模式则以保护模式为基础,在保护模式和虚拟 86 模式之间可以互相切换,但不能从实模式直接进入虚拟 86 模式或从虚拟 86 模式直接退到实模式。

  1. 实模式
  • 80386 处理器被复位或加电的时候以实模式启动,这时候处理器中的各寄存器以实模式的初始值工作。80386 处理器在实模式下的存储器寻址方式和 8086 是一样的 “段寄存器 X 16 + 偏移地址”,这时候的 32 位地址线只使用了低 20 位。

  • 在实模式下 80386 处理器寻址的地址就是内存中实际的物理地址。在实模式下,所有的段都是可读、写、执行的。

  • 实模式下 80386 不支持优先级,所有指令相当于工作在特权级(优先级 0),所以可以执行所有特权指令。实模式下不支持硬件生的多任务切换。

  • 实模式下的中断处理方式和 8086 处理器相同,也用中断向量表来定位中断服务程序地址。

  • 从编程角度来看,实模式下的 80386 处理器的优点是可以使用 32 位寄存器

  1. 保护模式
  • 当 80386 工作在保护模式下的时候,它所有的功能都可以使用。

  • 所有的 32 根地址线都可以寻址,物理寻址空间高达 4G

  • 支持内存分页机制,提供了对虚拟内存的良好支持。主要还是为了运行大型程序和真正实现多任务。

  • 支持优先级机制,不同的程序可以运行在不同的优先级上。优先级分为 4 个级别(0级~3级),操作系统运行在优先级 0 上,应用程序运行在比较低的级别上;配合良好的检查机制后,既可以在任务间实现数据的安全共享也可以很好的隔离各个人物。

  • 从实模式切换到保护模式是通过修改控制寄存器 CR0 的控制位 PE (位 0) 来实现的。在这之前还需要建立保护模式必须的一些数据表。

PS:DOS 操作系统运行在实模式下,而 windows 操作系统运行在保护模式下

  1. 虚拟 86 模式
  • 虚拟 86 模式是为了在保护模式下运行 8086 程序而设置的。主要是为了向下兼容 MS-DOS 程序,所以 80386 处理器又设计了一个虚拟 86 模式

  • 虚拟 86 模式下,80386 支持任务切换和内存分页。在 windows 操作系统中,有一部分程序专门来管理虚拟 86 模式的任务,称为虚拟 86 管理程序。

  • 虚拟 86 的工作模式实际上是在实模式和保护模式的混合。虚拟 86 模式采用了和 8086 一样的寻址方式。但是多个虚拟 86 任务不能同时使用同意位置的 1M 地址空间,否则会引起冲突。所以操作系统利用分页机制将不同的虚拟 86 任务的地址空间映射到不同的物理地址上去,这样每个虚拟 86 任务看起来都认为自己在使用 0~1MB 的地址空间。

  • 8086 代码中有相当一部分指令在保护模式下属于特权指令。如果不让这些指令执行,8086 代码就无法工作。所以 86 管理程序采用模拟的方法来完成这些指令。

  • 在虚拟 86 模式中,特权只能怪执行的时候会引起保护异常。虚拟 86 管理程序在异常处理程序中检查产生异常的指令,如果是中断指令,则从虚拟 86 任务的中断向量表中取出中断处理程序的入口地址,并将控制转移过去;如果是危操作系统指令,则简单地忽略这些指令,在异常处理程序返回的时候直接返回下一条指令。

windows 的内存管理

  1. DOS 操作系的统内存安排

1-1.jpg

  1. 80386 的内存寻址方式
    当 80386 处理器工作在保护模式和虚拟 86 模式的时候,可以使用全部的 32 根地址线访问 4G 大的内存。因为80386 所有的通用寄存器都是 32 位的。2^32 相当于 4G,所以可以用任何通用寄存器来间接寻址,不必分段就已经可以访问到所有的内存地址。

1-2.jpg

在保护模式下,对一个地址空间定义了一些安全上的属性,这些属性一共有 64 位字长的属性数据,这些属性数据被称为段描述符(Segment Descriptor)

80386 的段寄存器是 16 位的,无法放下保护模式下 64 位的段描述符,所以把所有段的段描述符顺序放在内存中的指定位置,组成一个段描述符表(Descriptor Table);而段寄存器中的 16 位用来做索引信息,制定这个段的属性用段描述符表中的第几个描述符来表示。这时,段寄存器中的信息不再是段地址,而是段选择器(Segment Selector)。

80386 中引入了两个新的寄存器来管理段描述符表。一个是 48 位的全局描述符寄存器 GDTR,一个是 16 位的局部描述符表寄存器 LDTR。

  • GDTR 指向的描述符表为全局描述符表 GDT(Global Descriptor Table)。它包含系统中所有任务都可用的段描述符,通常包含描述符操作系统所使用的代码段、数据段和堆栈段及各个任务的 LDT 段等;全局描述符表只有一个。

  • LDTR 则指向局部描述符表 LDT(Local Descriptor Table)。80386 处理器设计成每个任务都有独立的 LDT。它包含有每个任务私有的代码段、数据段和堆栈描述符,也包含了该任务所使用的一些门描述符,如任务门和调用门描述符等。

不同的任务的局部描述符表分别组成不同的内存段,描述这些内存段的描述符当作系统描述符放在全局描述符表中。和 GDTR 直接指向内存地址不同,LDTR 和 CS,DS 等段选择器一样只存放索引值,指向局部描述符表内存段对应的描述符在全局描述符表中的位置。随着任务的切换,只要改变 LDTR 的值,系统当前的局部描述符表 LDT 也随之切换,这样便于各任务之间数据的隔离。但 GDT 并不随着任务的切换而切换。

16 位的段选择器中只有高 13 位表示索引值。剩下的 3 各 数据位中,第 0,1位表示程序当前的优先级 RPL;第 2 位 TI 位用来表示在段描述符的位置:TI=0 表示在 GDT 中,TI=1 表示在 LDT 中。

1-3.jpg

如上图所示:在保护模式下,同样以 xxxx:yyyyyyyy 格式表示一个虚拟地址。单凭段选择器中的数值 xxxx 根本无法反映出段的基地址在哪里。对于这个地址首先要看 xxxx 的 TI 位是否为 0 。

  • 如果 TI=0 的话,则先从 GDTR 寄存器中获取 GDT 的基地址(步骤1),然后再 GDT 中以段寄存器 xxxx 的高 13 位当作位置索引得到段描述符(步骤2).段描述符中包含段额基地址、限长、优先级等各属性,这就得到了段的起始地址(步骤3)

  • 如果 xxxx 的 TI=1 的话,这表示段描述符再 LDT 中,这时第一步的操作还是从 GDTR 寄存器中获取 GDT 的基地址(步骤1’),并且要从 LDTR 中获取 LDT 所在段的位置索引(步骤2’);然后以这个位置的索引再 GDT 中得到 LDT 段的位置(步骤3’);然后才是用 xxxx 做索引从 LDT 段中获得段描述符(步骤4’),再以这个段描述符得到段的基地址等信息(步骤5’) 。最后基地址+偏移地址=线性地址。

  1. 80386 的内存分页机制
    保护模式下 “段选择器+偏移地址吧” 转换后的地址被称为 “线性地址” 而不是 “物理地址”。线性地址可能是物理地址,也可能不是,这取决于 80386 的内存分页机制是否被使用。

再单任务的 DOS 系统中,一个应用程序可以使用所有的空闲内存。程序退出后,操作系统回收所有的碎片内存并且合并成一个大块内存继续供下一个程序使用。内存合并过程中的一个极端情况就是当系统中与多个 TSR 程序时,早装入内存的 TSR 被卸载后,后入的 TSR 会留再内存的中间部位,把空闲内存隔成两个区域。这时应用程序使用的最大内存块只能是这两块中较大的一块,无法将它们合并使用。

对于一个多任务操作系统,是不能容忍内存的碎片化。否则经过一段时间后,即使空闲内存的总和很大,也可能出现任何一片内存都小到无法装入执行程序的地步。所以多任务操作系统中碎片内存的合并是个很重要的问题。

因此 80386 处理器设计了分页机制来解决此问题。80386 处理器把 4KB 大小的一块内存当做一 “页” 内存,每页物理内存可以根据 “页目录” 和 “页表”,随意映射到不同的线性地址上。这样就可以将物理地址不连续的内存映射连到一起,在线性地址上视为连续的。在 80386 处理器中,除了和 CR3 寄存器(指定当前目录地址)相关的指令使用的是物理地址外,其他所有寄存器都是用线性地址寻址。

1-4.png

如上图所示:一个 xxxx:yyyyyyyy 格式的虚拟地址,经过图1.3 所示的段地址转换步骤后得到 32 位的线性地址 zzzzzzzz(步骤1).当禁用分页机制时,线性地址就是物理地址,处理器直接从物理内存存取数据(步骤2);当启用分页机制时,得到线性地址的方法还是一样(步骤1’),但是还要根据页目录和页表指定的映射关系把地址映射到物理内存的真正位置上(步骤3’)。然后 CPU 以映射后的物理地址在物理内存中存取数据。

内存分页管理只有在保护模式下才能实现,实模式下不支持分页机制。但是不管在那种模式下,所有寻址指令使用的都是线性地址,程序不用关心数据最后存放在物理内存的哪个地方。

页表规定的不仅是地址的映射,同时还规定了页的访问属性,如是否可写、可读和可执行等。比如把代码所在的内存页设置为可读与可执行,那么权限不够的代码向他写入数据就会引发保护异常。利用这个机制可以在硬件层次上支持虚拟内存的实现。

如图 1.5 所示,页表可以指定一个页面并不真正映射到物理内存中。这样,访问这个页的指令会引发异常错误。这时,处理器会自动转移到页面异常处理程序中去。操作系统可以在异常处理程序中将硬盘上的虚拟内存读到内存中并修改页表重新映射,然后重新执行引发异常的指令。这样指令就可以正常执行下去。

1-5.png

  1. windows 的内存安排

windows 系统一般在硬盘上建立大小为物理内存的两倍左右的交换文件用作虚拟内存。利用 80386 处理器的内存分页机制,交换文件在寻址上可以很方便的作为物理内存使用。只需要在真正访问到的时候将硬盘文件的内容读入物理内存,然后重新将线性地址映射到这块物理内存就可以了。同样的道理,被执行的可执行文件不必真正安装到内存中,只要在页表中建立映射关系,以后真正运行到某处代码的时候在将它调入到物理内存。

如果把虚拟内存暂时先视为物理内存的一部分,从物理内存的层次看,windwos 操作系统和 DOS 一样,也是 所有的内容共享内存,比如操作系统使用的代码和数据,当前执行中的所有程序的代码和数据以及这些程序调用的 DLL 的代码和数据等。如下图所示:

1-6.png

但是从应用程序代码的层次看,也就是说从分页映射后线性地址的层次看,内存的安排却不是这样的。因为 windows 十一个分时的多任务操作系统,CPU 时间被分成一个个时间片后分配给不同程序轮流使用,在一个程序的时间片中,和这个程序执行无关的部分(如其他程序的代码和数据)并不需要映射到线性地址中去。

如上图所示:windows 操作系统通过切换不同的页表内容让线性地址在不同的时间片中映射不同的内容。在物理内存中,操作系统和DLL的代码需要提供给每个应用程序调用,所以在所有时间片中都必须被映射;用户程序只在自己所属的时间片被映射;而用户 DLL 则有选择的被映射。假设程序 A 和程序 C 都要用到 xxx.dll ,那么物理内存中 xxx.dll 的代码在图中的时间片 1 和 n 中被映射,其他的时间片就不需要映射,当然,物理内存中只需要一份 xxx.dll 的代码。

由此可以引出 win32 编程中几个很重要的概念:

  • 每个应用都有自己的 4GB 寻址空间。该空间可存放操作系统、系统 DLL 和用户 DLL 的代码,它们之中都有各种函数供应用程序调用。再出去其他的一些空间,余下的是应用程序的代码、数据和可以分配的地址空间。

  • 不同的应用程序的线性地址空间是隔离的。虽然它们在物理内存中同时存在,但在某个程序所属的时间片中,其他应用程序的代码和数据没有被映射到可寻址的线性地址中,所以是不可访问的。从编程角度看,程序可以使用 4Gb 的寻址空间,而且这个空间是 “私有” 的。

  • DLL 程序没有自己 “私有” 的空间。它们总是被映射到其他应用程序的地址空间中,当做其他应用程序的一部分运行。原因很简单,如果它不和其他应用程序不属于一个地址空间,应用程序就不能调用它。

  1. 从 win32 汇编角度看内存寻址

windows 操作系统为用户的代码段、数据段、和堆栈段全部预订好了段描述符。这些段的起始地址为 0,限长为 ffffffff,所以用它们可以直接寻址全部的 4GB 地址空间。程序开始执行的时候,CS,DS,ES 和 SS 都已经指向了争取的描述符,在整个程序的生命周期内,程序员不必改动这些段寄存器,页不必关心它们的值究竟是多少(实际上想改也改不了)。

所以对 win32 汇编程序来说,整个源程序中不用出现段寄存器的身影,用户在使用中不必去关心段寄存器。

windows 特权保护

  1. 80386 的中断和异常
    异常指令执行中发生不可忽略的错误时,处理器用和中断处理相同的方法挂起当前运行的程序转移到异常处理程序中。异常处理程序决定在修正错误后是否回到原来的地方继续执行。

在保护模式下,中断或异常处理往往从用户代码切换到系统代码中执行。由于保护模式下的代码有优先级之分,因此出现了从优先级低的应用程序转移到优先级高的系统代码中的问题,如果优先级低的代码能够任意调用优先级高的代码,就相当于拥有高优先级代码的权限。为了使高优先级的代码能够安全地被优先级低的代码调用,为此保护模式下增加了 “门” 的概念。“门” 指向某个优先级高的程序所规定的入口点,所有优先级低的程序调用优先级高的程序只能通过重定向,进入门所规定的入口点。这样可以避免低级别的程序代码从任意位置进入优先级高的程序问题。保护模式下的中断和异常等服务程序也要从 “门” 进入,80386 的门分为中断门、自陷门和任务门几种。

在保护模式下要表示一个中断或异常服务程序的信息需要用 8 个字节,包括门的种类以及 xxxx:yyyyyyyy 格式的入口地址等。这组信息叫做 “中断描述符”。这样,中断向量表就无法采用和实模式下同样的 4 字节-组的格式。保护模式下把所有的中断描述符放在一起组成 “中断描述符表” IDT(Interrupt Descriptor Table)。IDT 不再放在固定的地址 0000h 处,而是采用可编程设置的方式,支持的中断数量也可以设置。为此 80386 处理器引入了一个新的 48 位寄存器 IDTR。IDTR 的高 32 位指定了 IDT 在内存中的基地址(线性地址),低 16 位指定了 IDT 的长度,相当于指定了可以支持的中断数量。

如下图所示:保护模式下发生了异常或中断时,处理器先根据 IDTR 寄存器得到中断描述符的地址,然后取出 n 号中断/异常的门描述符,再从描述符中得到中断服务程序的地址 xxxx:yyyyyyy,经过段地址转换后得到服务程序的 32 位线性地址并转移后执行。

1-7.png

由于保护模式下用中断门可以从低优先级的代码调用高优先级的代码,所以不能让用户程序写中断描述符表,否则会引发安全问题。所以用户程序也就没有权限将中断地址指定到自己的代码中来。

在 windows 中,操作系统使用动态链接库来代替中断服务程序提供系统功能,所以 win32 汇编中 int 指令也失去了存在的意义。这就是在 win32 汇编源代码中看不到 int 指令的原因。其实那些调用 API 的指令就相当于在 DOS 系统中使用 int 指令来完成系统功能。

  1. 80386 的保护机制

80286 之前的处理器只支持单任务,一个程序可任意访问系统中的任何资源,因此操作系统并没有安全性可言。如果多任务操作系统有程序任意访问系统资源就可能会导致其他程序无法运行,所以 80286 及以上的处理器引入了优先级的概念。

80386 处理器共设置了 4 个优先级(0级~3级)。0级是最高级(特权级);3级是最低级(用户级);1级好玩2级介于它们之间。特权级代码一般是操作系统代码,可以访问全部系统资源;其他级别的代码一般是用户程序,可以访问的资源受到限制。

80386 采用保护机制主要是为了检查和防止低级别代码的越权操作,如果访问不该访问的数据、端口以及调用高优先级的代码等。保护机制主要是由下列几方面组成:

  • 段的类型检查——段的类型是由段描述符指定的,主要属性有是否可执行,是否可读可写等。而 CS,DS 和 SS 等段选择器是否能装入某种类型的段描述符是有限制的。如不可执行的段不能装入 CS;不可读的段不能装入 DS 与 ES 等数据段寄存器;不可写的段不能装入 SS 等。如果段类型检查不通过,则处理器会产生一般性保护异常和堆栈异常。

  • 页的类型检查——除了可以在段级别上指定整个段是否可读写外,在页表中也可以为每个页指定是否可写。对于特权级下的执行代码,所有的项都是可写的。但对于1,2,3级的代码,还要根据页表中的 R/W 项决定是否可写,企图对只读的页进行写操作会产生页异常。

  • 访问数据时的级别检查——优先级低的代码布尔诺能够访问优先级高的数据段。80386 的段描述符中有一个 DPL 域(描述符优先级),表示这个段可以被访问的最低优先级。而段选择器中含有 RPL 域(请求优先级),表示当前执行代码的优先级。只有 DPL 在数值大于或等于 RPL 的值的时候,该段才是可以访问的,否则会产生一般性保护异常。

  • 控制转移检查——在处理器中,有很多指令可以实现控制转移,如 jmp,call,ret,int 和 iret 等指令。但优先级低的代码不能随意转移到优先级高的代码中,所以遇到这些指令的时候,处理器要检查转移的目的位置是否合法。

  • 指令集的检查——有两类指令可以影响到保护机制。第一类是改变 GDT,LDT,IDT 以及控制寄存器等滚啊缉拿寄存器的指令,称为敏感指令。特权指令只有在优先级 0 上才能运行,而敏感指令取决于 efalgs 寄存器中的 IOPL 位。只有 IOPL 位表示的优先级高于等于当前代码段的优先级时,指令才能执行。

  • I/O 操作的保护——I/O地址也是受保护对象。因为通过 I/O 操作可以绕过系统对很多硬件进行控制。80386 可以独立为 I/O 空间提供保护,每个任务有个 TSS(任务状态段)来记录任务切换的信息。TSS 中有个 I/O 允许位图,用来表示对应的 I/O 端口是否可以操作。某个 I/O 地址在位图中的对应数据位为 0 则表示可以操作;如果为 1 则还要看 efalgs 中的 IOPL 位,这时只有 IOPL 位表示的优先级高于等于当前代码段的优先级,才允许访问该 I/O。

  1. windows 的保护机制
    在 windows 下,操作系统运行于 0 级,应用程序 3 级。因为 Alpha 计算机只支持两个优先级,为了便于将应用程序移植到 Alpha 计算机上,windows 操作系统不适用 1级和 2级这两个优先级。

windows 操作系统充分利用 80386 的保护机制,所有和操作系统密切相关的资源都是受保护的。运行于优先级 3 上的用户程序有很多限制,只有在写 VxD、WDM 等驱动程序的时候才能使用全部资源。在 win32 汇编程序中要注意避免以下越权操作(写驱动程序例外):

  • 所有的特权指令都是不可执行的,但是读取重要寄存器的指令是可以执行的。

  • windows 在页表中把代码段和数据段中的内存页赋予不同的属性。代码段是不可写的,数据段中页只有变量部分的页面是可写的。所以虽然可以寻址所有的 4GB 空间,但访问超出权限规定以外的内存还是会引发保护异常。

  • 在 windows 98 中,系统硬件用的 I/O 端口是受保护的,但其余的则可以操作。如果用户在机器中插了一块自己的卡,用的是 300h 等系统未定义的端口,那么在应用程序中就可以直接操作,但要操作 3f8h (串口)和 1f0h (硬盘接口)等系统已定义的端口就不行了。在 windows NT 中,任何的端口操作都是不允许的。

如果违反了 windows 规定的 “保护条例”,那么会引发保护异常,处理器会毫不犹豫的把控制权转移到对应的异常处理程序中去。windows 会在处理程序中用 “非法操作” 对话框把用户的程序判死刑,并且会用蓝屏来通知用户程序试图访问不存在的内存页。

如果程序调用的 DLL 中有错,那么错误还会算在应用程序头上。win9x 本身是 32位和16位混合的操作系统,为了兼容 DOS 和 win16 程序。很多的保护措施就力不从心。所以系统提供的 DLL 内部反而常常出现越权操作,以至于蓝屏不断。

  • 本文标题:80386 汇编-基础知识
  • 本文作者:9unk
  • 创建时间:2020-10-10 14:18:28
  • 本文链接:https://9unkk.github.io/2020/10/10/80386-hui-bian-ji-chu-zhi-shi/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!