理解单片机系统

一、理解CPU的三种工作模式

从80386开始,CPU有三种工作方式:实模式(real-mode)、保护模式(protected-mode)和虚拟8086模式。只有在刚刚启动的时候是实模式,等到操作系统运行起来以后就切换到保护模式。实模式只能访问地址在1M以下的内存称为常规内存,我们把地址在1M 以上的内存称为扩展内存。在保护模式下,全部32条地址线有效,可寻址高达4G字节的物理地址空间; 扩充存储器分段管理机制存储器分页管理机制(可选的)不仅为存储器共享和保护提供了硬件支持,而且为实现虚拟存储器提供了硬件支持;支持多任务,能够快速地进行任务切换(switch)和保护任务环境(context); 4个特权级和完善的特权检查机制,既能实现资源共享又能保证代码和数据的安全和保密及任务的隔离; 支持虚拟8086方式,便于执行8086程序。

实模式(Real Mode)

它是 Intel公司80286及以后的x86(如80386,80486和80586等)处理器为了兼容以前的处理器(CPU)的一种操作模式。实模式被特殊定义为20位地址内存可访问空间上,这就意味着它的容量是2的20次幂(1M)的可访问内存空间(物理内存和BIOS-ROM),软件可通过这些地址直接访问BIOS程序和外围硬件。实模式下处理器没有硬件级的内存保护概念和多道任务的工作模式。但是为了向下兼容以前的处理器,所以80286及以后的x86系列处理器在开机启动时仍然先工作在实模式下。80186和早期的处理器仅有一种操作模式,就是后来我们所定义的实模式。实模式虽然能访问到1M的地址空间,但是由于BIOS的映射作用(即BIOS占用了部分空间地址资源),所以真正能使用的物理内存空间(内存条),也就是在640k到924k之间。1M 地址空间组成是由 16位的段地址和16位的段内偏移地址组成的。用公式表示为:物理地址=左移4位的段地址+偏移地址。

80286处理器体系结构引入了地址保护模式的概念,处理器能够对内存及一些其他外围设备做硬件级的保护设置(保护设置实质上就是屏蔽一些地址的访问)。使用这些新的特性,然而必不可少一些额外的在80186及以前处理器没有的操作规程。自从最初的x86微处理器规格以后,它对程序开发完全向下兼容,80286芯片被制作成启动时继承了以前版本芯片的特性。它工作在实模式下时暂时先关闭了新增的保护功能特性等,因此能使以往的软件继续工作在新的芯片下。直到今天,甚至最新的x86处理器都是在计算机加电启动时都是工作在实模式下,它能运行为以前处理器芯片写的程序。

DOS操作系统(例如 MS-DOS,DR-DOS)工作在实模式下,微软Windows早期的版本(它本质上是运行在DOS上的图形用户界面应用程序,实际上本身并不是一个操作系统)也是运行在实模式下,直到Windows3.0,它运行期间既有实模式又有保护模式,所以说它是一种混合模式工作。它的保护模式运行有两种不同意义(因为80286并没有完全地实现80386及以后的保护模式功能):
“标准保护模式”:这就是程序运行在保护模式下;
“虚拟保护模式”:它实质上还是实模式,是实模式上模拟的保护模式也使用32位地址寻址方式。Windows3.1彻底删除了对实模式的支持。在80286处理器芯片以后,Windows3.1成为主流操作系统(Windows/80286不是主流产品)。目前差不多所有的X86系列处理器操作系统(Linux,Windows95 and later,OS/2等)都是在启动时进行处理器设置而进入保护模式的。

实模式工作机理:

  • 对于8086/8088来说计算实际地址是用绝对地址对1M求模。8086的地址线的物理结构:20根,也就是它可以物理寻址的内存范围为2^20个字节,即1 M空间,但由于8086/8088所使用的寄存器都是16位,能够表示的地址范围只有0-64K,这和1M地址空间来比较也太小了,所以为了在8086/8088下能够访问1M内存,Intel采取了分段寻址的模式:16位段基地址:16位偏移EA,其绝对地址计算方法为:16位基地址左移4位+16位偏移=20位地址。比如:DS=1000H EA=FFFFH 那么绝对地址就为:10000H +0FFFFH = 1FFFFH 地址单元 。通过这种方法来实现使用16位寄存器访问1M的地址空间,这种技术是处理器内部实现的,通过上述分段技术模式,能够表示的最大内存为: FFFFh: FFFFh=FFFF0h+FFFFh=10FFEFh=1M+64K-16Bytes(1M多余出来的部分被称做高端内存区HMA)。但8086/8088只有20位地址线,只能够访问1M地址范围的数据,所以如果访问100000h~10FFEFh之间的内存(大于1M空间),则必须有第21根地址线来参与寻址(8086/8088没有)。因此,当程序员给出超过1M(100000H-10FFEFH)的地址时,因为逻辑上正常,系统并不认为其访问越界而产生异常,而是自动从0开始计算,也就是说系统计算实际地址的时候是按照对1M求模的方式进行的,这种技术被称为wrap-around。
  • 对于80286或以上的CPU通过A20 GATE来控制A20地址线。技术发展到了 80286,虽然系统的地址总线由原来的20根发展为24根,这样能够访问的内存可以达到2^24=16M,但是Intel在设计80286时提出的目标是向下兼容,所以在实模式下系统所表现的行为应该和8086/8088所表现的完全一样,也就是说,当启动运行在实模式下时,80386以及后续系列应该和8086/8088完全兼容仍然使用A20地址线。所以说80286芯片存在一个BUG:它开设A20地址线时,在保护模式下,如果程序员访问100000H-10FFEFH之间的内存,系统将实际访问这块内存(没有wrap-around技术);如果在实模式下,也可以访问100000H-10FFEFH间的内存,这时采用的是wrap-around技术,这时两种模式下访问同一个内存区却会得到不同的数据,因此说是一个bug。我们来看一副图:

    为了解决上述兼容性问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根) 的有效性,被称为A20 Gate:

  • 如果A20 Gate被打开,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域;
  • 如果A20 Gate被禁止,则当程序员给出100000H-10FFEFH之间的地址的时候,系统仍然使用8086/8088的方式即取模方式(8086仿真)。绝大多数IBM PC兼容机默认的A20 Gate是被禁止的。现在许多新型PC上存在直接通过BIOS功能调用来控制A20 Gate的功能。

保护模式(Protected Mode)

在实模式下,在80286以及更高系列的PC中,即使A20 Gate被打开,在实模式下所能够访问的内存最大也只能为10FFEFH,尽管它们的地址总线所能够访问的能力都大大超过这个限制。为了能够访问10FFEFH以上的内存,则必须进入保护模式。

(286是Intel 80286的另一种叫法) 它又被称作为虚拟地址保护模式。尽管在Intel 80286手册中已经提出了虚地址保护模式,但实际上它只是一个指引,真正的32位地址出现在Intel 80386上。保护模式本身是80286及以后兼容处理器序列之后产成的一种操作模式,它具有许多特性设计为提高系统的多道任务和系统的稳定性。例如内存的保护,分页机制和硬件虚拟存储的支持。现代多数的x86处理器操作系统都运行在保护模式下,包括Linux,Free BSD,和Windows3.0(它也运行在实模式下,为了和Windows 2.x应用程序兼容)及以后的版本。

80286及以后的处理器另一种工作模式是实模式(仅当系统启动的一瞬间),本着向下兼容的原则屏蔽保护模式特性,从而容许老的软件能够运行在新的芯片上。作为一个设计规范,所有的x86系列处理器,除嵌入式Intel80387之外,都是系统启动工作在实模式下,确保遗留下的操作系统向下兼容。它们都必须被启动程序(操作系统程序最初运行代码)重新设置而相应进入保护模式的,在这之前任何的保护模式特性都是无效的。在现代计算机中,这种匹配进入保护模式是操作系统启动时最前沿的动作之一。

在被调停的多道任务程序中,它可以从新工作在实模式下是相当可能的。保护模式的特性是阻止被其他任务或系统内核破坏已经不健全的程序的运行,保护模式也有对硬件的支持,例如中断运行程序,移动运行进程文档到另一个进程和置空多任务的保护功能。

386及以后系列处理器不仅具有保护模式又具有32位寄存器,结果导致了处理功能的混乱,因为80286虽然支持保护模式,但是它的寄存器都是16位的,它是通过自身程序设定而模拟出的32位,并非32位寄存器处理。归咎于这种混乱现象,它促使Windows/386 及以后的版本彻底抛弃80286的虚拟保护模式,以后保护模式的操作系统都是运行在80386以上,不再运行在80286(尽管80286模式支持保护模式),所以说80286是一个过渡芯片,它是一个过渡产品。

尽管 286和386处理器能够实现保护模式和兼容以前的版本,但是内存的1M以上空间还是不易存取,由于内存地址的回绕,IBM PC XT (现以废弃)设计一种模拟系统,它能过欺骗手段访问到1M以上的地址空间,就是开通了A20地址线。在保护模式里,前32个中断为处理器异常预留,例如,中断0D(十进制13)常规保护故障和中断00是除数为零异常。

如果要访问更多的内存,则必须进入保护模式,那么,在保护模式下,A20 Gate对于内存访问有什么影响呢?

为了搞清楚这一点,我们先来看一看A20的工作原理。A20,从它的名字就可以看出来,其实它就是对于A20(从0开始数)的特殊处理(也就是对第21根地址线的处理)。如果A20 Gate被禁止,对于80286来说,其地址为24根地址线,其地址表示为EFFFFF;对于80386极其随后的32根地址线芯片来说,其地址表示为FFEFFFFF。这种表示的意思是:

  • 如果A20 Gate被禁止。则其第A20在CPU做地址访问的时候是无效的,永远只能被作为0。所以,在保护模式下,如果A20 Gate被禁止,则可以访问的内存只能是奇数1M段,即1M,3M,5M…,也就是00000-FFFFF,200000-2FFFFF,300000-3FFFFF…
  • 如果A20 Gate被打开。则其第20-bit是有效的,其值既可以是0,又可以是1。那么就可以使A20线传递实际的地址信号。如果A20 Gate被打开,则可以访问的内存则是连续的。

实模式和保护模式的区别

从表面上看,保护模式和实模式并没有太大的区别,二者都使用了内存段、中断和设备驱动来处理硬件,但二者有很多不同之处。我们知道,在实模式中,内存被划分成段,每个段的大小为 64KB ,而这样的段地址可以用 16位来表示。内存段的处理是通过和段寄存器相关联的内部机制来处理的,这些段寄存器( CS 、 DS 、 SS 和ES )的内容形成了物理地址的一部分。具体来说,最终的物理地址是由 16 位的段地址和 16 位的段内偏移地址组成的。用公式表示为:物理地址 = 左移 4 位的段地址 + 偏移地址。在保护模式下,段是通过一系列被称之为 “ 描述符表 ” 的表所定义的。段寄存器存储的是指向这些表的指针。用于定义内存段的表有两种:全局描述符表 (GDT) 和局部描述符表 (LDT) 。GDT 是一个段描述符数组,其中包含所有应用程序都可以使用的基本描述符。在实模式中,段长是固定的 ( 为 64KB) ,而在保护模式中,段长是可变的,其最大可达 4GB 。LDT 也是段描述符的一个数组。与 GDT 不同,LDT 是一个段,其中存放的是局部的、不需要全局共享的段描述符。每一个操作系统都必须定义一个 GDT ,而每一个正在运行的任务都会有一个相应的 LDT 。每一个描述符的长度是 8 个字节,格式如图 3 所示。当段寄存器被加载的时候,段基地址就会从相应的表入口获得。描述符的内容会被存储在一个程序员不可见的影像寄存器 (shadow register) 之中,以便下一次同一个段可以使用该信息而不用每次都到表中提取。物理地址由 16 位或者 32 位的偏移加上影像寄存器中的基址组成。实模式和保护模式的不同可以从下图很清楚地看出来。

实模式地址

保护模式地址

总结:保护模式同实模式的根本区别是进程内存受保护与否。可寻址空间的区别只是这一原因的果。实模式将整个物理内存看成分段的区域,程序代码和数据位于不同区域,系统程序和用户程序没有区别对待,而且每一个指针都是指向"实在"的物理地址。这样一来,用户程序的一个指针如果指向了系统程序区域或其他用户程序区域,如果这个指针修改了这个区域的某一个值,那么对于这个被修改的系统程序或用户程序,其后果就很可能是灾难性的。为了克服这种低劣的内存管理方式,处理器厂商开发出保护模式。这样,物理内存地址不能直接被程序访问,程序内部的地址(虚拟地址)要由操作系统转化为物理地址去访问,程序对此一无所知。

至此,进程有了严格的边界,任何其他进程根本没有办法访问不属于自己的物理内存区域,甚至在自己的虚拟地址范围内也不是可以任意访问的,因为有一些虚拟区域已经被放进一些公共系统运行库。这些区域也不能随便修改,若修改就会有: SIGSEGV(linux 段错误);非法内存访问对话框(windows 对话框)。

CPU启动环境为16位实模式,之后可以切换到保护模式。但从保护模式无法切换回实模式 。对于80X86处理器来说,从80386处理器开始,除了以前的实模式外,还增添了保护模式和V86模式。实模式和V86模式都是为了和8086兼容而设置的。

实模式: 
      内存寻址方式为:段式寻址,即物理地址=段地址*16   +   段内偏移地址 
      可寻址任意地址,所有指令都相当于工作在特权级。
      dos工作在实模式下 
保护模式: 
      内存寻址方式为:支持内存分页和虚拟内存 
      支持多任务,可依靠硬件用一条指令即可实现任务切换,不同任务可工作在不同的优先级下,操作系统工作在最高优先级0上,应用程序则运行在较低优先级上。从实模式到保护模式,需要建立GDT、IDT等数据表,然后通过修改控制寄存 器CR0的控制位(位0)来实现。
      Windows工作在保护模式下。
虚拟8086模式: 
内存寻址方式:段式寻址,与实模式一样 
        支持多任务和内存分页 
        v86模式主要是为了在保护模式下兼容以前的实模式应用,即可支持多任务,但每个任务都是实模式的工作方式。另外,中断和异常等的处理对于不同的工作模式都是不同的,具体的可以去参看一些相关书籍。

二、理解8086微机系统的组成

1、对于汇编程序而言,我们需要关心CPU中的寄存器、存储器地址、端口(I/O地址)
【内存单元的两个元素】: 地址(编号)和值(内容)。
【字节、字、双字】:8086的内存以字节编址,每个内存单元有唯一的地址(物理地址),可以存放一个字节。字:一个字占据两个连续的字节。双字:双字占据两个连续的字。
【数据的地址对齐】:字单元安排在偶地址(xxx0B),双字单元安排在模4地址(xx00B)等。对于非对齐地址的数据,处理器访问时需要多次访问存储器,这样做花费时间较多。
【总线】:8086的系统总线有3种:数据总线;地址总线:8086CPU外部一共有20条地址总线,但在CPU内部一次只能传送16位地址;控制总线
【I/O】:I/O地址叫做端口,通常采用十六进制数来表达端口:8086的I/O端口为16位,可支持64k个8位端口;I/O地址范围为:0000H ~ FFFFH
【8086的功能结构】:总线接口单元BIU:主要负责读取指令和操作数。执行单元EU:主要负责指令译码和执行。

2、汇编语言程序、汇编程序、连接程序、调试程序
汇编程序:汇编程序将汇编语言源程序翻译(或称作“汇编”)成机器代码目标模块。
.ASM -> .OBJ;注意区分汇编程序与汇编语言源程序。
连接程序:连接程序将汇编后的目标模块转换为可执行程序。
调试程序:调试程序以便排错、分析等。

3、寄存器组

16位通用寄存器】
AX、BX、CX、DX、SI、DI、BP、SP
其中AX、BX、CX、DX可以分作高8位和低8位的两个独立寄存器。如:AH和AL。我们对其中8位的操作,并不影响另外对应的8位数据。

1、数据寄存器ax、bx、cx、dx
数据寄存器用来存放计算的结果和操作数,也可以存放地址。
每个寄存器又有它们各自的专用目的。

AX——累加器,使用频度最高,用于算术、逻辑运算以及与外设传送信息等;
BX——基址寄存器,常用做存放存储器地址;
CX——计数器,作为循环和串操作等指令中的隐含计数器;
DX——数据寄存器,常用来存放双字长数据的高16位,或存放外设端口地址。

2、变址寄存器si、di
变址寄存器常用于存储器寻址时提供地址

SI是源变址寄存器

DI是目的变址寄存器
串操作类指令中,SI和DI具有特别的功能

3、指针寄存器sp、bp
指针寄存器用于寻址内存堆栈内的数据:
SP为堆栈指针寄存器,指示栈顶的偏移地址。
SP不能再用于其他目的,具有专用目的。SP始终指向栈顶。
BP为基址指针寄存器,表示数据在堆栈段中的基地址。
SP和BP寄存器与SS段寄存器联合使用以确定堆栈段中的存储单元地址

4、堆栈
8086中堆栈通常有处理器自动维持,由堆栈段寄存器SS和堆栈指针寄存器SP共同指示。

5、指令指针寄存器IP
指示代码段中指令的偏移地址。与代码段寄存器CS连用(CS:IP)。

6、标志寄存器
标志(flag)用于反映指令执行结果或控制指令执行形式。

16位的标志寄存器——程序状态字PSW寄存器。

其中,状态标志(6个,CF ZF SF PF OF AF)——用来记录程序运行结果的状态信息,许多指令的执行都将相应地设置它。控制标志(3个,DF IF TF)——可由程序根据需要用指令设置,用于控制处理器执行指令的方式。

  • 进位标志CF(Carry Flag):当运算结果的最高有效位有进位(加法)或借位(减法)时,CF=1,or CF=0;
  • 零标志位ZF(Zero Flag):若运算结果为0时,ZF=1,or ZF=0;
  • 符号标志位SF(Sign Flag):运算结果最高位为1,则SF=1,or SF=0。
    有符号数据用最高有效位表示数据的符号。所以,最高有效位就是符号标志的状态
  • 奇偶标志位PF(Parity Flag):当运算结果最低字节中“1”的个数为零或偶数时,PF=1;or PF=0。
    PF标志仅反映最低8位中“1”的个数是偶或奇,即使是进行16位字操作。
  • 溢出标志OF(Overflow Flag):若算术结果有溢出,OF=1,or OF=0;【什么是溢出?溢出判断】
  • 辅助进位标志AF(Auxiliary Carry Flag):运算时D3位(低半字节)有进位或借位时,AF=1,or AF=0。这个标志主要由处理器内部使用,用于十进制算术运算调整指令中,用户一般不必关心。
  • 方向标志DF(Direction Flag):用于串操作指令中,控制地址的变化方向:设置DF=0,存储器地址自动增加;设置DF=1,存储器地址自动减少。CLD指令复位方向标志:DF=0;STD指令置位方向标志:DF=1。
  • 中断允许标志IF(Interrupt-enable Flag):用于控制外部可屏蔽中断是否可以被处理器响应:设置IF=1,允许中断;设置IF=0,禁止中断。CLI指令复位中断标志:IF=0;STI指令置位中断标志:IF=1。
  • 陷阱标志TF(Trap Flag):用于控制处理器进入单步操作模式:设置TF=0,处理器正常工作;设置TF=1,处理器单步执行指令。单步执行指令——处理器在每条指令执行结束时,便产生一个编号为1的内部中断。这种内部中断称为单步中断。所以TF也称为单步标志。

7、段寄存器
段地址——段的起始地址的高16位地址。段内再由16位二进制数来寻址。
偏移地址——段内存储单元到段首地址的字节的距离。
物理地址——用20位二进制数表示。地址范围为00000H ~ FFFFFH。物理地址唯一标识一个存储单元。
逻辑地址——段地址:偏移地址,逻辑地址不唯一。
物理地址=段地址x16+偏移地址

8086有4个16位的段寄存器。

代码段CS(Code Segment):指明代码段的 起始地址。
代码段用来存放程序的指令序列。指令指针寄存器IP指示下一条指令的偏移地址。处理器利用CS:IP取得下一条要执行的指令。

堆栈段SS(Stack Segment):指明堆栈段的起始地址。
堆栈段确定堆栈所在的主存区域。堆栈指针寄存器SP指示堆栈栈顶的偏移地址。处理器利用SS:SP操作堆栈栈顶的数据。

数据段DS(Data Segment):指明数据段的起始地址。
数据段存放运行程序所用的数据。各种主存寻址方式(有效地址EA)得到存储器中操作数的偏移地址。处理器利用DS:EA存取数据段中的数据。

附加段ES(Extra Segment):指明附加段的起始地址。
附加段是附加的数据段,也用于数据的保存。各种主存寻址方式(有效地址EA)得到存储器中操作数的偏移地址。处理器利用ES:EA存取附加段中的数据。
串操作指令将附加段作为其目的操作数的存放区域。

【关于分段】

1、8086对逻辑段的要求:
① 段地址低4位均为0
② 每段最大不超过64KB(216 B),但并不要求必须为64KB
③ 各段之间可以独立,也可以有重叠

2、如何分配各个逻辑段:
① 程序的指令序列必须安排在代码段。
② 程序使用的堆栈一定在堆栈段。
③ 程序中的数据默认是安排在数据段,也经常安排在附加段,尤其是串操作的目的区必须是附加段。数据的存放比较灵活,实际上可以存放在任何一种逻辑段中。

8、段超越前缀指令
没有指明时,一般的数据访问在DS段;使用BP访问主存,则在SS段
默认的情况允许改变,需要使用段超越前缀指令;8086指令系统中有4个,用于明确指定数据所在的逻辑段:
CS: ;代码段超越,使用代码段的数据
SS: ;堆栈段超越,使用堆栈段的数据
DS: ;数据段超越,使用数据段的数据
ES: ;附加段超越,使用附加段的数据

【段超越示例】

【不允许使用段超越的情况】
串处理指令的目的串必须用ES段;PUSH指令的目的和POP指令的源必须用SS段;指令必须存放在CS段。

【段寄存器的使用规定】

【补充】

【什么是溢出】
处理器内部以补码表示有符号数。8位表达的整数范围是:+127~-12816位表达的范围是:+32767~-32768。如果运算结果超出这个范围,就产生了溢出。有溢出,说明有符号数的运算结果不正确。

【溢出和进位】
溢出标志OF和进位标志CF是两个意义不同的标志。
进位标志表示无符号数运算结果是否超出范围,运算结果仍然正确;
溢出标志表示有符号数运算结果是否超出范围,运算结果已经不正确。

【如何运用溢出和进位】
处理器对两个操作数进行运算时,按照无符号数求得结果,并相应设置进位标志CF;同时,根据是否超出有符号数的范围设置溢出标志OF。应该利用哪个标志,则由程序员来决定。也就是说,如果将参加运算的操作数认为是无符号数,就应该关心进位;认为是有符号数,则要注意是否溢出。

【溢出判断】
判断运算结果是否溢出有一个简单的规则:
只有当两个相同符号数相加(包括不同符号数相减),而运算结果的符号与原数据符号相反时,产生溢出;因为,此时的运算结果显然不正确。其他情况下,则不会产生溢出。

补充

1.什么是逻辑地址?
逻辑地址是用户编程时使用的地址,分为段地址和偏移地址两部分。
逻辑地址表示形式:3020:055AH---------(汇编语言中,数字后面加H表示16进制)

2.为什么要用逻辑地址?(逻辑地址的产生背景)
8086cpu访问存储器时,地址寄存器(16位)要先向地址总线发出地址信号(地址总线是专门用来存取内存地址的,故与内存单元有关,20位),而地址寄存器只有16位,从地址寄存器发出的地址信号,所能访问的存储空间只有2^16 = 65536 = 64KB,达不到20位地址总线所提供的地址范围。针对这种情况,就把内存地址分为若干段,每段有一些存储单元构成。用段地址指出是哪一段,偏移地址标明是段中的哪一个单元。

3.什么叫段地址,偏移地址?之间有什么关系?
Ⅰ.把内存地址分为若干段,每段有一些存储单元构成。用段地址指出是哪一段(若是指向同一个存储单元,段地址可以不一样,无非就是偏移地址不一样而已,但是都可以指向同一个物理地址,因此段地址只是一个被访问的存储单元(变量)的起始地址,并不是固定的),偏移地址标明是段中的哪一个单元。
Ⅱ.段地址和偏移地址都是16位2进制数。
Ⅲ.段地址和偏移地址有多种组合,故存在多个地址组合指向同一个存储单元上。

4.逻辑地址唯一么?
不唯一,因为段地址和偏移有多种组合,故存在多个地址组合指向同一个存储单元上。例如:3020:055AH和3000:07AAH就是两种组合,但都是指向同一个存储单元,

5.cpu执行程序时,采用的是逻辑地址还是物理地址?
物理地址---用户编程时采用的逻辑地址在cpu执行程序时都要转换成物理地址。这是由cpu的地址加法器完成的。

6.逻辑地址怎样转换为物理地址?
转换时,先将16位的段地址左移4位,相当于乘以16或者16进制的10H,再和偏移地址相加。转换公式为:物理地址 = 段地址*10H + 偏移地址。如:将3020:055AH转换为物理地址:----= 3020*10H(左移四位)+055AH = 3075AH

7.段与偏移地址是什么关系?
段是由存储单元构成的,段包含偏移地址对应的存储单元。即偏移地址对应的字节存储单元在段中。.

8.段的大小指的是什么?
指的是这个段包含存储单元的多少。

9.将内存分段的依据?以及段的相关知识
段地址和偏移地址都是16位二进制数,每段最大64K字节单元(2^16=65536 = 64KB),每段最小16个字节单元(硬性规定),也可以100个,1000个到最多达到65536个。偏移地址范围:0000H --- FFFFH

10.什么叫小段?
规定每16个字节单元为一小段。

三、理解“逻辑地址、线性地址、物理地址和虚拟地址”

1、各种地址概念

物理地址(physical address)
用于内存芯片级的单元寻址,与处理器和CPU连接的地址总线相对应。
——这个概念应该是这几个概念中最好理解的一个,但是值得一提的是,虽然可以直接把物理地址理解成插在机器上那根内存本身,把内存看成一个从0字节一直到最大空量逐字节的编号的大数组,然后把这个数组叫做物理地址,但是事实上,这只是一个硬件提供给软件的抽像,内存的寻址方式并不是这样。所以,说它是“与地址总线相对应”,是更贴切一些,不过抛开对物理内存寻址方式的考虑,直接把物理地址与物理的内存一一对应,也是可以接受的。也许错误的理解更利于形而上的抽像。
虚拟内存(virtual memory)
这是对整个内存的抽像描述。它是相对于物理内存来讲的,可以直接理解成“不直实的”,“假的”内存,例如,一个0x08000000内存地址,它并不对就物理地址上那个大数组中0x08000000 - 1那个地址元素;之所以是这样,是因为现代操作系统都提供了一种内存管理的抽像,即虚拟内存(virtual memory)。进程使用虚拟内存中的地址,由操作系统协助相关硬件,把它“转换”成真正的物理地址。这个“转换”,是所有问题讨论的关键。有了这样的抽像,一个程序,就可以使用比真实物理地址大得多的地址空间,甚至多个进程可以使用相同的地址。不奇怪,因为转换后的物理地址并非相同的。
——可以把连接后的程序反编译看一下,发现连接器已经为程序分配了一个地址,例如,要调用某个函数A,代码不是call A,而是call 0x0811111111 ,也就是说,函数A的地址已经被定下来了。没有这样的“转换”,没有虚拟地址的概念,这样做是根本行不通的。打住了,这个问题再说下去,就收不住了。
逻辑地址(logical address)
Intel为了兼容,将远古时代的段式内存管理方式保留了下来。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。以上例,我们说的连接器为A分配的0x08111111这个地址就是逻辑地址。不过不好意思,这样说,好像又违背了Intel中段式管理中,对逻辑地址要求,“一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量],也就是说,上例中那个0x08111111,应该表示为[A的代码段标识符: 0x08111111],这样,才完整一些”
线性地址(linear address)
线性地址或也叫虚拟地址(virtual address)跟逻辑地址类似,它也是一个不真实的地址,如果逻辑地址是对应的硬件平台段式管理转换前地址的话,那么线性地址则对应了硬件页式内存的转换前地址。
CPU将一个虚拟内存空间中的地址转换为物理地址,需要进行两步:首先将给定一个逻辑地址(其实是段内偏移量,这个一定要理解!!!),CPU要利用其段式内存管理单元,先将为个逻辑地址转换成一个线程地址,再利用其页式内存管理单元,转换为最终物理地址。
这样做两次转换,的确是非常麻烦而且没有必要的,因为直接可以把线性地址抽像给进程。之所以这样冗余,Intel完全是为了兼容而已。

//导读注意:下面2-5部分,分别依次按照CPU段式内存管理、Linux段式内存管理、CPU页式内存管理、Linux页式内存管理

2CPU段式内存管理,逻辑地址如何转换为线性地址

一个逻辑地址由两部份组成,段标识符: 段内偏移量。段标识符是由一个16位长的字段组成,称为段选择符。其中前13位是一个索引号。后面3位包含一些硬件细节,如图:

索引号,或者直接理解成数组下标——那它总要对应一个数组吧,它又是什么东东的索引呢?这个东东就是“段描述符(segment descriptor)”,呵呵,段描述符具体地址描述了一个段(对于“段”这个字眼的理解,我是把它想像成,拿了一把刀,把虚拟内存,砍成若干的截——段)。这样,很多个段描述符,就组了一个数组,叫“段描述符表”,这样,可以通过段标识符的前13位,直接在段描述符表中找到一个具体的段描述符,这个描述符就描述了一个段,我刚才对段的抽像不太准确,因为看看描述符里面究竟有什么东东——也就是它究竟是如何描述的,就理解段究竟有什么东东了,每一个段描述符由8个字节组成,如下图:

这些东东很复杂,虽然可以利用一个数据结构来定义它,不过,我这里只关心一样,就是Base字段,它描述了一个段的开始位置的线性地址。
Intel设计的本意是,一些全局的段描述符,就放在“全局段描述符表(GDT)”中,一些局部的,例如每个进程自己的,就放在所谓的“局部段描述符表(LDT)”中。那究竟什么时候该用GDT,什么时候该用LDT呢?这是由段选择符中的T1字段表示的,=0,表示用GDT,=1表示用LDT。
GDT在内存中的地址和大小存放在CPU的gdtr控制寄存器中,而LDT则在ldtr寄存器中。
好多概念,像绕口令一样。这张图看起来要直观些:

首先,给定一个完整的逻辑地址[段选择符:段内偏移地址],
1、看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。我们就有了一个数组了。
2、拿出段选择符中前13位,可以在这个数组中,查找到对应的段描述符,这样,它了Base,即基地址就知道了。
3、把Base + offset,就是要转换的线性地址了。
还是挺简单的,对于软件来讲,原则上就需要把硬件转换所需的信息准备好,就可以让硬件来完成这个转换了。OK,来看看Linux怎么做的。

3Linux的段式内存管理

Intel要求两次转换,这样虽说是兼容了,但是却是很冗余,呵呵,没办法,硬件要求这样做了,软件就只能照办,怎么着也得形式主义一样。另一方面,其它某些硬件平台,没有二次转换的概念,Linux也需要提供一个高层抽像,来提供一个统一的界面。所以,Linux的段式管理,事实上只是“哄骗”了一下硬件而已。按照Intel的本意,全局的用GDT,每个进程自己的用LDT——不过Linux则对所有的进程都使用了相同的段来对指令和数据寻址。即用户数据段,用户代码段,对应的,内核中的是内核数据段和内核代码段。这样做没有什么奇怪的,本来就是走形式嘛,像我们写年终总结一样。
include/asm-i386/segment.h

  1. #define GDT_ENTRY_DEFAULT_USER_CS        14
  2. #define __USER_CS (GDT_ENTRY_DEFAULT_USER_CS * 8 + 3)
  3. #define GDT_ENTRY_DEFAULT_USER_DS        15
  4. #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3)
  5. #define GDT_ENTRY_KERNEL_BASE        12
  6. #define GDT_ENTRY_KERNEL_CS       (GDT_ENTRY_KERNEL_BASE + 0)
  7. #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)
  8. #define GDT_ENTRY_KERNEL_DS     (GDT_ENTRY_KERNEL_BASE + 1)
  9. #define __KERNEL_DS     (GDT_ENTRY_KERNEL_DS * 8)

复制代码
把其中的宏替换成数值,则为:

  1. #define __USER_CS 115        [00000000 1110  0  11]
  2. #define __USER_DS 123        [00000000 1111  0  11]
  3. #define __KERNEL_CS 96      [00000000 1100  0  00]
  4. #define __KERNEL_DS 104    [00000000 1101  0  00]

复制代码
方括号后是这四个段选择符的16位二制表示,它们的索引号和T1字段值也可以算出来了

  1. __USER_CS              index= 14   T1=0
  2. __USER_DS              index= 15   T1=0
  3. __KERNEL_CS           index=  12  T1=0
  4. __KERNEL_DS           index= 13   T1=0

复制代码
T1均为0,则表示都使用了GDT,再来看初始化GDT的内容中相应的12-15项(arch/i386/head.S):

  1. .quad 0x00cf9a000000ffff        /* 0x60 kernel 4GB code at 0x00000000 */
  2. .quad 0x00cf92000000ffff        /* 0x68 kernel 4GB data at 0x00000000 */
  3. .quad 0x00cffa000000ffff        /* 0x73 user 4GB code at 0x00000000 */
  4. .quad 0x00cff2000000ffff        /* 0x7b user 4GB data at 0x00000000 */

复制代码
按照前面段描述符表中的描述,可以把它们展开,发现其16-31位全为0,即四个段的基地址全为0。
这样,给定一个段内偏移地址,按照前面转换公式,0 + 段内偏移,转换为线性地址,可以得出重要的结论,“在Linux下,逻辑地址与线性地址总是一致(是一致,不是有些人说的相同)的,即逻辑地址的偏移量字段的值与线性地址的值总是相同的。!!!”
忽略了太多的细节,例如段的权限检查。呵呵。
Linux中,绝大部份进程并不例用LDT,除非使用Wine ,仿真Windows程序的时候。

4.CPU的页式内存管理

CPU的页式内存管理单元,负责把一个线性地址,最终翻译为一个物理地址(注意,相互独立的进程都有自己的页目录和页表,就算多个进程具有相同的线性地址,最后转换到物理地址也是不一样的,所以不会互相干扰)。从管理和效率的角度出发,线性地址被分为以固定长度为单位的组,称为页(page),例如一个32位的机器,线性地址最大可为4G,可以用4KB为一个页来划分,这页,整个线性地址就被划分为一个tatol_page[2^20]的大数组,共有2的20个次方个页。这个大数组我们称之为页目录。目录中的每一个目录项,就是一个地址——对应的页的地址。另一类“页”,我们称之为物理页,或者是页框、页桢的。是分页单元把所有的物理内存也划分为固定长度的管理单位,它的长度一般与内存页是一一对应的。
这里注意到,这个total_page数组有2^20个成员,每个成员是一个地址(32位机,一个地址也就是4字节),那么要单单要表示这么一个数组,就要占去4MB的内存空间。为了节省空间,引入了一个二级管理模式的机器来组织分页单元。文字描述太累,看图直观一些:

如上图,
1、分页单元中,页目录是唯一的,它的地址放在CPU的cr3寄存器中,是进行地址转换的开始点。
2、每一个活动的进程,因为都有其独立的对应的虚似内存(页目录也是唯一的),那么它也对应了一个独立的页目录地址。——运行一个进程,需要将它的页目录地址放到cr3寄存器中,将别个的保存下来。
3、每一个32位的线性地址被划分为三部份,面目录索引(10位):页表索引(10位):偏移(12位)

依据以下步骤进行转换:
1、从cr3中取出进程的页目录地址(操作系统负责在调度进程的时候,把这个地址装入对应寄存器);
2、根据线性地址前十位,在数组中,找到对应的索引项,因为引入了二级管理模式,页目录中的项,不再是页的地址,而是一个页表的地址。(又引入了一个数组),页的地址被放到页表中去了。
3、根据线性地址的中间十位,在页表(也是数组)中找到页的起始地址;
4、将页的起始地址与线性地址中最后12位相加,得到最终我们想要的葫芦;

这个转换过程,应该说还是非常简单地。全部由硬件完成,虽然多了一道手续,但是节约了大量的内存,还是值得的。那么再简单地验证一下:
1、这样的二级模式是否仍能够表示4G的地址;
页目录共有:2^10项,也就是说有这么多个页表
每个目表对应了:2^10页;
每个页中可寻址:2^12个字节。
还是2^32 = 4GB

2、这样的二级模式是否真的节约了空间;
也就是算一下页目录项和页表项共占空间 (2^10 * 4 + 2 ^10 *4) = 8KB。哎,……怎么说呢!!!
红色错误,标注一下,后文贴中有此讨论。。。。。。
按<深入理解计算机系统>中的解释,二级模式空间的节约是从两个方面实现的:
A、如果一级页表中的一个页表条目为空,那么那所指的二级页表就根本不会存在。这表现出一种巨大的潜在节约,因为对于一个典型的程序,4GB虚拟地址空间的大部份都会是未分配的;
B、只有一级页表才需要总是在主存中。虚拟存储器系统可以在需要时创建,并页面调入或调出二级页表,这就减少了主存的压力。只有最经常使用的二级页表才需要缓存在主存中。——不过Linux并没有完全享受这种福利,它的页表目录和与已分配页面相关的页表都是常驻内存的。
值得一提的是,虽然页目录和页表中的项,都是4个字节,32位,但是它们都只用高20位,低12位屏蔽为0——把页表的低12屏蔽为0,是很好理解的,因为这样,它刚好和一个页面大小对应起来,大家都成整数增加。计算起来就方便多了。但是,为什么同时也要把页目录低12位屏蔽掉呢?因为按同样的道理,只要屏蔽其低10位就可以了,不过我想,因为12>10,这样,可以让页目录和页表使用相同的数据结构,方便。
本贴只介绍一般性转换的原理,扩展分页、页的保护机制、PAE模式的分页这些麻烦点的东东就不啰嗦了……可以参考其它专业书籍。

5.Linux的页式内存管理

原理上来讲,Linux只需要为每个进程分配好所需数据结构,放到内存中,然后在调度进程的时候,切换寄存器cr3,剩下的就交给硬件来完成了(呵呵,事实上要复杂得多,不过偶只分析最基本的流程)。
前面说了i386的二级页管理架构,不过有些CPU,还有三级,甚至四级架构,Linux为了在更高层次提供抽像,为每个CPU提供统一的界面。提供了一个四层页管理架构,来兼容这些二级、三级、四级管理架构的CPU。这四级分别为:
页全局目录PGD(对应刚才的页目录)
页上级目录PUD(新引进的)
页中间目录PMD(也就新引进的)
页表PT(对应刚才的页表)。

整个转换依据硬件转换原理,只是多了二次数组的索引罢了,如下图:

那么,对于使用二级管理架构32位的硬件,现在又是四级转换了,它们怎么能够协调地工作起来呢?嗯,来看这种情况下,怎么来划分线性地址吧!
从硬件的角度,32位地址被分成了三部份——也就是说,不管理软件怎么做,最终落实到硬件,也只认识这三位老大。
从软件的角度,由于多引入了两部份,,也就是说,共有五部份。——要让二层架构的硬件认识五部份也很容易,在地址划分的时候,将页上级目录和页中间目录的长度设置为0就可以了。
这样,操作系统见到的是五部份,硬件还是按它死板的三部份划分,也不会出错,也就是说大家共建了和谐计算机系统。
这样,虽说是多此一举,但是考虑到64位地址,使用四层转换架构的CPU,我们就不再把中间两个设为0了,这样,软件与硬件再次和谐——抽像就是强大呀!!!
例如,一个逻辑地址已经被转换成了线性地址,0x08147258,换成二制进,也就是:
0000100000 0101000111 001001011000
内核对这个地址进行划分
PGD = 0000100000
PUD = 0
PMD = 0
PT = 0101000111
offset = 001001011000
现在来理解Linux针对硬件的花招,因为硬件根本看不到所谓PUD,PMD,所以,本质上要求PGD索引,直接就对应了PT的地址。而不是再到PUD和PMD中去查数组(虽然它们两个在线性地址中,长度为0,2^0 =1,也就是说,它们都是有一个数组元素的数组),那么,内核如何合理安排地址呢?
从软件的角度上来讲,因为它的项只有一个,32位,刚好可以存放与PGD中长度一样的地址指针。那么所谓先到PUD,到到PMD中做映射转换,就变成了保持原值不变,一一转手就可以了。这样,就实现了“逻辑上指向一个PUD,再指向一个PDM,但在物理上是直接指向相应的PT的这个抽像,因为硬件根本不知道有PUD、PMD这个东西”。

然后交给硬件,硬件对这个地址进行划分,看到的是:
页目录 = 0000100000
PT = 0101000111
offset = 001001011000
嗯,先根据0000100000(32),在页目录数组中索引,找到其元素中的地址,取其高20位,找到页表的地址,页表的地址是由内核动态分配的,接着,再加一个offset,就是最终的物理地址了。

参见:
“http://www.cnblogs.com/diyingyun/archive/2012/01/03/2311327.html”
"http://blog.sina.com.cn/s/blog_79ba23780102vz77.html"
"https://www.cnblogs.com/exRunner/p/7531850.html"

理解单片机系统—汇编语言相关推荐

  1. 通过STC12C5A08S2实战项目来理解单片机是怎么工作的

    通过STC12C5A08S2实战项目来理解单片机是怎么工作的 在原理图当中,实际上也把按键流到了外部中断接口上,也就是会把外部中断零和外部中断一实际上是留了两个按键,留了两个按键的接口,这样的话,你也 ...

  2. 51单片机(汇编语言)实现十进制转十六进制

    51单片机(汇编语言)实现十进制转十六进制 任务描述 任务原理 方案Ⅰ 思路 代码 运行结果 算法补充 循环四位移位 带进位的乘法 2bytes + 1byte 方案Ⅱ 任务描述 DEC0 DEC1 ...

  3. 理解Android系统的进程间通信原理(二)----RPC机制

    理解Android系统中的轻量级解决方案RPC的原理,需要先回顾一下JAVA中的RMI(Remote Method Invocation)这个易于使用的纯JAVA方案(用来实现分布式应用).有关RMI ...

  4. 理解Android系统的进程间通信原理------RPC机制

    理解Android系统中的轻量级解决方案RPC的原理,需要先回顾一下JAVA中的RMI(Remote Method Invocation)这个易于使用的纯JAVA方案(用来实现分布式应用).有关RMI ...

  5. RAM测试方法 C语言实现,有哪些常用单片机系统RAM测试方法?基于种子和逐位倒转的RAM故障测试法有什么优点?...

    在各种单片机应用系统中, 存储 器的正常与否直接关系到该系统的正常工作.为了提高系统的可靠性,对系统的可靠性进行 测试 是十分必要的.通过 测试 可以有效地发现并解决因 存储 器发生故障对系统带来的破 ...

  6. 理解Linux系统中的load average(图文版)转载

    理解Linux系统中的load average(图文版) 博客分类: Linux linux load nagios  一.什么是load average? linux系统中的Load对当前CPU工作 ...

  7. 深入理解linux系统的目录结构

    深入理解linux系统的目录结构(总结的非常详细) 作者:佚名 字体:[增加 减小] 来源:互联网 时间:04-09 14:34:20 我要评论 对于每一个Linux学习者来说,了解Linux文件系统 ...

  8. 单片机系统中的红外通信接口

    摘要:本文结合复费率电能表中红外通信的设计实从事贸易,介绍了单片机系统中红外通信的软硬件设计方法,并给出了具体的电路原理和通信源程序. 关键词:单片机,红外通信,遥控 在许多基于单片机的应用系统中,系 ...

  9. EMC测试仪器_电巢学堂:单片机系统EMC测试和故障排除

    原标题:电巢学堂:单片机系统EMC测试和故障排除 对于从事单片机应用系统(软硬件)设计的工程技术人员来说,掌握一定的EMC测试技术是十分必要的. 一.关于EMC EMC:Electromagnetic ...

最新文章

  1. Go 内存对齐的那些事儿
  2. 推荐系统——GBDT+LR
  3. CA的ITSM产品Unicenter介绍
  4. Mvc 前台 匿名对象
  5. 如何整理MacOS的菜单栏图标
  6. ztree 使用教程
  7. 计算机网络第七版和课后答案百度网盘下载
  8. sketch 3.8.1(破解版涵盖3.0,3.7,3.8.0以上版本) 安装and使用指南(20160524)更新)
  9. 帆软报表参数传给网络报表_自定义报表之自定义参数界面
  10. Excel快捷键大全 Excel2013/2010/2007/2003常用快捷键大全【转】
  11. 零基础-微信小程序入门教程
  12. 服务器系统如用pe和做镜像,用IMAGEX捕获系统镜像,并通过PE从网络分发的方法...
  13. 利用云服务器发布项目
  14. Android混淆篇 small-video-record(ffmpeg)视频压缩框架混淆
  15. verilog自学笔记
  16. oracle 数据库口令,oracle的口令资料
  17. c4d打开没反应_(图文+视频)C4D野教程:来制作一个类似蹦床的布料动画
  18. Photoshop如何快速选择定位到锁定状态的图层
  19. 企业级旅行App源码、高仿爱鲜蜂源码、iOS Arkit测距源码等
  20. 用python写一个贪吃蛇小游戏

热门文章

  1. NYOJ 927 The partial sum problem 【DFS】+【剪枝】
  2. 图形化代码阅读工具——Scitools Understand
  3. Oracle使用goldengate分别向Oracle和mysql双路的单向复制
  4. java处理XSS过滤的方法
  5. Oracle数据文件的备份与恢复
  6. Spring表单的initBinder:绑定表单复杂属性
  7. Celery简介及Docker测试环境搭建
  8. django3.2.8使用simpleui注意事项
  9. 使用gparted live分区工具对VMware及ESXI(vsphere)虚拟机进行根目录扩容(可视化界面操作)
  10. 【网址收藏】podman安装及使用简单介绍