汇编语言笔记

第 1 章 基础知识

汇编语言发展至今,有3类指令组成:

  • 汇编指令:机器码的助记符,有对应的机器码
  • 伪指令:没有对应的机器码,由编译器执行,计算机并不执行
  • 其他符号:如+ - * /等,由编译器识别,没有对应的机器码

汇编语言的核心是汇编指令,它决定了汇编语言的特性。

记住!所谓的汇编地址,其实就是相对偏移量,要么是相对段的偏移量,要么是相对程序开始的偏移量。只有程序加载到内存中,段地址确定以后,汇编地址才会随之确定下来,这时候就变成了物理地址。

第 2 章 寄存器

2.1 通用寄存器

通用寄存器 AX, BX, CX, DX。同时为了和上一代保持兼容,这4个寄存器每个又可以分成2个8位寄存器。如AX可以分成AH,AL

2.3 几条汇编指令

mov ax,62627

add ax, ax

sub ax,1

mul

div 除数

  • 16位除以8位,被除数放在AX中,除数可以由8位寄存器或者内存单元提供,结果的商放在AL中,余数放在AH中
  • 32位除以16位,被除数的高16位放在DX中,低16位放在AX中,结果的商放在AX中,余数放在DX中
2.5 16位结构的CPU

16、32位或者64位等x位的CPU具有以下几方面特性:

  • 运算器一次最多可以处理x位的数据
  • 寄存器的最大宽度位x位
  • 寄存器和运算器之间的数据通路位x位
2.6 8086CPU 给出物理地址的方法

8086CPU有20位地址总线,可以传送20位地址,达到1MB寻址能力。8086CPU又是16位结构,在内部一次性处理,传输,暂存的地址位16位。如果将地址从内部简单地发出,那么它只能送出16位地址,表现出寻址能力只有64KB。8086CPU采取在内部用两个16位地址合成的方法来形成一个20位的物理地址。

2.7 “段地址x16+偏移地址=物理地址”的本质含义

CPU访问内存时,用一个基础地址(段地址x16)和一个相对于基础地址的偏移地址相加,给出内存单元的物理地址。

2.9 段寄存器

8086CPU有4个段寄存器:

CS Code Segment,用来和IP配合,实现执行流跳转

DS Data Segment,用来进行数据操作

SS Stack Segment,用来进行栈操作

ES

段寄存器不支持直接赋值(硬件电路设计如此),如mov CS,100h之类的指令是不行的,只能从其他寄存器赋值过来,如:

1
2
mov ax,100h
mov cs,ax
2.10 CS 和 IP

CS和IP是8086CPU中两个最关键的寄存器,它们指示了CPU当前要读取指令的地址。CS为代码段寄存器,IP为指令指针寄存器,从名称上我们可以看出它们和指令的关系。在8086PC机中,任意时刻,设CS中的内容为M,IP中的内容为N,8086CPU将从内存的Mx16+N单元开始,读取一条指令并执行。

在8086CPU加电启动或复位后(即CPU刚开始工作时)CS和IP被设置成CS=FFFF,IP=0000,即在8086PC机刚启动时,CPU从内存FFFF0单元中读取指令执行,FFFF0单元中的指令时8086PC机开机后执行的第一条指令。

2.11 修改CS 和 IP 的指令

要修改寄存器的内容,可以用mov指令,但是CS/IP的值不能用mov直接设置。8086CPU为CS/IP提供了另外的指令来改变它们的值。能够改变CS/IP的内容的指令被统称为转移指令。

JMP 段地址:偏移地址指令可以用来修改CS/IP。如jmp 2AE3:3执行后,CS=2AE3,IP=0003,CPU将从2AE33处读取指令

JMP 合法寄存器 指令可以用来单独IP的内容。如jmp AX 执行前AX=1000,CS=2000,IP=003。执行后AX=1000,CS=2000,IP=1000

第 3 章 寄存器内存访问

3.2 DS 和 [address]

8086CPU中,内存地址由段地址和偏移地址组成。段地址通常用DS寄存器保存,比如要读取10000单元的内容,可以用如下程序:

1
2
3
mov bx,1000h
mov ds,bx
mov al,[0] ;把10000(1000:0)中的数据读到al中,对于内存寻址的语法是用`[...]`表示
3.7 CPU的栈机制

现在的CPU都有栈机制,8086也不例外。

PUSH ax 表示把ax的内容放到栈中

POP ax 表示把栈顶元素放到ax中

1
2
3
4
5
6
7
push ax的执行,由以下两步完成:
1 sp = sp - 2,ss:sp指向当前栈前面的单元,以当前栈顶前面的单元为新的栈顶
2 将ax中的内容送入ss:sp指向的内存单元处,ss:sp此时指向新栈顶

pop ax的执行和上面的相反:
1 将ss:sp指向的内存单元处的数据送入ax
2 sp = sp + 2,ss:sp指向当前栈顶下面的单元,以当前栈顶下面的单元为心的栈顶

CPU的SS寄存器保存着栈顶地址,SP保存着偏移地址。入栈时,栈顶从高地址向低地址方向增长。

8086CPU并不会保证栈越界,这个需要编译器或者业务逻辑来实现。

第 4 章 第一个程序

4.2 源程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:codesg

codesg segment

st: mov ax,0123h
mov bx,0456h
add ax,bx
add ax,ax

mov ax,4c00
int 21h

codesg ends

end st

; end命令除了通知编译器程序结束外,还可以通知编译器程序入口在什么地方。
; 如上述的 end st,st就是一个入口说明符
  1. 伪指令

    • segment
    1
    2
    3
    xxx segment
    ;code ...
    xxx ends

    segmentends是一对伪指令关键字,用来定义一个段。

    一个汇编程序是由多个段组成的,这些段被用来存放代码、数据、或者当作栈空间来使用。一个有意义的汇编程序至少要有一个段,用来存放代码。

    • end 汇编语言的结束标记。在编译过程中,如果遇到end指令,就结束编译

    • assume 用来将特定用途的段和相关寄存器关联起来

  2. 程序结构

    汇编程序的几个基本要素和简单框架如下:

    1. 首先是要定义一个段,如abc。并在段中写入指令和数据

      1
      2
      3
      abc segment
      ; code ...
      abc ends
    2. 用end关键词来指定结束位置

    3. 将段和指定的段寄存器绑定起来

第 5 章 [BX]和loop指令

5.2 loop指令
1
2
3
4
		mov cx, loop_count
tag_name:
; code ...
loop tag_name

loop指令的格式是:loop 标号,CPU执行loop指令的时候,要进行两部操作:1. cx = cx - 1 ; 2. 判断cx中的值,不为0则转至标号处执行程序,如果为0则向下执行。

cx的值影响loop指令的执行状态,通常(一般是这样,也有例外)我们用loop实现循环时,cx就是用来存放循环次数的地方。

5.6 段前缀

在进行内存访问时,如指令mov ax,[bx]内存单元的偏移地址由bx给出,而段地址默认在ds中。我们可以在访问内存单元的指令中显式地给出内存单元的段地址所在的段寄存器。如:mov ax,ds:[bx] 。这样显式地指明内存单元段地址的叫做段前缀。

第 6 章 包含多个段的程序

6.3 将数据、代码、栈放入不同的段
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, ds:data, ss:stack

data segment vstart = 0
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
data ends

stack segment
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
stack ends

code segment
start: mov ax,stack
mov ss,ax
mov sp,20h ;设置栈顶ss:sp指向stack:20

mov ax,data
mov ds,ax ;ds指向data段

mov bx,0 ;ds:bx 指向data段中地第一个单元

mov cx,8

s: push [bx]
add bx,2
loop s ;以上将data段中的0~15单元中的8个字型数据依次入栈

mov bx,0

mov cx,8
s0: pop [bx]
add bx,2
loop s0 ;以上依次出栈8个字型数据到data段地0~15单元中

mov ax,4c00h
int 21h
code ends

end start

注意:

所有的segment的地址都是从程序开始(也就是从0开始)处计算的,在nasm编译器里面可以用:

1
section.段名称.start  ;来表示段的地址

不过segment内的地址会有点不一样。如果segment声明处有vstart=xx,则段内地址是从xx开始计算,如vstart=0x7c00,则第一条指令地址就是0x7c00,如果vstart=0,则第一条指令地址就是0。如果没有则是从整个程序头部开始计算。

第 7 章 更灵活的定位内存地址的方法

常用的寄存器除了前面提到的以外,还有sidi,这两个寄存器不能被分成两个独立8位寄存器的。从名字上来看应该是source index和destination index,主要用来做数据转移时记录index偏移

7.10 不同的寻址方式的灵活应用
  • [常量] 用一个常量来表示地址,可用于直接定位一个内存单元
  • [bx] 用一个变量表示内存地址,可用于间接定位一个内存单元
  • [bx+常量] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元
  • [bx+si] 用两个变量表示地址
  • [bx+si+常量] 用两个变量和一个常量表示地址

第 8 章 数据处理的两个基本问题

8.5 指令要处理的数据有多长

8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明时子操作还是字节操作。

  1. 通过寄存器名字指明要处理的数据尺寸。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ;下面的指令中,寄存器指明了指令进行的是字操作
    mov ax,1
    mov bx,ds:[0]
    mov ds,ax
    mov ds:[0],ax
    inc ax
    add ax,1000
    ;下面的指令中,寄存器指明了指令进行的是字节操作
    mov al,1
    mov al,bl
    mov al,ds:[0]
    mov ds:[0],al
    inc al
    add dl,100

    来源和目的地的寄存器长度需要一样,不然会报错。如果有数据源是内存地址或者常量,会根据寄存器的宽度来扩展。如果常量的长度超过寄存器宽度,也会报错。

  2. 在没有寄存器名字的情况下,可以用操作符x ptr指明内存单元的长度,x可以是word或byte。

    1
    2
    mov word ptr ds:[0],1  ;字操作
    mov byte ptr ds:[0],1 ;字节操作
  3. 如push/pop等操作默认就是按字来进行的。

第 9 章 转移指令的原理

可以修改IP,或者同时修改CS和IP的指令统称为转移指令。换句话说转移指令就是可以控制CPU执行内存某处代码的指令。

9.1 操作符offset

offset操作符在汇编语言中时由编译器处理的符号,它的功能是取得标号地偏移地址。如下面的程序:

1
2
3
4
5
6
7
8
assume cs:codesg

codesg segment
start: mov ax,offset start ;想当于mov ax,0
s: mov ax,offset s ;相当于mov ax,3
codesg ends

end start
9.2 依据位移进行转移的jmp指令

jmp short 标号 转移到标号处执行指令。这种格式的jmp是段内短转移。j不会带上目的地址,仅仅指示记录了相对于当前IP的偏移量。

jmp far ptr 标号 实现的是段间转移,又称为远转移。会带上CS:IP的地址

9.7 jcxz指令

jcxz 标号(如果cx寄存器的值位0,就转移到标号所在处执行) 指令是有条件转移指令,所有的有条件转移指令都是短转移,机器码中包含的是ip的位移而不是目的地址。

第 10 章 CALL 和 RET 指令

call 和 ret指令都是转移指令,它们都修改IP或者同时修改CS和IP。它们经常被一起用来实现子程序的设计。

10.1 ret和retf

ret指令执行时,cpu进行2步操作:

1
2
1. ip = ss * 16 + sp
2. sp = sp + 2

retf指令执行时,cpu进行4步操作:

1
2
3
4
1. ip = ss * 16 + sp
2. sp = sp + 2
3. cs = ss * 16 + sp
4. sp = sp + 2

可以看出,ret相当于汇编指令的

1
pop IP

retf相当于

1
2
pop IP
pop CS
10.2 call指令

call 标号 进行两部操作:

  1. 将当前IP或CS和IP压入栈中
  2. 转移

call指令不能实现短转移,除此之外,call和jmp基本上差不多。

10.4 转移地目的地地址在指令中的call指令

call far ptr 标号 实现段间转移

第 11 章 标志寄存器

8086CUP中的flag寄存器用来当作标志寄存器,大部分算数或者逻辑运算指令(add/sub/mul/div/inc/dec/or/and)会对标志寄存器有影响。而push/pop等传送指令则没有影响。而且有些指令的执行会影响多个标志位,比如指令sub al,al执行后,ZF、PF、SF等都会受影响。

1
2
15  14  13  12  11  10  09  08  07  06  05  04  03  02  01  00
OF DF IF TF SF ZF AF PF CF
11.1 ZF位

flag的第6位是ZF(Zero Flag)零标志位,记录相关指令执行以后,其结果是否位0,如果是0则位1

11.2 PF位

flag的第2位是PF(Parity Flag)奇偶标志位,记录相关指令执行以后,其结果的所有bit中1的个数是否位偶数个。如果是偶数就是1,否则就是0

11.3 SF位

flag的第7位是SF(Symbol Flag)符号标志位,记录相关指令执行以后,其结果是否位负。如果是负则为1,否则就是0

计算机中,有有符号数和无符号数,不过计算机都是统一用补码处理。所以同一个数据可以有两种解释,这里SF位就是表示如果数据按照有符号数运算时,结果是否位负数。

11.4 CF位

flag的第0位是CF(Carry Flag)进位标志位。一般情况下,无符号数运算时,它记录了运算结果的最高有效位向更高位的进位值,或从更高的借位值。

11.5 OF位

flag的第11位是OF(Over Flag)溢出标志位。有符号数运算时,它记录了运算结果是否有溢出。

11.6 adc指令

adc是带进位加法的指令,它利用了CF位上记录的进位值

1
adc obj1,obj2   ;obj1 = obj1+obj2+CF

主要用于对大于当前CPU字长的数据进行分步求和

11.7 sbb指令

和adc类似,在计算时会减去CF位上的值

11.8 cmp指令
1
cmp obj1,obj2   ; 实际上是执行的 obj1-obj2,结果放在ZF里

cmp指令相当于减法指令,只不过没有保存结果。如果两个数一样,则ZF为1,否则为0。如果obj1<obj2则在计算过程中会产生借位,此时CF=1,否则CF=0。cmp指令的结果可能会影响多个标志位

11.9 检测比较结果的条件转移指令

和高级语言的条件判断类似,这类指令会根据特定条件来执行跳转,执行不同的代码。如之前用过的jcxz,就是jump cx zero的缩写,如果cx是0的话就执行跳转。

除了jcxz外,还有很多常用的跳转指令

1
2
3
4
5
6
je     ;jump equal, zf=1
jne ;jump not equal, zf=0
jb ;jump below, cf=1
jnb ;jump not below, cf=0
ja ;jump above, cf=0 且 zf=0
jna ;jump not above, cf=1 或 zf=1
11.10 DF标志和串传送指令

flag的第10位是DF(Direction Flag)方向标志位。控制si/di的增加或者减小。DF=0,si/di递增,DF=1,si/di递减。

1
2
movsb  ;相当于mov es:[di], byte ptr ds:[si]
movsw ;相当于mov es:[di], word ptr ds:[si]

这两个命令常用来进行数据拷贝,通常搭配rep命令一起使用

1
2
3
4
mov cx, 3
cld ;表示clear director,用于把DF变成0
;对应的有std(set director),用于把DF变成1
rep movsb ;rep 命令表示重复movsb,重复的次数放在cx寄存器中
11.11 pushf和popf

pushf把标志寄存器压栈,popf是把寄存器弹出

常用指令列表

  • shr 右移
1
shr al,1

将一个寄存器或内存单元中的数据向右移位。将最右侧移出的一位写入CF中。最左侧用0补充。如果移动位数大于1,需要把移动位数放入cl中。这个指令不会进行符号位扩展,具有符号位扩展的指令是sar

  • shl 左移
1
shl al,1

将一个寄存器或内存单元中的数据向左移位。将最左移出的一位写入CF中。最右侧用0补充。如果移动位数大于1,需要把移动位数放入cl中。

  • repe movsw 表示在CX不为0时,且ZF=1重复执行后面的串处理指令。
  • MOVSW 移动一个字,如果DF=0,所以每次搬运完 SI、DI 会增加 2,而 CX 减少一
  • add / adc add是加法,adc是带进位加法,会将CF位的值一起加到目标操作数中
  • pusha / pushad 把所有通用寄存器压栈,d表示double word
  • bswap 表示byte swap,专用于大小尾变换的指令。如eax=00112233h,则变换后eax=33221100h
  • MOVZX 指令(进行全零扩展并传送)将源操作数复制到目的操作数,并把目的操作数 0 扩展到 16 位或 32 位。这条指令只用于无符号整数
  • MOVSX 指令(进行符号扩展并传送)将源操作数内容复制到目的操作数,并把目的操作数符号扩展到 16 位或 32 位。这条指令只用于有符号整数

  • 跳转相关

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
JB   ;无符号小于则跳转
JNB ;无符号不小于则跳转
JBE ;无符号小于等于则跳转 同JNA
JNBE ;无符号不小于等于则跳转 同JA

JG ;有符号大于则跳转
JNG ;有符号不大于则跳转
JGE ;有符号大于等于则跳转 同JNL
JNGE ;有符号不大于等于则跳转 同JL

JL ;有符号小于则跳转
JNL ;有符号不小于则跳转
JLE ;有符号小于等于则跳转 同JNG
JNLE ;有符号不小于等于则跳转 同JG

JZ ;为零则跳转
JNZ ;不为零则跳转

JS ;为负则跳转
JNS ;不为负则跳转

JC ;进位则跳转
JNC ;不进位则跳转

JO ;溢出则跳转
JNO ;不溢出则跳转

JP;为偶则跳转
JNP ;不为偶则跳转

JPE ;奇偶位置位则跳转 同JP
JPO ;奇偶位复位则跳转 同JNP

;https://blog.csdn.net/zmmycsdn/article/details/78511948

引用