整數的每個位元都可以用來儲存一個二進位值。 由於許多情境都涉及二進位資訊,例如 true 或 false、包含或排除、開啟或關閉,因此 N 位元整數的二進位表示法提供了一種精簡的方式,來編碼 N 個項目的二進位狀態。 這使得在組合語言中操作位元與位元組的能力不可或缺。 x86-64 指令集提供了各式各樣的位元操作指令。
這些指令會對運算元中的單一位元進行操作。
它們都接受兩個運算元,第二個運算元指出在第一個運算元中要操作的位元索引。 它們全都會將選取的位元複製到進位旗標(CF)。
| 名稱 | 說明 |
|---|---|
bt |
將位元複製到CF,而不修改任何運算元 |
bts |
將位元複製到CF,並在目的運算元中將它設為 1 |
btr |
將位元複製到CF,並在目的運算元中將它清除 |
btc |
將位元複製到CF,並在目的運算元中將它取補數(翻轉) |
位元運算會對運算元的所有位元執行。
每一個位元運算都有一個與該運算同名的指令:
| 名稱 | 說明 |
|---|---|
and |
若兩個位元都是 1,則為 1 |
or |
若至少其中一個位元是 1,則為 1 |
xor |
若兩個位元不同,則為 1 |
not |
若位元原本是 0 則為 1;若原本是 1 則為 0 |
它們大多接受兩個運算元,對兩者執行位元運算,並將結果存入目的運算元。
例外的是not,它只接受一個目的運算元。
當我們將 1 和 0 分別解讀為包含與排除時,這樣的整數就稱為位元遮罩(或簡稱遮罩)。
位元遮罩會「遮掉」項目,因為第i個位元中的 0 會排除第i個項目,而 1 則會包含它。
我們也常使用位元遮罩來包含整數中的某些位元,同時排除其他位元。
舉例來說,假設A是一個整數,其二進位表示法為:
| 索引 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| 位元 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
同樣地,假設M是一個整數,其二進位表示法為:
| 索引 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| 位元 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
兩者都是8位元的整數。
在這個情況下,我們可以說M選取了A的第0、2和3個位元,並排除其餘的位元。
前面討論過的位元指令,很適合用來以遮罩操作整數。 例如:
A中未被M選取的位元,請進行位元 AND 運算:A AND M。A中被M選取的位元,請進行位元 OR 運算:A OR M。test指令會對兩個運算元進行位元 AND,並根據結果設定旗標。
若 A 是第一個運算元,B 是第二個:
| 旗標 | 設定條件 |
|---|---|
CF |
永遠清除 |
ZF |
A AND B == 0 |
SF |
A AND B 的正負號位元被設定 |
OF |
永遠清除 |
這個指令接受兩個運算元並更新旗標,但不會修改它的運算元。
這些指令會將目的運算元中的位元移動第二個運算元所指定的位置數。
第二個運算元必須是常數(即immediate)或暫存器cl(rcx最低的8個位元)。
| 名稱 | 說明 |
|---|---|
shl/sal
|
將位元向左移位 |
shr/sar
|
將位元向右移位 |
請注意,第二個運算元中的計數值會被遮罩為5個位元,若目的運算元是64位元則為6個位元。
之後的任何位元都會被忽略。
這表示最大移位量是31,若運算元是64位元則為63。
shl和sal會執行完全相同的操作,其中一個是另一個的別名。
每當進行左移時,比移位長度更靠近序列末端的位元,會先被移到CF,然後被丟棄。
另一方面,開頭會加入與移位長度相同數量的新清除位元。
由於整數中的每個位元代表2的次方,向左移位 n 個位置的效果等同於將該整數乘以2ⁿ。
有兩個指令可以將位元向右移動:shr和sar。
每當使用這兩個指令中的任一個時,比移位長度更靠近序列開頭的位元,會先被移到CF,然後被丟棄。
另一方面,末端會加入與移位長度相同數量的新位元。
兩者的差別在於,shr會將0位元移到左端,而sar則在最高有效位元被設定時移入1,否則移入0。
這表示sar在有正負號整數的移位中會保留正負號。
由於整數中的每個位元代表2的次方,使用shr向右移位 n 個位置的效果等同於進行無正負號除法,除以2ⁿ。
同樣地,使用sar向右移位 n 個位置的效果等同於進行有正負號除法,除以2ⁿ。
這些指令會將目的運算元中的位元移動第二個運算元所指定的位置數。
第二個運算元必須是常數(即immediate)或暫存器cl(rcx最低的8個位元)。
旋轉與移位的差別在於,旋轉不會丟棄或加入任何位元。 原本會被移位丟棄的位元,會被移到另一端。 因此所有位元都會保留,只是全都換了位置。
| 名稱 | 說明 |
|---|---|
rol |
將位元向左旋轉 |
ror |
將位元向右旋轉 |
請注意,第二個運算元中的計數值會被遮罩為5個位元,若目的運算元是64位元則為6個位元。
之後的任何位元都會被忽略。
這表示最大旋轉量是31,若運算元是64位元則為63。
還有其他實用的位元操作指令:
| 名稱 | 說明 |
|---|---|
popcnt |
計算被設定的位元數量 |
bsr |
取得最高有效且被設定之位元的索引。若沒有設定位元,結果未定義 |
bsf |
取得最低有效且被設定之位元的索引。若沒有設定位元,結果未定義 |
這些指令都使用兩個16位元、32位元或64位元的運算元。
它們不能用於8位元的運算元。
你的朋友剛傳了一則夾帶重要祕密的訊息給你。 為了不讓其他人輕易讀取,這則訊息透過一連串的位元操作加密。 你需要撰寫方法來協助解密這則訊息。
以下是這個概念提到的單一位元指令:
| Name | Description |
|---|---|
| bt | 將該位元複製到CF,但不修改任何運算元 |
| bts | 將該位元複製到CF,並在目的運算元中將它設為 1 |
| btr | 將該位元複製到CF,並在目的運算元中將它清除 |
| btc | 將該位元複製到CF,並在目的運算元中將它取補數(翻轉) |
以下是這個概念提到的位元運算指令:
| Name | Description |
|---|---|
| and | 若兩個位元都是 1,則為 1 |
| or | 若其中至少一個位元是 1,則為 1 |
| xor | 若兩個位元不同,則為 1 |
| not | 若位元原本是 0 則為 1;若原本是 1 則為 0 |
以下是這個概念提到的移位指令:
| Name | Description |
|---|---|
| shl/sal | 將位元向左移位 |
| shr/sar | 將位元向右移位 |
以下是這個概念提到的旋轉指令:
| Name | Description |
|---|---|
| rol | 將位元向左旋轉 |
| ror | 將位元向右旋轉 |
以下是這個概念提到的其他指令:
| Name | Description |
|---|---|
| popcnt | 計算被設為 1 的位元數量 |
| bsr | 取得最高有效設定位元的索引。若沒有設定任何位元,結果未定義 |
| bsf | 取得最低有效設定位元的索引。若沒有設定任何位元,結果未定義 |
這則訊息編碼在一個 16 位元整數中。 不過,其中最高的 8 個位元其實不屬於訊息本身,而是解密時需要用到的遮罩。
實作extract_higher_bits函式,它接收一個 16 位元整數,並回傳其中最高的 8 個位元。
extract_higher_bits(0b1010010011000101)
// => 0b10100100
光是能取出遮罩還不夠,你還需要把訊息分離出來。
實作extract_lower_bits函式,它接收一個 16 位元整數,並回傳其中最低的 8 個位元。
extract_lower_bits(0b1010010011000101);
// => 0b11000101
有些位元在訊息和遮罩中都設為 1。 這是一項之後會用到的非常重要的資訊。
實作extract_redundant_bits函式,它接收一個同時編碼了訊息和遮罩的 16 位元整數,並回傳一個只設定了冗餘位元的 8 位元整數。
在回傳的數字中,若某個位元在訊息和遮罩中同為 1,就應該設為 1。
其他所有位元則應該_清除_。
extract_redundant_bits(0b1010010011000101);
// => 0b10000100
接著,有些位元需要根據遮罩在訊息中設為1。
實作set_message_bits函式,它接收一個同時編碼了訊息和遮罩的 16 位元整數,並回傳將訊息中的位元設為 1 的結果。
若遮罩中的某個位元是 1,訊息中對應的位元就應該設為 1。
其他所有位元則應該保持_不變_,也就是原本已設定的維持設定,原本已清除的維持清除。
set_message_bits(0b1010010011000101);
// => 0b11100101
有一個拼圖的線索並沒有明確出現在訊息裡:16 位元數字0b1011001100111100。
這個數字是你共用的私鑰,你應該用它來協助解密訊息。
為此,你必須先將私鑰的位元向左旋轉若干個位置。 旋轉的位置數量等於在訊息和遮罩中都設定為 1 的冗餘位元數量。
實作rotate_private_key函式,它接收一個同時編碼了訊息和遮罩的 16 位元整數,並回傳旋轉私鑰後的結果。
這個結果是一個 16 位元整數。
rotate_private_key(0b1010010011000101);
// => 0b1100110011110010
NASM(The Netwide Assembler,本課程所使用的組譯器)支援以0b為前置字元的二進位格式常數。
它也支援在常數中使用底線(_)作為分隔符號,方便閱讀:
PRIVATE_KEY equ 0b1011_0011_0011_1100
若要將私鑰用於解密,就必須先將它格式化,以分離出相關的位元。
要完整格式化一把私鑰,你必須:
翻轉後的位元,原本是 0 就變成 1,原本是 1 就變成 0。
實作format_private_key函式,它接收一個同時編碼了訊息和遮罩的 16 位元整數,並回傳完整格式化後的 8 位元私鑰。
format_private_key(0b1010010011000101);
// => 0b11000001
當你取得了所有相關位元都已設定的訊息,以及格式化後的私鑰,就可以把它們結合起來,得到最終的訊息。
最終的訊息是一個 16 位元整數,其中:
實作decrypt_message函式,它接收一個同時編碼了訊息和遮罩的 16 位元整數,並回傳訊息已完全解密的 16 位元整數。
這個函式應該使用你以format_private_key產生的格式化私鑰,以及以set_message_bits設定了所有相關位元的訊息。
decrypt_message(0b1010010011000101);
// => 0b1100000111100101