王爽老师的<<汇编语言>>自学入门是最好的入门书籍,没有之一。
对汇编笔记更详细的整理及剖析,见博文Win32反汇编。
英文缩写
段地址(SA) segment address 偏移地址(EA) effective address
JCXZ---->jump when CX is zero
ret return
retf return far
补码
计算机中存储的整数,都是以补码的形式存储存在的
nop 指令
nop 指令占一个字节
2.12 我们用到的常用Debug功能。
- 用Debug的R命令查看、改变CPU寄存器的内容;
- 用Debug的D命令查看内存中的内容;
- 用Debug的E命令改写内存中的内容;
- 用Debug的U命令将内存中的机器指令翻译成汇编指令;
- 用Debug的T命令执行一条机器指令;
- 用Debug的A命令以汇编指令的格式在内存中写入一条机器指令。
3.2 DS
DS是段寄存器,通常存放要访问数据段的地址。
8086CPU不支持直接将数据传送到段寄存器,这属于硬件设计问题,需要用一个寄存器进行中转。
5.2 loop
loop是循环指令,在执行loop指令时,首先会检查cx - 1是否为0,不为零跳转到loop后面的标号出,为零不跳转。
例如:loop s,s为标号,实质代表的是一个地址。
7.7 SI DI
si di 是8086CPU中的两个和bx功能相近寄存器,需要注意的是 si di不能够分成两个8位的寄存器使用
8.1 寄存器bp
只要在[...]中使用寄存器 bp,而指令中没有显示的给出段地址,段地址就默认在ss中
只要在[...]中有bp这个寄存器,那么就是在栈中。
例如 mov ax, [bp] ax = (ss*16 + bp)
8.3
mov ax, ds:[bp] 从数据段地址和偏移地址指向的内存读取数据 给ax
mov ax, es:[bx] ....
mov ax, ss:[bx + si]....
需要思考下面这行指令
是把指令寄存器和偏移地址的值给ax
还是把 cs:ip 指向内存的值给ax呢???
mov ax, cs:[bx + si + 8]
根据上面的疑问 写段代码验证一下
assume cs:codesg
codesg segment
start: mov ax, 0
mov ax, cs:[0]
mov ax, 4c00h
int 21h
codesg ends
end start
编译 链接 上面的代码 在dosbox中debug 单步执行
-r 查看目前各个寄存器的状态
用 -u 查看机器码对应的汇编指令
-t 第一次单步执行后的AX状态
-t 第二次单步执行后的AX状态
由此得出结论 mov ax, cs:[0] ax的值为 cs:[0] 指向内存的值
8.5 byte ptr / word ptr 作用
在没有寄存器名存在的情况下,用操作符 X ptr 指明内存单元的长度,X 在汇编指令中可以为word或byte。
下面的指令 用word ptr 指明了指令访问的内存单元为一个字单元
mov word ptr ds:[0], 1
用byte ptr 指明了指令访问的内存单元为一个字节单元
mov byte ptr ds:[0], 1
push 指令 只进行字操作
8.7 div 指令
div 是除法指令,使用div时应注意以下问题
需要注意的是被除数的位数是除数的2倍
1 除数:有8位和16位两种,在一个reg或内存单元中。
2 被除数:默认在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数位16位,被除数则为32位,在DX和AX中存放,DX存放高16位,AX存放低16位,则AX存除法的商,DX存放除法的余数。
格式如下:
div reg
div 内存
div byte ptr ds:[0] 除数是一个8位的内存数据
al = ax / (ds*16 + 0)的商
ah = ax / (ds*16 + 0)的余数
div word ptr ds:[0] 除数是一个16位的内存数据
ax = (dx*10000h + ax) / (ds*16 + 0)的商
dx = (dx*10000h + ax) / (ds*16 + 0)的余数
8.8 伪指令
db 定义 字节 型数据(占8位)
dw 定义 字 型数据(占16位)
dd 定义 双字 型数据(占32位)
8.9 dup
dup 是一个操作符,在汇编语言中同 同db,dw,dd 等一样,也是由编译器识别处理的符号。
它是和db,dw,dd等数据定义伪指令配合使用的,用来进行数据的重复
举例:
(1) db 3 dup (0)
表示定义了3个字节,它们的值都是0 相当于 db 0,0,0
(2) db 3 dup (0, 1, 2)
表示定义了9个字节,它们的值是 0,1,2,0,1,2,0,1,2
相当于db 0,1,2,0,1,2,0,1,2
(3) db 3 dup ('abc', 'ABC')
表示定义了18个字节,它们是abcABCabcABCabcABC
相当于db 'abcABCabcABCabcABC'
思考:(3) 为什么不写成 'abc', 'ABC', 'abc', 'ABC', 'abc', 'ABC'
实质是 'abcABCabcABCabcABC' == 'abc', 'ABC', 'abc', 'ABC', 'abc', 'ABC' 在内存中是连续的
思考:(2) 为什么不写成(3)的形式呢
实质是 012012012(内存中一个值) != 0,1,2,0,1,2,0,1,2(内存中9个值)
格式:
db/dw/dd 重复次数 dup (重复的字节/字/双字)
如果要定义一个200个字节的栈段
statck segement
db 200 dup (0)
statck ends
9 转移指令的原理
9.1 操作符 offset
操作符 offset 在汇编语言中是由编译器处理的符号,它的功能是取得标号的偏移地址
codesg segemnt
start: mov ax, offset start 相当于 mov ax, start(此时 start 的值为 0)
s: mov ax, offset s 相当于 mov ax, s(此时 s 的值为 3)
codesg ends
9.2 jmp 指令
jmp 为无条件转移指令
转移指令的定义:控制CPU执行内存中某处代码的指令。
只修改IP时,称为段内转移,段内转移分两种
1 IP的范围 -128-127 称为 短转移 如 jmp short 标号 或则 jpm 标号
功能为 IP = IP + 8位 位移(8位:一个字节的整数)
2 IP的范围 -32768-32767 称为 近转移 如 jmp near ptr 标号 或则 jmp reg(16位 reg)
功能位 IP = IP + 16位 位移(16位:2个字节的整数)
以上的段内短转移或是段内近转移都不会在内存机器码中看到目标偏移地址,能够看到的是相对偏移地址,汇编代码和debug查看汇编代码对比
assume cs:codesg
codesg segment
start:mov ax,0
jmp short s
add ax,1
s:inc ax
codesg ends
end start
jmp 汇编这行 对应的机器码并没有偏移目标地址 而EB03中的03代表的是相对偏移地址,那么又是如何跳转到 CS:0008处的呢?
1 CPU 读取了 EB03 这条指令到指令缓存器
2 IP = IP + 所读取的指令长度, IP = 0003 + 0002,从而指向下一条指令 0005
3 CPU 执行指令缓存器中的指令 EB 03, IP = 0005 + 0003
9.4 转移的目标地址在指令中 jmp 指令
上一节讲的jmp指令,其对应的机器码中没有转移的目标地址,而是相对于当前IP的转移位移。
jmp far ptr 标号 实现的是段间的转移。
far ptr 标号 指明了用标号的段地址和标号的偏移地址修改CS和IP
assume cs:codesg
codesg segment
start:mov ax,0
mov bx,0
jmp far ptr s
db 256 dup (0)
s:add ax,1
inc ax
codesg ends
end start
仔细看jmp行的机器码及汇编代码
9.5 转移地址在寄存器中的 jmp 指令
指令格式 jmp 16 位reg
功能:IP = (16位reg)
举例 jmp ax 相当于 mov IP, ax
9.6 转移地址在内存中的 jmp 指令
1 jmp word ptr 内存单元(段内转移)
功能:从内存单元地址开始处存放着一个字,是转移的目的偏移地址。
mov ax, 0123H
mov ds:[0], ax
jmp word ptr ds:[0] 只修改IP
执行后的IP = 0123H
2 jmp dword ptr 内存单元地址(段间转移)
功能:从内存单元开始处存放着两个字,高地址处的字是目的的段地址,低地址处的字是转移目标的偏移地址。
CS = (内存单元地址 + 2)
IP = (内存单元地址)
mov ax, 0123H
mov ds:[0], ax
mov word ptr ds:[2], 0
jmp dword ptr ds:[0] CS IP 同时修改 高地址为段地址,低地址为偏移地址
执行后 CS = 0,IP = 0123H
9.7 jcxz 指令
是 jump when CX is zero 缩写
功能:只要 CX 为 0 就跳转
if(0 == CX)
{
jmp short 标号
}
用C语言和汇编语言进行综合描述
9.8 loop 指令
if(0 != CX)
{
jmp short 标号
}
执行一次 loop指令 CX = CX -1
以上就是loop指令的描述
9.9 根据位移进行转移的意义
jmp short 标号
jmp near ptr 标号
jcxz short 标号
loop 标号
以上都是根据位移 进行转移的汇编指令,没有具体的转移目标地址,在内存中的浮动装配更灵活。
10 CALL 和 RET RETF 指令
ret 指令用栈中的数据,修改IP,从而实现近转移(-32768-32767 栈中的数据都是以字的形式)
retf 用栈中的数据修改 CS IP 内容 从而实现远转移
CPU 执行 ret 指令时 进行下面两个步骤
1 IP = ss*16 + sp(书写格式容易造成误解,应该是:mov ip,ss:[sp],取出栈里面存的IP的值)
2 sp = sp + 2
以上等同于 pop IP
CPU 执行 retf 指令时 进行四个步骤
1 IP = ss*16 + sp(书写格式容易造成误解,应该是:mov ip,ss:[sp],取出栈里面存的IP的值)
2 sp = sp + 2
3 CS = ss*16 + sp(书写格式容易造成误解,应该是:mov ip,ss:[sp],取出栈里面存的cs的值)
4 sp = sp + 2
以上等同于
pop ip
pop cx
共同点是 都要先给ip值
10.2 call 指令
CPU 执行 call指令时,进行两步操作
1 将当前的IP或CS和IP压入栈中
2 转移
10.3 依据位移进行转移的 call 指令
call 标号(将当前IP压栈后,转到标号处执行指令)
CPU执行此种格式的指令时,进行如下操作
sp = sp -2
ss*16 + sp = ip//正常的指令是 mov ss:[sp], ip
ip = ip + 16位 位移
以上用汇编表示如下:
push ip
jmp near ptr 标号
思考如下代码
1000:0 b8 00 00 mov ax, 0
1000:3 e8 01 00 call s
1000:6 40 inc ax
1000:7 58 s:pop ax
思考程序运行后 ax的值为多少? 答案是6
10.4 转移的目标地址在指令中call中的指令
call far ptr 标号
实现的是段间的转移,CPU执行此指令时,进行如下操作
sp = sp -2
ss*16 + sp = cs// mov ss:[sp], cs
sp = sp -2
ss*16 + sp = ip// mov ss:[sp], sp
cs = 标号所在段的地址
ip = 标号所在段的偏移地址
用汇编语法解释如下
push cs
push ip
jmp far ptr 标号
思考如下,段地址、偏移地址都在 call far ptr s 这行的机器码中
10.5 转移地址在寄存器中的 call指令
call 16位 reg
sp = sp -2
ss*16 + sp = ip// mov ss:[sp], ip
ip = reg(16位)
//
用汇编语法解释位
push ip
jmp reg(16位)
//
jmp reg == mov ip, reg
注意 call ax 所在行的机器码中并没有偏移地址的机器码
10.6 转移地址在内存中的call指令
call word ptr 内存单元
用汇编语法解释
push IP
jmp word ptr 内存单元地址
call dword ptr 内存单元
用汇编语法解释
push cs
push ip
jmp dword 内存单元地址
10.7 call ret 配合使用
call 是入栈 ret或retf 是出栈 入栈时存储的是 IP或是CS + IP,所以出栈可以取得IP或是IP + CS
由此完成像函数调用过程,实质高级语言的函数调用过程就是执行汇编的call ret/retf 过程
关于函数的参数,可以用寄存器也可以用栈,用栈的寻址方式指定N个参数
关于函数的返回值,可以用寄存器也可以用栈,用栈的寻址方式指定N个参数
参数少用寄存器 参数多用栈
10.8 mul 指令
mul 是乘法指令
一、两个相乘的数,位数要相同,若不同则用高位寄存器或是内存地址
二、8位的乘法结果默认放到AX中,16位的乘法结果的高位放到DX中,结果的低位放到AX中
格式如下
// AX = AX * reg
mul reg
// AX = AX * 内存一个(字节)值
mul 内存单元8位
// dx = ax * 内存一个(字)的值 (结果高位)
// ax = ax * 内存一个(字)的值 (结果低位)
mul 内存16位
11 标志寄存器
flag
flag寄存器的结构 16位
flag寄存器和其它寄存器不一样,其它寄存器都是都是用来存放数据,都是整个寄存器具有一个含义。而flag寄存器是按位起作用的,也就是说它的每一位都有专门的含义,记录特定的信息
ZF标志
ZF(zero flag)
零标志位,记录相关指令执行后,其结果是否为0,如果结果为0,那么ZF = 1,否则 ZF = 0。
mov ax, 1
sub ax, 1
CPU执行后,其结果ax为 0,那么flag寄存器的第六位标志位ZF的值为1
执行逻辑或算术运算(add、sub、mul、div、inc、or、and等)会影响flag寄存器的标志位,而 mov、push、pop 等不会。
PF标志
PF(parity flag)
奇偶标志位,记录相关指令执行后,其结果的所有bit位中 1 的个数是否位偶数,如果为偶数 pf =1,否者 pf = 0。
mov al,1
add al,10
执行后,结果为00001011B,其中有3(奇数)个1,则PF=0;
mov al,1
or al,10
执行后,结果为00000011B,其中有2(偶数)个1,则PF=1
sub al, al
执行后,结果为00000000B,其中有0 (偶数) 个1,则pf=1
SF标志
SF(Sign Flag)
mov al, 10000001B //129或则是-127(补码)
add al, 00000001B //1
结果:al=10000010B
以上是计算机只运行一次的二进制结果,但是结果却有两种意义
如果进行的是无符号运算,其结果为 130
如果进行的是有符号运算,其结果为-126,但是无论是130也好,-126也好,其二进制的结果
10000010B
接下来说SF标志,若是当作有符号来运算,可以通过SF的值得知结果的正负,
若是当做无符号来运算,SF的值没有意义,
结果为负 sf=1,非负 sf=0
CF标志
CF(Carry Flag)
对无符号运算结果影响CF标志
记录进位值/借位值,当两个数相加,其结果无法用8bit表示,需要进一位时CF为1
mov al, 98H
add al, al
执行后,al= 30H,cf=1 cf记录了从最高有效位向更高位的进位值
add al, al
执行后 al = 30H,cf=0
以上是相加
当两个数做减法的时候,有可能向更高位借位
97H - 98H 将产生借位,借位后,相当于197H - 98H。而flag的CF位也可以用来记录这个借位值
mov al,97H
sub al,98H
执行后,al = FF,cf=1
sub al, al
执行后 al = 0,cf = 0
OF标志
OF(OverFlow Flag)
溢出的定义:在进行有符号运算的时候,结果超出了机器所能表示的范围称为溢出。
只要溢出产生,of = 1
一定要注意CF OF的区别:CF是对无符号运算有意义的标志位,OF是对有符号运算的标志位。
对于8位有符号数据 范围 -128~127
对于16位有符号数据范围 -32678~32677
mov al, 129
sub al, 2
首先 注意以上两行汇编,数值后面没有加上H,用的是10进制表示
cpu执行后的一种二进制结果,却有两种不同结果的含义(这就是整数在计算机中以补码存储的好处)
若是进行的时无符号运算 al = 129 - 2 = 127
若是进行的是有符号运算 al = -127(129的补码) - 2 = -131 显然 -131 已经超出8位有符号范围,
所以就是溢出了
adc指令
adc是带进位加法指令,它利用了CF位上记录的进位值。
指令格式:adc 操作对象1,操作对象2
功能:操作对象1 = 操作对象1 + 操作对象2 + CF
举例:adc ax, bx 实现的功能就是: ax = ax + bx + CF
adc 指令执行后同样会影响 CF(操作对象1 + 操作对象2 + CF 后的结果产生进位,那么就会影响CF,运行后的结果没有进位,那么就自然不会影响CF)
我们来看一下两个数据:用adc指令进行0198H和0183H相加是如何进行的呢:
01 98
+
01 83
1(进位值CF)
-----------------
03 1B
可以看出,加法可以分两步来进行:
(1)低位相加;
(2)高位相加再加上低位相加产生的进位值。
目前为止的加法add指令最多是16位进行,如何进行更高位的加法呢?
由上面的理论,我们完成两个24位数相加:
1EF000H
+
201000H
思路:用两个可以存储高位、低位的寄存器,可任意指定reg,并非一定是如下的AX、BX。
mov ax, 001EH // 用AX 存放高位
mov bx, F000H // 用BX 存放低位
第一个完整的数被拆分两部分,AX、BX。
执行和第二个数相加,自然是先从低位相加开始
add bx, 1000H
adc ax, 0020H
以上就完成了两个24位数的相加,思考如下代码是否可行?
adc bx, 1000H
adc ax, 0020H
同上面的代码唯一的区别第一行汇编用adc代替add。
答案是可行的。但是有一个先决的条件,CF必须要为 0。
因为 adc bx, 1000H 执行解释是 bx = bx + 1000H + CF,如果CF不为 0 结果是不正确的。
sbb指令
sbb是带借位减法指令。
指令格式:sbb 操作数1, 操作数2
功能:操作数1 = 操作数1 - 操作数2 - CF
举例,计算003E100OH–00202000H,结果放在ax,bx中,程序如下:
mov bx,1000H
mov ax,003EH
sub bx,2000H
sbb ax,0020H
sbb同adc同样设计的两条指令,不再叙述。