几年前,我为 FreeDOS 编写了一个名为 VMATH 的命令行数学程序。它只能对非常小的无符号整数执行极其简单的数学运算。由于最近 FreeDOS 社区对基本数学产生了一些兴趣,我改进了 VMATH,以提供对有符号 64 位整数的基本数学支持。
仅使用 16 位 8086 兼容汇编指令来操作大数字的过程并非易事。我想分享一些 VMATH 使用的技术示例。其中一些方法相当容易掌握。与此同时,另一些方法可能看起来有点奇怪。您甚至可以学习一种全新的执行基本数学运算的方法。
这里解释的用于加、减、乘、除 64 位整数的技术并不局限于 64 位。只要对汇编语言有基本的了解,这些函数就可以扩展到对任何位大小的整数进行数学运算。
在深入研究这些数学函数之前,我想从计算机的角度介绍一些数字的基础知识。
计算机如何读取数字
与 Intel 兼容的 CPU 以字节为单位存储数字的值,从最低有效位到最高有效位。每个字节由 8 个二进制位组成,两个字节组成一个字。
存储在内存中的 64 位数字使用 8 个字节(或 4 个字)。例如,值 74565(十六进制表示为 0x12345)看起来像这样
as bytes: db 0x45, 0x23, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00
as words: dw 0x2345, 0x0001, 0x0000, 0x0000
当从内存读取或写入数据时,CPU 会以正确的顺序处理字节。在比 8086 更现代的处理器上,可能有更大的组,例如四字,它可以将整个 64 位整数表示为 0x0000000000012345。
8086 CPU 不理解如此巨大的数字。在为 FreeDOS 编写程序时,您希望它可以在任何 PC 上运行,甚至是最初的 IBM PC 5150。您还希望使用可以扩展到任何大小整数的技术。更现代的 CPU 的功能实际上与我们无关。
为了进行整数数学运算,数据可以表示两种不同类型的数字。
第一种是无符号数,它使用其所有位来表示正数。它们的值可以从 0 到 (2 ^ (位数) - 1)。例如,8 位可以有从 0 到 255 的任何值,而 16 位范围从 0 到 65535,依此类推。
有符号整数非常相似。但是,数字的最高有效位表示数字是正数 (0) 还是负数 (1)。数字的第一部分是正数。它的范围可以从 0 到 (2 ^ (位数 - 1) - 1)。负数部分跟随正数部分,范围从其最小值 (0-(2 ^ (位数 - 1))) 到 -1。
例如,一个 8 位数字表示正数范围内的任何值,从 0 到 127,以及负数范围内的 -128 到 -1。为了帮助您可视化它,可以将 byte 视为数字集合 [0…127,-128…-1]。因为 -128 在集合中紧跟 127 之后,所以将 1 加到 127 等于 -128。虽然这可能看起来很奇怪和倒退,但实际上这使得在这个级别进行基本数学运算变得容易得多。
为了对非常大的整数执行基本的加法、减法、乘法和除法运算,您应该探索一些简单的例程来获取数字的绝对值或负值。一旦您开始对有符号整数进行数学运算,您将需要它们。
绝对值和负值
获取有符号整数的绝对值并不像最初看起来那么糟糕。由于无符号数和有符号数在内存中的表示方式,有一个相当简单的解决方案。您只需反转负数的所有位并加 1 即可得到结果。
如果您以前没有使用过二进制,这听起来可能很奇怪,但事实就是如此。为了给您一个例子,以负数的 8 位表示为例,例如 -5。由于它将接近 [0…127,-128…-1] 字节集的末尾,因此它在十六进制中将具有 0xfb 的值,在二进制中将具有 11111011 的值。如果您翻转所有位,您将得到 0x04,即二进制的 00000100。将 1 加到该结果,您就得到了答案。您刚刚将值从 -5 更改为 +5。
您可以用汇编语言编写此过程,以返回任何 64 位数字的绝对值
; syntax, NASM for DOS
proc_ABS:
; on entry, the SI register points to the memory location in the
; data segment (DS) for the program containing the 64-bit
; number that will be made positive.
; On exit, the Carry Flag (CF) is set if resulting number can
; not be made positive. This only happens with maximum
; negative value. Otherwise, CF is cleared.
; check most significant bit of highest byte
test [si+7], byte 0x80
; if not set, the number is positive
jz .done_ABS
; flip all the bits of word #4
not word [si+6]
not word [si+4] ; word #3
not word [si+2] ; word #2
not word [si] ; word #1
; increment the 1st word
inc word [si]
; if it did not roll over back to zero, done
jnz .done_ABS
; increment the 2nd word
inc word [si+2]
; if it rolled over, increment the next word
jnz .done_ABS
inc word [si+4]
jnz .done_ABS
; this cannot roll over
inc word [si+6]
; check most significant bit once more
test [si+7], byte 0x80
; if it is not set we were successful, done
jz .done_ABS
; overflow error, it reverted to Negative
stc
; set Carry Flag and return
ret
.done_ABS:
; Success, clear Carry Flag and return
clc
ret
正如您可能在示例中注意到的那样,该函数中可能会出现一个问题。由于正数和负数表示为二进制值的方式,最大负数不能变为正数。对于 8 位数字,最大负值为 -128。如果您翻转 -128(二进制 1__0000000)的所有位,您将得到 127(二进制 0__1111111),即最大正值。如果您将 1 加到该结果,它将溢出回到相同的负数 (-128)。
要将正数变为负数,您可以重复用于获取绝对值的过程。示例过程非常相似,只是您要确保数字在开始时不是负数。
; syntax, NASM for DOS
proc_NEG:
; on entry, the SI points to the memory location
; for the number to be made negative.
; on exit, the Carry Flag is always clear.
; check most significant bit of highest byte
test [si+7], byte 0x80
; if it is set, the number is negative
jnz .done_NEG
not word [si+6] ; flip all the bits of word #4
not word [si+4] ; word #3
not word [si+2] ; word #2
not word [si] ; word #1
inc word [si] ; increment the 1st word
; if it did not roll over back to zero, done
jnz .done_NEG
; increment the 2nd word
inc word [si+2]
; if it rolled over, increment the next word
jnz .done_NEG
inc word [si+4]
jnz .done_NEG
; this cannot roll over or revert back to
inc word [si+6]
; positive.
.done_NEG:
clc ; Success, clear Carry Flag and return
ret
由于绝对值函数和负值函数之间有这么多共享代码,因此应该将它们组合起来以节省一些字节。当此类代码组合在一起时,还有其他好处。首先,它有助于防止简单的排版错误。它还可以减少测试要求。此外,源代码通常变得更易于阅读、遵循和理解。有时,对于一系列冗长的汇编指令,很容易忘记实际发生了什么。但就目前而言,我们可以继续前进。
获取数字的绝对值或负值并不是很困难。但是,当我们开始对有符号整数进行数学运算时,这些函数将在以后变得至关重要。
既然我已经介绍了整数如何在位级别表示以及创建了一些基本例程来稍微操作它们的基础知识,我们就可以开始有趣的事情了。
让我们做一些数学运算吧!
评论已关闭。