TranslateProject/translated/tech/20221026.0 ⭐️⭐️⭐️ Doing 64-bit math on a 16-bit system.md
2022-12-08 17:22:19 +08:00

8.2 KiB
Raw Blame History

在16位系统上做64位数学

只需要一点点汇编的基础理解,这些函数就能适应体任意大小的整型数学运算。

几年前,我为 FreeDOS 写了一个命令行数学程序叫做 VMATH。它可以在很小的无符号整型上执行十分简单的数学运算。出于近来对 FreeDOS 社区里基本数学的兴趣,我改进了 VMATH 使其可以为有符号64位整型提供基本的数学支持。

仅使用兼容16位 8086 的汇编来操控大型数字的过程并不直接。我希望能够分享一些在 VMATH 中用到的技术的例子。其中一些方法掌握起来还挺容易。同时,也有着别的看起来有点奇怪的方法。你甚至可能学到一种全新的进行基本数学运算的方式。

接下来要讲的加除会用到的技术将不局限于将不局限于64位整型。只需要一点点汇编的基础理解这些函数就能适应任意大小的整型数学运算。

在深挖这些数学函数前,我想要覆盖一些计算机看数字的基础视角。

计算机是如何读取数字的

一个兼容 Intel 的 CPU 以字节 (byte) 的形式贮存数字储存顺序为从最低有效字节到最高有效字节。每个字节由8个二进位组成两个字节组成一个字 (word)。

一个储存在内存里的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 编程时,你想要写的是一个能在任意电脑上跑的程序,甚至是早期的 IBM PC 5150。你想要使用能够适应任意大小整型的技术。我们并不关心现代 CPU 的能力。

为了能做整型运算,我们的数据需要表达两种不同类型的数字。

第一种是无符号整型,其使用了所有字位来表达一个正数。无符号整型的值域为从 0(2 ^ (字位数量) - 1)。例如8位数可以是 0255 之间的任意值而16位数则在 065535 之间,以此类推。

有符号整型也很类似。不同之处在于数字的最显著位代表了这个数是一个整数 (0) 还是一个负数 (1)。有符号整型的值域前半部分位正数,正数值域是从 0(2 ^ (字位数量 - 1) - 1)。整型值域的后半部分为负数,负数值域则从 (0-(2 ^ (字位数量 - 1)))-1

比如说一个8位数代表着 0127 之间的任意正数,以及 -128-1 之间的任意负数。为了能更好的理解这一点,想象 字节 为一列数组 [0...127,-128...-1]。因为 -128 在数组内紧跟着 1271271 等于 -128。当然这可能看起来有点奇怪甚至反常,但这其实让这个层级的基本数学运算变简单了。

为了能够对大型整型进行简单的加,减,乘,除,你应该摸索一些简单的公式来计算一个数的绝对值或负值。你在做有符号整型运算的时候会用上它们的。

绝对值与负值

计算一个有符号整型的绝对值并没有它看起来的那么糟糕。由于无符号和有符号数字在内存里的储存形式,我们其实有一个简单的方案。你只需要翻转一个负数的所有字位,得出的结果再加 1

如果你从没接触过二进制的话这可能听上去有点奇怪但这就是这么工作的。让我们来举一个例子取一个负数的8位表达比如说 -5。因为 -5 靠近 [0...127,-128...-1] 字节组末端,它的十六进制值为 0xfb,二进制值为 11111011。如果你翻转了所有字位,你会得到 0x04 或二进制值 00000100。结果加 1 你就得到了你的答案。你刚刚把 -5 的值变成了 +5

你可以用汇编写下这个程序用以返回任意64位数字的绝对值

; 语法NASM for DOS
proc_ABS:
  ; 启动时SI寄存器会指向数据段 (DS) 内的内存位置,那里存放着程序内包含着
  ; 会被转正的64位数。
  ; 结束时如果结果数字不能被转正Carry Flag (CF) 会被设置。这种情况只
  ;  有在遇到最大负值时会发生。其余情况CF 不会被设置。
  
  ; 检查最高字节的最高位
  test [si+7], byte 0x80
  ; 如不为1值为正值
  jz .done_ABS
  ; 翻转字的所有字位 #4
  not word [si+6]
  not word [si+4]       ; 字 #3
  not word [si+2]       ; 字 #2
  not word [si]                 ; 字 #1
  ; 字#1 加一
  inc word [si]
  ; 如结果不为0结束
  jnz .done_ABS
  ; 字#2 加一
  inc word [si+2]
  ; 如结果为0进位下一个字
  jnz .done_ABS
  inc word [si+4]
  jnz .done_ABS
  ; 此处无法进位
  inc word [si+6]
  ; 再一次检查最高位
  test [si+7], byte 0x80
  ; 如不为1我们成功了结束
  jz .done_ABS
  ; 溢出错误,它被转成了负数
  stc
  ; 设置 Carry Flag 并返回
  ret
.done_ABS:
  ; 成功,清理 Carry Flag 并返回
  clc
  ret

你可能已经注意到了这个函数有一个潜在问题。由于正负数的二进制值表达方式最大负数无法被转成正数。以8位数为例最大负数是 -128。如果你翻转了 -128 的所有位数 (二进制1__0000000)你会得到127 (二进制0__1111111) 即最大正值。如果你对结果加 1,它会因溢出回到同样的负数 (-128)。

你只需要重复计算绝对值的步骤就可以将正数转成负数。以下的程序十分相似,你唯一需要确认的就是一开始的数字不是已经负了。

; 语法, NASM for DOS
proc_NEG:
  ; 开始时SI会指向需要转负的数字在内存里的位置。
  ; 结束时Carry Flag永远不会被设置。
  
  ; 检查最高字节的最高位
  test [si+7], byte 0x80
  ; 如为1数已经是负数
  jnz .done_NEG
  not word [si+6]       ; 翻转字的所有字位 #4
  not word [si+4]       ; 字 #3
  not word [si+2]       ; 字 #2
  not word [si]                 ; 字 #1
  inc word [si]                 ; 字#1 加一
  ; 如结果不为0结束
  jnz .done_NEG
  ; 字#2 加一
  inc word [si+2]
  ; 如结果为0进位下一个字
  jnz .done_NEG
  inc word [si+4]
  jnz .done_NEG
  ; 此处无法进位或转化
  inc word [si+6]
  ; 正。
.done_NEG:
  clc                   ; 成功,清理 Carry Flag 并返回
  ret

看着这些绝对值与负值函数间的通用代码,它们应该被结合起来来节约一些字节。结合代码也会带来额外的好处。首先,结合代码能帮助防止简单的笔误。这样也可以减少测试的要求。进一步来讲,这样通常会让代码变得简单易懂。在阅读一长串的汇编指令时,忘记读到哪是常有的事。现在,我们可以不管这些。

计算一个数的绝对值或负值并不难。但是,这些函数对于我们即将开始的有符号整型数学运算至关重要。

我已经覆盖了整型数字在字位层的表达的基础,也创造了可以改变这些数字的基本程序,现在我们可以做点有趣的了。

让我们来做些数学吧!


via: https://opensource.com/article/22/10/64-bit-math

作者:Jerome Shidel 选题:lkxed 译者:yzuowei 校对:校对者ID

本文由 LCTT 原创编译,Linux中国 荣誉推出