1、为何除法会溢出
看到这个标题,你可能会问汇编中不是有div指令来实现除法运算吗?为何咱们还要本身写一个子程序来实现除法?为了说明咱们为何须要本身写一个实现除法的子程序,还得从除法为何会发生溢出提及。
在汇编中,若是要使用除法运算,咱们可使用div指令,它实现的就是除法的功能,可是它是一个很是容易,甚至说不可避免会发生溢出的指令,下面来看看它的工做方式,咱们就能知道个中源由。注:这里所说的除法溢出并非指分母为0而发生溢出的状况。
div的工做方式:
(1)除数:有8位和16位两种,在一个寄存器或内存单元中
(2)被除数:默认放在AX或DX和AX中,若是除数为8位,则被除数为16位,默认在AX中存放;若是除数为16位,被除数为32位,在DX和AX中存放,DX存放高16位,AX存放低16位
(3)结果:若是除数为8位,则AL(AX的低8位)存储除法操做的商,AH(AX的高8位)存储除法操做的余数;若是除数为16们,则AX存储除法操做的商,DX存放除法操做的余数。
用一个表格来表示上述的工做方式,以下表所示:
除数 |
被除数
|
结果
|
8位 |
16位,AX
|
商:AL,余数:AH
|
16位
|
32位,DX(高16位)+AX(低16位)
|
商:AX,余数:DX
|
就这么一看彷佛尚未什么问题,下面我就以一个例子来讲明一下种工做方式下的除法的致命缺陷。
为了更加容易地说明,咱们以除数为8位的状况来讲明,假设咱们的被除数为65535(16位的最大值)存储在AX中,除数为1,存储在CL中,而后执行除法指令: div CL。根据上面的说法,结果都是放在AX中的,余数为0,固然这没有问题,然而商为65535要放在AL中倒是放不下的,由于AL能存放的最大值只为255,因此此时就会发生溢出。咱们能够看到65535/1 = 255,这显然与我位正常的除法结果不符。
在这里你能够认为我举了一个很是极端的例子,可是这种状况却并非极端的状况,对于除数为8位的除法,只要商在255以上就会发生这种溢出。除数为16位的原理及状况与这里相同,只是它是当商为65535以上是就会发生溢出而已。
2、如何解决这个溢出问题
既然咱们知道了问题的根源,要解决它就不困难了。为了统一并且不发生溢出,咱们能够把全部的除法的商都用32位来保存,即全部的被除数都用32位,全部的除数都用16位,全部的商都用32位,全部的余数都用16位来保存,就能够解决这个问题,由于一个数除以一个整数后,不可能大于其以前的值。而对于8位的除法,除数和被除数的高位全用0补全便可。为了达到这个目的,咱们就不能使用默认的除法指令div了,而须要咱们写代码来实现咱们自定义的除法。
考虑到除法是一个经常使用的操做,因此咱们能够编写一个子程序来实现除法,在进行除法运算时,直接调用咱们本身定义的子程序来完成任务,而不直接使用div指令。
3、如何实现这个功能
要实现除法还得使用div指令,只是咱们能够作一些特殊的处理,让咱们自定义的除法不发生溢出,由于咱们使用的是除数为16位的除法,也就是说,咱们只要能保证除法的商不大于65535就不会发生问题。为了实现这个功能,咱们首先要知道一个关于除法的公式(H表示X的高位,L表示X的低位):
X/N = int(H/N)* 2^16 + [rem(H/N)* 2^16+L]/N
这个公式告诉咱们32位的被除数与16位的除数,能够拆分为两个除数和被除数都为16位的数的除法,而后经过加法来获得一样的结果,这个是很是重要的。由于咱们能够把H和L独立起来考虑,当进行H/N时,由于H存储在DX中,咱们能够先把DX中的内容复制到AX中(操做以前把AX的内容保存好),再把DX的内容置为0,这样产生的除法操做的商就必定能放在一个16位的寄存器AX中。对于公式中的其余除法运算也采用相同的操做,而后经过把除法以前产生的结果进行相加,就能产生咱们的无溢出除法子程序。
4、实现代码
基于这个公式,咱们实现的代码以下:
;子程序名称:divdw ;功能:进行不会产生溢出的除法运算,被除数为dword型 ; 除数为word型,结果为dword型 ;参数: (ax)=dword型数据的低16位 ; (dx)=dword型数据的高16位 ; (cx)=除数 ;返回: (dx)=结果的高16位,(ax)=结果的低16位 ; (cx)=余数 ;计算公式:X/N=int(H/N)*2^16+[rem(H/N)*2^16+L]/N divdw: jcxz divdw_return ;除数cx为0,直接返回 push bx ;做为一个临时存储器使用,先保存bx的值 push ax ;保存低位 mov ax, dx ;把高位放在低位中 mov dx, 0 ;把高位置0 div cx ;执行H/N,高位相除的余数保存在dx中 mov bx, ax ;把商保存在bx寄存器中 pop ax ;执行rem(H/N)*2^16+L div cx ;执行[rem(H/N)*2^16+L]/N,商保存在ax中 mov cx, dx ;用cx寄存器保存余数 mov dx, bx ;把bx的值复制到dx,即执行int(H/N)*2^16 ;因为[rem(H/N)*2^16+L]/N已保存于ax中, ;即同时完成+运算 pop bx ;恢复bx的值 divdw_return: ret
5、程序分析
一、
为了消除分母为0的状况,咱们在子程序的一开始处就判断分母CX的值,若其值为0,则直接返回。
二、在分析时必定要记住,div把DX看成高16位来看,把AX看成低16来看,咱们的子程序的调用者也是如此。因此当程序解释DX的值时,其值为真实值乘以2^16,例如DX中的值为1,AX中的值也为1,则由于DX为高16位,因此被解释为1*2^16 = 65536,则AX做为低16位来处理,其值为1。因此32位的数的值为65536+1 = 65537,也就是说32位数的值为DX*2^16+AX。
push ax ;保存低位
mov ax, dx ;把高位放在低位中
mov dx, 0 ;把高位置0
div cx ;执行H/N,高位相除的余数保存在dx中
就是前面所说的把把H和L独立起来考虑,当H/N时,先把DX中的内容(即H)复制到AX中,再把DX的内容置为0,而后再与CX相除。
三、
pop ax ;执行rem(H/N)*2^16+L
div cx ;执行[rem(H/N)*2^16+L]/N,商保存在ax中
由于前面的除法操做即H/N的余数保存在DX中,又因为在除法操做div时,DX是当高位来处理的,因此在DX中的余数的值就会至关于其原来的值乘以2^16,而后再把以前保存在栈中的X的低位L恢复到ax中来,div指令把AX看成低16位来处理,因此也就至关于DX*2^16+AX,也就是rem(H/N)*2^16+L,以后的除法就不用解释了。
四、
mov cx, dx ;用cx寄存器保存余数
把前面div操做中产生的余数(保存在dx中)复制到cx中来,由于根据函数的说明,咱们是用cx来保存余数的,而不是默认的dx。
五、
mov dx, bx
因为bx先前保存着H/N的商,而DX又是高16位,因此把bx的值恢复到dx中就至关于int(H/N)*2^16,因为AX是低16位,而以前的操做又已经把结果(即[rem(H/N)*2^16+L]/N的商)保存在AX中,因此这步结束后,也就至关于执行完int(H/N)*2^16+[rem(H/N)*2^16+L]/N的整个操做完成了。
能够看到,这段看来小小的代码就这样有很是巧妙的方式,利用了div指令的特性,把DX看做高16位和把AX看做低16位和余数保存在DX中,商保存在AX中的特性,从而实现了咱们的无溢出除法的功能了。