#X86 Assemble Language [8080_mem_model]: http://data.linuxtoy.cn/image/8080_mem_model.png "The memory model for 8080 CPU" [8080_mem_model_in_8086]: http://data.linuxtoy.cn/image/8080_mem_model_in_8086.png "The 8080 memory model in 8086" [access_1MB_through_64K_blinder]: http://data.linuxtoy.cn/image/access_1MB_through_64K_blinder.png "access 1MB through 64K blinder" [mem_addr_and_segment_addr]: http://data.linuxtoy.cn/image/mem_addr_and_segment_addr.png "mem address and segment address" [extend_general_registers]: http://data.linuxtoy.cn/image/extend_general_registers.png "extend general registers" [real_mode_plat_model]: http://data.linuxtoy.cn/image/real_mode_plat_model.png "real mode plat model" [real_mode_segmented_model]: http://data.linuxtoy.cn/image/real_mode_segmented_model.png "real mode segmented model" [protected_mode_plat_model]: http://data.linuxtoy.cn/image/protected_mode_plat_model.png "protected plat mode" # 内存寻址模式 实模式平面模型(real mode flat model) 实模式段模型(real mode segmented model) 保护模式平面模型(protected mode flat model) 保护模式平面模型只能用于支持IA-32体系结构的386及更新的cpu中. ## 实模式平面模型 先从8080开始讲述这几种内存模式的发展过程. 1974年intel推出了8080 CPU, 8080拥有一个8位的CPU, 一次能处理8位的信息. 然后它却拥有16根地址线。CPU的位数由它的通用寄存器的位宽决定。 16根地址线可以寻址64K字节. 8080主要使用的操作系统是CP/M-80, CP/M-80有些与众不同,它出现在已安装内存的顶部。 主要的原因是为了让临时程序拥有一致的内存起点. (临时程序可以理解为非操作系统的应用程序,它被加载到内存中,只有需要的时候才运行) CP/M-80从磁盘读取一个程序并运行时,它将程序加载到距离0地址256个字节的地址0x100的地方。 内存的前256个字节叫程序段前缀(Program Segment Prefix, 简称为PSP)。 PSP里面包含用于磁盘输入输出的通用内存缓冲区和一些零碎的信息。 8080和CP/M-80的内存模式如下图: ![8080 memory model][8080_mem_model] ## 实模式段模型 ### 8086 Intel 1978年设计了8086, 是一个16位处理器,有20根地址线,可以直接寻址1MB的存储空间. Intel希望人们更容易地将较老的CP/M-80软件从8080移植到8086. 要想做这件事情,一种方法是确保16位的寻址系统仍然能够工作。 所以尽管8086的能够寻址的范围是8080的16倍,Intel将8086设计为可让程序 只占用1M内存里的64K字节段,并完全在它里面运行,就好像它是小型的8080内存系统一样。 这是通过段寄存器来实现的,先将段寄存器看做位于CPU内部的内存指针。 这种方式见下图: ![8086内存系统中的8080内存模式][8080_mem_model_in_8086] 代码段寄存器CS指向8086 1MB内存中某一位置,这个位置作为64K内存的起点 在这个区域中,一个快速转换的CP/M-80程序能够执行。 从短期来看,这个设计非常明智,因为两年内,许多的CP/M-80程序被转换成了8086程序。 但是从长远来看,这是一个非常糟糕的设想。 当程序员们企图从头开始创建全新的,与8080从未谋面且不需要段模型的程序时, 问题变得很严重,因为段模型控制着8086的体系结构 一些一次需要大于64KB内存的程序,不得不使用64KB的内存块, 通过切换段寄存器的值在这些内存块之间切换。 ### 向后兼容和虚拟86模式 现代的x86 CPU能够在更大的内存范围内寻址, 对于8086和8088而言,计算机真正拥有20根地址线和1MB内存, 386及其之后的CPU能够寻址4MB内存,而不必把它分成更小的内存段. 当32位CPU工作在保护模式实模型下时,一个段是4GB, 所以大多数情况下,一个内存段足够. 为了维护古老的8086和8088的向后兼容,较新的CPU可以将自己限制在老式CPU 能够寻址和执行的范围内. 当一个奔腾系列的CPU要运行为实模式段模型编写的软件时, 它采用一项简洁的技术,暂时把自己变成一个8086. 这叫做virtual-86 mode. 当在Windows NT及更高版本下启动一个MS-DOS窗口时,你就在使用虚拟86模式。 ### 实模式段模型 在是模式段模型里,x86 CPU可以看到整整1MB内存, 也就是说CPU可以将自己设置为 只使用32根地址线中的20位,进而向存储器系统传递20位的地址。 用眼罩来打个比喻: 虽然CPU可以看到整整1MB字节的内存,但是他被限制为只能使用16位的眼罩来看这1MB的内存。 ![使用64K的眼罩来访问1MB的内存空间][access_1MB_through_64K_blinder] 左边这个长长的矩形代表1MB内存,CPU可以在实模式段模型下对它进行寻址。 中间是一个纸板,在纸板上切出一个1字节宽,65536个字节长的插槽。 CPU可以在这1MB内存范围内向上或者向下滑动这个纸板。 但是在任意时间内,它只能访问65536个字节。 段的本质: 段是一个内存区域,它从一个段落(paragraph)边界开始,并且扩展一定数量的字节。 在是模式分段模型中,这个数量小于等于64K 段落是一个内存度量,就像字节,字,双字一样, 段落的大小是16个字节。 任何能够被16整处的内存地址都被称为一个段落边界。 在实模式段模型中,一个程序可以只使用几个段,但是每个段可能开始于65535个段落 边界中的任何一个。 把段开始的那个段落边界的编号成为该段的段地址。 ![段地址][mem_addr_and_segment_addr] 如上图,没一条阴影条都是一个段地址。每16个地址开始一个段。 最大的段地址是0FFFFh, 对应的内存地址是0FFFF0h, 距离实模式的1MB内存顶部16个字节。 对于64K个段地址而言,只有五六个在某一时段被真正用作段的开始。 将段地址想象为可以放置段的插槽。 8086 8088 80286是16位的CPU, 那么如何把20位的地址放入16位的寄存器中呢? 实现方式是把20位的地址放入两个16位的寄存器. 这样以来位于实模式下的1MB内存中的存储位置不是只有一个地址,而是两个。 一个字节的完整地址包含它所在的段地址和距离那个段的起始位置的字节数。 表示方式如下: 0001:0019 该地址位于0001h段内,偏移地址为0x0019h 当两个数字之间用冒号隔开,用于指定一个地址时,不需要再为这两个16进制数的 结尾加上h. 一个地址的表示方式不是唯一的, 0001:0019也可以表示为0000:0029 或者0002:0009 # registers ## 段寄存器 在8086 8088 80286中有4个专门设计用于存放段地址的段寄存器. 386及后来的处理器中又多了两个. CS: code segment, 代码段,CS存放当前执行指令的代码段的段地址 DS: data segment, 数据段,变量或者其他数据存放在数据段的某些偏移位置处 SS: stack segment,堆栈段 ES: extra segment,附加段寄存器 FS: extra segment,FS和FS也是附件段,按照E F G的字母顺序来命名的。 GS: extra segment FS和GS只出现在386及更高版本的intel x86 CPU中. ## 通用寄存器 | 16bit | 32bit | 64bit | | -- | -- | -- | | AX (AH AL) | EAX | RAX | | BX (BH BL) | EBX | RBX | | CX (CH CL) | ECX | RCX | | DX (DH DL) | EDX | RDX | | BP | EBP | RBP | | SI | ESI | RSI | | DI | EDI | RDI | | SP | ESP | ESP | 当intel在1986年将x86体系扩展到32位时,把上面这8个寄存器的大小增大了一倍 在原有名称前加上前缀E, 命名为 EAX, EBX, ECX, EDX, EBP, ESI, EDI, ESP ![扩展通用寄存器][extend_general_registers] ## 指令指针寄存器 IP(instruction pointer) | 16bit | 32bit | 64bit | | -- | -- | -- | | IP | EIP | RIP | IP包含当前代码段中下一条即将执行的机器指令的偏移地址。 既然IP包含了下一条指令的偏移地址,那么段地址放在什么地方呢? 实模式段模型中: 段地址放在CS中, CS和IP一起能够表示一个20位的地址, 保存了下一条即将执行的指令的完整地址。 实模式平面模型和保护模式平面模型中: CS的值由操作系统设置,并且保持不变。 在实模式段模型中CS会经常变化, 在实模式平面模型和保护模式平面模型CS的至几乎不会变化 (保护模式下的所有段寄存器都隶属于操作系统,一般的应用程序不应该修改它). ## 标志寄存器 CPU内部,有一种额外类型的寄存器,即表示寄存器。 在8086 8088 80286中,标志寄存器是16bit, 名称为FLAGS。 在80386及更新的CPU中,标志寄存器是32bit,名称为EFLAGS。 # 三种主要的汇编编程模型 主要的区别是进行内存寻址时对寄存器的使用。 实模式下 CPU可以看到的只有1MB的内存 ## 实模式平面模型: ![实模式平面模型][real_mode_plat_model] 程序和它处理的数据都必须位于一个64KB大小的内存块。 所有的段寄存器都被设置为你能使用的64K内存块的起始位置(加载运行这些程序时, 操作系统会设置这些段寄存器),你可以完全忘记有段寄存器。 ## 实模式段模型: 程序可以看到实模式下CPU可用的整个1MB内存,通过16位段基址+16位偏移地址组成一个20位的地址。 地址的翻译由CPU内部完成,惯用的表示是在段寄存器和偏移寄存器之间用一个冒号隔开。 SS:SP SS:BP CS:BX DS:SI ES:DI ![实模式段模型][real_mode_segmented_model] 与实模式平面模型相比,上图显示了所有的内存, 而不只是实模式平面模型程序运行时分配的一个小小的内存块。 为实模型段模式编写的程序可以看到所有的实模式内存。 上图显示了2个代码段和两个数据段,实际上可以有任意合理数量的代码段和数据段。 可以在同一时间访问多个数据段,因为段寄存器DS ES FS GS 可以作为数据段基址。 但是你只有一个代码段寄存器CS,下一条即将执行的指令位于 CS:IP, 不能直接加载一个值到CS中来改变代码段,如果需要在代码段中跳转的话,跳转指令可以实现到 另一个代码段的跳转,它将为你改变CS中的值。 操作系统和你的程序一起存在于内存中,同时还有一些重要的数据表。 在实模式下,你可能粗心使用段寄存器而破坏部分操作系统,而导致操作系统崩溃。 正是这一危险促使intel在80386及后面的cpu上建立新的属性来支持保护模式。 ## 保护模式平面模型: 自从1986年386出现开始,Intel已经实现了很好的保护模式体系结构。 但是应用程序只通过他们本身并不能使用保护模式,应用程序能够在其内部运行之前 操作系统必须建立和管理一个保护模式。MS-DOS做不到这一点,直到1994年windows NT才能做到。 而linux中没有实模式“遗留问题”需要处理,它出现1992年以来,它就一直运行在保护模式下。 ![保护模式平面模型][protected_mode_plat_model] 保护模式平面模型,程序看到的是一块地址从0到4G-1的内存。 每个地址都是1个32位的值, 所有通用寄存器的大小都是32位,因此一个通用寄存器可以指向整个4GB空间中的任何位置。 指令指针寄存器也是32位,所以EIP可以指向4GB空间中任何地方的机器指令。 段寄存器被认为是操作系统的一部分,用户程序即不能直接读取他们,也不能直接修改他们。 他们的工作是定义这4GB内存出现在物理内存或者虚拟内存中的什么地方。 实模式平面模型与保护模式平面模型的区别: 实模式平面模式下,程序拥有整个64K内存空间. 保护模式平面模型,程序只能拥有分配给它的4GB内存中的一部分,而其他部分仍属于操作系统。 在保护模式平面模型下, 一个通用寄存器可以存放4GB空间内任意位置的地址,但当真正读取或者写入 某些位置时,会被操作系统禁止,并触发运行时错误。