一、为什么除法会溢出
看到这个标题,你可能会问汇编中不是有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以上是就会发生溢出而已。
 
二、如何解决这个溢出问题
既然我们知道了问题的根源,要解决它就不困难了。为了统一而且不发生溢出,我们可以把所有的除法的商都用32位来保存,即所有的被除数都用32位,所有的除数都用16位,所有的商都用32位,所有的余数都用16位来保存,就可以解决这个问题,因为一个数除以一个整数后,不可能大于其之前的值。而对于8位的除法,除数和被除数的高位全用0补全即可。为了达到这个目的,我们就不能使用默认的除法指令div了,而需要我们写代码来实现我们自定义的除法。
 
考虑到除法是一个常用的操作,所以我们可以编写一个子程序来实现除法,在进行除法运算时,直接调用我们自己定义的子程序来完成任务,而不直接使用div指令。
 
三、如何实现这个功能
要实现除法还得使用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中。对于公式中的其他除法运算也采用相同的操作,然后通过把除法之前产生的结果进行相加,就能产生我们的无溢出除法子程序。
 
四、实现代码
基于这个公式,我们实现的代码如下:
;子程序名称: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

五、程序分析
1、
为了消除分母为0的情况,我们在子程序的一开始处就判断分母CX的值,若其值为0,则直接返回。
 
2、在分析时一定要记住,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相除。
 
3、
  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,之后的除法就不用解释了。
 
4、
  mov cx, dx ;用cx寄存器保存余数
把前面div操作中产生的余数(保存在dx中)复制到cx中来,因为根据函数的说明,我们是用cx来保存余数的,而不是默认的dx。
 
5、
  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中的特性,从而实现了我们的无溢出除法的功能了。
 
 
12-23 17:46