汇编

单位说明

在计算机中最小的信息单位,称之为位(bit,又称比特)。 存储器中所包含存储单元的数量称为存储容量,其计量基本单位是字节(Byte。简称B)

  • 数据传输是以大多是以“位”(bit,又名“比特”)为单位。一个位就代表一个0或1 。 1bit = 1b
  • 数据存储是以“字节”(Byte)为单位。1B = 8bit = 8b
  • 1KB = 1024B = 1024 * 8b = 8Kb

总线 (8086处理器16bit CPU)

  • 地址总线: 它的宽度决定了CPU的寻址能力。8086的地址总线宽度是20,所以寻址能力是1MB。N根地址总线,那么它可以寻2^N个内存单元,即1总线宽度 = 21B = 2B( 2^20B = 1024B 1024B = 1024KB = 1MB)

CPU寻址示例图

  • 数据总线: 它的宽度决定了CPU的单次数据传送量,也就是数据传送速度,8086的数据总线宽度是16,所以单次最大传递2个字节的数据
  • 控制总线: 它的宽度决定了CPU对其他器件的控制能力、能有多少种控制

寄存器

寄存器是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和地址。

  • 8086有14个寄存器,每个寄存器都是16位的,每个存放2个字节
    8086内部寄存器

通用寄存器

  • AX、BX、CX、DX这4个寄存器通常用来存放一般性的数据,称为通用寄存器(有时也有特定用途)
  • 通常,CPU会先将内存中的数据存储到通用寄存器中,然后再对通用寄存器中的数据进行运算

  • 假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间

    • CPU首先会将红色内存空间的值放到AX寄存器中:mov ax,红色内存空间
    • 然后让AX寄存器与1相加:add ax,1
    • 最后将值赋值给内存空间:mov 蓝色内存空间,ax
  • AX、BX、CX、DX这4个通用寄存器都是16位的,如下图所示

  • 上一代8086的寄存器都是8位的,为了保证兼容, AX、BX、CX、DX都可分为2个独立的8位寄存器来使用H代表高位寄存器
    L代表低位寄存器

字和字节

  • 在汇编的数据存储中,有2个比较常用的单位

    • 字节:byte,1个字节由8bit组成,可以存储在8位寄存器中
    • 字:word,1个字由2个字节组成,这2个字节分别称为字的高字节和低字节
  • 比如数据20000(4E20H,0100111000100000B),高字节的值是78,低字节的值是32

  • 1个字可以存在1个16位寄存器中,这个字的高字节、低字节分别存储在这个寄存器的高8位寄存器、低8位寄存器中

8086寻址方式

  • CPU访问内存单元时,要通过内存单元的地址,所有的内存单元都有唯一的地址,叫做物理地址
  • 8086有20位地址总线,可以传送20位的地址,1MB的寻址能力
  • 但它又是16位结构(数据总线)的CPU,它内部能够一次性处理、传输、暂时存储的地址为16位。如果将地址从内部简单地发出,那么它只能送出16位的地址,表现出来的寻址能力只有64KB
    • 存在的问题:CPU的寻址能力是1MB,但数组总线是16位,一次性能处理的数据为64KB
    • 解决方案: 8086采用一种在内部用2个16位地址合成的方法来生成1个20位的物理地址

物理地址 段地址 偏移地址
0x32AF1
0x32AF1 0x3200 0x0AF1
0x32AF1 0x32AF 0x0001
0x32AF1 0x32A0 0x00F1
0x32AF1 0x3000 0x2AF1

内存分段管理

  • 8086是用“基础地址(段地址×16) + 偏移地址 = 物理地址”的方式给出物理地址
  • 为了开发方便,我们可以采取分段的方法来管理内存,比如:

    • 地址10000H~100FFH的内存单元组成一个段,该段的起始地址(基础地址)为10000H,段地址为1000H,大小为100H
    • 地址10000H~1007FH、10080H~100FFH的内存单元组成2个段,它们的起始地址(基础地址)为:10000H和10080H,段地址为1000H和1008H,大小都为80H
  • 在编程时可以根据需要,将若干连续地址的内存单元看做一个段,用段地址×16定为段的起始地址(基础地址),用偏移地址定位段中的内存单元

    • 段地址×16必然是16的倍数,所以一个段的起始地址(基础地址)也一定是16的倍数
    • 偏移地址为16位,16位地址的寻址能力为64KB,所以一个段的长度最大为64KB

段寄存器

  • 8086在访问内存时要由相关部件提供内存单元的段地址和偏移地址,送入地址加法器合成物理地址
  • 段地址在8086的段寄存器中存放
  • 8086有4个段寄存器:CS、DS、SS、ES,当CPU需要访问内存时由这4个段寄存器提供内存单元的段地址
    • CS (Code Segment):代码段寄存器
    • DS (Data Segment):数据段寄存器
    • SS (Stack Segment):堆栈段寄存器
    • ES (Extra Segment):附加段寄存器

CS和IP

  • CS为代码段寄存器,IP为指令指针寄存器。它们指示了CPU当前要读取指令的地址
  • 任意时刻,8086CPU都会将CS:IP指向的指令作为下一条需要执行的指令
  • 8086CPU的工作过程
    1. 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲寄存器
    2. IP = IP + 上一条指令的长度,从而指向下一条指令
    3. 执行指令。转向步骤1,重复整个过程

JMP指令

  • CPU从何处执行指令是由CS、IP中的内容决定的。我们可以通过改变CS、IP的内容来控制CPU执行目标指令
  • mov(传送指令)可以用来修改大部分寄存器的值,但不能用于设置CS、IP的值
  • 8086提供了另外的指令来修改CS、IP的值,这些指令统称为转移指令,最简单的是jmp指令
  • 若想同时修改CS和IP的内容,使用jmp 段地址: 偏移地址来完成

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    jmp 2AE3:3 
    CS: 0x2AE3
    IP: 0x0003
    则从内存地址0x2AE33处读取指令


    jmp 3:0B16
    CS: 0x0003
    IP: 0x0B16
    则从内存地址0x00B46 (CS*16+IP) 处读取指令
  • 若指向修改IP的内容,使用jmp 某一合法寄存器来完成、

    1
    2
    3
    4
    5
    jmp ax  -> 执行前: ax = 1000H, CS = 2000H, IP = 0003H
    执行后: ax = 1000H, CS = 2000H, IP = 1000H

    jmp bx -> 执行前: bx = 1003H, CS = 2000H, IP = 1000H
    执行后: bx = 1003H, CS = 2000H, IP = 1003H

DS和[address]

  • CPU要读写一个内存单元时,必须要先给出这个内存单元的地址,在8086中,内存地址由段地址和偏移地址组成
  • 8086中有一个DS段寄存器,通常用来存放要访问数据的段地址

    1
    2
    3
    mov bx,1000H
    mov ds,bx
    mov al,[0]
    • 上面3条指令的作用是将内存中10000H(1000:0)的数据,赋值给al的过程
    • mov al:[address] 作用是将DS:[address]中的值给al
    • al是8位的寄存器,所以将一个字节的数据给al
  • 8086不支持将数据直接送入段寄存器中,mov ds,1000H是错误的
  • 写几条指令,将al中的数据送入内存单元1000H中

    1
    2
    3
    mov bx, 1000H
    mov ds, bx
    mov [0], al
  • 8086CPU在编程时,可以将一组内存单元定义为一个段。我们可以将一组长度为N(N<=64KB)、地址连续、起始地址为16倍数的内存单元当做专门存储数据的内存空间,称为数据段。比如用123B0H~123B9H这段内存空间来存放数据,我们就可以认为123B0H~123B9H是一个数据段,它的段地址为123BH,长度为10字节。用DS存放数据段的段地址,再根据需要,用相关指令访问数据段中的具体单元

完整的汇编

1
2
3
4
5
6
7
8
9
10
11
assume cs:code
code segment
mov ax, 1122h
mov bx, 3344h
add ax, bx

;退出程序
mov ah, 4ch
int 21h
code ends
end
  • 汇编语言由2类指令组成
    • 汇编指令,如mov、add、sub等。有对应的机器指令,可以被编译为机器指令,最终被CPU执行
      • 伪指令,如assume、 segment、ends、end等。没有对应的机器指令,由编译器解析,最终不被CPU执行
      • db(define byte) 自定义字节; dw(define word)自定义字
  • 注释以分号开头
  • segment和ends的作用是定义一个段,segment代表一个段的开始,ends代表一个段的结束
  • 一个有意义的汇编程序中,至少要有一个段作为代码段存放代码
  • assume用作代码段的code段和CPU中的cs寄存器关联起来
  • 编译器遇到end时,就结束对源程序的编译

中断

从本质上来讲,中断是一种电信号,当设备有某种事件发生时,它就会产生中断,通过总线把电信号发送给中断控制器。如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到中断处理程序的入口点,进行中断处理

  • 中断是由于软件的或硬件的信号,使得CPU暂停当前的任务,转而去执行另一段子程序。也就是说,在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂时中止当前程序的执行转而处理这个新情况的过程就叫做中断
  • 中断的分类
    • 硬中断(外中断),由外部设备(比如网卡、硬盘)随机引发的,比如当网卡收到数据包的时候,就会发出一个中断
      • 软中断(内中断),由执行中断指令产生的,可以通过程序控制触发
  • 可以通过指令int n产生中断
    • n是中断码,内存中有一张中断向量表,用来存放中断码对应中断处理程序的入口地址
      • CPU在接收到中断信号后,暂停当前正在执行的程序,跳转到中断码对应的中断向量表地址处,去执行中断处理程序
  • 常见中断
    • int 10h用于执行BIOS中断
      • int 3是“断点中断”,用于调试程序
      • int 21h用于执行DOS系统功能调用,AH寄存器存储功能号
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
;hello world! 字符串的输出
;寄存器关联:代码段code与代码段寄存器CS关联,
;数据段data与数据段寄存器DS关联。
assume CS:code,DS:data ;注意:assume是伪指令,在扫描编译时不翻译

;data数据段定义
data segment
string db 'Hello world$' ;切忌串结束符$, string:声明,使用可去掉offset
data ends

;代码段定义
code segment
;程序开始
start:
mov ax,data ;将数据段段地址装入AX寄存器
mov ds,ax ;将数据段段地址通过通用寄存器AX装入DS
mov dx,offset string ;将串的段内地址装入DX,
mov ah,09h ;调用DOS的09H号功能,传入参数DS:DX=串地址,'$'结束字符串
int 21h
mov ah,4ch ;调用DOS的4CH号功能,带返回码结束,返回码存在于AL
int 21h
code ends ;代码段定义结束
end start ;程序结束

  • 栈:是一种具有特殊的访问方式的存储空间(后进先出, Last In Out Firt,LIFO)
  • 8086会将CS作为代码段的段地址,将CS:IP指向的指令作为下一条需要取出执行的指令
  • 8086会将DS作为数据段的段地址,mov ax,[address]就是取出DS:address的内存数据放到ax寄存器中
  • 8086会将SS作为栈段的段地址,任意时刻,SS:SP指向栈顶元素
  • 8086提供了PUSH(入栈)和POP(出栈)指令来操作栈段的数据。比如push ax是将ax的数据入栈,pop ax是将栈顶的数据送入ax

push ax

  • SP = SP - 2,SS:SP 指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶;
  • 将ax中的内容送入 SS:SP 指向的内存单元处,SS:SP此时指向新栈顶

pop ax

  • 将SS:SP指向的内存单元处的数据送入ax中
  • SP=SP+2,SS:SP 指向当前栈顶下面的单元,以当前栈顶下面的单元为新的栈顶

通过栈交换ax, bx内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:code
code segment
start:
mov ax, 1000h
mov ss, ax
mov sp, 0010h
mov ax, 1122h
mov bx, 2233h
;通过栈的特性交换
push ax
push bx
pop ax
pop bx
;exit
mov ah, 4ch
int 21h
code ends
end start

栈段

  • 对于8086来说,在编程时,可以根据需要,将一组内存单元定义为一个段
  • 我们可以将一组长度为N(N<=64KB)、地址连续、起始地址为16倍数的内存单元,当做栈空间来使用,称为栈段。比如用10010H~1001FH这段内存空间当做栈来使用,我们就可以认为10010H~1001FH是一个栈段,它的段地址为1001H,长度为16字节
  • 用SS存放栈段的段地址,用SP存放栈顶的偏移地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
assume cs:code,ss:stack,ds:data   

;栈段(存放数据,比如高级语言中的局部变量)
stack segment
db 20 dup(1) ;定义一个20个字节的空栈,默认值为1
stack ends

;数据段(存放数据,比如高级语言中的全局变量)
data segment
db 20 dup(0)
str db "Hello World!$"
data ends

code segment
start:
;栈初始化
mov ax, stack
mov ss, ax

;exit
mov ax, 0x004ch
int 21h
code ends
end start

栈顶指针,指向0x07114内存空间
定义栈段

Loop指令

  • loop指令和cx寄存器配合使用,用于循环操作类似高级语言的for,while
  • 使用格式:

    1
    2
    3
    4
        mov cx,循环次数
    标号:
    循环执行的程序代码
    loop 标号
  • loop指令执行流程

    1. 先将cx寄存器的值 - 1, cx = cx - 1
    2. 判断cx 的值; 如果不为零执行标号的代码,又执行步骤1;如果为零执行loop后面的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
;通过loop求2的6次方的和64
assume cs:code

code segment
start:
mov ax, 2
mov cx, 0x05h ;设置循环次数
s: add ax, ax
loop s
; exit
mov ah, 4ch
int 21h
code ends
end start

结果: ax: 0x0040h = 64

通过loop求2的6次方的和64

Call和ret指令

  • 函数栈平衡:保证函数调用前后的栈顶是一致的
    • 外平栈:由函数外部保持栈平衡
    • 内平栈:由函数内部保持栈平衡

      Call

  • call标号
  • 将下一条指令的偏移地址入栈
  • 跳转到定位的地址执行指令

    ret

  • ret指令就是将栈顶的值POP给IP
示例1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
assume cs:code,ds:data

data segment
str db 'Hello world!$'
data ends

code segment
start:
mov ax, data
mov ds, ax
;调用打印函数
call print
;exit
mov ah, 4ch
int 21h

print:
mov dx, offset str
mov ah, 09h
int 21h
ret
code ends
end start

call print 之后,将下一条指令地址(07118h-07119h)的偏移地址0008h入栈, 根据print标号指向的内存地址的偏移地址赋值给IP(0x000C),然后执行print函数
call print

print 函数结束后,调用ret,将下一条指令偏移地址出栈,赋值给IP
ret

示例2:外平栈

函数执行前后, SP不变

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
assume cs:code,ss:stack

stack segment
db 20 dup(0)
stack ends
code segment
start:
mov ax, stack
mov ss, ax

push 1
push 2
push 3
call sum
add sp, 6 ;外平栈操作 SP恢复为默认值0x0014

push 1
push 2
push 3
call sum
add sp, 6 ;外平栈操作 SP恢复为默认值0x0014

push 1
push 2
push 3
call sum
add sp, 6 ;外平栈操作 SP恢复为默认值0x0014

;exit
mov ah, 4ch
int 21h
;参数:传递两个字型参数,参数分别用bx,dx存放
;返回值:返回值存放在ax中
sum:
mov bp, sp ;sp不能加减,bp可以
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
add ax, ss:[bp+6]
ret
code ends
end start

示例3:内平栈

函数执行前后, SP不变

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
assume cs:code,ss:stack

stack segment
db 20 dup(0)
stack ends
code segment
start:
mov ax, stack
mov ss, ax

push 1
push 2
push 3
call sum

push 1
push 2
push 3
call sum

push 1
push 2
push 3
call sum

;exit
mov ah, 4ch
int 21h
;参数:传递两个字型参数,参数分别用bx,dx存放
;返回值:返回值存放在ax中
sum:
mov bp, sp ;sp不能加减,bp可以
mov ax, ss:[bp+2]
add ax, ss:[bp+4]
add ax, ss:[bp+6]
ret 6 ;SP恢复为默认值0x0014
code ends
end start
示例4(重要):完整函数调用过程,对bp进行现场保护

完整函数调用过程:

  1. push参数(64位cpu 任性使用寄存器)
  2. call指令调用(将下一条指令地址入栈)
  3. 保护bp寄存器,将sp赋值给bp
  4. 提升sp指针,作为局部变量空间(sp 减去值)
  5. 保护寄存器
  6. 业务逻辑
  7. 恢复寄存器
  8. 恢复sp(sp指向bp/sp 加上值)
  9. 恢复bp(pop bp)
  10. 返回(ret)
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
C语言:
int sum(int a, int b)
{
int c = 1;
int d = 2;
return a + b + c + d;
}

sum(3,4)


汇编:
assume cs:code,ss:stack

stack segment
db 40 dup(0)
stack ends

code segment
start:
mov ax, stack
mov ss, ax

;假设初值
mov bp, 1002h
mov bx, 1003h
mov cx, 1004h
mov dx, 1005h

;执行求和函数
push 03h ;传递参数
push 02h ;传递参数
call sum ;调用函数

;exit

mov ah, 4ch
int 21h

sum:
push bp ;现场保护,防止多个函数调用改变bp
mov bp, sp
sub sp, 20 ;20字节留作局部变量
;保护寄存器
push bx
push cx
push dx

;*业务逻辑代码*

;申明局部变量
mov ss:[bp-2], 1h
mov ss:[bp-4], 2h

;有可能修改寄存器
mov bx,2h
mov cx,3h
mov dx,4h

;计算结果
mov ax, [bp+4] ; bp 指向call 下条指令地址,取形参要加4
add ax, [bp+6]
add ax, [bp-2] ;局部变量
add ax, [bp-4]

;sp指向寄存器保护位置
pop dx
pop cx
pop bx

;清空局部变量
mov sp,bp
pop bp; 现场保护恢复
ret 4 ;栈平衡
code ends
end start

调用函数前

调用函数后

SP BP BX CX DX
调用函数前 0x0028 0x1002 0x1003 0x1004 0x1005
调用函数后 0x0028 0x1002 0x1003 0x1004 0x1005