mirror of
https://github.com/LCTT/TranslateProject.git
synced 2024-12-26 21:30:55 +08:00
[手动选题][tech]: 20221026.0 ⭐️⭐️⭐️ Doing 64-bit math on a 16-bit system.md
This commit is contained in:
parent
a973471040
commit
cbdff408b3
@ -0,0 +1,156 @@
|
||||
[#]: 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: " "
|
||||
[#]: 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
|
||||
|
Loading…
Reference in New Issue
Block a user