计算机原理学习(5)– x86-16 CPU和内存管理

 

前言

 

 

前面我们已经了解了计算机硬件的工作原理,以及操作系统的发展。我们知道是内存把计算机硬件和软件联系了起来。不夸张的说,了解了软件在内存中的结构,就基本了解了程序最底层的运行原理。所以从这一篇开始,将深入的讨论计算机中内存管理和布局。内存的管理同计算机硬件以及擦做系统是分不开的。这一篇我们主要讨论早期x86 CPU和DOS系统对于内存的管理。

 

 

1. 8086 CPU

 

说到CPU,我们第一个想到的应该就是Intel。 1971年11月15号,Intel发布了全球第一款微处理器Intel 4004,这是一个主频只有108KHz的4bit处理器。而后又发布了8bit的8008处理器。而我们最熟悉的应该就是8086,为什么?因为随便找一本汇编的书籍看看,都会有8086四个大字。因为8086标志着Intel x86体系结构的CPU的开始。而且8086/8088开始用于便携电脑,所以我们就从8086开始介绍。80186除8086内核,另外包括了中断控制器、定时器、DMA、I/O、UART、片选电路等外设。

 

 

1.1 8086/8088内存访问

 

8086是x86体系结构的开始,他采用了16bit,但是地址线却用了20位。前面介绍CPU工作原理的时候哦我们知道,CPU内部有一个PC计数器,用来存储下一个要执行的物理地址。但是16位的寄存器如何存储20位的地址呢?

不仅仅是8086,我们发现之前的CPU的位宽和可寻址范围都不是对应的关系,而且4004和8008也找不到地址线位宽。对于8080来说,地址有16位,而它内部有1个主累加器和5个次累加器,所以它使用2个寄存器组合来访问16位地址。而对于8086,并没有采用相同的方式,而是参考了PDP-11小型机,设计出了分段寻址技术

因为CPU一次能送出的地址是16位,要访问20位地址的存储器就需要使用2个16位的地址计算表示一个20位的地址。这里采用的办法是,将内存分为不同逻辑段,每段段有自己的段地址(16位),而段内数据地址则是相对于段首地址的偏移地址(16位)。而且段之间是可以相邻或者重叠的。

 

因为偏移地址是16位,所以最大的范围是64K,而对于1M内存来说,最少有16个逻辑段。而因为段寄存器也是16位,所以段的物理地址需要是16的倍数,表示为0xXXXX0。这样的地址可以压缩为0xXXXX,所以段首地址的高16位表示段值。所以段首的物理地址 = 段值 * 0x10。那么偏移地址是相对于段首地址来说的,那么要访问的物理地址公式为:物理地址 = 段值*0x10 + 偏移地址

 

这里介绍一下逻辑地址的概念,逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量“,表示为 [段标识符:段内偏移量]。

 

 

 

1.2 8086寄存器

 

 

关于8086CPU的架构图,我们在前面的文章中出现了多次。这里我们只看看8086的寄存器。我们知道寄存器对于CPU来时是非常的重要,无论是取指令还是做运算都需要寄存器来存放数据。我们在多线程编程中经常听到一个词是切换上下文,这里所指的上下文就包含CPU寄存器的值,当然这个是后话,后面会介绍。

 

8086一共有14个16位寄存器,具体分类如上图:

  • AX,BX,CX,DX 是主寄存器,又叫通用寄存器, 为了兼容8位CPU,采用2个8位寄存器组合而成,这4个寄存器主要用作数据寄存器,节省从寄存器存取操作的时间。
  • SI, DI是变址寄存器,主要用于操作字符串,当然也可以作为通用寄存器;而BP,SP是指针寄存器,主要用于堆栈操作。
  • CS,DS,ES,SS是段寄存器,是为了内存分段而设置的,用来存放逻辑段的段值。
  • IP 是指令指针寄存器,类似前面提到的PC计数器,这里用作表示下一条指令相对于CS段的偏移
  • Flags标志寄存器,主要用于表示CPU计算的运算结果和状态。

 

关于这些寄存器具体作用可以参考:8086 CPU 寄存器简介

 

 

1.3 8086段寄存器的引用

 

从8086开始,采用了分段式的内存管理,于是在访问内存时不在像以前那样,拿到地址直接送到总线进行访问,而是需要通过计算得到的。这就涉及到使用那个段寄存器和偏移量。一般来说代码不需要指定要访问的段,总线可以自行判断,当然也可以显示的指定。

比如我们在获取下一条指令地址时,就是用CS*16+IP,而当执行一条取数据指令时,就是用DS*16+有效地址来访问,上表就定义了不同操作时是用到的寄存器和偏移。

 

这里可能有一个疑问:程序是如何被加载到不同内存段呢?这个其实和程序的编译,可执行文件的结构以及操作系统有关,有此可见一项新技术的使用是需要硬件和软件相互配合的。这些会在后面介绍。

 

 

1.4 8086寻址方式

 

表示指令中操作数所在的方法称为寻址方式。

 

 

1.4.1 立即寻址

操作数直接包含在指令中,比如MOV AX, 1234H, 一般用于给存储器或寄存器赋值。

 

1.4.2 寄存器寻址

 

操作数存放在寄存器中,而指令中存放的是寄存器号,比如MOV SI, AX。这里可以用到的寄存器有AX,BX,CX,DX,SI,DI,SP,BP.

 

 

1.4.3 直接寻址

 

操作数在存储器中,指令中直接包含存储器的有效地址。比如MOV AX, [1234H]。从前面知道,这里的有效地址并不是真正的物理地址而是偏移地址,因为这是一条取数据操作指令,所以在没指定段的时候,默认访问的是DS数据段。最后得到真正的物理地址。

当然也可以指定要访问的段比如: MOV AX ES:[1234H],这种寻址方式只适用于段小于64K的情况,在程序中存储器的有效地址一般用变量表示。

 

 

1.4.4 寄存器间接寻址

 

操作数存放在寄存器中,操作数的有效地址存放在SI, DI, BX, BP这4个寄存器中。在一般情况下如果有效地址在SI,DI,BX中则访问DS段,而当在BP中时则访问SS段。比如MOV AX,[SI]

同样,也可以指定有效地址要访问的段,比如MOV AX, CS:[BX]。

 

 

1.4.5 寄存器相对寻址

 

操作数在存储器中,操作时的有效地址存放在SI, DI, BX, BP寄存器并加上一个8位或16位的位移量中。比如MOV AX, [DI+1234H],计算方法和寄存器间接寻址相同,只是多加上一个偏移量。

这种寻址方式有利于实现高级语言中对结构类型数据所实施的操作。

 

 

1.4.6 基址加变址寻址

 

操作数在存储器中,操作时的有效地址是由基址寄存器BX或BP和变址寄存器SI或DI相加得到的。比如MOV AX, [BX+DI]

这种寻址方式一般用来处理数组的访问,基址寄存器存放数组首地址,而变址寄存器来定位数组的每个元素。也可以写作MOV AX,[BX][DI]。

 

 

1.4.7 相对基址加变址寻址

 

操作数存放在存储器,操作数有效地址由基址寄存器BX或BP,变址寄存器SI或DI, 以及一个8位或16位位移量相加得到的。比如MOV AX, [BX+DI-2]

 

 

 

2. 程序的内存结构

 

 

前面我们介绍过程序编译的过程,当代码被编译成可执行文件后,当运行程序时,通过装入程序把我们的可执行文件从磁盘装载到内存中,然后指定程序入口地址,CPU变开始顺序的执行。这里就涉及到CPU如何定位程序的地址的问题。其实对于早期程序编写,装入我也不是很了解,资料也挺少的。

 

 

2.1 早期程序的装入

 

我们知道,早期的计算机中,CPU并没有对内存分段,所以程序运行时,获取的下一条指令或数据的地址就是真实的内存地址。于是早期的程序在编译时就需要确定装载后在了在内存中的绝对位置。比如装载到内存的0x00000010处,那么在编译时生成的指定和数据,就是基于0x00000010向上扩展。

当程序被装入内存后,装载程序把PC计数器设置为0x00000010,然以CPU开始执行我们的程序。这种方式需要对内存使用情况非常熟悉。

 

 

2.2 8086程序的装入和运行

 

8086CPU将内存分割成了不同的段,于是指令和数据的有效地址并不是真正的物理地址而是相对于段首地址的偏移地址。CPU在取地址时会进行计算,所以我们在编译程序时无法确定程序在内存中绝对的位置。而且前面介绍了8086的寻址方式,我们知道在不指定段寄存器的时候,如果是取指定会使用CS,而如果是取操作数则是使用DS。所以我们程序的数据和指令必须装载到不同的段中。

 

我们知道,内存并没有被真正的分段,而是通过CPU中段寄存器来存放不同段的首地址。所以最好的办法是我们对程序也进行分段,把数据放在程序的数据段中,而代码则放在代码段中。这样在编译的时候,每个数据会每条指定的地址都是相对于段的偏移量,我们只需要设置CPU的CS,DS端寄存的地址。8086汇编语言中就有段定义语句,就是为了和存储器结构对应。

 

上图显示了汇编程序编译后背加载到内存的情况:

  1. 8086的汇编支持定义逻辑段,这里定义了逻辑段和代码段。(我们知道汇编语言是CPU指令的一种翻译形式,和硬件密切相关,所以对于8086来说有特定的汇编语言)
  2. 在程序编译时,给每条指令和数据分配一个偏移地址。
  3. 在加载程序之前,载入程序会在内存中找到可以存放每个段的地址,并且把段值写入到执行文件中,可以理解为给CSEG和DSEG赋值。
  4. 正式载入程序时,会把CSEG,DSEG加载到之前找到的物理地址,并且会去更新CS寄存器为CSEG的值。
  5. 载入程序把IP寄存器设置为第一条指令的偏移地址。
  6. 把控制权交给我们的程序,此时如图CS = 0x0002(开始执行CS*16 + IP)
  7. 执行第一条语句,把DSEG的段值写入到AX,然后执行第二条,把AX的值写入到DS, 也就我们设置了DS寄存器的段值。
  8. 当执行到MOV AX, VAR1时,因为是取数据操作,所以从DS段取数据,此时DS=0x0001。 最终VAR1在内存中的地址 = 0x0001*0x10 + 0x0000 = 0x00010。

 

这里CS和DS的值不是编译时确定的,而是在分配段内存时获得的。但是DS的值是从DSEG写入的,那么DSEG的值是不是加载程序写入到执行文件中的呢?不是很确定。另外或许的是物理内存地址,而CS中并不是物理内存,是不是要用物理内存/0x10来计算出CS和DS的值呢?

 

 

2.3 8086的多种模式

 

前面说过了,段的大小最大为64位,那么如果我们的程序的段大于64K或者是对于之前的8位程序,又是如何运行的呢?实际上8086根据分段的内存结构,有六种运行方式。对于8位机上的程序可以不考虑段地址直接以.com可执行文件以“微模式”在8086上运行。这是当时8086与MS-DOS作为新平台取得市场成功的关键原因——大量已存的CP/M应用程序能很快得到利用。而对于大于64k的段则运行在大模式中。这块内容完全不懂,有兴趣就自己研究吧。

 

 

3. MS-DOS内存结构

 

前面讨论完了CPU的内存访问方式,最后讨论一下操作系统的内存管理。这里选用了MS-DOS操作系统。这里并没有指定是那一个版本,只是从大体上去介绍内存管理方式。MS-DOS是一个单任务的批处理操作系统,同时只能有一个.COM或.EXE文件被执行,所做系统没有任务调度功能,使用.BAT文件可以实现批处理功能。而早期的Windows1.x也是基于MS-DOS,只是增加了图形界面。

3.1 早期MS-DOS的内存布局

 

IBM和微软在设计DOS操作系统时,当时CPU主要是8086,采用了20位地址线,所以最大内存访问只1M。但是在在当时普片采用8bitCPU,最大内存访问64K的CPU来说,这个内存空间已经相当大了,所以MS-DOS就基于这个进行了设计。

DOS把内存划分问了2个区域:

  • 常规内存: 也叫基本内存,是内存低端的640K。DOS系统本身、中断向量表、系统数据、驱动程序都会常驻在这一段内存中,剩余的就是用户程序可以使用的空间,大概600K,而随着MS-DOS功能越来越多,DOS本身所占用的空间也越来越大。而MS-DOS提供的内存管理,也主要是管理常规内存块。
  • 上位内存:这一块内存区域是从640K-1M,是给外接卡设备的数据缓冲区以及ROM-BIOS使用的。 在这段内存空间中有一些空闲的空间,称为UMB(Upper Memory Block)。这些内存DOS系统无法进行管理。EMM386.exe可以管理这一部分内存。

 

3.2 MS-DOS 扩充内存(EMS)

 

随着PC的发展,越来越多硬件支持MS-DOS, 越来越多的软件开始在MS-DOS上被使用,于是640K内存中,操作系统,驱动程序,杀毒程序,常驻程序的体积越来越来,而应用程序可以使用的空间越来越小。为了能让8086使用更大的内存,Intel和MS联合推出了EMS(Expanded Memory Specification)扩充内存。通过主板上的扩展槽,最多可以支持32M内存。

 

但是CPU并不不能直接访问扩充的内存,通过扩充内存管理程序,使用了上位内存空间中的64K空余内存(UMB),这64K内存被分成4个页,每页16K,这部分页称为“页框架”,EMS内存也分成一个个16K的页,总数可达2000个。使用EMS的程序最多允许同时访问4个页,当程序要访问到某个页时,内存控制板就把相应EMS页的内容复制到页框架中让程序读写,读写完后把页框架中页的内容复制回相应的EMS内存页,再把别的EMS页内容复制到页框架中让程序读写。所以也被称为“调页式扩充内存“。

 

 

 

3.3 实模式和高位内存(HMA)

 

1982年,Intel发布了新的80286 CPU,址线扩展到24位,最多可以访问16M内存。 但是在80286无法兼容8086上的程序,所以Intel提出了实模式和保护模式两种方式来解决这个问题。在实模式下,80286依然只使用20位的地址线,最多访问1M内存,以前的8086程序可以正常运行,而在保护模式下,程序可以使用全部的16M内存。所以实模式,其实就是8086的运行模式。

 

在8086内存访问时,当段值+偏移都为最大时:FFFF0h+FFFFh=10FFEFh=1M+64K,得到的地址超出了1M的范围,这一块地址称为高位地址。但是因为只有20根地址线,所以会采用一种wrap-around的技术,将地址对1M求模,得到内存地址。但是80286开始拥有了24条地址线,于是在实模式下,当使用10FFEFh地址访问时,因为A20地址线的存在,所以会直接访问内存这一块的地址。

IBM为了解决这个问题,采用了用键盘控制器来控制A20,称为A20 Gate,当打开的时候,可以访问到高位内存,而禁用时则和8086行为一样。IBM-PC大部分禁用了A20 Gate,现在大多PC通过BIOS调用来控制A20 Gate。 关于A20可以参考:对A20 GATE的思考

 

 

3.4 实模式和扩展内存(XMS)

 

80286有24根地址线,最大内存容量可以达到16M,但是现在的问题在于MS-DOS本身是运行于实模式下的,所以即便处理器支持更大的内存,也无法使用。所以DOS上的应用程序最多只能使用640K的内存,这个也就是我们经常听到DOS程序的640K限制的问题。但这并不是8086时期硬件导致640K限制。

为了解决这个问题就使用看扩展内存XMS(Extended Memory Specification)。当然只有在80286和更高的处理器才才能支持。几乎所有使用DOS的机器上超过1M的内存都是扩展内存。扩展内存同样不能被DOS直接使用,DOS5.0以后提供了Himem.sys这个扩展内存管理程序,可以通过它来管理扩展内存。 Emm386.exe可以把扩展内存(XMS)仿真成扩充内存(EMS),以满足一些要求使用扩充内存的程序。

 

 

3.5 MS-DOS和保护模式

 

在DOS操作系统下,无论CPU支持多大的内存空间,程序都只能使用常规内存空间的640K内存,运行在实模式中。但是80286之后的CPU可以支持保护模式,于是就有一些程序可以通过DPMI(DOS Protocted Mode Interface), DOS扩展器程序比如DOS4GW.exe使得CPU进入到保护模式,从而直接访问扩展的内存,但是此时,已经不是在DOS环境下了。而且对于80286来说一旦切换到保护模式就无法回到实模式,只能reset CPU。

 

实际上大多介绍的保护模式是指80386的32位保护模式,而非80286的16位保护模式。而80386之后,保护模式基本没有大的变化,后面将会详细介绍32位保护模式下的内存结构和管理。

 

 

4. MS-DOS 内存管理

 

 

了解了MS-DOS的内存结构,最后我们看看MS-DOS是怎么管理内存的。这一部分主要是看DOS如何管理常规内存的640K。

 

 

4.1 MS-DOS系统模块

 

我们可以看到在640K的常规内存中,一些系统模块专用了一部分内存。MS-DOS主要由一个引导程序好3个模块程序完成启动

  • BOOT: 这是一个位于磁盘0扇区上的引导程序,主要是检查DOS系统盘,并把IO.SYS加载的
  • IO.SYS是系统的输入输出模块,ROM-BIOS 是固化早BIOS中的设备驱动; 系统启动时IO.SYS接收到命令转换为设备控制命令,由ROM-BIOS中的驱动程序完成设备操作。
  • MSDOS.SYS 这个是DOS的内核,主要进行内存,磁盘,外设的管理
  • COMMOND.COM: 这个是MS-DOS和用户之间的接口程序,用于接收用户输入的命令并执行。

 

 

4.2 MS-DOS 进程

 

MS-DOS是一个单任务的操作系统,所以不存在任务调度(80286也不支持多任务,80386支持任务切换)。当代码编写好生成可执行文件之后,被加载到内存空间时,而在程序内存空间前有一个256字节的程序段前缀(PSP)。

而在DOS中,还有一个环境块EVB用来记录环境变量,可以把他看做是PSP的扩展。

 

 

4.3 内存管理

 

当程序被加载的时候,需要向物理内存申请空间并加载程序。那么如何知道那些内存可以被使用呢? MS-DOS采用了内存控制块(MCB)来标识物理内存块。DOS的内存块以节为单位,一节等于16个字节,每个内存块的前面都有一个一节的MCB来描述这个内存块。

  • 标志位: 一个字节,Z标识是最后一个分区,M标识不是最后一个分区
  • 拥有者:当这个字段为0时,标识是一个没有使用内存块,否则存放的是拥有此内存块的进程的PSP段地址。
  • 内存块大小: 以节为单位,不包括MCB块。

 

所以通过一个MCB块,可以使用MCB块地址+内存块大小+1 就能知道下一个MCB块的地址。这样整个内存就被串联起来。下图展示了MS-DOS 3.3启动后内存的情况。

 

内存一共被分成了3部分:

  1. 第一部分中的块主要是系统使用;
  2. 第二部分是COMMAND.CMD程序,一共使用了3个块,其中程序数据和PSP还有环境各站一个块,构成了COMMAND的进程实体。
  3. 最后一部分内存块是给暂驻程序TPA(Transient Program Area),也就是我们程序使用的用户内存空间。

 

 

 

4.4 用户程序的装载

 

当我们要把一个EXE程序装载到内存时,装载程序会检查EXE的头部信息,检查TAP的容量并确装载的段的地址。而装载时可以分为低位载入和高位载入。

 

 

而在载入一个COM文件时,因为COM文件没有头部信息,并且COM文件限制不能大于64K。

 

我们知道,在程序载入后,需要设置段寄存器的值,程序才能正确的被运行,下面列出了执行文件被加载后段寄存器的情况

具体可以看:读书笔记DOS下可执行文件的加载

 

 

 

总结:

 

 

这一篇主要介绍了x86-16 处理器的内存结构和访问方式,后面还介绍了MS-DOS操作系统是如何管理内存的。 因为这一部分很久远,并且我也没怎么接触过,所以查阅了很多料,费了好多时间。但是可以找到的资料并不是很多。 但是我们的主要目的是了解早期的CPU和操作系统是如何管理内存的,程序是如何加载运行的。

 

后面我们会介绍x86-32 CPU的内存管理,有了这里了解到的知识就能更好的理解为什么现在的电脑是这样运行的,为什么使用保护模式,为什么使用虚拟内存。

 

 

 

参考:

 

Intel 8086

 

8086处理器六种模式

 

《80×86汇编语言程序设计教程》

 

MS-DOS

 

 

DOS 内存的知识

 

纯DOS下内存的管理—实模式下访问4GB内存

 

实模式、保护模式和虚拟模式

 

DOS下XMS,EMS,DPMI,DOS4GW研究 pdf

 

DOS下可执行文件的加载


如果本文对您有帮助,可以扫描下方二维码打赏!您的支持是我的动力!
微信打赏 支付宝打赏

2 评论

cc_net进行回复 取消回复

您的电子邮箱地址不会被公开。 必填项已用*标注