1. 什么是汇编语言的寻址模式(或者编址模式)
机器指令是计算机CPU可执行的指令,一条指令要包含操作符和操作的对象—操作数。对于Intel Processors指令集而言,有的指令没有操作数,有的有多个操作数(大于1个)。所谓寻址模式,就是我们写的代码要指明操作数在那里,这样CPU才能正确的找到操作数,执行这条指令。一条指令的操作数,可以直接包含在指令中,不需要去别的地方寻找;也可以是放在寄存器中,如果是放在寄存器中,直接取寄存器就行了;麻烦的就是操作数放在内存中,因为内存地址的形成相对复杂,你不能直接在内存的可访问地址范围内随便写一个地址,必须要按一定的规则形成内存地址,内存中的数据,有的是取这个内存的数据就可以,所谓直接方式,有的需要计算得出其存放地址,再通过地址取得内存中的数据,所谓间接方式。
2. 一些寻址的原则和容易混淆的地方
2.1 术语的翻译与称呼
opcode(operation code):操作码
operands:操作数
source:源操作数
destination:目标操作数
2.2 除了MOVS指令外,任何指令都不允许内存向内存传输数据。
2.3 有些操作数是显式指定的,有些则是隐含的。
2.4 源操作数的来源
(1) the instruction itself (an immediate operand)(来源于指令集本身,即包含在指令中,不用变量标号表示,就是直接数字,给他一个名称立即数。)。
(2) a register(来自某个寄存器)。
(3) a memory location(来自某个内存地址)。
(4) an I/O port(来自输入/输出端口)。
2.5 当指令返回数据给目的操作数时,可以返回数据到如下地方:
(1) a register(返给某个寄存器)。
(2) a memory location(返给某个内存地址)。
(3) an I/O port(返给输入/输出端口)。
2.6 这里所有指令以32位为示例。
3. Operands(操作数)
3.1 Immediate Operands(立即操作数)
将数据编码在指令中作为源操作数,这样的源操作数称为立即操作数,简称立即方式,或称立即寻址。例如
MOV EAX,1234H
这时寄存器EAX显示为EAX = 00001234
这里区别于直接内存方式,这里不用变量代码常数,常数直接写在代码中,即源操作数不来自寄存器,不来自内存,也不来自I/O端口,不需要别的地方查找,编译后生成的可执行文件中这个常数就已经编码在文件中了,即立即就能用了。
3.2 Register Operands(寄存器操作数)
源操作数或者目的操作数可以是如下任何寄存器,取决于具体的指令:
(1)32-bit general-purpose registers (EAX, EBX, ECX, EDX, ESI, EDI, ESP, or EBP),即8个通用寄存器。
(2)segment registers (CS, DS, SS, ES, FS, and GS),即6个段寄存器。
(3) EFLAGS register(标识寄存器)。
(4) x87 FPU registers (ST0 through ST7, status word, control word, tag word, data operand pointer, and instruction poiter.(8个浮点寄存器),注意在很多国内作者编写的汇编语言教材中并没有讲浮点寄存器,也没有讲解浮点汇编指令,甚至提都没有提一笔。
(5) MMX registers (MM0 through MM7)(MultiMedia eXtensions):8个多媒体扩展寄存器。
(6) XMM registers (XMM0 through XMM7) and the MXCSR register: Streaming SIMD Extensions (SSE) is a single instruction, multiple data (SIMD) instruction set extension to the x86 architecture。(即SSE加入的8个128位寄存器XMM0到XMM7。)
(7) control registers (CR0, CR2, CR3, and CR4) and system table pointer registers (GDTR, LDTR, IDTR, and task register。(控制寄存器和系统表指针寄存器,以及任务寄存器)。
(8) debug registers (DR0, DR1, DR2, DR3, DR6, and DR7)。(调试寄存器)
(9) MSR registers(Model Specific Register)。(指的是在x86架构处理器中,一系列用于控制CPU运行、功能开关、调试、跟踪程序执行、监测CPU性能等方面的寄存器。)
3.3 Memory Operands(内存操作数)
在内存中的源操作数或者目的操作数是通过内存地址来访问的,其内存地址由segment selector(段选择子)和offset(偏移)构成。段选择子指定了包含操作数的内存段,偏移指定了这个操作数的线性地址或者有效地址。偏移可以是32位或16位。如下图所示:
什么又是段选择子,什么又是线性地址,什么又是有效地址呢?这在很多国产教材中都是一笔跳过,模糊不清,不知道是作者本身就不清楚还是觉得不重要,对于初学者来说极为恼火,这都没搞清楚后面的寻址模式怎么搞懂?
要搞清楚这些个问题,我们需要了解Intel IA32架构中对内存的组织,看看文档是上怎么来讲解的。
When employing the processor’s memory management facilities, programs do not directly address physical memory. Instead, they access memory using one of three memory models: flat, segmented, or real address mode:
(使用处理器的内存管理设施时,程序不能直接存取物理内存。而是使用以下三种内存模型中的一种存取内存:平坦型,分段型,或者实模式地址模型。下面分别介绍三种内存地址取存模型。
(1) Real-address mode memory model(实地址模式内存存取模式):real mode(实模式)是专为8086和8088处理器而设计的,不是针对其它处理器设计的。是这两款处理器出现时特有的产物,它要解决在16位字长的机器里怎么提供20位地址的问题,而采用的办法是采用存储器地址分段的方法。实模式下允许处理器寻址的最大内存空间为:
220 = 1048576 = 1024 K = 1M
因此实模式操作允许处理器寻址的内存空间仅仅为前面的1MB字节的内存空间,其它型号处理器不管是Pentium4还是Core2处理器,在实模式下也只能访问前面的1MB字节的空间。这1MB字节的内存空间就称为real memory(实内存),conventional memory(常规内存,相对护展内存而言),或者DOS memory system(DOS内存系统)。DOS系统是在实模式下运行的,而Windows系统不使用实模式,64位操作系统不能运行实模式下写的程序,要运行只能采取模拟的办法。
在实模式这里出现了内存的分段的概念(简称为段吧)。按照实模式的机制,需要把内存分成若干段,每个段内的地址空间是线性增长的,每个段的大小可以达64K,这样段内地址正好可以用16位表示:216 = 65535 = 64K。事实上,可以根据需要来确定段的任意大小,只要不超过64K。但是段不能起 始于任意地址,而必须从任意一小段的首地址开始。
下面就以64K为一个段来讲内存的划分。如下图
从图上看出,段的起始地址,即是每个分段在内存中的地址。同时,offset(偏移)和displacement(位移)的概念出现了,这两个概念常常让人混淆不清,究竟啥是偏移,啥是位移?
- (segment)内存段:对内存按固定大小的分成的小段。在汇编译语言中经常提到的段寄存器,存放的就是这个分段在内存中的起始地址。如上图中,某一个段以10000H为段的开始地址,以1F000为段的结束地址,即下一个段的开始地址。这个时候,如果要对这一段内存进行某种操作,首先要把其首地址放入段寄存器。
- Offset(偏移):偏移是相对于段的起始地址的字节数,如上图所示,某一段起始地址为10000H,有一个F000字节的内存段。但是这个F000字节的结束处的地址为:10000H+F000=1F0000H。即段的地址是相对于整个内存而言的,而移偏只是相对于当前段的起始地址而言的从0开始数的字节数,当计算其内存地址时要加上其段的首地址。那什么时候又出现一个displacement(位移)的概念呢?比如在以10000H为起始段的段的某两个位置存放了两个变量var1,var2,假设var2的地址更高,那么var2-var1的字节数就移为displacement(位移量),一点点微小区别。
- 物理地址与逻辑地址:
什么是物理地址,什么又是逻辑地址呢?其实上面讲的都是逻辑地址,并不是实际的物理地址。8086的地址总线是20位的,能够表达的最大地址是220-1=0FFFFFH,所以它的直接寻址能力就是1M。只要给出一个20位的地址,就能从1MB内存中唯 一的读出一个字节来。位是CPU的寄存器和数据通路是设计成16位的,最大的存取单位和运算单位是16位的字,所以只能用字表示内存地址。那要怎样才能达到1MB的物理内存呢,可以有很多选择。一种方法是用“段基址:偏移”表示法。这在前面示例时的内存地址,就是这种表示法。
用两个字节来表示一个内存地址,第一个叫段基础,第二个叫偏移,他们合起来就构成了一个逻辑地址,这是实模式下逻辑地址的定义,即其不是真正的物理地址,是程序中表示的地址。BIU在接到一个逻辑地址的时候,先把这个由两个字节构成的逻辑地址按照某种规则映射成20位的物理址,而后用这个物理地址选取内存中的指定单元。
这个映射规则是这样的:段基址向左移动4位(相当于乘以16),再加上偏移,得到20位的物理地址。
段基址x16+偏移=物理地址
因此,我们平时调试程序时见到的都只是逻辑地址。以上讲的是实模式下逻辑地址换算成物理地址的方法。
在保护模式存储器寻址中,仍然要求程序员在程序中指定逻辑地址。而这个逻辑地址转换成物理地址的转换则相对比较复杂,保护模式下,逻辑地址由段选择子和偏移地址两部分来组成,选择子放在段寄存器中,但它不能直接表示段基地址,而是由操作系统通过一定的方法取得段基地址,再和偏移地址相加,从而求得所选单元的行理地址。这在保护模式时会讲到。
(2) Segmented memory model(段内存模式):看看手册上怎么说?
Memory appears to a program as a group of independent address spaces called segments. Code, data, and stacks are typically contained in separate segments. To address a byte in a segment, a program issues a logical address. This consists of a segment selector and an offset (logical addresses are often referred to as far pointers). The segment selector identifies the segment to be accessed and the offset identifies a byte in the address space of the segment. Programs running on an IA-32 processor can address up to 16,383 segments of different sizes and types, and each segment can be as large as 232 bytes. Internally, all the segments that are defined for a system are mapped into the processor’s linear address space. To access a memory location, the processor thus translates each logical address into a linear address. This translation is transparent to the application program. The primary reason for using segmented memory is to increase the reliability of programs and systems. For example, placing a program’s stack in a separate segment prevents the stack from growing into the code or data space and overwriting instructions or data, respectively.
内存在程序看来,就是一个一个单独的段,代码段,数据段,堆栈段就是典型的独立段。要访问一个段内存字节,程序关注的是逻辑地址,而不是其物理地址。处理器将每个逻辑地址转换成线性地址,这个转换对应用程序是透明的。分段的主要原因是对于编程和系统来说是可靠的,防止程序越界等。因此,这个内存模式工作在保护模式下,不能用于实模式工。
其思想和实内存模式差不多。区别在于,取段的地址时,不是直接取段的首地址,而是引入了segment selector(段选择子)这个概念,段选择子由segment descriptor(段描述符)表达,段描述符记录了这个段的内存位置,长度,以及访问权限,放在一个descriptor table中。在个内存模型中,这个偏移又称为effective address(有效地址)。描述符的base address(基址部分)表示内存段的起始地址。
段描述符又分为global descriptors(system descriptors),local descriptors(application descriptors)。
从出下图中看看其结构
(3) Flat memory model (平坦内存模式)
Memory appears to a program as a single, continuous address space. This space is called a linear address space. Code, data, and stacks are all contained in this address space. Linear address space is byte addressable, with addresses running contiguously from 0 to 232 - 1 (if not in 64-bit mode). An address for any byte in linear address space is called a linear address.
对程序来讲,其所见的地址空间是一个单一连续的线性地址空间,代码段,数据段,堆栈段都包括在这个线性地址空间中。线性地址空间是按字节编址的,其大小从0到232 – 1连续分布。
这三种内存模型的示意图如下
4. 专用寄存器
有些通用寄存器同时又有专用功能,一些专用寄存器就只有专用功能,说明如下:
- EAX — Accumulator for operands and results data(通用寄存器,同时又作为操作的累加器和存放返回值)
- EBX — Pointer to data in the DS segment(通用寄存器,同时又用于存放数据段寄存器指针)
- ECX — Counter for string and loop operations(通用寄存器,同时又作为字符串和循环操作的计数器)
- EDX — I/O pointer (通用寄存器,同时又作为输入/输出接口指针寄存器)
- ESI — Pointer to data in the segment pointed to by the DS register; source pointer for string operations(通用寄存器,同时用于字符串操作数的源变址)
- EDI — Pointer to data (or destination) in the segment pointed to by the ES register; destination pointer for string operations(通用寄存器,同时用于字符串操作数的目的变址)
- ESP — Stack pointer (in the SS segment) 堆栈段指针
- EBP — Pointer to data on the stack (in the SS segment) 数据堆栈指针
5. 寻址模式
5.1 Immediate Addressing(立即数寻址)
因为源操作数就在指令中,被编码成可执行代码跟在操作码后面。这里要注意区分的是,立即数是写在代码中的常数,目的操作数是寄存器。而从寄存器或者内存中传输数据是变量,不是立即数。
MOV EAX,1234H ;这是立即数
MOV EAX,[1234H] ;这是引用内存地址,不是立即数
var DWORD 1234H
MOV EAX,var ;这是引用内存,不是立即数。
5.2 Direct Data Addressing(直接数据寻址)
直接数据寻址有两种形式,但两者共同点都是在段的逻辑地址(以后说的内存地址都是指逻辑地址,相对于物理地址而言)后面加一段位移(这里以段地址为起点,到某一个内存位置的距离,在这里也可以说是偏移)来形成地址:
(1) direct addressing(直接地址寻址)
也就是说直接用这个内存地址去取得这个数据。比如这样
var1 DWORD 1234H
MOV EAX,var1
也可以写成
MOV EAX,[var1]
如果var1这个变量标号的地址是00404024H
那我们也可以直接写这个地址
MOV EAX, [00404024H]
这样表示从理论上讲是可以的,但是实际上一般汇编器不支持这样表示。
为了和立即数区分,这里要加[]号,表示这是地址,汇编器会解析这个符号得到内存值。var1实际上是一个内存地址的符号表示,高级语言中定义的变量是同样的道理。
(2) displacement addressing(位移地址寻址)
段基址加一段位移形成地址,和直接地直寻基本没有什么区别。形如
MOV EAX,DS:[1234H]
唯一区别是,在生成机机器码的时候,direct addressing占用4个字节,而displacement addressing占用3个字节。
理论上来说,这样也是可以的
MOV [00404024H],EAX
但是一般汇编器不允许,如masm会报下面错误:
immediate operand not allowed
但加上段前缀就可以,这样
MOV DS:[00404024H],EAX
5.3 Register Addressing(寄存器寻址)
从源寄存器拷贝数据到目的寄存器。
MOV EAX,ECX
MOV EAX,1234H;立即寻址方式
MOV ECX,EAX;寄存器寻址
5.4 Register Indirect Addressing(寄存器间接寻址)
寄存器间接寻址可以访问可访问的任何内存地址的数据,其地址由存放在EBP,EBX,EDI和ESI四个寄存器中的偏移构成,其组合有多种形式。
当使用寄存器间接寻址,或者其它任何使用到寄存器EBX,EDI,或者ESI寄存器的寻址模式时,默认使用的是data segment(数据段)。
当使用EBP寄存器寻址内存的时候,默认使用的是stack segment(堆栈段)。特别注意,寄存器必须放在方括号里,两个寄存器可以放在一个方括号里,也可以分开放在两个方括号里,以表示形成址,偏移则可以放在方括号的前中后。
组合示例如下:
一个寄存器构成间接寻址:
MOV EAX,[EBX]
MOV EAX,[EBP]
MOV EAX,[ESI]
MOV EAX,[EDI]
使用一个寄存器和一个偏移间接寻址:
MOV EAX,[EBX+20];或者MOV EAX,20[EBX]
param DWORD 4
MOV EAX,[EBP+param]
MOV EAX,20[SI];或者MOV EAX,[ESI+20]
array DB 1,2,3,4,5
MOV EAX,array[EDI]
使用两个寄存器:
MOV EAX,[EBX+SI]; MOV EAX,[EBX][ESI];
MOV EAX,[EBP+ESI]; MOV EAX,[EBP][ESI]
MOV EAX,[EBX][EDI];或者MOV EAX,[EBX+EDI]
MOV EAX,[EBP][EDI];或者MOV EAX,[EBP+EDI]
使用两个寄存器加一个偏移
MOV EAX,[EBX+ESI+20]
MOV EAX,[EBP+ESI]param
MOV EAX,array[EBP][EDI]4
MOV EAX,20[EBX][EDI]
5.5 Base-Plus-Index Addressing(基址加变址寻址)
Index是可变的,所以称为变址寻址。为什么使用变址呢,因为前面的寻址方式,都是只是通过一个地址,一次访问到一个固定的内存单元,但是对数组这种情况,要访问数组的元索,这个内存地址取一个元素,地址就要变化一次,因此这种情况是变址。
基址加变址寻址和寄存器间接访问内存有点相似,对于访问数组来讲,基址寄存器通常存放的是这个数组的起始地址,变址寄存器则存放的数组元素的相对位置。
其实这里没有看到本质区别,单独把它列出来,只是为了分得更细一点,从名字称乎上可以与寻址模式对应起来,可以看到那个是基址,那个是变址,那个是偏移,那个又是相对位移。从名字上来说,这里只用到2个寄存器构成地址,即一个基址寄存器,一个变址寄存器。
其实指的是寄存器间接寻址中的使用两个寄存器构成偏移的情况,比如
MOV ECX,[EAX+EBX]
在这里,我们称EAX为(Base)基址,而EBX为Index(变址),如果这里用到了EBP寄存器,数据位于堆栈段,而不是数据段里。
对于一维数组的访问,可以使用这种方式。
5.6 Register Relative Addressing(寄存器相对寻址)
可以归结为寄存器间接寻址的一种情况。和基址变址寻址相似,只是基址寻址的两个分量都是寄存器,即一个基址加一个变化地址的寄存器。而这里是在1个寄存器的基址上加了个偏移(或者称为位移也好),而不是变址寄存器,这里说的相对就是指这个相对于内存上某一个内存位置的偏移。
例如
MOV EAX,[EBX+20];这里20是相对偏移
即1个地寄存,再加1个偏移,构成基址+相对偏移的Register Relative Addressing(寄存器相对寻址)。
5.7 Base Relative-Plus-Index Addressing(基址相对寻址加变址寻址)
可以归结为寄存器间接寻址的一种情况。也就是将基址变址寻址与基址相对寻址结合,这样就用到2个寄存器,再加一个或多个偏移构成地址。
例如
MOV EAX,array[EBP][EDI]4;1个基址寄存器,1个变址寄存器,1个相对偏移array(相对于基段的基址),1个相对位移4(相对于数组的起始地址)。
5.8 Scaled-Index Addressing(按比例变址寻址)
这种寻址模式是80386到Core2的系列处理器专有的寻址模式。实现方式是一个寄存器作基地址,另一个寄存器乘以一个scaling factor(比例因子),两者构成操作数地址。这个比例因子的取值分别为:1,2,4,8。1的情况可以省略。
例如
MOV EAX,[EBX+2*ECX]
详细组合见图:
5.9 RIP Relative Addressing(RIP指针相对寻址)
这种寻址模式只用在64位环境下,内存模式为flat模型的环境下。
6. 段的选择
段的选择可以是显式的或者隐式的,处理器按照一套规则自动选择段是一个隐式的过程。通过指定segment selector(段选择子)来显示的指定那个段,例如:
MOV ES:[EBX], EAX
下表是Intel手册上列出的默认加载段的规则。
7. (POINTER DATA TYPES)指针数据类型
在非64位的模式下,架构定义了两种类型的指针,near pointer(近指针) 和 far pointer(远指针).近指针是一个段内的32位的偏移地址(也称为effective address(有效地址)),近指针在flat模式内存结构中可以访问所有可访问的内存,或者隐式的访问segment模式的内存。远指针是一个(logical address)逻辑地址,由一个16位的段选择子和1个32位(或16位)的偏移组成,远指针在段式模型的内存结构中必须显示的指定,否则不能访问其内存。其结构如下图: