【汇编语言】笔记 9~17章
1~8章:https://www.cnblogs.com/orangelsk/articles/16185577.html
转移指令的原理
转移指令用作单独修改IP或是同时修改CS和IP,为了让CPU执行我们指定的指令。
若只修改IP,则转移指令可分为:
-
段内短转移。IP修改范围为(-128~127)
-
段内近转移。IP修改范围为(-32768~32767)
若同时修改CS和IP,则转移指令可称为:
- 段间转移。这种方式对CS和IP的范围没有限制。
操作符offset
offset用来得出某个标号的偏移地址,例如:
s: mov bx,0
mov ax,offset s
offset s可得出s处的偏移地址,并将其赋给ax。
jmp指令
段内短转移
1、jmp short s
通过指明标号,将IP修改为标号所在处的偏移地址。
mov ax,0
jmp short s
add ax,2
s:inc ax
段内近转移
1、jmp near ptr s
和段内短转移类似,只不过范围大一些。
2、jmp ax
从寄存器中获取数据,作为IP的值。
mov ax,0123h
jmp ax
3、 jmp word ptr [0]
从内存中获取数据,作为IP的值。
mov ax,0123h
mov [bx],ax
jmp word ptr [bx]
段间转移
1、jmp far ptr s
执行后,CS=标号所在段的段地址,IP=标号所在段的偏移地址
c0 segment
jmp far ptr s
c0 ends
c segment
s: mov ax,0123h
c ends
2、jmp dword ptr [0]
dword表示操作的数据长度有2个字(4个字节),执行后,CS=高地址字的数据,IP=低地址字的数据
mov word ptr [0],10h
mov word ptr [2],76ah
jmp dword ptr [0]
CS=076a,IP=10h
jmp指令原理
jmp指令数据无条件的转移指令,在运行jmp指令时,CPU及内存会经过如下过程:
1、计算 CS * 16+IP,确定指令所在内存地址
2、将内存中的指令加载到指令缓冲期
3、IP指向下一条命令的偏移地址
4、CPU执行指令
想要jmp达到跳转的效果,有两种方式:
1、指明目标地址,如 jmp 076a:0010 或 jmp 0123
2、指明偏移地址,如 jmp 3,表示跳转到(当前地址+3)处的指令
例如 jmp short s,其编译后的机器码中包含的是偏移地址;jmp far s 编译后,机器码存储的是目标地址。
jmp short s
当程序运行到jmp指令时,IP已经指向了下一条命令,因此IP只需要+3。EB03中,03代表的就是偏移长度(或称作位移)。
位移的范围从-128到127,也称作8位位移(8位bit)。8位位移 = 标号所在偏移地址 - jmp下一条指令所在偏移地址。
jmp near ptr s 的原理相同,只不过是16位位移。
jmp far ptr s
编译后的指令是明确的地址。jmp ddword ptr [0] 的原理相同。
jcxz、loop
语法:jcxz 标号
jcxz指令是条件转移指令,同时属于短转移指令,位移范围为-128~127。它的运行机制是:若cx=0,则IP=IP+8位位移;否则执行下一条指令。用 C语言描述为:if (cx == 0) jcxz 标号。
语法:loop 标号
loop指令是循环转移指令,同属于短转移指令,运行机制是:若cx!=0,则jmp 标号;否则执行下一条指令。用C语言描述为:if (cx != 0) jmp 标号。
根据位移进行转移的好处
当程序载入内存,机器码记录的是位移,那么整段程序可以存储器其他位置并成功执行。若机器码记录的是绝对地址,那么程序移动到其他地址后,jmp跳转的是固定地址,程序会出错。
但是如果只将jmp指令移动到其他位置,执行时仍会出错。
CALL和RET指令
ret 和 retf
ret指令运行过程:
pop IP
retf指令运行过程:
pop IP
pop CS
call
call 指令无法实现短转移。
call指令运行过程:
将IP或CS和IP压栈
转移
1、call s
段内近转移。
push ip
jmp near ptr s
原理也是IP = IP + 16位位移。不像jmp指令可通过 short 控制位移长度。
2、call ax
段内近转移。
push ip
jmp ax
3、call word ptr [0]
段内近转移。
push ip
jmp word ptr [0]
4、call far ptr s
段间转移。
push cs
push ip
jmp far ptr s
5、call dword ptr [0]
段间转移。
push cs
push ip
jmp dword ptr [0]
ret 和 call 搭配使用
call之后,将下一条指令的IP/CS和IP push到栈中,之后跳转到其他标号/位置运行指令,这个其他位置可以用来编写子程序(这个过程个人理解为跳转到其他函数),在子程序最后通过ret/retf,pop栈中的IP/CS和IP,达到函数返回的效果,这样可以继续执行之前call指令下方的指令。
assume cs:code
code segment
main:
;
call sub1 ; 调用子程序sub1
;
mov ax,4c00h
int 21h
sub1:
;
call sub2 ; 调用子程序sub2
;
ret ; 子程序sub1返回
sub2:
;
ret ; 子程序sub2返回
code ends
end
mul指令
mul指令用作乘法运算,语法为 mul 寄存器
或 mul 内存单元
。mul运算的两个因子,要么都是8位,要么都是16位。
- 8位相乘:另一个因子默认存放在al中。运算的结果,存于ax中。
- 16位相乘:另一个因子默认存放在ax中。运算的结果存于ax和dx中,ax存放低8位,dx存放高8位。
模块化设计子程序
子程序类似高级语言的函数,需要有参数传递和结果返回的机制。在汇编语言中,是如何解决的?
问题一:参数传递和返回
- 通过寄存器传递参数,并将结果赋给寄存器。
这种做法相当于使用全局变量用作数据传递,编写多个子程序都能共享寄存器。若是参数过多,寄存器不够用,那么可以将参数分配到连续的内存上。
- 使用栈传递参数
先将参数push到栈段中,再运行call指令。这样做,参数会被拷贝到栈中,变得相对独立。子程序中,也能通过偏移量明确参数的位置。
问题二:寄存器冲突
外部调用程序和子程序共用一个寄存器,冲突不可避免。解决方案为:
- 备份现场 - 数据运算 - 恢复现场
在子程序运行到逻辑代码之前,先将当前用到的寄存器备份到内存(push到栈中),之后运算就不需要担心冲突。运算结束后,将备份重新赋值给寄存器。
fun: push cx
push si
; 进行运算
pop si
pop cx
ret
标志寄存器
标志寄存器是16位的寄存器,它的每一位bit具有特殊的含义。在不同的CPU中,标志寄存器的位数和发挥作用的bit位可能不同。
ZF,零标志位
相关指令执行后,结果是否为0。若为0,则ZF=1,否则ZF=0。
PF,奇偶标志位
相关指令执行后,结果的所有bit位中1的个数是否为偶数。若为偶数,则PF=1,否则PF=0。
SF,符号标志位
SF将运算看作有符号的运算,物理意义上的运算结果(最高bit位是0还是1)为负数,SF=1,否则SF=0。
不能通过SF判断逻辑运算结果。 逻辑上运算结果的正负,不一定与物理bit位显示的正负一致。因为有可能发生溢出,导致逻辑结果虽然是负数,但最高bit位是0。
CF,进位标志位
CF将运算看作无符号的运算,最高位产生了进位,或是最高位需要借位,CF=1,否则CF=0。
inc不影响CF位。
另外,shl/shr指令执行之后,将最后移处的一位写入标志寄存器的CF位中。左移/右移之后的空位补0。
OF,溢出标志位
OF将运算看作有符号的运算,逻辑运算的结果产生了溢出,则OF=1,否则OF=0。
DF,方向标志位
每次串传送指令结束后,如果DF=0,则si、di递增;
每次串传送指令结束后,如果DF=1,则si、di递减。
TF,单步标志位
每条指令执行后,如果标志寄存器的TF=1,则进入单步中断。
IF,可屏蔽标志位
CPU检测到可屏蔽中断信息,若IF=1,则响应;若IF=0,则不响应中断。
adc 和 sbb
-
adc
语法:adc [对象1],[对象2]
运算:对象1 = 对象1 + 对象2 + CF -
sbb
语法:sbb [对象1],[对象2]
运算:对象1 = 对象1 - 对象2 - CF
当计算大位数相加或相减时,可以通过adc和sbb指令计算。例如48bit数相加,拆分成高16bit、次高16bit、低16bit,只需:
sub ax,ax ; 将CF置为0
adc [低16bit],[低16bit]
adc [次高16bit],[次高16bit]
adc [高16bit],[高16bit]
sbb计算大位数相减,原理同理。
cmp
语法:cmp [对象1],[对象2]
运算:对象1 - 对象2
cmp指令不会存储运算结果,但是会影响指令寄存器,通过标志位可以判断对象1和对象2的大小关系。
cmp将运算看作无符号运算:
- zf=1,则 ax = bx
- zf=0,则 ax != bx
- cf=1,则 ax < bx
- cf=0,则 ax >= bx
- cf=0且zf=0,则 ax > bx
- cf=1或zf=1,则 ax <= bx
cmp将运算看作有符号运算:
- sf=1且of=0,则 ax < bx
- sf=1且of=1,则 ax > bx
- sf=0且of=0,则 ax >= bx
- sf=0且of=1,则 ax < bx
检测比较结果的转移指令
cmp指令与上述转移指令搭配使用,可以通过判断大小进行指令转移。例如:求一段内存中大于32的数字个数
mov ax,0
mov cx,8
s:
cmp byte ptr [bx],32
jna next
inc ax
next:
inc bx
loop s
这种搭配使用类似于一种编程技巧,本质是通过改变标志位实现跳转,所以并不是只有cmp指令可以达到类似的效果,其他的指令也可以。
串送指令(movsb、movsw)
串传送指令的相关内容:
每次串传送指令结束后,如果DF=0,则si、di递增;
每次串传送指令结束后,如果DF=1,则si、di递减。
-
movsb指令
语法:movsb
作用:将ds:si指向的字节内容,移动到es:di指向的字节。并根据DF的值,将si、di加1或减1。 -
movsw指令
语法:movsw
作用:将ds:si指向的字内容,移动到es:di指向的字。并根据DF的值,将si、di加2或减2。 -
rep指令
语法:rep movsb 或 rep movsw
作用:将某一连续内存的数据移动到另一处连续内存中。类似于:
s: mov es:[di],byte ptr ds:[si]
inc si ; DF=0
inc di
loop s
cld、std
cld:将DF置为0
std:将DF置为1
pushf、popf
-
pushf
语法:pushf
作用:将标志寄存器压栈 -
popf
语法:popf
作用:将栈顶内容弹出,并赋值给标志寄存器
Debug中的标志寄存器
内中断
概念
-
内中断
CPU内部发生某些问题,导致执行中的程序中断,即停止执行。 -
中断信号
用于标志是哪种问题引发的中断。8086CPU称之为中断信息码,有8位bit,可表示256种中断源。例如:
(1)除法溢出:0
(2)单步执行:1
(3)执行into指令:4
(4)执行int n指令:n -
中断处理程序
用于处理CPU内部问题。 -
中断向量表
用于确定处理不同中断源的中断处理程序,所在内存中的位置。
该表位于内存的0000:0000~03ff,大小为1024字节。可分为256个单元,每个单元2个字,低字存放偏移地址,高字存放段地址。0号单元对应0号中断源。
中断过程
中断发生后,若需要处理中断,则由某硬件执行下述过程,这一过程无法被程序员修改。
(1)CPU获得中断信息码N
(2)将标志寄存器入栈,pushf
(3)修改标志寄存器,TF=0 IF=0
(4)将当前CS入栈,push CS
(5)将当前IP入栈,push IP
(6)通过中断向量表确定中断处理程序位置,CS=[N* 4+2] IP=[N* 4]
(7)运行中断处理程序
中断处理程序过程
(1)保存程序用到的寄存器
(2)处理中断
(3)恢复使用的寄存器
(4)用iret指令返回
iret指令的作用:与中断过程适配,就像call与ret搭配,达到跳转程序的效果。
pop IP
pop CS
popf
单步中断、TF标志寄存器
单步中断是内中断的一种,中断信息码为1。每条指令执行后,如果标志寄存器的TF=1,则进入单步中断。
因此中断过程中,必须将TF置为0之后再运行中断处理程序,否则中断处理程序将一直陷入中断。
利用CPU提供单步中断的机制,可以实现单步跟踪程序的效果。
不响应中断
Debug中借助单步中断可以逐条跟踪程序,在运行
mov ss,ax
mov sp,10
中的mov ss,ax
后,会发现mov sp,10
也随之运行。这是因为像ss赋值的指令不会响应任何中断,不运行中断处理程序,Debug就无法跟踪。最终显示的结果就是一下运行两行指令。
mov ss不响应中断,是为了让我们在后面立即运行mov sp,这样可以保证ss和sp同步修改。防止因为中断导致ss修改而sp却未修改,进而导致栈使用出错。
编写中断处理程序
assume cs:code
code segment
start:
; 将子程序复制到 0000:0200h
mov ax,cs
mov ds,ax
mov si,offset func
mov ax,0
mov es,ax
mov di,200h
; 使用movsb命令连续复制
mov cx,offset funcend - offset func
cld
rep movsb
; 修改中断向量表中,具体指向的处理程序位置
mov word ptr es:[0],200h
mov word ptr es:[2],0
mov ax,4c00h
int 21h
func:
; 子程序内容
iret
funcend:
nop ; 用于求子程序大小
code ends
end start
int指令
执行int指令可以触发中断,去中断向量找到n号表项,进而找到中断例程在内存的位置,进而执行中断例程。使用int就像使用call,也算一种函数调用。
BIOS和DOS中断例程的安装过程
1、计算机开机,CPU接受电源,自动执行0FFFF:0000处的一条jmp指令,跳转到ROM处的BIOS程序
2、BIOS的中断例程是写死在ROM中,只需BIOS程序修改中断向量表,将其指向ROM处的地址
3、BIOS调用int 19h,将操作系统调入内存,由操作系统控制CPU等硬件资源
4、DOS将中断例程装入内存,并设置中断向量
BIOS中断例程
一个中断例程中包含许多子程序,通过设置ah的值,运行我们想要的子程序:
DOS中断例程
21h号中断例程,4c号子程序:用于程序返回。
21h号中断例程,9号子程序:用于在光标位置打印ds:dx处的字符串。
端口
端口的读写
CPU的总线除了接连内存,还连接到其他芯片,例如显卡、网卡芯片,这些芯片上的寄存器就是端口。CPU也通过地址总线确定读写哪个寄存器,在PC中,CPU的定位地址范围为0~65535。
通过 in、out命令对端口进行读写。读写的数据必须事先存放于al或ax中。若读写8位数据,则存放在al;若读写16位数据,则存放于ax。
对0~255以内的端口进行读写:
in al,20h ; 从20h端口读入一个字节
out 20h,al ; 往20h端口写入一个字节
对256~65535以内的端口进行读写,端口号放在dx中:
mov dx,3f8h ; 事先确定端口
in al,dx
out dx,al
shl和shr指令
shl命令将一个寄存器或内存单元的值左移。shr用于右移。
位移大小不同,shl/shr指令用法不同:
- 位移1位
mov al,01001000b
shl al,1
- 位移多位
mov al,01001000b
mov cl,5 ; 位移的长度写入cl中
shl al,cl
shl/shr指令执行之后,将最后移处的一位写入标志寄存器的CF位中。左移/右移之后的空位补0。
CMOS RAM芯片
特点:
-
CMOS芯片包含一个实时钟,一个128个单元的存储器
-
芯片靠电池工作,断电不影响时钟,存储器信息不会丢失
-
实时钟存储在128个单元存储器的0~dh单元中,14个字节
-
读写芯片RAM数据,需要先将几号单元写入70h号端口,然后从71号端口读写数据
RAM中时间的存储方式:
每个单位分别用一个字节存储。单位对应的单元号如下图:
BCD码:以4位2进制数表示十进制数。CMOS RAM中,时间单位一个字节,由两个BCD码组成。
外中断
由外部设备和CPU交互导致CPU中断指令,执行外设指令的操作,就是外中断。一般IO设备都会引发外中断,比如键盘敲入信息,在显示屏幕上显示,就会触发外中断。
可屏蔽和不可屏蔽
CPU可以决定是否响应外部的中断。
-
可屏蔽的外中断
CPU检测到可屏蔽的外中断信息时,检查IF标志,若IF=1,则响应外中断;若IF=0,则不响应外中断。 -
不可屏蔽的外中断
在8086CPU中,不可屏蔽中断的中断信息码固定为2,因此在处理中断程序时不需要CPU获取中断信息码。
sti、cli
若想在运行中断例程时仍然响应可屏蔽中断,则可自行设置IF的值。
sti:设置IF=1
cli:设置IF=0
PC机处理键盘中断
60h端口:键盘输入转化为电信号,最终转变为数据存入芯片端口,该端口就是主板的60h端口。CPU需要从60h端口读取数据并处理。
扫描码:从60h端口读取的数据是某个按键的扫描码,当按键按下时,传入60h的叫通码;当按键抬起,传入60h的叫断码。断码=通码+80h。
BIOS键盘缓冲区:属于BIOS的一块内存区域,当触发int 9中断例程,可以存放15个键盘输入,每个输入一个字,高字节存放扫描码,低字节存放字符码。
状态字节:0047:17处的字节,用于表示键盘的控制键和切换键的状态。8位bit中的每个bit代表一个按键状态。
中断过程:
-
键盘输入
-
触发9号中断
当60h端口产生数据,相关芯片向CPU发出int 9中断信号。CPU检测IF标志决定是否响应中断。 -
执行int 9中断例程
BIOS的int 9中断例程主要做:
(1)从60h端口读取数据,判断扫描码属于字符按键还是控制按键
(2)字符按键则读取扫描码和字符码进入BIOS键盘缓冲区
(3)控制按键则改变状态字节的bit位
(4)向相关芯片发出应答信息
指令系统小结
直接定址表
直接定址表就是哈希表,通过数组下标定位某个单元。在汇编中通过“不带:的标号”表示,“不带:号”的标号既可以表示地址也可以表示内存单元。
a[si]就算是直接定址表。a是“不带:号”的标号。
能表示单元长度的标号
assume cs:code
code segment
a db 1,2,3,4,5,6,7,8
b dw 0
code ends
end
对于标号a、b,它既能表示这段连续内存的偏移地址,也可以表示连续内存中的某块单元。
标号表示内存单元的例子:
- mov ax,b 相当于 mov ax,cs:[8]
- 但是 mov al,b 会编译错误,因此 b 表示的长度是一个字
- mov b,2 相当于 mov cs:[8],2
- inc b 相当于 inc word ptr cs:[8]
标号表示偏移地址的例子:
- mov al,a[si] 相当于 mov al,cs:0[si]
- 类似 idata[si]
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b ; 相当于 c dw offset a,offset b
c dd a,b ; 相当于 c dd offset a,seg a,offset b,seg b 双字中低字表示偏移地址,高字表示段地址
data ends
在其他段中使用标号
assume cs:code,ds:data
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
data ends
code segment
mov ax,data
mov ds,ax
mov al,a[0]
add b,ax
code ends
end
如果想在code段中使用标号a、b。需要提前将段名与寄存器关联。
程序入口的直接定址表
之前说过BIOS提供的int中断例程中包含很多子程序,通过ah设置具体调用哪个子程序。利用直接定址表,可以实现这种效果:
assume cs:code,ds:data
data segment
table dw sub1,sub2,sub3
data ends
code segment
mov al,ah
mov ah,0
mov bx,ax
call word ptr table[bx]
sub1: ; ...
sub2: ; ...
sub3: ; ...
code ends
end
使用BIOS进行键盘输入和磁盘读写
int 9处理键盘输入
(1)按下键盘,CPU响应int 9中断,运行中断例程
(2)CPU读取60h端口的数据
(3)CPU检查数据是否为字符码,
- (1)若为字符码,检查状态字节。比如状态字节表示shift被按下,则字符码a应转为A。再将扫描码和字符码送入BIOS缓冲区(扫描码占高地址,字符码占低地址)
- (2)若为状态码,则修改状态字节
int 16读取BIOS缓冲区
缓冲区是可以存房15个字的循环队列。从队列读取数据的方式如下:
mov ah,0 ; int 16的0号子程序用来从缓冲区读取一个字符
int 16
(1)程序调用int 16
(2)若缓冲区中有值,则将数据读出,ah存放扫描码,al存放ASCII码
(3)若缓冲区无值,则停止等待直到缓冲区有值存入
(4)将缓冲区的被读取的值删除
int 13读写磁盘
在3.5英寸的软盘中:有上下2面,每面80个磁道,每个磁道有18个扇区。每个扇区为512个字节。
读0面0道1号扇区,到0:200h处
mov ax,0
mov es,ax
mov bx,200h
mov ah,2 ; 2号程序为读扇区
mov al,1 ; 读取的扇区数
mov ch,0 ; 磁道号
mov cl,1 ; 扇区号
mov dl,0 ; 驱动器号,对于软驱:0表示软驱A,1表示软驱B;对于硬盘:80h表示C盘,81h表示D盘
mov dh,0 ; 磁头号,软盘有两面,一面一个磁头,0面即0号磁头
int 13
返回参数:
操作成功:ah=0,al=读入的扇区数亮
操作失败:ah表示出错代码
将0:200h处数据写0面0道1号扇区:
mov ax,0
mov es,ax
mov bx,200h
mov ah,3 ; 3号程序为写扇区
mov al,1 ; 写入的扇区数
mov ch,0 ; 磁道号
mov cl,1 ; 扇区号
mov dh,0 ; 磁头号
mov dl,0 ; 驱动器号
int 13
返回参数:
操作成功:ah=0,al=写入的扇区数亮
操作失败:ah表示出错代码