mirror of
https://github.com/LCTT/TranslateProject.git
synced 2025-01-01 21:50:13 +08:00
commit
9196266674
@ -1,156 +0,0 @@
|
||||
[#]: subject: "Doing 64-bit math on a 16-bit system"
|
||||
[#]: via: "https://opensource.com/article/22/10/64-bit-math"
|
||||
[#]: author: "Jerome Shidel https://opensource.com/users/shidel"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: "yzuowei"
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
Doing 64-bit math on a 16-bit system
|
||||
======
|
||||
|
||||
With a little basic understanding of assembly, these functions could be scaled to do math on integers of any bit size.
|
||||
|
||||
A few years ago, I wrote a command-line math program for FreeDOS called VMATH. It was capable of performing only extremely simple mathematical operations on very small unsigned integers. With some recent interest in basic math in the FreeDOS community, I improved VMATH to provide basic math support on signed 64-bit integers.
|
||||
|
||||
The process of manipulating big numbers using only 16-bit 8086 compatible assembly instructions is not straightforward. I would like to share some samples of the techniques used by VMATH. Some of the methods used are fairly easy to grasp. Meanwhile, others can seem a little strange. You may even learn an entirely new way of performing some basic math.
|
||||
|
||||
The techniques explained here to add, subtract, multiply, and divide 64-bit integers are not limited to just 64-bits. With a little basic understanding of assembly, these functions could be scaled to do math on integers of any bit size.
|
||||
|
||||
Before digging into those math functions, I want to cover some basics of numbers from the computer's perspective.
|
||||
|
||||
### How computers read numbers
|
||||
|
||||
An Intel-compatible CPU stores the value of numbers in bytes from least to most significant. Each byte is made up of 8 binary bits and two bytes make up a word.
|
||||
|
||||
A 64-bit number that is stored in memory uses 8 bytes (or 4 words). For example, a value of 74565 (0x12345in hexadecimal) looks something like this:
|
||||
|
||||
```
|
||||
as bytes: db 0x45, 0x23, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00
|
||||
as words: dw 0x2345, 0x0001, 0x0000, 0x0000
|
||||
```
|
||||
|
||||
When reading or writing data to memory, the CPU processes the bytes in the correct order. On a processor more modern than an 8086, there can be larger groups, such as a quadword which can represent the entire 64-bit integer as **0x0000000000012345**.
|
||||
|
||||
The 8086 CPU doesn't understand such gigantic numbers. When writing a program for FreeDOS, you want something that can run on any PC, even an original IBM PC 5150. You also want to use techniques that can be scaled to any size integer. The capabilities of a more modern CPU do not really concern us.
|
||||
|
||||
For the purpose of doing integer math, the data can represent two different types of numbers.
|
||||
|
||||
The first is unsigned which uses all of its bit to represent a positive number. Their value can be from **0** up to **(2 ^ (numberofbits) - 1)**. For example, 8 bits can have any value from **0** to **255**, with 16 bits ranging from **0** to **65535**, and so on.
|
||||
|
||||
Signed integers are very similar. However, the most significant bit of the number represents whether the number is positive (**0**) or negative (**1**). The first portion of the number is positive. It can range from **0** up to **(2 ^ (numberofbits - 1) - 1)**. The negative portion follows the positive, ranging from its lowest value **(0-(2 ^ (numberofbits - 1)))** up to **-1**.
|
||||
|
||||
For example, an 8-bit number represents any value from **0** to **127** in the positive range, and **-128** through **-1** in the negative range. To help visualize it, consider the **byte** as the set of numbers **[0…127,-128…-1]**. Because **-128** follows **127** in the set, adding **1** to **127** equals **-128**. While this may seem strange and backward, it actually makes doing basic math at this level much easier.
|
||||
|
||||
To perform basic addition, subtraction, multiplication, and division of very big integers, you should explore some simple routines to get a number's absolute or negative value. You will need them once you start doing math on signed integers.
|
||||
|
||||
### Absolute and negative values
|
||||
|
||||
Getting the absolute value of a signed integer is not as bad as it may first seem. Because of how unsigned and signed numbers are represented in memory, there is a fairly easy solution. You can simply invert all the bits of a negative number and add **1** to get the result.
|
||||
|
||||
That might sound odd if you haven't worked in binary before, but that is how works. To give you an example, take an 8-bit representation of a negative number, such as **-5**. Since it would be near the end of the **[0…127,-128…-1]** byte set, it would have a value of **0xfb** in hexadecimal, or **11111011** in binary. If you flip all the bits, you get **0x04**, or **00000100** in binary. Add **1** to that result and you have the answer. You just changed the value from **-5** to **+5**.
|
||||
|
||||
You can write this procedure in assembly to return the absolute value of any 64-bit number:
|
||||
|
||||
```
|
||||
; 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
|
||||
```
|
||||
|
||||
As you may have noticed in the example, there is an issue that can occur in the function. Because of how positive and negative numbers are represented as binary values, the maximum negative number cannot be made positive. For 8-bit numbers, the maximum negative value is **-128**. If you flip all of the bits for **-128** (binary1__0000000), you get **127** (binary0__1111111) the maximum positive value. If you add **1** to that result, it will overflow back to the same negative number (-128).
|
||||
|
||||
To turn a positive number negative, you can just repeat the process you used to get the absolute value. The example procedure is very similar, except you want to make sure the number is not already negative at the start.
|
||||
|
||||
```
|
||||
; 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
|
||||
```
|
||||
|
||||
With all of that shared code between the absolute and negative functions, they should be combined to save some bytes. There are additional benefits when such code is combined. For one, it helps prevent simple typographic errors. It also can reduce testing requirements. Moreover, the source generally becomes easier to read, follow, and understand. Sometimes with a long series of assembly instructions, it is easy to lose track of what is actually happening. But for now, we can move along.
|
||||
|
||||
Getting the absolute or negative value of a number was not very difficult. But, those functions will be critically important later on when we start doing math on signed integers.
|
||||
|
||||
Now that I've covered the basics of how integer numbers are represented at the bit level and created a couple of basic routines to manipulate them a little, we can get to the fun stuff.
|
||||
|
||||
Let's do some math!
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
via: https://opensource.com/article/22/10/64-bit-math
|
||||
|
||||
作者:[Jerome Shidel][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[译者ID](https://github.com/译者ID)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://opensource.com/users/shidel
|
||||
[b]: https://github.com/lkxed
|
||||
|
@ -0,0 +1,157 @@
|
||||
[#]: subject: "Doing 64-bit math on a 16-bit system"
|
||||
[#]: via: "https://opensource.com/article/22/10/64-bit-math"
|
||||
[#]: author: "Jerome Shidel https://opensource.com/users/shidel"
|
||||
[#]: collector: "lkxed"
|
||||
[#]: translator: "yzuowei "
|
||||
[#]: reviewer: " "
|
||||
[#]: publisher: " "
|
||||
[#]: url: " "
|
||||
|
||||
在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位数可以是 **0** 到 **255** 之间的任意值,而16位数则在 **0** 到
|
||||
**65535** 之间,以此类推。
|
||||
|
||||
有符号整型也很类似。不同之处在于数字的最显著位代表了这个数是一个整数 (**0**) 还是一个负数 (**1**)。有符号整型的值域前半部分位正数,正数值域是从 **0** 到 **(2 ^ (字位数量 - 1) - 1)**。整型值域的后半部分为负数,负数值域则从 **(0-(2 ^ (字位数量 - 1)))** 到 **-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位数。
|
||||
; 结束时,如果结果数字不能被转正,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][a]
|
||||
选题:[lkxed][b]
|
||||
译者:[yzuowei](https://github.com/yzuowei)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
[a]: https://opensource.com/users/shidel
|
||||
[b]: https://github.com/lkxed
|
||||
|
Loading…
Reference in New Issue
Block a user