【汇编语言】笔记 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

image

当程序运行到jmp指令时,IP已经指向了下一条命令,因此IP只需要+3。EB03中,03代表的就是偏移长度(或称作位移)。

位移的范围从-128到127,也称作8位位移(8位bit)。8位位移 = 标号所在偏移地址 - jmp下一条指令所在偏移地址。

jmp near ptr s 的原理相同,只不过是16位位移。

jmp far ptr s

image

编译后的指令是明确的地址。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指令。这样做,参数会被拷贝到栈中,变得相对独立。子程序中,也能通过偏移量明确参数的位置。

image

问题二:寄存器冲突

外部调用程序和子程序共用一个寄存器,冲突不可避免。解决方案为:

  • 备份现场 - 数据运算 - 恢复现场

在子程序运行到逻辑代码之前,先将当前用到的寄存器备份到内存(push到栈中),之后运算就不需要担心冲突。运算结束后,将备份重新赋值给寄存器。

fun:	push cx
	push si
	; 进行运算
	pop si
	pop cx
	ret

标志寄存器

标志寄存器是16位的寄存器,它的每一位bit具有特殊的含义。在不同的CPU中,标志寄存器的位数和发挥作用的bit位可能不同。

image

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。

image

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

检测比较结果的转移指令

image

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中的标志寄存器

image

内中断

概念

  • 内中断
    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的值,运行我们想要的子程序:

image

image

DOS中断例程

21h号中断例程,4c号子程序:用于程序返回。

image

21h号中断例程,9号子程序:用于在光标位置打印ds:dx处的字符串。

image

端口

端口的读写

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中时间的存储方式:

每个单位分别用一个字节存储。单位对应的单元号如下图:

image

BCD码:以4位2进制数表示十进制数。CMOS RAM中,时间单位一个字节,由两个BCD码组成。

外中断

由外部设备和CPU交互导致CPU中断指令,执行外设指令的操作,就是外中断。一般IO设备都会引发外中断,比如键盘敲入信息,在显示屏幕上显示,就会触发外中断。

image

可屏蔽和不可屏蔽

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)向相关芯片发出应答信息

指令系统小结

image
image

直接定址表

直接定址表就是哈希表,通过数组下标定位某个单元。在汇编中通过“不带:的标号”表示,“不带:号”的标号既可以表示地址也可以表示内存单元。

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表示出错代码

posted @ 2022-05-17 20:35  moon_orange  阅读(185)  评论(0编辑  收藏  举报