X86汇编语言学习教程之2 ———— 基础知识
1 数制
1.1 计算机中的数制与编码
一方面我们赞叹计算机的运算能力是多么强大,能处理多么复杂的数学计算;而另一方面我们又说计算机的智商是多么的低下,因为它只认识2个数:0和1。这样初学者往往会产生疑惑:用0和1怎样表示复杂的数字及怎样进行运算?
1.1.1 数制
十进制我们在各种数学计算中所用的传统的数制,它用十个阿拉伯符号“0~9”来表示数字“零~九”,大于“九”的数就不能用一位来表示了,必须进位,如它用“10”来表示“十”,用“100”表示“一百”,因此它的进位特点是:逢十进一。
二进制是计算机中所用的数制,它用两个阿拉伯符号“0和1”来表示数字“零和一”,大于“一”的数就不能用一位来表示了,必须进位,则它用“10”来表示“二”,用“100”表示“四”,因此它的进位特点是:逢二进一。
十六进制用十个阿拉伯符号和六个英文字母“0~9,A~F”来表示数字“零~十五”,大于“十五”的数据必须进位,则用“10”表示“十六”,用“100”表示“二百五十六”,因此它的进位特点是:逢十六进一。
任意一个数都可以用以上三种数制中的任一种来表示,一般地说,无论采用任种数制,任何一个数都可表示为:
在不同的数制中,式中基数“10”所表示的数值是不同的,在二进制中表示“二”;在十进制中表示“十”;在十六进制中表示“十六”。
式中Ki为基数“10”的第i次幂的系数,在二进制中可以是“0或1”;在十进制中可以是“0~9”;在十六进制中可以是“0~9,A~F”。
1.1.2 数制之间的转换
我们来规定一下以后的论述中数制的表示方法,用括号后的下标D、B和H分别表示十、二和十六进制数。例如(268)D表示十进制数;(1011001)B表示二进制数;(A8C7)H表示十六进制数。
1.二进制转换为十进制
可以先将二进制数按式2.1展开,然后将基数“10”换成十进制数“2”,再按十进制计算得到十进制数。
2.十进制转换为二进制
可以将十进制数分为整数和小数两个部分。将整数部分不断地除以2,把所得余数(只有0和1)按顺序从低(位0开始)到高位进行排列,直至商为0,所排列的余数即为所求二进制整数。例如(39)D转换为
则(39)D=(100111)B。
将十进制数的小数部分不断地乘以2,把所得乘积的整数部分(只有0和1)取出按顺序从高(位-1开始)到低位进行排列,直至误差满足要求。
3.二进制和十六进制之间的转换
由于16是2的4次幂,每4位二进制数对应1位十六进制数,因此它们之间的转换十分简单。将二进制整数部分从最低位开始每4位一组,不足4位的高位补0,然后将每组直接转换为十六进制即可。
例如 (11011011011010)B = (0011 0110 1101 1010)B = (36DA)H。
十六进制数向二进制数的转换也十分简单,只要将十六进制数的每一位直接转换为二进制数即可。
例如 (A 6F3)H = (1010 0110 1111 0011)B,因此可以说十六进制是二进制的缩写形式。
十六进制与十进制之间的转换可以参考二进制与十进制的转换。如十六进制转换为十进制,只要将“2”换成“16”,且将系数转换为十进制,再按十进制计算即可。
1.1.3 计算机的数据单位
在计算机中,常用的数据单位有位、字节、半字和字,微处理器根据位数的不同支持8位字节、16位半字或32位字的数据类型。
(1)位(bit):它是一个二进制数的位,位是是计算机数据的最小单位,一个位只有0和1 两种状态(21)。为了表示更多的信息,就必须将更多位组合起来使用,比如两位就有00、01、10、11四种状态(22),以此类推。
(2)字节(Byte): 通常将8位二进制作为一个字节,即1B=8bit,那么一个字节就可以表示0-255种状态或一个字节或十六进制数的0-FF之间的数,8位微处理器的数据是以字节方式存储的。
(3)半字:从偶数地址开始连续的2个字节构成一个半字,半字的数据类型为2个连续的字节,有些32位微处理器的数据是以半字方式存储的,比如32位ARM微处理器支持的Thumb指令的长度就刚好是一个半字。
(4)字:以能被4整除的地址开始的连续的4个字节构成1个字,字的数据类型为4个连续的字节,32位微处理器的数据全部支持以字方式存储的格式,比如32位ARM微处理器支持的ARM指令的长度就刚好是一个字。
1.1.4 二进制的算术运算
1. 加法运算
计算机中有加法器,两个二进制数可以直接相加,加法规则是:
0+0=0,0+1=1,1+1=10
例如两个8位二进制数相加10010011 + 10101001=10111100,向高位的进位为1。
2. 减法运算
计算机中无减法,减法也是通过加法器完成的,这里引入补码的概念,可以举一个例子说明一下,指针式钟表,假设要将时钟从5点拨到2点,有2种拨发,一种是逆时钟拨3个时格,相当于5减3等于2;另一个拨发是顺时针拨9个时格,相当于5加9等于2,这样以来可以说对时钟这种模式为12计数制来说,9和3互补,9是3的补码,反之依然。对于刚才时钟拔法我们可以写出如下算式:
5-3 = 5-(12-9) = 5+9-12 = 2
一个n位二进制数原码N,它的补码可定义为 (N)补=2n-N。
补码的概念是为了方便计算机做减法运算方便而引入的,因此二进制正数不用关心它的补码;而二进制负数的补码,为它的原码按位取反加1。
例如,8位二进制数 (-1)补= (11111110 + 1)B = (11111111)B。
在计算机中负数是用它的补码来表示的。用补码做减法运算很方便,我们说数A减去数B等于数A加上数B的补码,且要舍去进位。
例如,计算8位二进制数减法
(58-39)D=(00111010-00100111)B=(00111010+11011001)B=(00010011)B=(19)D。
2 X86-16
8086结构与时序是理解和掌握 8086 微型计算机硬件结构的基础,存储器组织的内容对理解汇编语言程序设计是非常重要的。
8086内部的结构特点,主要是要掌握其内部寄存器组的结构,包括有哪些寄存器、寄存器组的一般用途,包括 8086 状态寄存器各位的定义。因为作为微处理器的应用者,主要关心的是和应用有关系的内容。
8086 寄存器结构:
通用寄存器组:
数据寄存器( AX 、 BX 、 CX 、 DX )
指针寄存器
堆栈指针寄存器 SP
基址指针寄存器 BP
变址寄存器
源变址寄存器 SI
目的变址寄存器 DI
段寄存器:
CS :代码段寄存器
DS :数据段寄存器
SS :堆栈段寄存器
ES :附加段寄存器
状态标志寄存器 F
通用寄存器组中的寄存器 AX , BX , CX , DX 通常用作存放一般性的中间结果,这些寄存器都是 16 位的,但也能拆成两个 8 位寄存器使用,例如 AX 寄存器中存放 1234H ,则等价于 AH 中存放 12H , AL 中存放 34H 。
标志寄存器存放程序运行过程中的一些特殊的状态标志,如进位 / 借位、零标志等。对标志寄存器各标志位的含义应该能正确的理解。如进位 / 借位标志指无符号数加减运算时,最高位向前面的进位或借位。零标志置位一般表示运算结果是零,当然哪些 ” 运算 ” 指令对零标志有影响,在指令系统中有详细的介绍。
8086外部特性,包括 8086 引脚功能也是大家要掌握的重要内容,因为它们和应用密切相关。 8086 引脚中重点要掌握最小方式下信号线的功能。
关于8086 引脚特性时,我们应特别注意以下一些问题:
• 信号的功能
• 信号分类的观点
• 信号的流动方向
• 信号的有效形式
• 信号多功能、分时使用的情况
所谓 信号分类 的观点 ,指的是按照 3 总线的分类的观点将各引脚信号分为数据总线
DB 、地址总线 AB 、控制总线 CB ,这样有利于对信号功能的理解。
信号的流动方向 , 指的是引脚信号相对于 CPU 流动的方向。可以有输入 (IN) 、输出 (OUT) 、双向 (IN/OUT) 三种情况。在学习各引脚信号的功能时,我们首先要注意此信号的方向。它是输入的、输出的、还是双向的。
信号的有效形式 , 指的是引脚信号是什么电平有效。可能是高电平有效,或低电平有效,还可能是上升沿有效,或下降沿有效。
有一些信号,标注为“三态”,这表示该信号除了可以输出高电平及低电平以外,还具有 高阻态输出。 此类信号具有与外界隔离的功能。
信号多功能、分时使用的情况 , 指的是某些引脚具有两种功能定义,随着芯片工作在不同模式(“最大方式”或“最小方式”)下,这些引脚信号取不同的定义,例如 /WR ( /LOCK ),即引脚 33 。另外,还有些引脚具有分时输出不同信号的能力,这种情况我们称为 分时复用, 例如 AD15—AD0 , 分时作为地址及数据总线使用。
例: ALE 信号,
其功能是:当 CPU 的地址 / 数据复用总线上输出地址信号时,此信号为高电平,此信号的下降沿将使 CPU 外的地址锁存器锁存住地址 / 数据复用总线上的地址信号,即使复用总线上的地址信号消失也不影响对片外的存储器的寻址。
其信号流动方向:输出。
信号有效形式:高电平表示地址 / 数据复用总线上输出的地址信号,下降沿可用于锁存地址信号。
多功能:最大方式下 CPU 不输出 ALE 信号,该引脚作为表示指令队列状态的 QS1 信号输出。
8086 引脚时序是应用 8086 的重要基础。学习时要搞清楚:什么是时序(引脚信号在时钟的同步下变化的时间长短与次序的关系)、 8086 总线周期的含义,要能看懂基本的总线周期的时序图(例如最小方式下总线读写时序图)。
搞清以下的时间关系:
时钟周期:由片外输入 CLK 引脚的时钟脉冲信号,是系统定时的基本脉冲,其频率称为主频,其周期称为时钟周期。也称 T 状态。时钟周期是 CPU 执行各种操作的最小时钟单位。
总线周期,指的是 8086CPU 通过总线,执行一次访问存储器或访问 I / O 端口的操作或操作的时间。
指令周期: CPU 执行某条指令所需要的时间。
显然,从时间长短关系看:时钟周期 < 总线周期 < 指令周期
此外,等待周期 Tw 的意义也很重要,它是 CPU 在总线读写周期中插入的额外的时钟周期,正常的总线读写周期需要 4 个 T 状态。它是为了使较慢的存储器或端口能可靠完成读写操作而插入的。 CPU 可以在总线周期中插入 1 个或多个 Tw 周期。
对于存储器组织,要掌握8086 系统分段存储的特点:
• 为什么要分段?
(因为 16 位寄存器难以表示 20 位地址)
• 怎样分段?
( 64K 为一段,从最低地址 00000H 开始,每隔 16 个单元就是一个段的起始边界,例如 00000H 开始的 64K 为一个段, 00010H 开始的 64K 又为一个段,如此等等)
• 段地址、偏移地址、物理地址的含义和关系是怎样的?
物理地址(实际地址) = 段地址 *16+ 偏移地址(有效地址)
堆栈的含义和作用如何?
堆栈是内存中一个按先进后出方式存取的一段内存区域。其作用主要是用于主程序调用子程序是保存返回地址。
一些基本问题,如规则存放与非规则存放的含义、初始启动地址( 0ffff0H )的意义等。
本节必须理解8086内部寄存器结构、 8086时序、 8086存储体系结构(存储器组织)。
3 X86-32
80386处理器是Intel公司80x86发展史上的里程碑,它不但兼容先前的8086/8088,80186,80286处理器,而且也为后来的486,Pentium(586),Pentium Pro(686)的发展打下了坚实的基础,对于我们程序员来讲更重要的是:我们关心80386在指令上到底有哪些扩展呢?80386有哪些寻址方式呢?毫无疑问,它不但兼容了8086的所有指令,而且还对它们进行增强.
呵呵,我知道有很多人问我CPU已经发展到PentiumIIII,没有必要学习80386的汇编。其实不然,80386处理器中的保护模式,虚拟8086模式以及地址的段页管理机制,虚拟内存这些都是以后处理器的核心。所以说80386是后续发展处理器的基础,比如说80486实质上80386+80387协处理,这块协处理器主要用于处理浮点运算,Pentium处理器在80386指令的基础上增加了57条指令,8个数据类型,8个64位的寄存器来处理多媒体。从这一点来看,完全有必要了解80386ASM,这就好像学习80386,必须先要熟练掌握8086。
1.80386的的寄存器:
80386的寄存器可以分为8组:通用寄存器,段寄存器,指令指针寄存器,标志寄存器,系统地址寄存器,控制寄存器,调试寄存器,测试寄存器,它们的宽度都是32位的。本篇主要介绍80386的寄存器。
A1.General Register(通用寄存器)
EAX,EBX,ECX,EDX,ESI,EDI,ESP,EBP,它们的低16位就是8086的AX,BX,CX,DX,SI,DI,SP,BP,它们的含义如下:
EAX:累加器
EBX:基址寄存器
ECX:计数器
EDX:数据寄存器
ESI:源地址指针寄存器
EDI:目的地址指针寄存器
EBP:基址指针寄存器
ESP:堆栈指针寄存器
这些寄存器可以将低16位单独存取,也就是8086的AX,BX,CX,DX,SI,DI,SP,BP,在存取这些寄存器的低16位(AX,BX,CX,DX,SI,DI,SP,BP),它
们的高16位不受影响,同时和8086一样对于AX,BX,CX,DX这四个寄存器来讲,可以单独存取它们的高8位和低8位(AH,AL,BH,BL,CH,CL,DH,DL)
A2:Segment Register(段寄存器)
除了8086的4个段外(CS,DS,ES,SS),80386还增加了两个段FS,GS,这些段寄存器都是16位的,它们的含义如下:
CS:代码段(Code Segment)
DS:数据段(Data Segment)
ES:附加数据段(Extra Segment)
SS:堆栈段(Stack Segment)
FS:附加段
GS 附加段
A3:Instruction Pointer(指令指针寄存器)
EIP,它的低16位就是8086的IP,它存储的是下一条要执行指令的地址。
A4:Flag Register(标志寄存器)
EFLAGS,和8086的16位标志寄存器相比,增加了4个控制位,不过这4个控制位它们在实模下不起作,这四个控制位分别是:
a.IOPL(I/O Privilege Level),I/O特权级字段,它的宽度为2bit,它指定了I/O指令的特权级。如果当前的特权级别在数值上小于或等于IOPL,那么I/O指令可执行。否则,将发生一个保护性异常。
b.NT(Nested Task):控制中断返回指令IRET,它宽度为1位。NT=0,用堆栈中保存的值恢复EFLAGS,CS和EIP从而实现中断返回;NT=1,则通过任务切换实现中断返回。
c.RF(Restart Flag):重启标志,它的宽度是1位。它主要控制是否接受调试故障。RF=0接受,RF=1忽略。如果你的程序每一条指令都被成功执行,那么RF会被清0。而当接受到一个非调试故障时,处理器置RF=1。
d.VM(Virtual Machine):虚拟8086模式(用软件来模拟8086的模式,所以也称虚拟机)。VM=0,处理器工作在一般的保护模式下;VM=1,工作在V8086模式下。
其它16个标志位的含义和8086一样,在这里也重温一遍:
e.CF(Carry Flag):进位标志位,由CLC,STC两标志位来控制
f.PF(Parity Flag):奇偶标志位
g.AF(Assistant Flag):辅助进位标志位
h.ZF(Zero Flag):零标志位
i.SF(Singal Flag):符号标志位
j.IF(Interrupt Flag):中断允许标志位,由CLI,STI两条指令来控制
k.DF(Direction Flag):向量标志位,由CLD,STD两条指令来控制
l.OF(Overflow Flag):溢出标志位。
80386处理器的寻址方式
在实式模式下,80386处理器的最大寻址空间仍然为1M,和8086/8088相似。即段地址*10H+段内偏移地址,从而形成20位地址。此种模式下,段基址是16的倍数,长度最大不超过64K。
在保护模式下,80386处理器可以使用所有的物理内存。段基址可以是32位,也可以不是16的倍数,同时它的最大长度为4G,这与8086完全不同,在形成逻辑地址时用段基址直接加上段内偏移地址,而并不将段基址左移4位(乘以16)。通常情况下,除了访问堆栈外,默认的段都为DS,有跨段前缀就另当别论了。在以BP,EBP,ESP作为基址寄存器时,这时默认的段寄存器应该是SS,举几个简单的例子:
MOV EAX,[SI];这里的段寄存器是DS
MOV EAX,FS:[ESI];这里的段寄存器是FS,因为指令中使用跨段前缀显示指定了
MOV EAX,[BP];这里的段寄存器是SS,因为指令中使用了BP作为基址寄存器
MOV EAX,GS:[BP];这里段寄存器是GS,因为指令中使用跨段前缀显示指定了
80386中32位数的操作的顺序是“高高低低”,即是说高16-》高16,高8-》高8,低16-》低16,低8-》低8,这和8086相似。同时80386微处理器兼容所有8086的寻址方式,而且对8086的寻址方式有很大的改进和扩展。在8086下,只允许BP,BX,SI,DI作为寻址寄存器,但在80386下,8个通用寄存器都可以作为寻址寄存器。不过有一点要注意的是在基址变址寄存器寻址方式或相对基址变址寻址方式中,段寄存器由基址寄存器来确定,而不是由变址寄存器来确定,同时除ESP外其它的7个通用寄存器都可以作为变址寄存器,用代码来表示就是:
MOV EAX,[EBP+ESP+2];这条指令是错误的,因为不可以用ESP作为变址寄存器
MOV EAX,[EBP+ESI+10H];这里的段寄存器应该有基址寄存器来决定。基址寄存器是BP,那么这里的段寄存就是SS
MOV EAX,GS:[EBP+EDI+100H];不用看了,这里的段寄存器应该是GS,因为指令通过跨段前缀显示指定了
80386支持的基地址+变址+位移量寻址进一步满足了高级语言支持的数据类型。对于C语言来讲,普通变量,数组,结构体,结构体的数组,数组的构体我们既可存放在栈中(静态定义-static definition),也可以存放在堆中(动态定义-dynamic definition),用ASM也一样可以实现。基址变址寄存器提供了两个可以改变的部分,而位移量则是静态的。看下面的例子:
//Variables in C Programming-Language,the corresponding ASM will list below
void main()
{
int a;//普通的变量,用ASM寻址时直接用DS:[一位移量],如DS:[2000],属于直接寻址方式
int array[24];//数组,用ASM寻址时用DS:[BX+SI*4],4表示整型的长度,属于基址变址寻址方式
struct abc
{
int a,b,c;
float d;
};
struct abc aa;//结构体,用ASM寻址时DS:[BX+Shift],Shift代表位移量,属于寄存器相对寻址方式
struct abc aa[100];//结构体数组,用ASM寻址时用DS:[BX+SI*sizeof(abc)+Shift],属于相对基址变址寻址方式
struct cde
{
int array[100];
float e,f,g;
};
struct cde ccc;//数组结构体,用ASM寻址时用DS:[BX+SI*4+Shift],属于相对基址变址寻址方式
}
80386与8086的寻址方式差不多完全一样,只不过80386的寻址方式更灵活,它的操作数有32位,16位,8位。
让我们再重温一下8086的寻址方式:
a.立即寻址,所谓立即寻址就是操作数就在指令中,比如说:MOV AX,5678H
b.直接寻址,即直接包含操作数的有效地址EA,比如说MOV AX,[1234]
c.寄存器间址寻址,用寄存器的内容来作为操作数的有效地址,比如说SI=1234,MOV AX,[SI],8086下可用的寄存器只有4个:BX,BP,SI,DI,80386下8个通用的寄存器都可以使用。
d.寄存器相对寻址,即在寄存器间址寻址方式的基础上再加一个位移量,位移量可以是8位也可以是16位,比如说MOV AX,[BX+90H]。
e.基址变址寻址,即操作数的有效地址由一基址寄存器和一变址寄存器产生,如MOV AX,[BX+SI]。那么在8086下,只有SI,DI可以作为变址寄存器,在80386下除ESP外的其它7个通用寄存器都可以作为变址寄存器,比如说MOV AX,[BX+SI]。
f.相对基址变址寻址,在e寻址方式的基础上加上一位移量,比如说MOV AX,[BX+SI+100H]。
在8086下,我们如进行字节或字操作,往往要加上伪指令WORD PTR或BYTE PTR。在80386下不用显示指定,处理器会自动处理,当发现目的操作为8位时,处理器就会进行8位操作,同理当发现目的操作为16位,处理器就会进行16位操作,80386下以目的操作数的长度为准,以下几条简单的传送指令:
MOV AL,CS:[EAX];8位操作,段寄存器是CS,寻址方式是寄存器间址寻址
MOV AL,ES:[BX];8位操作,段寄存器是ES,寻址方式是寄存器间址寻址
MOV EDX,[EDX+EBX+1234H];32位操作,段寄存器是DS,寻址方式是相对基址变址寻址
MOV AX,[EBX+ESI*4];16位操作,段寄存器是DS,寻址方式是基址变址寻址
MOV BH,ES:[EBX+EDI+900H];8位操作,段寄存器是ES,寻址方式是相对基址变址寻址
MOV DL,[EBP+ESI+1900H];8位操作,段寄存是SS,因为用了EBP作为基址寄存器。寻址方式是相对基址变址寻址
4 X86-64和Intel64