第 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 | mov ax,100h |
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 | mov bx,1000h |
3.7 CPU的栈机制
现在的CPU都有栈机制,8086也不例外。
PUSH
ax 表示把ax的内容放到栈中
POP
ax 表示把栈顶元素放到ax中
1 | push ax的执行,由以下两步完成: |
CPU的SS
寄存器保存着栈顶地址,SP
保存着偏移地址。入栈时,栈顶从高地址向低地址方向增长。
8086CPU并不会保证栈越界,这个需要编译器或者业务逻辑来实现。
第 4 章 第一个程序
4.2 源程序
1 | assume cs:codesg |
伪指令
segment
1
2
3xxx segment
;code ...
xxx endssegment
和ends
是一对伪指令关键字,用来定义一个段。一个汇编程序是由多个段组成的,这些段被用来存放代码、数据、或者当作栈空间来使用。一个有意义的汇编程序至少要有一个段,用来存放代码。
end
汇编语言的结束标记。在编译过程中,如果遇到end指令,就结束编译assume
用来将特定用途的段和相关寄存器关联起来
程序结构
汇编程序的几个基本要素和简单框架如下:
首先是要定义一个段,如abc。并在段中写入指令和数据
1
2
3abc segment
; code ...
abc ends用end关键词来指定结束位置
将段和指定的段寄存器绑定起来
第 5 章 [BX]和loop指令
5.2 loop指令
1 | mov cx, loop_count |
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 | assume cs:code, ds:data, ss:stack |
注意:
所有的segment的地址都是从程序开始(也就是从0开始)处计算的,在nasm编译器里面可以用:
1 | section.段名称.start ;来表示段的地址 |
不过segment内的地址会有点不一样。如果segment声明处有vstart=xx,则段内地址是从xx开始计算,如vstart=0x7c00,则第一条指令地址就是0x7c00,如果vstart=0,则第一条指令地址就是0。如果没有则是从整个程序头部开始计算。
第 7 章 更灵活的定位内存地址的方法
常用的寄存器除了前面提到的以外,还有si
和di
,这两个寄存器不能被分成两个独立8位寄存器的。从名字上来看应该是source index和destination index,主要用来做数据转移时记录index偏移
7.10 不同的寻址方式的灵活应用
- [常量] 用一个常量来表示地址,可用于直接定位一个内存单元
- [bx] 用一个变量表示内存地址,可用于间接定位一个内存单元
- [bx+常量] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元
- [bx+si] 用两个变量表示地址
- [bx+si+常量] 用两个变量和一个常量表示地址
第 8 章 数据处理的两个基本问题
8.5 指令要处理的数据有多长
8086CPU的指令,可以处理两种尺寸的数据,byte和word。所以在机器指令中要指明时子操作还是字节操作。
通过寄存器名字指明要处理的数据尺寸。
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来源和目的地的寄存器长度需要一样,不然会报错。如果有数据源是内存地址或者常量,会根据寄存器的宽度来扩展。如果常量的长度超过寄存器宽度,也会报错。
在没有寄存器名字的情况下,可以用操作符x ptr指明内存单元的长度,x可以是word或byte。
1
2mov word ptr ds:[0],1 ;字操作
mov byte ptr ds:[0],1 ;字节操作如push/pop等操作默认就是按字来进行的。
第 9 章 转移指令的原理
可以修改IP,或者同时修改CS和IP的指令统称为转移指令。换句话说转移指令就是可以控制CPU执行内存某处代码的指令。
9.1 操作符offset
offset操作符在汇编语言中时由编译器处理的符号,它的功能是取得标号地偏移地址。如下面的程序:
1 | assume cs:codesg |
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 | 1. ip = ss * 16 + sp |
retf指令执行时,cpu进行4步操作:
1 | 1. ip = ss * 16 + sp |
可以看出,ret相当于汇编指令的
1 | pop IP |
retf相当于
1 | pop IP |
10.2 call指令
call 标号
进行两部操作:
- 将当前IP或CS和IP压入栈中
- 转移
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 | 15 14 13 12 11 10 09 08 07 06 05 04 03 02 01 00 |
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 | je ;jump equal, zf=1 |
11.10 DF标志和串传送指令
flag的第10位是DF(Direction Flag)方向标志位。控制si/di的增加或者减小。DF=0,si/di递增,DF=1,si/di递减。
1 | movsb ;相当于mov es:[di], byte ptr ds:[si] |
这两个命令常用来进行数据拷贝,通常搭配rep命令一起使用
1 | mov cx, 3 |
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 | JB ;无符号小于则跳转 |