Linux中國

如何在 16 位系統上進行 64 位數學運算

只要對彙編有一點基本的了解,這些函數就能擴展到任意位長的整型數學運算

幾年前,我為 FreeDOS 寫了一個叫做 VMATH 的命令行數學程序。它只能在很小的無符號整型上執行十分簡單的數學運算。隨著近來 FreeDOS 社區里對基礎數學的興趣,我改進了 VMATH 使其可以為有符號 64 位整型提供基本的數學支持。

僅使用 16 位 8086 兼容的彙編指令來操控大型數字的過程並不簡單。我希望能夠分享一些在 VMATH 中用到的技術例子。其中一些方法掌握起來相當容易。而另外一些方法則看起來有點奇怪。你甚至可能學到一種進行基本數學運算的全新方式。

接下來要講的加、減、乘、除會用到的技術將不局限於並不局限於 64 位整型。只要對彙編有一點基本的了解,這些函數就能擴展到任意位長的整型數學運算。

在深入研究這些數學函數前,我想先從計算機的角度介紹一下數字的一些基本知識。

計算機是如何讀取數字的

一個英特爾兼容的 CPU 以 位元組 Byte 的形式貯存數字,儲存順序為從最低有效位元組到最高有效位元組。每個位元組由 8 個二進 Bit 組成,兩個位元組組成一個 Word

一個儲存在內存里的 64 位整型佔用了 8 個位元組(即 4 個字)。例如,數字 74565(十六進位表示為 0x12345)的值長得是這個樣子的:

用位元組表示:db 0x45, 0x23, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00
用字表示:dw 0x2345, 0x0001, 0x0000, 0x0000

當讀取或寫入數據到內存時,CPU 會以正確的順序處理這些位元組。對於比 8086 更現代的處理器而言,數據分組可以再大些,比如一個 四字組 Quadword 就可以表達整個 64 位整型 0x0000000000012345

8086 CPU 不能理解這麼大的數字。當為 FreeDOS 編程時,你想要寫的是一個能在任意電腦上跑的程序,甚至是原始的 IBM PC 5150。你想要使用能夠擴展到任意大小整型的技術。我們其實並不關心更現代 CPU 的能力。

為了能做整型運算,我們的數據需要表達兩種不同類型的數字。

第一種是 無符號 unsigned 整型,其使用了所有的位來表達一個正數。無符號整型的值域為從 02<sup> 位長</sup> - 1。例如,8 位數可以是 0255 之間的任意值,而 16 位數則在 065535 之間,以此類推。

有符號整型也很類似。不同之處在於數字的最高位代表了這個數是一個正數(0)還是一個負數 (1)。有符號整型的值域前半部分為正數,正數值域是從 02<sup> 位長 - 1</sup> - 1。整型值域的後半部分為負數,負數值域則從 0 - 2<sup> 位長 - 1</sup>-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 位數。
  ; 結束時,如果結果數字不能被轉正,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 選題:lkxed 譯者:yzuowei 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的電子郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國