---
author: Jerome Shidel
category: 软件开发
comments_data: []
count:
  commentnum: 0
  favtimes: 0
  likes: 0
  sharetimes: 0
  viewnum: 2217
date: '2022-12-09 15:08:00'
editorchoice: false
excerpt: 只要对汇编有一点基本的了解，这些函数就能扩展到任意位长的整型数学运算。
fromurl: https://opensource.com/article/22/10/64-bit-math
id: 15332
islctt: true
largepic: /images/p/202212/09/150829g7c7x5e22qqo53c4.jpg
permalink: /article-15332-1.html
pic: /images/p/202212/09/150829g7c7x5e22qqo53c4.jpg.thumb.jpg
related: []
reviewer: wxy
selector: lkxed
summary: 只要对汇编有一点基本的了解，这些函数就能扩展到任意位长的整型数学运算。
tags:
- 数学运算
thumb: false
title: 如何在 16 位系统上进行 64 位数学运算
titlepic: true
translator: yzuowei
updated: '2022-12-09 15:08:00'
---

![](/images/p/202212/09/150829g7c7x5e22qqo53c4.jpg)



> 
> 只要对汇编有一点基本的了解，这些函数就能扩展到任意位长的整型数学运算。
> 
> 
> 


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


仅使用 16 位 8086 兼容的汇编指令来操控大型数字的过程并不简单。我希望能够分享一些在 VMATH 中用到的技术例子。其中一些方法掌握起来相当容易。而另外一些方法则看起来有点奇怪。你甚至可能学到一种进行基本数学运算的全新方式。


接下来要讲的加、减、乘、除会用到的技术将不局限于并不局限于 64 位整型。只要对汇编有一点基本的了解，这些函数就能扩展到任意位长的整型数学运算。


在深入研究这些数学函数前，我想先从计算机的角度介绍一下数字的一些基本知识。


### 计算机是如何读取数字的


一个英特尔兼容的 CPU 以<ruby> 字节 <rt>  Byte </rt></ruby>的形式贮存数字，储存顺序为从最低有效字节到最高有效字节。每个字节由 8 个二进<ruby> 位 <rt>  Bit </rt></ruby>组成，两个字节组成一个<ruby> 字 <rt>  Word </rt></ruby>。


一个储存在内存里的 64 位整型占用了 8 个字节（即 4 个字）。例如，数字 `74565`（十六进制表示为 `0x12345`）的值长得是这个样子的：



```
用字节表示：db 0x45, 0x23, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00
用字表示：dw 0x2345, 0x0001, 0x0000, 0x0000

```

当读取或写入数据到内存时，CPU 会以正确的顺序处理这些字节。对于比 8086 更现代的处理器而言，数据分组可以再大些，比如一个<ruby> 四字组 <rt>  Quadword </rt></ruby>就可以表达整个 64 位整型 `0x0000000000012345`。


8086 CPU 不能理解这么大的数字。当为 FreeDOS 编程时，你想要写的是一个能在任意电脑上跑的程序，甚至是原始的 IBM PC 5150。你想要使用能够扩展到任意大小整型的技术。我们其实并不关心更现代 CPU 的能力。


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


第一种是<ruby> 无符号 <rt>  unsigned </rt></ruby>整型，其使用了所有的位来表达一个正数。无符号整型的值域为从 `0` 到 `2<sup> 位长</sup> - 1`。例如，8 位数可以是 `0` 到 `255` 之间的任意值，而 16 位数则在 `0` 到 `65535` 之间，以此类推。


有符号整型也很类似。不同之处在于数字的最高位代表了这个数是一个正数（`0`）还是一个负数 （`1`）。有符号整型的值域前半部分为正数，正数值域是从 `0` 到 `2<sup> 位长 - 1</sup> - 1`。整型值域的后半部分为负数，负数值域则从 `0 - 2<sup> 位长 - 1</sup>` 到 `-1`。


比如说，一个 8 位数代表着 `0` 到 `127` 之间的任意正数，以及 `-128` 到 `-1` 之间的任意负数。为了能更好的理解这一点，想象 **字节** 为一列数组 `[0...127,-128...-1]`。因为 `-128` 在数组内紧跟着 `127`，`127` 加 `1` 等于 `-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 位数。
  ; 结束时，如果结果数字不能被转正，CF 寄存器会被设置。这种情况只
  ; 有在遇到最大负值时会发生。其余情况，CF 不会被设置。
  
  ; 检查最高字节的最高位
  test [si+7], byte 0x80
  ; 如不为 1，值为正值
  jz .done_ABS
  ; 翻转所有位
  not word [si+6]       ; 字 #4
  not word [si+4]       ; 字 #3
  not word [si+2]       ; 字 #2
  not word [si]         ; 字 #1
  ; 字 #1 加 1
  inc word [si]
  ; 如结果不为 0，结束
  jnz .done_ABS
  ; 字 #2 加 1
  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
  ; 设置 CF 并返回
  ret
.done_ABS:
  ; 成功，清理 CF 并返回
  clc
  ret

```

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


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



```
; 语法， NASM for DOS
proc_NEG:
  ; 开始时，SI 会指向需要转负的数字在内存里的位置。
  ; 结束时，CF 永远不会被设置。
  
  ; 检查最高字节的最高位
  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 加 1
  ; 如结果不为 0，结束
  jnz .done_NEG
  ; 字 #2 加 1
  inc word [si+2]
  ; 如结果为 0，进位下一个字
  jnz .done_NEG
  inc word [si+4]
  jnz .done_NEG
  ; 此处无法进位或转化
  inc word [si+6]
  ; 正。
.done_NEG:
  clc                   ; 成功，清理 CF 并返回
  ret

```

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


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


我已经介绍了整型数字在位这一层面的基本表示方法，也创造了可以改变这些数字的基本程序，现在我们可以做点有趣的了。


让我们来做些数学运算吧！




---


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


作者：[Jerome Shidel](https://opensource.com/users/shidel) 选题：[lkxed](https://github.com/lkxed) 译者：[yzuowei](https://github.com/yzuowei) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出