这里是8086汇编语言的总结,使用的汇编器是nasm。
汇编程序的基本构造
首先我们先来看一段C++代码:
|
|
这一部分代码是将二进制文件中的数据读出来并且存储到data结构体里面。
我们就通过这个程序来分析一下一个程序最基本需要什么。
首先他有代码。在汇编语言中,代码被放在代码段中,代码段中的代码就是程序要执行的代码。
其次我们看到有d啊m啊这些变量,也就是说有数据。在汇编中,数据被放在数据段中。
最后还有一个我们并不能从表面看出来的组成,被称为栈段,也就是作为栈使用的段。
所谓段,其实就是对内存进行划分,将内存划分为一块一块的,这一块专门存储数据,我们就叫它数据段咯。这一块专门存储代码,我们就叫他代码段咯。
带有段的程序
大多数情况下,程序是带有段的。段可以更好地帮助程序员来区分各部分代码的职责。在nasm汇编器中,你可以使用section
来声明段:
|
|
这里section codes
声明了代码段,表示代码从这里开始执行。section datas
声明了数据段,表示这里存放着数据。
开头的jmp _start
表示跳转到_start
处开始执行。需要注意的是,声明了段并不代表你的寄存器也指向了段。所以这里的:
|
|
将datas(段名代表这个段开头在内存中的位置)放入ds。也就是我们必须手动关联段地址寄存器和段。开头的jmp _start
当然也可以写成:
|
|
来强制指定,不过这里使用jmp显然更快。
没有段的程序
程序也可以是没有段的。这个时候整个程序是一个段(nasm规定没有section指定的地方自成一段)你自己就要想办法划分你的程序空间,比如:
|
|
可以看到这里没有将ds指向msg处,而是将es指向msg处,将ds指向显存。其实并没有什么硬性规定说ds就要指向程序的数据部分。只要是段地址寄存器就可以指向任意的段地址,ds被用于指向程序数据也只是惯用方法而已。
所以如果你想要自己写汇编的话,必须把你的栈段,数据段,代码段管理好,不然可能会影响其他的程序。
实模式和保护模式
8086是在实模式下工作的,而现在我们的32位和64位电脑是在保护模式下工作的。
通俗来说,实模式就是程序之间可以互相干预,我可以通过汇编代码影响你的内存,你也可以影响我的内存。这样很显然系统不稳定。所以在之后Intel处理器就推出了保护模式,各个程序有自己的内存空间,通过寄存器引用其他程序的内存是非法的。
8086是在实模式下编写的代码,可以有很大自由操控内存。
寄存器
寄存器是CPU中存储数据的地方,8086CPU寄存器有AX,BX,CX,DX,DS,SS,ES,CS,SP,IP,DI,SI,BP,标志寄存器
通用寄存器
通用寄存器可以存储任意的数据,有AX,BX,CX,DX
四个。虽然说是通用寄存器,但是每个寄存器其实还是有自己不同的职责的。
AX
AX是最常用于
- 作为数据中转站(将数据放入段地址寄存器中)
- 作为各种指令的参数存放处或者结果存放处(如mul和int指令)
BX
一般来说称为“基址寄存器”,你可以将BX
中的B理解为Base Address中的Base。
BX广泛用于指定内存地址,而且在通用寄存器中也只有它可以指定内存地址。
CX
计数寄存器(Count Register),和循环有关的指令都是以CX内的值来决定循环次数的。
DX
DX和AX的用途差不多
段地址寄存器及其匹配寄存器
段寄存器用于存储段的段地址。由于8086采用段地址*16+偏移地址的方式寻址,所以一般来说还有一个寄存器用于存储偏移地址,也就是和段地址寄存器匹配的辅助寄存器。
由于8086内是16位寄存器,所以每个寄存器最大可以是0xFFFF
地址,那么根据段地址*16+偏移地址
可知,段地址和其辅助寄存器最大可以寻址0xFFFF*16+0xFFFF=0xFFFFF=2^20=1MB
(由于溢出导致最大只能是0xFFFFF)。所以每个段最大不可能超过1MB。
由于段地址*16+偏移地址
的表示方法很常用,所以我们也使用段地址:偏移地址
的方式来简写。
段地址寄存器都是不能直接复制的,也就是说你不能mov ds, 0x3d
这种,而必须通过一个通用寄存器间接赋值,比如:
|
|
数据段DS(Data Segment)
DS是数据段(data segment)寄存器。数据段用于存放数据。
指令段CS(Construct Segment)和IP(Instruct Pointer)
CS是指令段(construction segment)寄存器,指令段用于存放指令。
你的指令就是在CS:IP
处,每次CPU执行完一条指令就会将IP加上这条指令的长度来指向下一条指令。所以你可以通过改变CS和IP来改变指令的执行顺序。
栈段SS(Stack Segment)和SP(Stack Pointer)
SS是栈段(stack segment)寄存器,栈段用于当作栈使用,但是CPU不能保证栈段溢出,程序员要自己关心栈段的大小和是否越界。
SP指向栈段的栈顶。每次使用push
和pop
的栈指令时都会影响SP。
ES(Extra Segment)
这个寄存器也是段寄存器,但是是额外的段寄存器。他是当你不想改变DS,CS,SS的时候,可以通过ES来指向段:
|
|
这里我们的DS指向数据了,但是我们想将DS中的10个字节的数据放到0x7C0中,但是DS又不能改变,那怎么办呢?这个时候就可以使用ES来窒息那个0x7C0处来帮助移动了。
这里同时也展示了SI和DI寄存器的使用方式。
DI, SI, BP
DI, SI
寄存器(Destination, Source)一般用于辅助内存中数据的移动。DI一般指向数据目的地,SI一般指向数据源地址。
BP
的话一般用于辅助栈段,一般用于取代SP来操纵栈。也就是说当你不想移动SP的时候你可以通过BP来辅助,就像ES辅助其他寄存器一样。
标志寄存器
标志寄存器不是用于存放用户数据的,而是存储系统数据的,且是按位起作用的,也就是说每一位的作用都不一样。
ZF(Zero Flag)
0标志位
表示算数运算,and,or等运算的结果是否为0
PF(Parity Flag)
奇偶校验位
记录算数运算,and,or等运算的结果中所有1的个数
- 为奇数:0
- 为偶数:1
SF
负数标志位
相关运算之后结果是否为负数。如果为负数SF=1否则SF=0
需要注意的是只有我们将数据视为有符号数据的时候SF才有意义,虽然无符号数据在某些情况下也会改变SF, 但是这种改变是没有意义的
CF(Count Flag)
进位标志位
在进行无符号数运算的时候,记录了运算结果最高位有效位向更高位是否进位,或者是否借位,如果进位/借位CF=1否则CF=0。
这个位通常在带位加减法或者大数据加减(用多个于16位保存一个数)中使用。
OF(Overflow Flag)
溢出标志位
在进行有符号数运算的时候,如果溢出位1,否则位0
DF(Decrease Flag)
递减标志位
DF表示用于控制在使用MOVSB,MOVSW
指令之后SI,DI
寄存器递增递减的问题。DF=1时SI,DI会自减,否则自增。
使用CLD(CLear DF)
指令置DF=0,STD(SeT DF)
指令置DF=1
IF(Interupt Flag)
可屏蔽中断位
为1的时候CPU接收到可屏蔽中断时会响应,为0时不响应。
使用STI(SeT IF)
设置IF为1,CLI(CLear IF)
设置IF为0
TF(Trap Flag)
TF跟踪标志,也被称为陷阱标志
TF=1,机器进入单步工作方式,每条机器指令执行后,显示结果及寄存器状态,若TF=0,则机器处在连续工作方式。此标志为调试机器或调试程序发现故障而设置。
AF
AF是辅助进位标志
通常在BCD码运算中用于判断是否发生一个字节中的低4位向高4位的进位或者借位。
汇编指令
汇编指令是机器码的助记符,最后汇编的时候会将汇编指令变成机器码。
需要注意的是,汇编指令有需要一个操作数的,两个操作数的和没有操作数的。一般来说有两个操作数的指令中,两个操作数不能都是内存地址和立即数或者两者的混淆,一般来说必须有一个寄存器。
运算
加减
加减分别是ADD,ADC
和SUB,SBB
,格式是(四个格式一样)
ADD 操作数1, 操作数2
所有的结果都会放在操作数1中。
其中ADD,SUB
是不进位加减,ADC,SBB
是进位加减,也就是说会使用CF
寄存器中的值来加减。
同样还有INC, DEC
的单操作数指令将操作数内容自增/自减1:
|
|
乘除
分别是DIV
和MUL
MUL和IMUL
MUL是无符号乘法,IMUL是有符号乘法,分为两个情况:
- 乘数都是8位,那么其中一个乘数默认放在AL中,另一个放在内存或者8位寄存器中。结果存放在AX中
- 乘数都是16位,那么其中一个乘数默认在AX中,另一个上。结果的高位存放在DX中,低位存放在AX中
DIV和IDIV
DIV是无符号数除法,IDIV是有符号数除法,除数总是需要被指定(可以是寄存器或者内存),分为两个情况:
- 被除数为16位,除数为8位,那么被除数默认放在AX中,结果AL存放商,AH存放余数
- 被除数为32位,除数为16位,那么DX存放被除数高16位,AX存放低16位。结果商放在AX中,余数放在DX中
按位运算
- 和
AND
- 或
OR
- 非
NOT
- 异或
XOR
TEST
:将两个操作数进行逻辑与运算,并根据运算结果设置相关的标志位。但是,Test命令的两个操作数不会被改变。只是改变标志寄存器而已。
移位运算
移位运算分为两种:
- 逻辑移位:移出去的位丢弃,空缺位(vacant bit)用 0 填充。
- 算术移位:移出去的位丢弃,空缺位(vacant bit)用“符号位”来填充,所以一般用在右移运算中
还有一种特殊的叫做循环移位,就是移出去的位会补在新加的位上。
那么显然也得有对应函数:
- 逻辑左移
SHL
- 逻辑右移
SHR
- 算数左移
SAL
- 算数右移
SAR
- 循环左移
ROL
- 循环右移
ROR
会将最后移入/移出的位写入CF中。如果移动位数大于1,那么必须将位数放在CL中.
移动指令
移动指令将一个地方的数移动到另一个地方
MOV
MOV指令应该是汇编语言中最常用的指令了。MOV指令需要两个参数,可以将后面参数的值移动到前面参数去。
MOV 操作数1, 操作数2
MOVSB, MOVSW
MOVSB和MOVSW是将DS:SI
指向地址中的值送到ES:DI
地址中,然后在根据DF
标志觉得DS和SI是自增还是自减:
- DF=1自减
- DF=0自增
其中MOVSB
是对字节操作,MOVSW
是对字操作
XCHG
XCHG
用于交换寄存器或者内存地址之间的值:
XCHG 操作数1, 操作数2
对栈的操作
对栈的操作就四个函数:
-
push
和pop
,push将数据压入栈,pop将数据弹出栈需要注意:push和pop的参数只能是寄存器或者内存地址,不能是立即数
-
pushf
和popf
,分别将标志寄存器压入和弹出栈。这为操作标志寄存器的值提供了方法pushf没有参数
转移指令
转移分为
- 段内转移:只会修改IP,段内转移按照转移的距离又分为:
- 段内短转移:转移范围为-128~127
- 段内近转移:转移范围为-32768~32767
- 段间转移:同时修改CS和IP
JMP
无条件转移指令可以按照标号转移或者按照地址转移
- 按照标号转移:
JMP SHORT S
段内短转移到S处JMP NEAR PTR S
段内近转移到S处JMP FAR PTR S
段间转移到S处
- 按照寄存器和地址转移:
JMP 段地址:偏移地址
,直接修改CS,IP,段间转移JMP 寄存器
,将寄存器的值赋予IP,段内转移
LOOP
条件转移指令,段内转移,作用是当CX的值不是0的时候跳转到标号,并且将CX值自减
JCXZ(Jump if CX is Zero)
条件转移指令,段内转移,和LOOP很像,当CX为0的时候跳转到标号,但是不会改动CX的值
子过程相关指令
CALL和RET,RETF
CALL指令会先将CS和IP压入栈中,然后修改CS,IP的值(如果直接CALL
保存和修改IP的值,如果是CALL FAR PTR
保存和修改CS和IP的值)
CALL指令不能实现短转移。
RET指令用来和CALL
搭配,从栈中弹出保存的IP的值并且修改现在的IP值
RETF时用来和CALL FAR PTR
搭配,弹出CS和IP的值并且修改现在的CS和IP的值
带有参数的RET和REFT
RET和REFT后面可以跟一个立即数,这样在弹出和修改CS和IP值之后还会将IP的值加上这个立即数的值。这个普遍用在栈传递参数的技术中。
IRET
用于和中断INT
搭配使用,返回原来的程序。在中断程序的最后必须加上IRET
比较和比较相关的跳转指令
CMP
用于将两个数相减,本身不将结果存放在任何寄存器中,只是为了改变标志寄存器。
CMP和TEST的区别
cmp主要用于比较两数的关系,可以在有符号数和无符号数之间进行比较。两数相等影响ZF标志,无符号数影响CF标志,有符号数影响SF和OF标志。一般根据标志位的影响设置程序跳转,即根据不同的条件完成对应的程序片段,类似于高级语言的IF-ELSE结构。所有的有条件转移指令都可以用在这条指令之后。 test通常用于检测某些位是否为1,但又不希望改变操作数的场合。比如检查AL中的位6和位2是否有一位为1,可以用如下指令: test AL,01000100b,如果这两个位全为0.则ZF的值为1,否则清0,那么根据标志位设置的跳转就只能为jz或jnz 这两条指令的相同点是都不会影响操作数,只是通过标志寄存器的某些位反映运算结果。
JE,JNE,JA,JNA,JB,JNB跳转指令
用于和CMP搭配使用
- JE:当ZF位为1时跳转到标号
- JNE:当ZF位为0的时候跳转到标号
- JA:当CF位为0且ZF为0的时候跳转
- JNA:CF为1或ZF为1的时候跳转
- JB:当CF=1的时候跳转
- JNB:当CF为0的时候跳转
这些指令的英文很简单,分别是Jump Equal, Jump Not Equal, Jump Above, Jump Not Above, Jump Below, Jump Not Below
中断指令
INT
用于执行中断,过程如下:
- 取中断类型码n
- pushf,切令IF,TF为0
- CS,IP入栈
- (IP)=(n*4), CS=(n*4+2)
一般和IRET
指令配合使用。
所有的中断可以在这份文章中找到里面找到
端口指令
IN
IN指令用于从端口读入数据到寄存器,用法是
|
|
也就是说第一个数必须是AX或者AL(AH都不行),第二个必须是DX和DL
OUT
OUT用于将数据写入到端口的制定内存中,和IN指令相反:
|
|