8086汇编语言知识点汇总

这里是8086汇编语言的总结,使用的汇编器是nasm。

汇编程序的基本构造

首先我们先来看一段C++代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <cstdio>
#include <cstring>
#include "header.hpp"
using namespace std;

struct member{
    int a;
    int b;
};

struct data{
    member m1;
    member m2;
    string name;
};

int main(int argc,char** argv){
    FILE* file = fopen("data.mem", "rb+");
    if(file == nullptr){
        cout<<"file not open"<<endl;
        return -2;
    }
    data d;
    fread(&d, sizeof(d), 1, file);
    cout<<"read suuccessful"<<endl 
        <<"data.m1:"<<endl 
        <<"a:"<<d.m1.a<<endl 
        <<"b:"<<d.m1.b<<endl 
        <<"data.m2"<<endl
        <<"a:"<<d.m2.a<<endl 
        <<"b:"<<d.m2.b<<endl
        <<"name:"<<d.name<<endl;
    fclose(file);
    member m;
    memcpy(&m, &d, sizeof(member));
    cout<<"strong cast successful"<<endl 
            <<"m.a:"<<m.a<<endl 
            <<"m.b:"<<m.b<<endl;
    return 0;
}

这一部分代码是将二进制文件中的数据读出来并且存储到data结构体里面。

我们就通过这个程序来分析一下一个程序最基本需要什么。

首先他有代码。在汇编语言中,代码被放在代码段中,代码段中的代码就是程序要执行的代码。

其次我们看到有d啊m啊这些变量,也就是说有数据。在汇编中,数据被放在数据段中。

最后还有一个我们并不能从表面看出来的组成,被称为栈段,也就是作为栈使用的段。

所谓,其实就是对内存进行划分,将内存划分为一块一块的,这一块专门存储数据,我们就叫它数据段咯。这一块专门存储代码,我们就叫他代码段咯。

带有段的程序

大多数情况下,程序是带有段的。段可以更好地帮助程序员来区分各部分代码的职责。在nasm汇编器中,你可以使用section来声明段:

1
2
3
4
5
6
7
8
9
jmp _start

section codes vstart=0
_start:
    mov ax, datas
    mov ds, ax

section datas
    msg db "helloworld"

这里section codes声明了代码段,表示代码从这里开始执行。section datas声明了数据段,表示这里存放着数据。
开头的jmp _start表示跳转到_start处开始执行。需要注意的是,声明了段并不代表你的寄存器也指向了段。所以这里的:

1
2
mov ax, datas
mov ds, ax

将datas(段名代表这个段开头在内存中的位置)放入ds。也就是我们必须手动关联段地址寄存器和段。开头的jmp _start当然也可以写成:

1
2
3
mov ax, codes
mov cs, ax
mov ip, _start

来强制指定,不过这里使用jmp显然更快。

没有段的程序

程序也可以是没有段的。这个时候整个程序是一个段(nasm规定没有section指定的地方自成一段)你自己就要想办法划分你的程序空间,比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
jmp near main   ;一样先跳转到程序开始处。我们这里程序在main处开始
msg db 'Hello World'    ;这里是数据
main:
   mov ax, 0xB800
   mov es, ax
   mov di, 0
   mov ax, 0x7c0
   mov ds, ax   ;这里直接将ds指向显存
   mov si, msg
   mov cx,  main-msg
trans:
   mov byte al, [ds:si]
   mov byte [es:di], al
   inc di
   mov byte [es:di], 0x07
   inc di
   inc si
   loop trans

jmp near $
times 510-($-$$) db 0
db 0x55, 0xaa

可以看到这里没有将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这种,而必须通过一个通用寄存器间接赋值,比如:

1
2
mov ax, 0x7C0
mov ds, ax

数据段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指向栈段的栈顶。每次使用pushpop的栈指令时都会影响SP。

ES(Extra Segment)

这个寄存器也是段寄存器,但是是额外的段寄存器。他是当你不想改变DS,CS,SS的时候,可以通过ES来指向段:

1
2
3
4
5
6
7
8
mov ax, 0x7C0
mov es, ax
mov cx, 10
mov si, 0
l:
    mov byte [es:si], [ds:si]
    inc si
    loop

这里我们的DS指向数据了,但是我们想将DS中的10个字节的数据放到0x7C0中,但是DS又不能改变,那怎么办呢?这个时候就可以使用ES来窒息那个0x7C0处来帮助移动了。

这里同时也展示了SI和DI寄存器的使用方式。

DI, SI, BP

DI, SI寄存器(Destination, Source)一般用于辅助内存中数据的移动。DI一般指向数据目的地,SI一般指向数据源地址。

BP的话一般用于辅助栈段,一般用于取代SP来操纵栈。也就是说当你不想移动SP的时候你可以通过BP来辅助,就像ES辅助其他寄存器一样。

标志寄存器

标志寄存器不是用于存放用户数据的,而是存储系统数据的,且是按位起作用的,也就是说每一位的作用都不一样。

8086标志寄存器.jpg

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,ADCSUB,SBB,格式是(四个格式一样)

ADD 操作数1, 操作数2

所有的结果都会放在操作数1中。

其中ADD,SUB是不进位加减,ADC,SBB是进位加减,也就是说会使用CF寄存器中的值来加减。

同样还有INC, DEC的单操作数指令将操作数内容自增/自减1:

1
2
INC 操作数
DEC 操作数

乘除

分别是DIVMUL

MUL和IMUL

MUL是无符号乘法,IMUL是有符号乘法,分为两个情况:

  • 乘数都是8位,那么其中一个乘数默认放在AL中,另一个放在内存或者8位寄存器中。结果存放在AX中
  • 乘数都是16位,那么其中一个乘数默认在AX中,另一个上。结果的高位存放在DX中,低位存放在AX中

8086MUL

DIV和IDIV

DIV是无符号数除法,IDIV是有符号数除法,除数总是需要被指定(可以是寄存器或者内存),分为两个情况:

  • 被除数为16位,除数为8位,那么被除数默认放在AX中,结果AL存放商,AH存放余数
  • 被除数为32位,除数为16位,那么DX存放被除数高16位,AX存放低16位。结果商放在AX中,余数放在DX中

按位运算

  • AND
  • OR
  • NOT
  • 异或XOR
  • TEST:将两个操作数进行逻辑与运算,并根据运算结果设置相关的标志位。但是,Test命令的两个操作数不会被改变。只是改变标志寄存器而已。

TEST和CMP指令的区别见这里

移位运算

移位运算分为两种:

  • 逻辑移位:移出去的位丢弃,空缺位(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

对栈的操作

对栈的操作就四个函数:

  • pushpop,push将数据压入栈,pop将数据弹出栈

    需要注意:push和pop的参数只能是寄存器或者内存地址,不能是立即数

  • pushfpopf,分别将标志寄存器压入和弹出栈。这为操作标志寄存器的值提供了方法

    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和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指令用于从端口读入数据到寄存器,用法是

1
2
3
4
5
in ax, dx
in al, dl
in ax, dl
in al, dx
in ax/al, 立即数

也就是说第一个数必须是AX或者AL(AH都不行),第二个必须是DX和DL

OUT

OUT用于将数据写入到端口的制定内存中,和IN指令相反:

1
2
3
4
5
out dx, ax
out dl, al
out dx, al
out dl, ax
out 立即数m dx/dl
updatedupdated2023-06-182023-06-18