鱼C论坛

 找回密码
 立即注册
查看: 4023|回复: 1

[技术交流] 汇编编写无溢出除法的子程序

[复制链接]
发表于 2020-6-28 15:05:32 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能^_^

您需要 登录 才可以下载或查看,没有账号?立即注册

x
一、为什么除法会溢出
看到这个标题,你可能会问汇编中不是有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中。对于公式中的其他除法运算也采用相同的操作,然后通过把除法之前产生的结果进行相加,就能产生我们的无溢出除法子程序。

四、实现代码
基于这个公式,我们实现的代码如下:
1.        ;子程序名称:divdw  
2.        ;功能:进行不会产生溢出的除法运算,被除数为dword型  
3.        ;      除数为word型,结果为dword型  
4.        ;参数:    (ax)=dword型数据的低16位  
5.        ;       (dx)=dword型数据的高16位  
6.        ;       (cx)=除数  
7.        ;返回:    (dx)=结果的高16位,(ax)=结果的低16位  
8.        ;       (cx)=余数  
9.        ;计算公式:X/N=int(H/N)*2^16+[rem(H/N)*2^16+L]/N  
10.        divdw:  
11.            jcxz divdw_return   ;除数cx为0,直接返回  
12.            push bx         ;作为一个临时存储器使用,先保存bx的值  
13.                  
14.            push ax         ;保存低位  
15.            mov ax, dx      ;把高位放在低位中  
16.            mov dx, 0       ;把高位置0  
17.            div cx          ;执行H/N,高位相除的余数保存在dx中  
18.            mov bx, ax      ;把商保存在bx寄存器中  
19.            pop ax          ;执行rem(H/N)*2^16+L  
20.            div cx          ;执行[rem(H/N)*2^16+L]/N,商保存在ax中  
21.            mov cx, dx      ;用cx寄存器保存余数  
22.            mov dx, bx      ;把bx的值复制到dx,即执行int(H/N)*2^16  
23.                                ;由于[rem(H/N)*2^16+L]/N已保存于ax中,  
24.                                ;即同时完成+运算  
25.            pop bx          ;恢复bx的值  
26.            divdw_return:  
27.            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中的特性,从而实现了我们的无溢出除法的功能了。


评分

参与人数 1荣誉 +3 鱼币 +3 收起 理由
CodingCat_k + 3 + 3 很细节但是有用的干货

查看全部评分

想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复

使用道具 举报

发表于 2021-9-14 15:49:16 | 显示全部楼层
王爽教程里的例子吧,老兄
想知道小甲鱼最近在做啥?请访问 -> ilovefishc.com
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|鱼C工作室 ( 粤ICP备18085999号-1 | 粤公网安备 44051102000585号)

GMT+8, 2024-4-19 10:19

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表