--- tags: c, bitwise operation --- # 研讀 Bit Twiddling Hacks contributed by < `hsuedw` > ## [Compute the sign of an integer](https://graphics.stanford.edu/~seander/bithacks.html#CopyIntegerSign) 有兩個型別為 `int` 的變數 `v` 與 `sign` 。想要偵測 `v` 是否小於 `0` 。 ### 若 `v` 小於 `0` ,則 `sign` 被指派為 `-1` 。否則, `sign` 被指派為 `0` 。 * 第一種實作方法 :::spoiler {state=open} detect_sign_1.c ```c= int main() { int v = -7, sign; sign = -(v < 0); v = 9; sign = -(v < 0); return 0; } ``` ::: 若 `v` 的值小於 `0` ,則運算式 `v < 0` 的值為 `1` 。結果就是 `-1` 被指派給 `sign` 。 若 `v` 的值大於 `0` ,則運算式 `v < 0` 的值為 `0` 。結果就是 `0` 被指派給 `sign` 。 接下來,觀察 `detect_sign_1.c` 的 x86 組合語言 ```assembly= 0000000000001129 <main>: 1129: f3 0f 1e fa endbr64 112d: 55 push %rbp 112e: 48 89 e5 mov %rsp,%rbp 1131: c7 45 f8 f9 ff ff ff movl $0xfffffff9,-0x8(%rbp) 1138: 8b 45 f8 mov -0x8(%rbp),%eax 113b: c1 e8 1f shr $0x1f,%eax 113e: 0f b6 c0 movzbl %al,%eax 1141: f7 d8 neg %eax 1143: 89 45 fc mov %eax,-0x4(%rbp) 1146: c7 45 f8 09 00 00 00 movl $0x9,-0x8(%rbp) 114d: 8b 45 f8 mov -0x8(%rbp),%eax 1150: c1 e8 1f shr $0x1f,%eax 1153: 0f b6 c0 movzbl %al,%eax 1156: f7 d8 neg %eax 1158: 89 45 fc mov %eax,-0x4(%rbp) 115b: b8 00 00 00 00 mov $0x0,%eax 1160: 5d pop %rbp 1161: c3 retq 1162: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1) 1169: 00 00 00 116c: 0f 1f 40 00 nopl 0x0(%rax) ``` 組合語言的第 5 行到第 10 行對應到 C 語言的第 3 行到第 4 行。 從這段組合語言得到了啟發,我們可以把程式碼以下列想法改寫 1. 假設每個 byte 有 8 個 bit,且一個 `int` 型別的 object 在記憶體中佔用 4 個 byte。 2. 由於現今電腦對有號整數大多採用二補數 (two's complement) 編碼。所以,若變數 `v` 內的數值小於 `0` ,則其二補數編碼的 most significant bit 必為 `1` (如組合語言第 5 行的 `$0xfffffff9`)。反之,為 `0` (如組合語言第 11 行的 `$0x9`)。 3. 對變數 `v` 內的數值做 31 個 bit 的邏輯右移。若變數 `v` 內的數值小於 `0` ,則做完邏輯右移後,變數 `v` 內的數值變為 `1` 。否則,為 `0` 。 4. ==在 C 語言規範中,對小於 0 的有號整數做右移 (bitwise right shift) 是 implementation-defined。== > C99 Standard, 6.5.7 Bitwise shift operators > The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / 2E2. ==If E1 has a signed type and a negative value, the resulting value is implementation-defined.== 對小於 0 的有號數而言, `>>` 運算子是邏輯右移,還是算術右移?==因為使用 GCC 編譯程式碼。所以,對有號整數而言, `>>` 運算子是算術右移==。 > [Re: right shift of signed type](https://gcc.gnu.org/legacy-ml/gcc/2000-04/msg00152.html) > An arithmetic left shift is equivalent to a logical left shift > (as far as GCC is concerned). > > For right shifts, if the type is signed, then we use an arithmetic shift; > if the type is unsigned then we use a logical. 我不知道原作者是否觀察了組合語言後,得到了第二種實作方法的靈感。 * 第二種實作方法 :::spoiler {state=open} detect_sign_2.c ```c #define CHAR_BIT (8) int main() { int v = -7; int sign = -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1)); v = 9; sign = -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1)); return 0; } ``` ::: 事實上,我們可以不需要大費周章地把變數 `v` 轉為 `unsigned int` ,做完邏輯右移後,再轉回 `int` ,然後再加上負號。 我們可以利用下面幾個技巧簡化程式碼。 1. ==用 GCC 編譯的程式碼中,對有號整數而言 `>>` 運算子的行為是算術右移==。也就是在右移的時候,把 most significant bit 填入左邊空出來的 bit。 2. 在二補數編碼中, most significant bit 為 sign bit 。對正整數而言, sign bit 為 `0` 。對負整數而言, sign bit 為 `1` 。 所以,==在使用 GCC 的前提下==,可以把 `detect_sign_2.c` 的程式碼簡化如下。 :::spoiler {state=open} detect_sign_3.c ```c #define CHAR_BIT (8) int main() { int v = -7; int sign = v >> (sizeof(int) * CHAR_BIT - 1); v = 9; sign = v >> (sizeof(int) * CHAR_BIT - 1); return 0; } ``` ::: 也就是直接對變數 `v` 內的數值 (`int` 型別的數值為有號整數) 做算術右移 31 個 bit 。 若變數 `v` 內的數值小於 0 ,算術右移 31 個 bit 後,變數 `v` 內的數值為 `0xffffffff` ,這就是 `-1` 的二補數二進位編碼。 若變數 `v` 內的數值大於或等於 0 ,算術右移 31 個 bit 後,變數 `v` 內的數值為 `0x00000000` ,這就是 `0` 的二補數二進位編碼。 > 延伸閱讀: > * Computer Systems: A Programmer’s Perspective, 3rd ed, Ch 2 Representing and Manipulating Information > * [解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation) 要注意的是, ==`detect_sign_3.c` 不具 portability== 。因為對有號整數做 bitwise right shift 的行為取決於 compiler 的實做,不是 C 語言的規範。 ### 若 `v` 小於 `0` ,則 `sign` 被指派為 `-1` 。否則, `sign` 被指派為 `1` 。 :::spoiler {state=open} detect_sign_4.c ```c #define CHAR_BIT (8) int main() { int v = -7; int sign = +1 | (v >> (sizeof(int) * CHAR_BIT - 1)); v = 9; sign = +1 | (v >> (sizeof(int) * CHAR_BIT - 1)); return 0; } ``` ::: 延續 `detect_sign_3.c` 的實作,對 `(v >> (sizeof(int) * CHAR_BIT - 1))` 做 bitwise or ,可以實現當 `v` 小於 `0` ,則 `sign` 被指派為 `-1` 。否則, `sign` 被指派為 `1`。 如先前所述,這種寫法不具 portability。 ### 若 `v` 小於 `0` ,則 `sign` 被指派為 `-1` 。若 `v` 等於 `0` ,`sign` 被指派為 `0` 。若 `v` 大於 `0` ,`sign` 被指派為 `1` 。 :::spoiler {state=open} detect_sign_5.c ```c #define CHAR_BIT (8) int main() { int v = -7; int sign = (v != 0) | -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1)); v = 9; sign = (v != 0) | -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1)); v = 0; sign = (v != 0) | -(int)((unsigned int)((int)v) >> (sizeof(int) * CHAR_BIT - 1)); return 0; } ``` ::: :::spoiler {state=open} detect_sign_6.c ```c #define CHAR_BIT (8) int main() { int v = -7; int sign = (v != 0) | (v >> (sizeof(int) * CHAR_BIT - 1)); v = 9; sign = (v != 0) | (v >> (sizeof(int) * CHAR_BIT - 1)); v = 0; sign = (v != 0) | (v >> (sizeof(int) * CHAR_BIT - 1)); return 0; } ``` ::: `detect_sign_5.c` 雖然比較繁瑣,但具 protibility 。 `detect_sign_6.c` 則相反。 原作者還提供第三種實作方式。 :::spoiler {state=open} detect_sign_7.c ```c int main() { int v = -7; int sign = (v > 0) - (v < 0); v = 9; sign = (v > 0) - (v < 0); v = 0; sign = (v > 0) - (v < 0); return 0; } ``` ::: ### 若 `v` 為非 `0` ,則 `sign` 被指派為 `1` 。否則, `sign` 被指派為 `0` 。 :::spoiler {state=open} detect_sign_8.c ```c #define CHAR_BIT (8) int main() { int v = -7; int sign = 1 ^ ((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1)); v = 9; sign = 1 ^ ((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1)); v = 0; sign = 1 ^ ((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1)); return 0; } ``` ::: 由先前的討論可得知, * 若變數 `v` 的數值小於 `0` ,則 `((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1))` 運算式的計算結果為 `0x00000001` 。再與 `1` 做 `^` (bitwise xor) ,最後結果為 `0x00000000` 。 * 若變數 `v` 的數值為非 `0` ,則 `((unsigned int)v >> (sizeof(int) * CHAR_BIT - 1))` 運算式的計算結果為 `0x00000000` 。再與 `1` 做 `^` (bitwise xor) ,最後結果為 `0x00000001` 。 ## [Detect if two integers have opposite signs](https://graphics.stanford.edu/~seander/bithacks.html#DetectOppositeSigns) ```c #include <stdbool.h> int main() { int x = 0, y = 0; bool f = ((x ^ y) < 0); x = 1, y = 2; f = ((x ^ y) < 0); x = -1, y = -2; f = ((x ^ y) < 0); x = 1, y = -2; f = ((x ^ y) < 0); return 0; } ``` 由於現今電腦對有號整數大多採用二補數 (two's complement) 編碼。所以,在有號整數的二進位表示法中,其 most significant bit 為 sign bit。這裡借用 Computer Systems: A Programmer's Perspective 第三版,第二章中對二補數編碼的定義來說明 > Computer Systems: A Programmer's Perspective, 2.2.3 Two's-Complement Encodings > PRINCIPLE: Definition of two's-complement encoding > For vector $\vec{x} = [x_{w-1}, x_{w-2}, \cdots, x_{0}]$ > $$ > B2T_{w}(\vec{x}) \dot{=} -x_{w-1}2^{w-1} + \sum\limits_{i=0}^{w-2} x_{i}2^{i} \tag{2.3} > $$ 由公式 (2.3) 可以得知對小於 0 的有號整數而言,其二進位表示法中的 most signifcant bit 必為 `1`。否則,為 `0` 。 * 若兩有號整數 (`x` 與 `y`),一為負整數,另一為非負整數,則此兩有號整數的 most significant bit 必分別為 `1` 與 `0` 。 `x ^ y` 運算結果的 most siginificant bit 必為 `1` 。所以, `x ^ y` 運算結果必為負整數。 * 若兩有號整數 (`x` 與 `y`),同時大於等於 0 或同時小於 0 ,則此兩有號整數的 most significant bit 必同時分別為 `0` 或 `1` 。 `x ^ y` 運算結果的 most siginificant bit 必為 `0` 。所以, `x ^ y` 運算結果必為非負整數。 ## [Compute the minimum (min) or maximum (max) of two integers without branching](https://graphics.stanford.edu/~seander/bithacks.html#IntegerMinOrMax) ### 找最小值 #### 第一種方法 :::spoiler {state=open} min1.c ```c= int my_min(int x, int y) { return y ^ ((x ^ y) & -(x < y)); } int main() { int r = my_min(3, 5); r = my_min(9, 2); r = my_min(8, 8); return 0; } ``` ::: 整支程式的重點當然就是第 3 行中 `y ^ ((x ^ y) & -(x < y))` 這個運算式。把這個運算式拆解成幾個部分來看。 * `-(x < y)` 這個運算式扮演條件判斷的的角色。 * 如果 `x < y` 成立,則整個 `-(x < y)` 的結果是 `-1` 。由於電腦採用二補數表示有號整數,所以 `-1` 的二進位表示法為 `0xffffffff` (假設 `int` 型別 object 佔用 4 個 byte)。所以可以對整個運算式 `y ^ ((x ^ y) & -(x < y))` 做出下列改寫: `y ^ ((x ^ y) & -(x < y))` $\Rightarrow$ `y ^ ((x ^ y) & 0xffffffff)` $\Rightarrow$ `y ^ (x ^ y)` $\Rightarrow$ `x` * 如果 `x < y` 不成立 (也就是 `x` 大於 `y` ),則整個 `-(x < y)` 的結果是 `0` 。 `0` 的二進位表示法為 `0` 所以可以對整個運算式 `y ^ ((x ^ y) & -(x < y))` 做出下列改寫: `y ^ ((x ^ y) & -(x < y))` $\Rightarrow$ `y ^ ((x ^ y) & 0)` $\Rightarrow$ `y ^ 0` $\Rightarrow$ `y` #### 第二種方法 :::spoiler {state=open} min2.c ```c= #include <limits.h> int my_min(int x, int y) { return y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); } int main() { int r = my_min(3, 5); r = my_min(9, 2); r = my_min(8, 8); r = my_min(INT_MIN, INT_MIN); r = my_min(INT_MAX, INT_MAX); r = my_min(INT_MAX, INT_MIN); r = my_min(INT_MIN, INT_MAX); return 0; } ``` ::: 假設 `int` 型別的 object 在記憶體中佔用 4 個 byte ,每個 byte 有 8 個 bit。所以 `(x - y) >> (sizeof(int) * CHAR_BIT - 1)` 運算式會對 `(x - y)` 做 31 個 bit 的 bitwise right shift 31。 在==使用 GCC 編譯程式碼==的前提下。運算子 `>>` 對負整數而言是算術右移,對非負整數而言是邏輯右移。 1. 如果 `x - y` 為負整數 (也就是 `x` 小於 `y` ) ,則運算式 `(x - y) >> (sizeof(int) * CHAR_BIT - 1)` 的結果就是 `0xffffffff` 。那麼,我們可以對整個 `y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1)))` 運算式做下列改寫: `y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1)))` $\Rightarrow$ `y + ((x - y) & ((x - y) >> 31))` $\Rightarrow$ `y + ((x - y) & 0xffffffff)` $\Rightarrow$ `y + (x - y)` $\Rightarrow$ `x` 2. 如果 `x - y` 為正整數 (也就是 `x` 大於 `y` ) ,則運算式 `(x - y) >> (sizeof(int) * CHAR_BIT - 1)` 的結果就是 `0` 。那麼,我們可以對整個 `y + ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1)))` 運算式做下列改寫: $\Rightarrow$ `y + ((x - y) & ((x - y) >> 31))` $\Rightarrow$ `y + ((x - y) & 0)` $\Rightarrow$ `y + 0` $\Rightarrow$ `y` 然而第二種方法在第 15 行與第 16 行無法計算出正確答案。這兩行計算出來的答案都是 `INT_MAX` 而不是我們所期待的 `INT_MIN` 。這是 overflow 所造成的問題。所以==第二種方法的使用前提是 `x - y` 不能發生 overflow== 。 ### 找最大值 #### 第一種方法 ```c= int my_max(int x, int y) { return x ^ ((x ^ y) & -(x < y)); } int main() { int r = my_max(3, 5); r = my_max(9, 2); r = my_max(8, 8); return 0; } ``` * 運作原理 `min1.c` 相似。 * 其實,第 3 行中的運算式也可以寫成這樣 `y ^ ((x ^ y) & -(y < x))` 。 #### 第二種方法 ```c= #include <limits.h> int my_max(int x, int y) { return x - ((x - y) & ((x - y) >> (sizeof(int) * CHAR_BIT - 1))); } int main() { int r = my_max(3, 5); r = my_max(9, 2); r = my_max(8, 8); r = my_max(INT_MIN, INT_MIN); r = my_max(INT_MAX, INT_MAX); r = my_max(INT_MAX, INT_MIN); r = my_max(INT_MIN, INT_MAX); return 0; } ``` * 運作原理 `min2.c` 相似。 * 然而第二種方法在第 15 行與第 16 行無法計算出正確答案。這兩行計算出來的答案都是 `INT_MIN` 而不是我們所期待的 `INT_MAX` 。這是 overflow 所造成的問題。所以==第二種方法的使用前提是 `x - y` 不能發生 overflow== 。 ## [Determining if an integer is a power of 2](https://graphics.stanford.edu/~seander/bithacks.html#DetermineIfPowerOf2) ```c #include <stdbool.h> bool is_power_of_2(unsigned int v) { return v && !(v & (v - 1)); } int main() { bool f = is_power_of_2(100); f = is_power_of_2(1024); f = is_power_of_2(0); return 0; } ``` * 若變數 `v` 的值是 2 的冪 (power of 2),則它的二進位編碼中必定只有一個 bit 為 `1` 。假設變數 `v` 的數值為 `1024` ,它的二進位編碼為 10000000000$_{2}$ ,則運算式 `v - 1` 的結果為 01111111111$_{2}$ 。所以,運算式 `v & (v - 1)` 的結果就是 10000000000$_{2}$ & 01111111111$_{2}$ = 0 。 * 所以,==只要變數 `v` 的值是 2 的冪 ,運算式 `v & (v - 1)` 的結果就是 0== 。 * 有一個例外狀況需要處理。當變數 `v` 的值為 `0` 時, `v & (v - 1)` 的結果是 0 。但是, 0 並不是 2 的冪。所以把運算式改寫為 `v && !(v & (v - 1))` 。這樣就可以擋掉這個例外狀況。 ## Sign extending ### [Sign extending from a constant bit-width](https://graphics.stanford.edu/~seander/bithacks.html#FixedSignExtend) 假設有一個以二補數表示的有號數 `x` ,其二進位表示法由 b 個 bit 表示。然後,我們想要把 `x` 轉換成由 `int` 型別 (一般為 32 個 bit)的 object 表示。則左邊多出來的 bit 皆填 sign bit 。 ```c #define BIT_WIDTH (5) int main() { int x; // convert this from using BIT_WIDTH bits to a full int int r; // resulting sign extended number goes here struct { signed int x:BIT_WIDTH; } s; x = -3; r = s.x = x; x = 5; r = s.x = x; return 0; } ``` ``` (gdb) l 1 2 #define BIT_WIDTH (5) 3 int main() 4 { 5 int x; // convert this from using BIT_WIDTH bits to a full int 6 int r; // resulting sign extended number goes here 7 struct { 8 signed int x:BIT_WIDTH; 9 } s; 10 (gdb) 11 x = -3; 12 r = s.x = x; 13 14 x = 5; 15 r = s.x = x; 16 17 return 0; 18 } (gdb) b 11 Breakpoint 1 at 0x1131: file test.c, line 11. (gdb) r Starting program: /home/edward/workspace/linux-2022/gdb/test Breakpoint 1, main () at test.c:11 11 x = -3; (gdb) n 12 r = s.x = x; (gdb) x/4bx &r 0x7fffffffe2dc: 0x00 0x00 0x00 0x00 (gdb) n 14 x = 5; (gdb) x/4bx &r 0x7fffffffe2dc: 0xfd 0xff 0xff 0xff (gdb) n 15 r = s.x = x; (gdb) n 17 return 0; (gdb) x/4bx &r 0x7fffffffe2dc: 0x05 0x00 0x00 0x00 (gdb) ``` ### [Sign extending from a variable bit-width](https://graphics.stanford.edu/~seander/bithacks.html#VariableSignExtendj) 有時候我們想要把以 b 個 bit 表示的有號整數用更多 bit 表示,但我們又事先不知道 b 的確切數字。也就是說, `b` 是個變數。 ```c= int main() { unsigned b = 5; // number of bits representing the number in x int x = -3; // sign extend this b-bit number to r int r; // resulting sign-extended number int const m = 1U << (b - 1); // mask can be pre-computed if b is fixed x = x & ((1U << b) - 1); // (Skip this if bits in x above position b are already zero.) r = (x ^ m) - m; x = 3; x = x & ((1U << b) - 1); r = (x ^ m) - m; return 0; } ``` 以上面這個程式為例。變數 `b` (無號正整數)的數值為 `5` 。也就是用 5 個 bit 表示有號數。 第 6 行的變數 `m` 的值為 `0x00000010` 。 第 8 行。運算式 `(1U << b)` 的結果為 `0x00000020` 。接著,運算式 `(1U << b) - 1` 的結果為 `0x0000001f` 。 所以,整個運算式 `x & ((1U << b) - 1)` 的結果為 `0x0000001d` 。並把這個值指派給變數 `x` 。 * 也就是說,運算式 `(1U << b) - 1` 造出了一個 mask 。這個 mask 保留了變數 `x` 的 bit 0 ~ 4 ,其餘的 bit 全都清為 0 。 第 9 行。運算式 `x ^ m` 的結果為 `0x0000000d` 。所以,整個運算式 `(x ^ m) - m` 的結果為負整數 `0xfffffffd` ,並把這個值指派給變數 `r`。 * 也就是說,運算式 `x ^ m` 檢測變數 `x` 中的 sign bit (就是本範例中的 bit 4 )。 * 在這個範例中,以 5 個 bit 表示有號整數。所以有號整數的範圍為 -16 ~ 15 。 * 如果這 5 個 bit 表示一個負整數,則 sign bit (即 bit 4 ) 為 `1` 。那麼,運算式 `x ^ m` 相當於加 16 。則運算式 `(x ^ m) - m` 則是再減掉 `-16` ,就會回到原本的負整數。 * 如果這 5 個 bit 表示一個非負整數,則 sign bit (即 bit 4 ) 為 `0` 。那麼,運算式 `x ^ m` 相當於減 16 。則運算式 `(x ^ m) - m` 則是再減掉 `-16` ,就會回到原本的非負整數。 ### [Sign extending from a variable bit-width in 3 operations](https://graphics.stanford.edu/~seander/bithacks.html#VariableSignExtendRisky) ```c= #include <stdio.h> #include <limits.h> int main() { unsigned b = 2; // number of bits representing the number in x int x = 2; // sign extend this b-bit number to r int r; // resulting sign-extended number #define M(B) (1U << ((sizeof(x) * CHAR_BIT) - B)) // CHAR_BIT=bits/byte static int const multipliers[] = { 0, M(1), M(2), M(3), M(4), M(5), M(6), M(7), M(8), M(9), M(10), M(11), M(12), M(13), M(14), M(15), M(16), M(17), M(18), M(19), M(20), M(21), M(22), M(23), M(24), M(25), M(26), M(27), M(28), M(29), M(30), M(31), M(32) }; // (add more if using more than 64 bits) static int const divisors[] = { 1, ~M(1), M(2), M(3), M(4), M(5), M(6), M(7), M(8), M(9), M(10), M(11), M(12), M(13), M(14), M(15), M(16), M(17), M(18), M(19), M(20), M(21), M(22), M(23), M(24), M(25), M(26), M(27), M(28), M(29), M(30), M(31), M(32) }; // (add more for 64 bits) #undef M r = (x * multipliers[b]) / divisors[b]; printf("%d\n", r); return 0; } ``` 假設 1 個 byte 有 8 個 bit 。 `int` 與 `unsigned int` 都是佔用 4 個 byte 的記憶體空間。而我們要表示的有號整數佔用 b 個 bit 。 這個作法的核心想法就是 `(x << B) >> B` 。其中, `B = 32 - b` 。 在沒有發生 overflow 的前提下,運算式 `(x << B) >> B` 的結果與 `x` 所表示的數值相同。 #### 什麼時候會發生 overflow ? 如程式中的第 6~7 行所示,當變數 `b` 與 `x` 的數值皆為 `2` 時, $2 << 30 = 2 \times 2^{30} = 2^{31} > 2^{31} - 1$ 。會超過 `int` 型別所能表示的最大值 ($2^{31} - 1$),所以會發生 overflow。 把上面的程式編譯後,並執行。可得下列結果。 ``` $ gcc test.c -fsanitize=undefined -o test $ ./test test.c:27:12: runtime error: signed integer overflow: 2 * 1073741824 cannot be represented in type 'int' -2 ``` 原本預期程式會輸出 `2` 。但是因為 overflow ,得到的結果卻是 `-2` 。 由這個範例中的討論可以知道,只要 $log_{2}x + (32 - b) \ge 31$ 就會發生 overflow 。 在程式碼中,第 20 行的 `~M(1)` 跟 `multipliers` 與 `divisors` 這兩個陣列的其他元素看起來不大一樣。如果只用 1 個 bit 表示有號整數,那麼可以表示的數值範圍為 -1 ~ 0 。也就是當變數 `b` 與 `x` 皆為 `1` 時, $log_{2}x + (32 - b) = 31 \ge 31$ 。會發生 overflow 。無法套用 `(x << B) >> B` 這個想法。 ```c #include <stdio.h> #include <limits.h> int main() { unsigned b = 1; // number of bits representing the number in x int x = 1; // sign extend this b-bit number to r int r; // resulting sign-extended number #define M(B) (1U << ((sizeof(x) * CHAR_BIT) - B)) // CHAR_BIT=bits/byte int multiplier = M(b), divisor = M(b); r = (x * multiplier) / divisor; printf("%d\n", r); return 0; } ``` 編譯並執行後,可得下列結果。原本預期程式會輸出 `-1` 。但是因為 overflow ,得到的結果卻是 `1` 。 ``` $ gcc test.c -fsanitize=undefined -o test $ ./test 1 ``` 將 `divisor = M(b)` 改為 `divisor = ~M(b)` 。因為 `1 * M(1)` $\Rightarrow$ `1 << 31` $\Rightarrow$ `-2147483648` ,又 `~M(1)` $\Rightarrow$ `2147483647` 。所以 `-2147483648 / 2147483647` $\Rightarrow$ `-1` 。 ```c #include <stdio.h> #include <limits.h> int main() { unsigned b = 1; // number of bits representing the number in x int x = 1; // sign extend this b-bit number to r int r; // resulting sign-extended number #define M(B) (1U << ((sizeof(x) * CHAR_BIT) - B)) // CHAR_BIT=bits/byte int multiplier = M(b), divisor = ~M(b); r = (x * multiplier) / divisor; printf("%d\n", r); return 0; } ``` 編譯並執行後,就可以得到預期的結果。 ``` $ gcc test.c -fsanitize=undefined -o test $ ./test -1 ``` ## [Conditionally set or clear bits without branching](https://graphics.stanford.edu/~seander/bithacks.html#ConditionalSetOrClearBitsWithoutBranching) 不用 C 語言的 `if ... else ...` 語句 (即 without branch 或 branchless ) 做到下面這支程式的效果。也就是當變數 `f` 為 `1` 時,會按照變數 `m` 的 set bits 設定變數 `w` 中相對應的位元 。否則 (即變數 `f` 為 `0` ) ,會按照變數 `m` 的 set bits 清除變數 `w` 中相對應的位元 。變數 `f` 的值只會是 `0` 或 `1` 。 ```c #include <stdbool.h> int main() { bool f; // conditional flag unsigned int m; // the bit mask unsigned int w; // the word to modify if (f) w |= m; else w &= ~m; return 0; } ``` ### 方法一: ```c #include <stdbool.h> int main() { bool f = 1; // conditional flag unsigned int m = 0xabcdef12; // the bit mask unsigned int w = 0x12345678; // the word to modify w ^= (-f ^ w) & m; // w = w ^ ((-f ^ w) & m); return 0; } ``` 藉由觀察組合語言可以推得運算式 `w ^= (-f ^ w) & m` 等價於運算式 `w = w ^ ((-f ^ w) & m)` 。 ``` $ gcc test.c -O0 -g -o test $ objdump -d test | less 0000000000001129 <main>: 1129: f3 0f 1e fa endbr64 112d: 55 push %rbp 112e: 48 89 e5 mov %rsp,%rbp 1131: c7 45 f4 01 00 00 00 movl $0x1,-0xc(%rbp) 1138: c7 45 f8 12 ef cd ab movl $0xabcdef12,-0x8(%rbp) 113f: c7 45 fc 78 56 34 12 movl $0x12345678,-0x4(%rbp) 1146: 8b 45 f4 mov -0xc(%rbp),%eax 1149: f7 d8 neg %eax 114b: 33 45 fc xor -0x4(%rbp),%eax 114e: 23 45 f8 and -0x8(%rbp),%eax 1151: 31 45 fc xor %eax,-0x4(%rbp) 1154: b8 00 00 00 00 mov $0x0,%eax 1159: 5d pop %rbp 115a: c3 retq 115b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) ``` * 假設 1 個 byte 有 8 個 bit 。 1 個 `unsigned int` 佔用 4 個 byte 。 * $A \oplus B = (A \cdot \lnot B) + (\lnot A \cdot B)$ #### 變數 `f` 的數值為 `1` `w ^ ((-f ^ w) & m)` $\Rightarrow$ `w ^ ((-1 ^ w) & m)` $\Rightarrow$ `w ^ ((0xffffffff ^ w) & m)` $\Rightarrow$ `w ^ (w & m)` $\Rightarrow$ `(w & ~(w & m)) | (~w & (w & m))` $\Rightarrow$ `(w & (~w | ~m)) | (~w & (w & m))` $\Rightarrow$ `((w & ~w) | ( w & ~m)) | ((~w & w) & m)` $\Rightarrow$ `0 | ( w & ~m) | 0 & m` $\Rightarrow$ `( w & ~m) | 0` $\Rightarrow$ `w & ~m` | w | m | ~m | w & m | w ^ (w & m) | w & ~m | | --- | --- | ---- | ----- | ----------- | ------ | | 0 | 0 | 1 | 0 | 0 | 0 | | 0 | 1 | 0 | 0 | 0 | 0 | | 1 | 0 | 1 | 0 | 1 | 1 | | 1 | 1 | 0 | 1 | 0 | 0 | #### 變數 `f` 的數值為 `0` `w ^ ((-f ^ w) & m)` $\Rightarrow$ `w ^ ((0 ^ w) & m)` $\Rightarrow$ `w ^ (~w & m)` $\Rightarrow$ `(w & ~(~w & m)) | (~w & (~w & m))` $\Rightarrow$ `(w & (w | ~m)) | ((~w & ~w) & m)` $\Rightarrow$ `w | (~w & m)` $\Rightarrow$ `(w | ~w) & (w | m)` $\Rightarrow$ `1 & (w | m)` $\Rightarrow$ `w | m` | w | m | ~w | ~m | w \| ~m | w & (w \| ~m) | ~w & m | w ^ (~w & m) | w \| m | | --- | --- | ---- | ---- | ------- | ------------- | ------ | ------------ | ------ | | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | | 0 | 1 | 1 | 0 | 0 | 0 | 1 | 1 | 1 | | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 1 | ### 方法二: ```c #include <stdbool.h> int main() { bool f; // conditional flag unsigned int m; // the bit mask unsigned int w; // the word to modify w = (w & ~m) | (-f & m); return 0; } ``` #### 變數 `f` 的數值為 `1` `(w & ~m) | (-f & m)` $\Rightarrow$ `(w & ~m) | (-1 & m)` $\Rightarrow$ `(w & ~m) | (0xffffffff & m)` $\Rightarrow$ `(w & ~m) | m` $\Rightarrow$ `(w | m) & (~m | m)` $\Rightarrow$ `(w | m) & 1` $\Rightarrow$ `w | m` #### 變數 `f` 的數值為 `0` `(w & ~m) | (-f & m)` $\Rightarrow$ `(w & ~m) | (0 & m)` $\Rightarrow$ `(w & ~m) | 0` $\Rightarrow$ `w & ~m` ## [Conditionally negate a value without branching](https://graphics.stanford.edu/~seander/bithacks.html#ConditionalNegate) ### 方法一 在不使用分支的情況下,當 flag ( `fNegate` ) 的值為 `false` 時,取負數。 ```c #include <stdbool.h> int main() { bool fDontNegate = 0; // Flag indicating we should not negate v. int v = 1234; // Input value to negate if fDontNegate is false. int r = 5678; // result = fDontNegate ? v : -v; r = (fDontNegate ^ (fDontNegate - 1)) * v; return 0; } ``` 假設 `int` 佔用 4 個 byte ,每個 byte 有 8 個 bit 。 #### 當 fDontNegate 的值為 `0` 時 `r = (0 ^ (0 - 1)) * v;` $\Rightarrow$ `r = (0 ^ (-1)) * v;` $\Rightarrow$ `r = (0x00000000 ^ 0xffffffff) * v;` $\Rightarrow$ `r = (0xffffffff) * v;` $\Rightarrow$ `r = (-1) * v;` $\Rightarrow$ `r = -v;` 用 `gdb` 追蹤程式碼。看完下面組合語言程式碼的追蹤,可以得知上面的數學推導與電腦機器指令的執行過程是一致的。 對照一下上面的 C 語言程式碼,可以得知 `r = (fDontNegate ^ (fDontNegate - 1)) * v;` 這個 statement 對應到下面組合語言的第 14 ~ 18 行。 ```assemble= $ gcc test.c -O0 -g -o test $ gdb -q test Reading symbols from test... (gdb) disass main Dump of assembler code for function main: 0x0000000000001129 <+0>: endbr64 0x000000000000112d <+4>: push %rbp 0x000000000000112e <+5>: mov %rsp,%rbp 0x0000000000001131 <+8>: movb $0x0,-0x9(%rbp) 0x0000000000001135 <+12>: movl $0x4d2,-0x8(%rbp) 0x000000000000113c <+19>: movl $0x162e,-0x4(%rbp) 0x0000000000001143 <+26>: movzbl -0x9(%rbp),%eax 0x0000000000001147 <+30>: movzbl -0x9(%rbp),%edx 0x000000000000114b <+34>: sub $0x1,%edx 0x000000000000114e <+37>: xor %eax,%edx 0x0000000000001150 <+39>: mov -0x8(%rbp),%eax 0x0000000000001153 <+42>: imul %edx,%eax 0x0000000000001156 <+45>: mov %eax,-0x4(%rbp) 0x0000000000001159 <+48>: mov $0x0,%eax 0x000000000000115e <+53>: pop %rbp 0x000000000000115f <+54>: retq End of assembler dump. ``` 下面是用 `gdb` 追蹤組合語言的部分過程。 * 當執行完第 14 行 (`sub $0x1,%edx`) 後, CPU 暫存器 rdx 的內容由 `0` 轉為 `0xffffffff` 。 ```gdb (gdb) info register rdx rdx 0x0 0 (gdb) ni 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) info register rdx rdx 0xffffffff 4294967295 ``` * 當執行完第 15 行 (`xor %eax,%edx`) 後, CPU 暫存器 rdx 的內容維持 `0xffffffff`。 ```gdb (gdb) info register rax rax 0x0 0 (gdb) ni 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) info register rdx rdx 0xffffffff 4294967295 ```gdb * 當執行完第 16 行 (mov -0x8(%rbp),%eax) 與第 17 行 (`imul %edx,%eax`) 後, CPU 暫存器 rax 的內容變為 `0xfffffb2e` ,也就是 $-1234_{10}$ 。 ```gdb (gdb) info register rax rax 0x0 0 (gdb) ni 0x0000555555555153 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) info register rax rax 0x4d2 1234 (gdb) ni 0x0000555555555156 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) info register rax rax 0xfffffb2e 4294966062 ``` * 當執行完第 18 行 (mov %eax,-0x4(%rbp)) 後, 將計算結果 ($-1234_{10}$) 由 CPU 暫存器複製到記憶體位址 0x7fffffffe2cc (就是變數 `r`)中 。 因為 Intel CPU 架構採 little endian (least significant byte 放在低位址),所以正確的資料為 0xfffffb2e ,也就是 $-1234_{10}$ 。 ```gdb (gdb) info register rbp rbp 0x7fffffffe2d0 0x7fffffffe2d0 (gdb) x/4bx 0x7fffffffe2cc 0x7fffffffe2cc: 0x2e 0x16 0x00 0x00 (gdb) ni 10 return 0; (gdb) x/4bx 0x7fffffffe2cc 0x7fffffffe2cc: 0x2e 0xfb 0xff 0xff (gdb) ``` #### 當 fDontNegate 的值為 `1` 時 `r = (1 ^ (1 - 1)) * v;` $\Rightarrow$ `r = (1 ^ (0)) * v;` $\Rightarrow$ `r = (0x00000001 ^ (0x00000000)) * v;` $\Rightarrow$ `r = (0x00000001) * v;` $\Rightarrow$ `r = 1 * v;` $\Rightarrow$ `r = v;` 把 C code 中,變數 `fDontNegate` 的初值改為 `1` 。 下面是用 gdb 追蹤組合語言的部分過程。 ```assemble= $ gcc test.c -O0 -g -o test $ gdb -q test Reading symbols from test... (gdb) disass main Dump of assembler code for function main: 0x0000000000001129 <+0>: endbr64 0x000000000000112d <+4>: push %rbp 0x000000000000112e <+5>: mov %rsp,%rbp 0x0000000000001131 <+8>: movb $0x1,-0x9(%rbp) 0x0000000000001135 <+12>: movl $0x4d2,-0x8(%rbp) 0x000000000000113c <+19>: movl $0x162e,-0x4(%rbp) 0x0000000000001143 <+26>: movzbl -0x9(%rbp),%eax 0x0000000000001147 <+30>: movzbl -0x9(%rbp),%edx 0x000000000000114b <+34>: sub $0x1,%edx 0x000000000000114e <+37>: xor %eax,%edx 0x0000000000001150 <+39>: mov -0x8(%rbp),%eax 0x0000000000001153 <+42>: imul %edx,%eax 0x0000000000001156 <+45>: mov %eax,-0x4(%rbp) 0x0000000000001159 <+48>: mov $0x0,%eax 0x000000000000115e <+53>: pop %rbp 0x000000000000115f <+54>: retq End of assembler dump. ``` 一樣把注意力集中在第 14 ~ 18 行。 在程式執行到第 14 行時,變數 `fDontNegate` , `v` 與 `r` 的初值會被分別寫到 `0x7fffffffe2d7` (1 byte) , `0x7fffffffe2d8` (4 bytes) 與 `0x7fffffffe2dc` (4 bytes) 這三個記憶體位址。要注意的是, Intel CPU 採用 little endian 對記憶體讀寫資料。 ```gdb (gdb) info register rbp rbp 0x7fffffffe2e0 0x7fffffffe2e0 (gdb) x/4xb 0x7fffffffe2d7 0x7fffffffe2d7: 0x01 0x00 0x00 0x00 (gdb) x/4xb 0x7fffffffe2d8 0x7fffffffe2d8: 0xd2 0x04 0x00 0x00 (gdb) x/4xb 0x7fffffffe2dc 0x7fffffffe2dc: 0x2e 0x16 0x00 0x00 ``` 接著執行組合語言第 14 行 (`sub $0x1,%edx`)。因為在第 13 行已經把變數 `fDontNegate` (記憶體位址 `0x7fffffffe2d7` ) 的值複製給了 CPU 暫存器 `rdx` ,所執行完第 14 行後,暫存器 `rdx` 的值為 `0` 。 ```gdb (gdb) info register rdx rdx 0x1 1 (gdb) ni 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) info register rdx rdx 0x0 0 ``` 當執行完第 15 行 (xor %eax,%edx) 後, CPU 暫存器 rdx 的內容變為 `1`。因為暫存器 eax 在第 12 行已被設定為變數 `fDontNegate` 的值,也就是 `1` 。 ```gdb (gdb) ni 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) info register rdx rdx 0x1 1 ``` 當執行完第 16 行 ( `mov -0x8(%rbp),%eax` ) 與第 17 行 ( `imul %edx,%eax` ) 後, CPU 暫存器 rax 的內容變為 `0x4d2` ,也就是 `1234` 。 ```gdb (gdb) info register rax rax 0x1 1 (gdb) ni 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) ni 0x0000555555555153 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) info register rax rax 0x4d2 1234 ``` 當執行完第 18 行 ( `mov %eax,-0x4(%rbp)` ) 後, 將計算結果 ( $1234_{10}$ ) 由 CPU 暫存器複製到記憶體位址 `0x7fffffffe2dc` (就是變數 r)中 。 ```gdb (gdb) x/4dx 0x7fffffffe2dc 0x7fffffffe2dc : 0x2e 0x16 0x00 0x00 (gdb) ni 0x0000555555555156 9 r = (fDontNegate ^ (fDontNegate - 1)) * v; (gdb) ni 10 return 0; (gdb) x/4dx 0x7fffffffe2dc 0x7fffffffe2dc: 0xd2 0x04 0x00 0x00 ``` ### 方法二 在不使用分支的情況下,當 flag ( `fNegate` ) 的值為 `true` 時,取負數。 ```c #include <stdbool.h> // If you need to negate only when a flag is true, then use this: int main() { bool fNegate = 1; // Flag indicating if we should negate v. int v = 1234; // Input value to negate if fNegate is true. int r = 5678; // result = fNegate ? -v : v; r = (v ^ -fNegate) + fNegate; return 0; } ``` 假設 `int` 佔用 4 個 byte ,每個 byte 有 8 個 bit 。 #### 當 fDontNegate 的值為 `1` 時 `r = (v ^ -fNegate) + fNegate;` $\Rightarrow$ `r = (v ^ -1) + 1;` $\Rightarrow$ `r = (v ^ 0xffffffff) + 1;` $\Rightarrow$ `r = ~v + 1;` $\Rightarrow$ `r = -v;` 接著用 GDB 追蹤程式碼。 ```gdb= $ gcc test.c -O0 -g -o test $ gdb -q test Reading symbols from test... (gdb) disass main Dump of assembler code for function main: 0x0000000000001129 <+0>: endbr64 0x000000000000112d <+4>: push %rbp 0x000000000000112e <+5>: mov %rsp,%rbp 0x0000000000001131 <+8>: movb $0x1,-0x9(%rbp) 0x0000000000001135 <+12>: movl $0x4d2,-0x8(%rbp) 0x000000000000113c <+19>: movl $0x162e,-0x4(%rbp) 0x0000000000001143 <+26>: movzbl -0x9(%rbp),%eax 0x0000000000001147 <+30>: neg %eax 0x0000000000001149 <+32>: xor -0x8(%rbp),%eax 0x000000000000114c <+35>: mov %eax,%edx 0x000000000000114e <+37>: movzbl -0x9(%rbp),%eax 0x0000000000001152 <+41>: add %edx,%eax 0x0000000000001154 <+43>: mov %eax,-0x4(%rbp) 0x0000000000001157 <+46>: mov $0x0,%eax 0x000000000000115c <+51>: pop %rbp 0x000000000000115d <+52>: retq End of assembler dump. ``` 對照 C code 之後,可以很容易地理解到,組合語言中的第 9 到 11 行就是將變數 `fNegate` 、 `v` 以及 `r` 分別初始化為 `1` 、 `1234` 以及 `5678` 。所以,這三個變數的記憶體位址分別為 `0x7fffffffe247` (`-0x9(%rbp)`) 、 `0x7fffffffe248` (`-0x8(%rbp)`) 以及 `0x7fffffffe24c` (`-0x4(%rbp)`)。 ```gdb (gdb) info register rbp rbp 0x7fffffffe250 0x7fffffffe250 (gdb) x/1xb 0x7fffffffe247 0x7fffffffe247: 0x01 (gdb) x/4xb 0x7fffffffe248 0x7fffffffe248: 0xd2 0x04 0x00 0x00 (gdb) x/4xb 0x7fffffffe24c 0x7fffffffe24c: 0x2e 0x16 0x00 0x00 ``` 組合語言第 12 與 13 行將變數 `fNegate` 的值 (也就是記憶體位址 `0x7fffffffe247` 中的值) 複製到暫存器 `eax` 中。然後再對暫存器 `eax` 取負值。 ```gdb (gdb) info register rax rax 0x555555555129 93824992235817 (gdb) ni 0x0000555555555147 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0x1 1 (gdb) ni 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0xffffffff 4294967295 ``` 組合語言第 14 行將變數 `v` 的值 ( `0x04d2` ) 與 `0xffffffff` 也就是 ( `-1` ) 進行 xor 運算。得到的結果為 `~v` ( `0xfb2d` ) 。 ```gdb (gdb) ni 0x000055555555514c 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0xfffffb2d 4294966061 ``` 組合語言第 15 到 17 行將 `0xfb2d` 複製到暫存器 edx ,將變數 `fNegate` 的值 (`1`) 複製到暫存器 eax ,將暫存器 edx 中的值與暫存器 eax 中的值相加 ( `0xfb2d + 1` ) 後的和存入暫存器 eax 中。 ```gdb (gdb) info register rdx rdx 0x7fffffffe358 140737488347992 (gdb) ni 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rdx rdx 0xfffffb2d 4294966061 (gdb) info register rax rax 0xfffffb2d 4294966061 (gdb) ni 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0x1 1 (gdb) ni 0x0000555555555154 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0xfffffb2e 4294966062 ``` 最後,組合語言第 18 行,將最後的結果 `0xfb2e` ( `-1234` ) 複製到變數 `r` 中。 ```gdb (gdb) x/4bx 0x7fffffffe24c 0x7fffffffe24c: 0x2e 0x16 0x00 0x00 (gdb) ni 11 return 0; (gdb) x/4bx 0x7fffffffe24c 0x7fffffffe24c: 0x2e 0xfb 0xff 0xff ``` #### 當 fDontNegate 的值為 `0` 時 `r = (v ^ -fNegate) + fNegate;` $\Rightarrow$ `r = (v ^ 0) + 0;` $\Rightarrow$ `r = v + 0;` $\Rightarrow$ `r = v;` 在 C code 中,將變數 `fNegate` 的初值改為 `0` 。 接著用 GDB 追蹤程式碼。 ```gdb= $ gcc test.c -O0 -g -o test $ gdb -q test Reading symbols from test... (gdb) disass main Dump of assembler code for function main: 0x0000000000001129 <+0>: endbr64 0x000000000000112d <+4>: push %rbp 0x000000000000112e <+5>: mov %rsp,%rbp 0x0000000000001131 <+8>: movb $0x0,-0x9(%rbp) 0x0000000000001135 <+12>: movl $0x4d2,-0x8(%rbp) 0x000000000000113c <+19>: movl $0x162e,-0x4(%rbp) 0x0000000000001143 <+26>: movzbl -0x9(%rbp),%eax 0x0000000000001147 <+30>: neg %eax 0x0000000000001149 <+32>: xor -0x8(%rbp),%eax 0x000000000000114c <+35>: mov %eax,%edx 0x000000000000114e <+37>: movzbl -0x9(%rbp),%eax 0x0000000000001152 <+41>: add %edx,%eax 0x0000000000001154 <+43>: mov %eax,-0x4(%rbp) 0x0000000000001157 <+46>: mov $0x0,%eax 0x000000000000115c <+51>: pop %rbp 0x000000000000115d <+52>: retq End of assembler dump. ``` 和上一節一樣,組合語言中的第 9 到 11 行就是將變數 `fNegate` 、 `v` 以及 `r` 分別初始化為 `0` 、 `1234` 以及 `5678` 。所以,這三個變數的記憶體位址分別為 `0x7fffffffe247` (`-0x9(%rbp)`) 、 `0x7fffffffe248` (`-0x8(%rbp)`) 以及 `0x7fffffffe24c` (`-0x4(%rbp)`)。 ```gdb (gdb) x/1xb 0x7fffffffe247 0x7fffffffe247: 0x00 (gdb) x/4xb 0x7fffffffe248 0x7fffffffe248: 0xd2 0x04 0x00 0x00 (gdb) x/4xb 0x7fffffffe24c 0x7fffffffe24c: 0x2e 0x16 0x00 0x00 ``` 組合語言第 12 與 13 行將變數 fNegate 的值 (也就是記憶體位址 0x7fffffffe247 中的值) 複製到暫存器 eax 中。然後再對暫存器 eax 取負值。 ```gdb (gdb) info register rax rax 0x555555555129 93824992235817 (gdb) ni 0x0000555555555147 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0x0 0 (gdb) ni 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0x0 0 ``` 組合語言第 14 行將變數 v 的值 ( `0x04d2` ) 與 `0` 進行 xor 運算。得到的結果為 ~v ( `0x04d2` ) 。 ```gdb (gdb) ni 0x000055555555514c 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0x4d2 1234 ``` 組合語言第 15 到 17 行將 `0x04d2` 複製到暫存器 edx ,將變數 `fNegate` 的值 ( `0` ) 複製到暫存器 eax ,將暫存器 edx 中的值與暫存器 eax 中的值相加 ( `0x04d2 + 0` ) 後的和存入暫存器 eax 中。 ```gdb (gdb) info register rdx rdx 0x7fffffffe358 140737488347992 (gdb) ni 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rdx rdx 0x4d2 1234 (gdb) info register rax rax 0x4d2 1234 (gdb) ni 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0x0 0 (gdb) ni 0x0000555555555154 10 r = (v ^ -fNegate) + fNegate; (gdb) info register rax rax 0x4d2 1234 ``` 最後,組合語言第 18 行,將最後的結果 `0xfb2e` ( `-1234` ) 複製到變數 `r` 中。 ```gdb (gdb) x/4bx 0x7fffffffe24c 0x7fffffffe24c: 0x2e 0x16 0x00 0x00 (gdb) ni 11 return 0; (gdb) x/4bx 0x7fffffffe24c 0x7fffffffe24c: 0xd2 0x04 0x00 0x00 ``` ## [Merge bits from two values according to a mask](https://graphics.stanford.edu/~seander/bithacks.html#MaskedMerge) ```c int main() { unsigned int a; // value to merge in non-masked bits unsigned int b; // value to merge in masked bits unsigned int mask; // 1 where bits from b should be selected; 0 where from a. unsigned int r; // result of (a & ~mask) | (b & mask) goes here r = a ^ ((a ^ b) & mask); return 0; } ``` 這段程式碼中定義了 4 個 `unsigned int` 變數 `a` 、 `b` 、 `mask` 與 `r` 。若變數 `mask` 的 bit i 為 `1` 則變數 `r` 的 bit i 會與變數 `b` 的 bit i 相同。若變數 `mask` 的 bit i 為 `0` 則變數 `r` 的 bit i 會與變數 `a` 的 bit i 相同。 以下標 $_{i}$ 表示某個變數的 bit i 。 ### 當 $mask_{i}$ 為 `1` 時 $r_{i} = a_{i} \oplus ((a_{i} \oplus b_{i}) \land mask_{i})$ $\Rightarrow r_{i} = a_{i} \oplus ((a_{i} \oplus b_{i}) \land 1)$ $\Rightarrow r_{i} = a_{i} \oplus (a_{i} \oplus b_{i})$ $\Rightarrow r_{i} = b_{i}$ 所以,當變數 `mask` 的 bit i 為 `1` 則變數 `r` 的 bit i 會與變數 `b` 的 bit i 相同。 ### 當 $mask_{i}$ 為 `0` 時 $r_{i} = a_{i} \oplus ((a_{i} \oplus b_{i}) \land mask_{i})$ $\Rightarrow r_{i} = a_{i} \oplus ((a_{i} \oplus b_{i}) \land 0)$ $\Rightarrow r_{i} = a_{i} \oplus 0$ $\Rightarrow r_{i} = a_{i}$ 所以,當變數 `mask` 的 bit i 為 `0` 則變數 `r` 的 bit i 會與變數 `a` 的 bit i 相同。 ## Counting bits set ### [Counting bits set (naive way)](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetNaive) ```c int main() { unsigned int v; // count the number of bits set in v unsigned int c; // c accumulates the total bits set in v for (c = 0; v; v >>= 1) c += v & 1; return 0; } ``` 這支程式應該不需要多做解釋。 ### [Counting bits set by lookup table](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetTable) #### 方法一 ```c int main() { static const unsigned char BitsSetTable256[256] = { #define B2(n) n, n+1, n+1, n+2 #define B4(n) B2(n), B2(n+1), B2(n+1), B2(n+2) #define B6(n) B4(n), B4(n+1), B4(n+1), B4(n+2) B6(0), B6(1), B6(1), B6(2) }; unsigned int v; // count the number of bits set in 32-bit value v unsigned int c; // c is the total bits set in v // Option 1: c = BitsSetTable256[v & 0xff] + BitsSetTable256[(v >> 8) & 0xff] + BitsSetTable256[(v >> 16) & 0xff] + BitsSetTable256[v >> 24]; return 0; } ``` #### 方法二 ```c int main() { static const unsigned char BitsSetTable256[256] = { #define B2(n) n, n+1, n+1, n+2 #define B4(n) B2(n), B2(n+1), B2(n+1), B2(n+2) #define B6(n) B4(n), B4(n+1), B4(n+1), B4(n+2) B6(0), B6(1), B6(1), B6(2) }; unsigned int v; // count the number of bits set in 32-bit value v unsigned int c; // c is the total bits set in v // Option 2: unsigned char * p = (unsigned char *) &v; c = BitsSetTable256[p[0]] + BitsSetTable256[p[1]] + BitsSetTable256[p[2]] + BitsSetTable256[p[3]]; return 0; } ``` #### 另一種產生 table 的方法 ```c // To initially generate the table algorithmically: BitsSetTable256[0] = 0; for (int i = 0; i < 256; i++) { BitsSetTable256[i] = (i & 1) + BitsSetTable256[i / 2]; } ``` ### [Counting bits set, Brian Kernighan's way](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetKernighan) ### [Counting bits set in 14, 24, or 32-bit words using 64-bit instructions](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSet64) ### [Counting bits set, in parallel](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetParallel) ### [Count bits set (rank) from the most-significant bit upto a given position](https://graphics.stanford.edu/~seander/bithacks.html#CountBitsFromMSBToPos) ### [Select the bit position (from the most-significant bit) with the given count (rank)](https://graphics.stanford.edu/~seander/bithacks.html#SelectPosFromMSBRank) ## [Swapping values with XOR](https://graphics.stanford.edu/~seander/bithacks.html#SwappingValuesXOR) ```c= int main() { int x = 12, y = 77; x = x ^ y; y = x ^ y; x = x ^ y; return 0; } ``` 或者可以跟原作者一樣定義 macro 。 ```c #define SWAP(a, b) (((a) ^= (b)), ((b) ^= (a)), ((a) ^= (b))) ``` 接下來研究如何用 bitwise xor 運算子 (`^`) 交換兩個型別為 `int` 的變數的值。 | x | y | x ^ y | | --- | --- | ------ | | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 0 | 由 xor 的真值表可以看出==運算式 `x ^ x` 的結果是 0== 。 所以,第 5 行可以改寫如下: ```c=5 y = (12 ^ 77) ^ 77; ``` 所以,當第 5 行執行完畢後,變數 `y` 的值就是變數 `x` 一開始的值,也就是 `12` 。 接著看第 6 行。這時,變數 `x` 停留在第 4 行執行結束後的狀態,也就是 `x ^ y`。且此時變數 `y` 的值已經是變數 `x` 原本一開始的數值。所以可以把第 6 行改寫如下: ```c=6 x = (12 ^ 77) ^ 12; ``` 所以,第 6 行執行完畢後,變數 `x` 的數值就會是變數 `y` 原本一開始的數值,也就是 `77` 。 ### 有條件交換兩個變數的數值 ```c= #include <stdio.h> int main() { int x = 12, y = 77, cond = 0, t; t = x ^ y; y = y ^ (t & -cond); x = x ^ (t & -cond); return 0; } ``` * 假設整數型別 ( `int` ) 變數在記憶體中佔用 4 個 byte ,且每個 byte 有 8 個 bit 。 * 若變數 `cond` 的數值為 `0` ,則運算式 `t & -cond` 的結果就是 `0` ,導致第 6 行與第 7 行可寫如下。 ```c=6 y = 77 ^ 0; x = 12 ^ 0; ``` * 若變數 `cond` 的數值為 `1` ,則運算式 `-cond` 的結果就是 `-1` 。而 `-1` 的二補數編碼就是 `0xffffffff` 。那麼,運算式 `t & 0xffffffff` 的結果還是變數 `t` 的數值。所以,第 6 行與第 7 行可寫如下。 ```c=6 y = 77 ^(12 ^ 77) x = 12 ^(12 ^ 77) ``` 所以,當變數 `cond` 為 `0` 時,變數 `x` 與 `y` 的數值維持不變。當變數 `cond` 為 `1` 時,變數 `x` 與 `y` 的數值互換。 * NOTE: 這部分的內容參考 [看條件交換兩個數](https://hackmd.io/@0xff07/ORAORAORAORA#%E7%9C%8B%E6%A2%9D%E4%BB%B6%E4%BA%A4%E6%8F%9B%E5%85%A9%E5%80%8B%E6%95%B8) 的敘述。 ## 參考資料 * [Bit Twiddling Hacks](https://graphics.stanford.edu/~seander/bithacks.html) * [Bit Twiddling Hacks 解析 (一)](https://hackmd.io/@0xff07/ORAORAORAORA) * [Bit Twiddling Hacks 解析 (二)](https://hackmd.io/@0xff07/MUDAMUDAMUDA) * [Bit Twiddling Hacks 解析 (三)](https://hackmd.io/@0xff07/WRYYYYYYYYYY) * Computer Systems: A Programmer's Perspective, Third Edition * [解讀計算機編碼](https://hackmd.io/@sysprog/binary-representation) * [CS107 Guide to x86-64 - Stanford University](https://web.stanford.edu/class/archive/cs/cs107/cs107.1222/guide/x86-64.html)