📌 汇编语言是很多相关课程(如数据结构、操作系统、微机原理)的重要基础。但仅仅从课程的角度出发就太片面了,其实学习汇编语言可以深入理解计算机底层工作原理,提升代码效率,尤其在嵌入式系统和性能优化方面有重要作用。此外,它在逆向工程和安全领域不可或缺,帮助分析软件运行机制并增强漏洞修复能力。 本专栏的汇编语言学习章节主要是依据王爽老师的《汇编语言》来写的,和书中一样为了使学习的过程容易展开,我们采用以8086CPU为中央处理器的PC机来进行学习。
现在,我们将讨论用“查表”的方法编写相关程序的技巧。
任务:编写子程序,以十六进制的形式在屏幕中间显示给定的字节型数据。
一个字节需要用两个十六进制数码来表示,所以,子程序需要在屏幕上显示两个ASCII 字符。
我们当然要用“0”、“1”、“2”、“3”、“4”、“5” 、“6” 、“7” 、“8” 、“9” 、“A”、“B”、“C”、“D”、“E”、“F”这16个字符来显示十六进制数码。我们可以将一个byte的高4位和低4 位分开,分别用它们的值得到对应的数码字符。比如 2Bh ,我们可以得到高4 位的值为2,低4 位的值为11。
那么我们如何用这两个数值得到对应的数码字符“2”和“B”呢?
我知道,我们一看就知道是 2和 B ,但CPU它不懂,它只懂 1 和 0。最简单的办法就是一个一个地比较,如下:
如果数值为 0,则显示“0”;
如果数值为 1,则显示“1”; :
:
如果数值为15,则显示“F”;
我们可以看出,这样做,程序中要使用多条比较、转移指令。程序将比较长,混乱。
显然,我们希望能够在数值0~15和字符“0 ~ F”之间找到一种映射关系。这样我们用0~15间的任何数值,都可以通过这种映射关系直接得到“0”~“F”中对应的字符。
(1)
数值0~9和字符“0”~“9”之间的映射关系是很明显的,即:
数值 + 30h = 对应字符的ASCII值
0+30h = “0”的ASCII值
1+30h = “1”的ASCII值
:
:
2+30h = “2”的ASCII值
(2)
但是,10~15和“A”~“F”之间的映射关系是:
数值+37h=对应字符的ASCII值:
10+37h=“A”的ASCII值
11+37h=“B”的ASCII值
12+37h=“C”的ASCII值
: :
可见,我们是利用数值和字符之间的这种原本存在的映射关系,通过高 4 位和低4 位值得到对应的字符码。
但我们发现一个问题……由于映射关系的不同,我们在程序中必须进行一些比较,对于大于9的数值,我们要用不同的计算方法。
这样做,虽然使程序得到了简化。但是,如果我们希望用更简捷的算法,就要考虑用同一种映射关系从数值得到字符码。所以,我们就不能利用0~9和“0”~“9” 之间与 10~15 和 “A” ~“F ” 之间原有的映射关系。因为他们有两个映射关系,不满足我们的条件。
具体的做法是,我们建立一张表,表中依次存储字符“0”~“F”,我们可以通过数值0~15直接查找到对应的字符。
子程序如下:
;用al传送要显示的数据
showbyte:
jmp short show
table db '0123456789ABCDEF' ;字符表
show: push bx
push es
mov ah,al
shr ah,1
shr ah,1
shr ah,1
shr ah,1 ;右移4位,ah中得到高4位的值
and al,00001111b ;al中为低4位的值
mov bl,ah
mov bh,0
mov ah,table[bx] ;用高4位的值作为相对于table的偏移,取得对应的字符
mov bx,0b800h
mov es,bx
mov es:[160*12+40*2],ah
mov bl,al
mov bh,0
mov al,table[bx] ;用低4位的值作为相对于table的偏移,取得对应的字符
mov es:[160*12+40*2+2],al
pop es
pop bx
ret
可以看出,在子程序中,我们在数值0~15和字符“0”~“F ” 之间建立的映射关系为:以数值N为table 表中的偏移,可以找到对应的字符。
利用表,在两个数据集合之间建立一种映射关系,使我们可以用查表的方法根据给出的数据得到其在另一集合中的对应数据。
这样做的目的一般来说有三个:
在刚刚的子程序中,我们更多的是为了算法的清晰和简洁,而采用了查表的方法。下面我们来看一下,为了加快运算速度而采用查表的方法的情况。
任务:编写一个子程序,计算sin(x),x∈{0°,30°,60°,90°,120°,150°,180°},并在屏幕中间显示计算结果。
例如sin(30) 的结果显示为“0.5”。
我们可以利用麦克劳林公式来计算sin(x)。 x 为角度,麦克劳林公式中需要代入弧度,则:
可以看出,计算sin(x)需要进行多次乘法和除法。乘除是非常费时的运算,它们的执行时间大约是加法、比较等指令的5倍。
那么我们如何才能够不做乘除而计算sin(x)呢?
我们看一下需要计算的sin(x)的结果:
我们可以看出,其实用不着计算,可以占用一些内存空间来换取运算的速度。
将所要计算的sin(x) 的结果都存储到一张表中;然后用角度值来查表,找到对应的sin(x)的值。
我们用 ax 向子程序传递角度。
子程序如下:
showsin:
jmp short show
table dw ag0,ag30,ag60,ag90,ag120,ag150,ag180 ;字符串偏移地址表
ag0 db '0',0 ;sin(0)对应的字符串“0”
ag30 db '0.5',0 ;sin(0)对应的字符串“0.5”
ag60 db '0.866',0 ;sin(0)对应的字符串“0.866”
ag90 db '1',0 ;sin(0)对应的字符串“1”
ag120 db '0.866',0 ;sin(0)对应的字符串“0.866”
ag150 db '0.5',0 ;sin(0)对应的字符串“0.5”
ag180 db '0',0 ;sin(0)对应的字符串“0”
show: push bx
push es
push si
mov bx,0b800h
mov es,bx
;以下用角度值/30 作为相对于table的偏移量,取得对应的字符串的偏移地址,放在bx中
mov ah,0
mov bl,30
div bl
mov bl,al
mov bh,0
add bx,bx
mov bx,table[bx]
;以下显示sin(x)对应的字符串
mov si,160*12+40*2
shows:
mov ah,cs:[bx]
cmp ah,0
je showret
mov es:[si],ah
inc bx
add si,2
jmp shows
showret:
pop si
pop es
pop bx
ret
在上面的子程序中,我们在角度值x和表示 sin(x) 的字符串集合table 之间建立的映射关系为:以角度值 /30 为table 表中的偏移,可以找到对应的字符串的首地址。
编程的时候要注意程序的容错性,即对于错误的输入要有处理能力。
在上面的子程序中,我们还应该在加上对提供的角度值是否超范围的检测。
如果提供的角度值不在合法的集合中,程序将定位不到正确的字符串,出现错误。
对于角度值的检测,大家请自行完成。
上面的两个子程序中,我们将通过给出的数据进行计算或比较而得到结果的问题,转化为用给出的数据作为查表的依据,通过查表得到结果的问题。
具体的查表方法 ,是用查表的依据数据 ,直接计算出所要查找的元素在表中的位置。像这种可以通过依据数据,直接计算出所要找的元素的位置的表,我们称其为:直接定址表。
我们可以在直接定址表中存储子程序的地址,从而方便地实现不同子程序的调用。
实现一个子程序setscreen ,为显示输出提供如下功能:
那么入口参数如何设置呢?
入口参数说明:
下面,我们讨论一下各种功能如何实现:
我们将这4 个功能分别写为 4 个子程序,请大家根据编程思想,自行读懂下面的程序。
;功能子程序1:清屏
sub1:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,0
mov cx,2000
sub1s:
mov byte ptr es:[bx],' '
add bx,2
loop sub1s
pop es
pop cx
pop bx
ret ;sub1 ends
;功能子程序2:设置前景色
sub2:
push bx
push cx
push es
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub2s:
and byte ptr es:[bx],11111000b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret ;sub2 ends
;功能子程序3:设置背景色
sub3:
push bx
push cx
push es
mov cl,4
shl al,cl
mov bx,0b800h
mov es,bx
mov bx,1
mov cx,2000
sub3s:
and byte ptr es:[bx],10001111b
or es:[bx],al
add bx,2
loop sub2s
pop es
pop cx
pop bx
ret ; sub3 ends
;功能子程序4:向上滚动一行
sub4:
push cx
push si
push di
push es
push ds
mov si,0b800h
mov es,si
mov ds,si
mov si,160 ;ds:si指向第n+1行
mov di,0 ;es:di指向第n行
cld
mov cx,24 ;共复制24行
sub4s:
push cx
mov cx,160
rep movsb ;复制
pop cx
loop sub4s
mov cx,80
mov si,0
sub4s1:
mov byte ptr es:[160*24+si],' ' ;最后一行清空
add si,2
loop sub4s1
pop ds
pop es
pop di
pop si
pop cx
ret ;sub4 ends
我们可以将这些功能子程序的入口地址存储在一个表中,它们在表中的位置和功能号相对应。 对应的映射关系为:功能号*2=对应的功能子程序在地址表中的偏移。
程序如下:
setscreen: jmp short set
table dw sub1,sub2,sub3,sub4
set:
push bx
cmp ah,3 ;判断传递的是否大于 3
ja sret
mov bl,ah
mov bh,0
add bx,bx ;根据ah中的功能号计算对应子程序的地址在table表中的偏移
call word ptr table[bx] ;调用对应的功能子程序
sret:
pop bx
iret
当然,我们也可以将子程序setscreen如下实现:
setscreen:
cmp ah,0
je do1
cmp ah,1
je do2
cmp ah,2
je do3
cmp ah,3
je do4
jmp short sret
do1:
call sub1
jmp short sret
do2:
call sub2
jmp short sret
do3:
call sub3
jmp short sret
do4:
call sub4
sret:
iret
显然,用通过比较功能号进行转移的方法,程序结构比较混乱,不利于功能的扩充。比如说,在 setscreen 中再加入一个功能,则需要修改程序的逻辑,加入新的比较、转移指令。
用根据功能号查找地址表的方法,程序的结构清晰,便于扩充。如果加入一个新的功能子程序,那么只需要在地址表中加入它的入口地址就可以了。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下。
也可以点点关注,避免以后找不到我哦!