1.实模式简介
实模式是指x86架构CPU的一种工作模式,起源于8086处理器。
在8086处理器中一共有20根地址线,也就是说可以寻址1M左右的内存空间,但是前边我们提到过8086中的最大的寄存器长度才16位,小于20位。所以8086采用两个寄存器来进行内存寻址的操作,所以产生了程序分段的方法,8086正是工作在这样的模式下的,这种工作模式称之为实模式(但是实模式只是后来才有的概念,从保护模式出现后为了区分两种模式菜引入了这个称呼)。
2.8086存储器寻址
8086使用段寄存器(CS DS ES SS)来存放16位的段地址,在要寻址时只需要提供16位的偏移地址即可。段地址*16+偏移地址=实际的物理地址。这个运算过程时CPU自动在MMU(Memory Management Unit 内存管理单元)中完成的,我们只需要向CPU填充段地址再提供偏移地址就即可。虽然MMU在实模式下只会做这些简单的寻址运算,但是在保护模式中还充当了分页管理,这一点将在后边文章中说到。
为了方便表示 我们使用 16位段地址:16位偏移地址来表示
Example:
对地址 1234H:000FH
12340 (乘以16相当于16进制数最后添加一个0)
000F
————
1234F
即可得到物理地址:1234FH
2.内存寻址方式
首先,看到这个标题的第一个问题应该是:什么叫寻址方式?
寻址方式就是指寻找数据的方式 数据可以存放在内存中 也可以存放在寄存器中,当然也可以直接在程序中指定数值(称为立即数)
立即数一般使用16进制(H结尾),如89A0H,但是必须注意的是以字母开头的立即数必须前边添加一个零,如0FFH,这是由于以字母开头的立即数会被汇编器认为是符号或者变量,但是也可以使用0xFF来表示。
立即数也可以使用二进制表示:1010111B(注意要使用B结尾)
我们已经知道了内存寻址需要提供段寄存器和偏移地址即可,但是对于偏移地址的提供方式很多(可以在内存单元中查找 可以是立即数 也可以通过一个寄存器来提供 甚至可以通过多个寄存器来提供),所以内存的寻址方式也比较多,接下来具体介绍一下常用的几种寻址方式。
(1)变量寻址
在程序中使用变量名+内存分配伪指令+数据的方式可以生成一个变量,而变量名就是用符号(注意不可以使用nasm编译器保留的关键字 并且在nasm中变量名区分大小写 顺便一提 在masm中符号是不区分大小写的)
value1 DB 'A'
value2 DW 12,44
value3 DD 0xFFFF
DB为分配字节单元,DW为分配字单元(两个字节),DD为分配双字单元。
上边的代码中value1为一个值为‘A’的变量,value2为一个值为12,44的由两个字单元构成的变量,每个值占据一个字,并且先定义的在低内存中,后定义的在高内存中,这样的连续定义的数据会连续存放
在对符号变量寻址时,由于变量是存放在内存中的,要使用 [段寄存器:变量名]的方式来寻址。对于使用[DS:变量名]可以直接写为[变量名]。所以变量名相当于是定义的变量的首地址,但是这个地址只是一个16位的偏移地址(大家可以把变量想象为c语言中的数组)
mov AX,[DS:value1]
mov AX,[value1]
以上两种写法都是把把[DS:value1]的内存中的数传送到AX寄存器中
(补充一下,在masm中变量名的“[]”是可有可无的,但是在nasm中是必须添加的,并且在masm中是在[]外提供段寄存器,而在nasm中是在[]内提供段寄存器的,学过masm的朋友要注意哦!)
(2)寄存器间接寻址
如果说我们知道一个变量的内存中的地址是 1000H:FFFFH(即物理地址1FFFFH),但是我们把1000H放在DS中,把FFFFH放在寄存器SI中,我们应该怎么来寻址并且把这个数据放在BX中呢。
mov BX,[DS:SI]
mov BX,[SI]
使用以上的两种方法都可以寻址,我们称之为 寄存器间接寻址,并且前边提到过,使用DS寄存器作为段地址可以省略不写 并且一定要注意只有BX,BP,SI,DI可以使用这种寻址方式,BX SI DI省略段寄存器时默认使用DS作为段寄存器,而BP默认为SS。
(3)寄存器相对寻址
格式为 [段寄存器名:寄存器名+立即数]
首先进行立即数与寄存器中的数据相加作为偏移地址,使用段寄存器作为段地址来获得20位物理地址(同样使用DS为段寄存器可以省略DS)
mov BX,[ES:AX+0x1000]
(4)基址变址寻址
当我们的偏移地址存放在两个寄存器中,且是两个寄存器之和时,使用这种寻址方式。
其中一个寄存器为基址寄存器,另一个为变址寄存器,但是基址寄存器只能使用BX或者BP
mov [ES:BX+SI] ;基址寄存器为BX,变址为SI
关于基址+变址寻址方式操作数的段基址当使用基址寄存器BX时,默认的段是数据段DS;当使用基址寄存器BP时,默认的数据段是堆栈段SS。
(5)更多的寻址方式
当然nasm可使用的寻址方式(其实是8086提供的)不止这么多,但是为了防止读者难以消化,我们先介绍这几个比较简单的,后边遇到一些其他的寻址方式,我再单独讲解。
3.大小端与内存数据存放方式
我们前边说到了在内存中定义变量时使用的DB、DW或者DD来定义一个数据单元或者连续定义多个数据单元。如果我们使用以下的代码来定义:
value1 DB FFH
value2 DW FFH
这两个变量都只有一个单元,但是单元大小不同,每个单元都存放了同样的数,这两个变量有什么区别呢。这个问题就要牵扯到计算机大小端了。
大小端是指计算机多字节数据存放格式。计算机内存读取是用字节为基本单位,但是也可以定义多字节的数据类型,比如双字节的字(16位 从8086的字长延续下来的),对于一个字数据00FFH就要分为两个字节来存放(00H与FFH)。我们把00称为高字节,把FF称为低字节,即多字节数据从做往右为高到低字节。在内存中,物理地址大的称为高地址,反之称为低地址。
我们把多字节数据类型中的高字节存放在高地址,低字节存放在低地址的方式称为小端,反之称为大端。
8086以及后来的所有x86都采用小端(即现在我们使用的机器大多数都是小端机器)
在value2中定义了一个字单元,所以00存放在高地址,FF存放在低地址,假如[DS:value]的物理地址为1000AH(16位,即这个字单元的首地址),value2的存放方式如下:
同样的对于双字或者四字的数据类型也是使用这种存放方式(即反向的存放)
4.伪指令
前边的变量定义时我们使用了DB DD DW,这些都是nasm汇编的伪指令。
伪指令是指汇编器提供的功能,不属于汇编语言本身的功能。汇编器在编译ASM文件(汇编程序文件 在windows下以.ASM后缀的文件),在汇编器遇到伪指令时会把伪指令转化为相关的指令或者汇编器会根据伪指令做相关的操作。比如说汇编器在遇到DB指令时会将此处的代码变化为相应的数据。
另一个例子是equ伪指令,在编写汇编时可以使用如下方法定义一种伪指令层级的变量:
value1 equ 1001H
mov AX,value1
这两个指令的意思是 定义一个伪指令变量 value1,将value1的值移动到AX寄存器中。但是value1不同于之前的DB等指令定义的变量,这个变量在使用的时候没有加上“[]”。实际上,equ定义的变量类似于c语言中的宏定义,并且运行时不占用内存空间。nasm编译器在遇到value1时会自动将其替换为1001H。
nasm伪指令很多,后边遇到的时候再做详细讲解。
5.数据长度伪指令
有这样一段代码
value1 DB 'a','c'
mov AX,[value1]
mov [value1],1FH
我们在第一行定义了一个变量value1 总共占用两个字节 在第二行,我们将第一个字节(应为符号变量就代表了数据中的第一个字节单元地址)移动到AX寄存器中。这个时候就产生疑问了,我们是怎么知道我移动的是一个字节还是一整个字(两个字节)到AX寄存器中的呢?在ATT汇编格式中我们可以使用movb或者movw,但是在inter汇编中是没有这种写法的。但是我们注意到AX寄存器是16位的,所以实际上是移动了一整个字到AX中,如果我们想只移动一个字节,我们只需要用一个8位寄存器就好了,比如说AL。
所以说,这种数据移动长度的问题就这样被我们解决了!
但真的是这样吗?我们看看第三行
在第三行中我们把一个立即数放到value1中,但是这个立即数是多少呢?1FH也可以是001FH(因为立即数前边的0是可以省略的),也可以是0000001FH(占一个双字)。所以我们并不知道我们要向value1中存放多大的立即数
这个时候就可以使用我们的数据长度伪指令了
byte:字节
word:字
dword:双字
qword:四字
我们只需要把第三条指令改为如下形式:
mov byte [value1],1FH
这个时候伪指令指示了要把1FH存放在以value1为首偏移地址的位置,并且这个位置长度为一个字节。
数据长度相关的伪指令解决了一些数据传送过程的歧义性,一定要合理运用。
值得一提的是,这一列伪指令也可以作为规定符号地址长度的指令。符号在汇编中代表的是一个偏移地址,在实模式下,这个地址是16位的,如果我们想把这个地址扩展为32位的来取出,可以使用如下方法
mov EAX,dword value1
我们使用32位寄存器来存放这个扩展后的地址。
下期预告:
讲解实模式的重点内容 分段 以及 常用指令。