主講人: jserv / 課程討論區: 2016 年系統軟體課程
0.1 - 0.3 + 0.2
的結果竟然不等於 0 ?!
其他 integer overflow 案例:
When exactly do side-effects take place in C and C++?
int a = 41; a++; printf("%d\n", a); 42
int a = 41; a++ & printf("%d\n", a); undefined
int a = 41; a++ && printf("%d\n", a); 42
int a = 41; if (a++ < 42) printf("%d\n", a); 42
int a = 41; a = a++; printf("%d\n", a); undefined
以下的差異呢?
語法/語意分析
Left & Right
中 (位元運算),關於是先對 Left
還是 Right
取值,沒有明確順序Left && Right
來說 (邏輯運算),一定是先確保 Left
成立,才會作 Right
以下程式碼中:
a++ && printf();
左邊先運算 a++
,倘若為真,接著才會作右邊的 printf()
C 語言中,x & (x - 1) == 0
的數學意義
C 語言中,x | (x + 1)
又表示什麼?
以下 C 語言程式的 DETECT 巨集能做什麼?
#if LONG_MAX == 2147483647L
#define DETECT(X) \
(((X) - 0x01010101) & ~(X) & 0x80808080)
#else
#if LONG_MAX == 9223372036854775807L
#define DETECT(X) \
(((X) - 0x0101010101010101) & ~(X) & 0x8080808080808080)
#else
#error long int is not a 32bit or 64bit type.
#endif
#endif
DETECT
在偵測什麼?
測試這程式時,要注意到由於 LONG_MAX 定義在 <limits.h>
裡面,因此要記得作 #include
這個巨集的用途是在偵測是否為 0 或者說是否為 NULL char ’\0’,也因此,我們可以在 iOS 的原始程式碼 strlen 的實作中看到這一段。那,為什麼這一段程式碼可以用來偵測 NULL char ?
我們先思考 strlen() 該怎麼實做,以下實作一個簡單的版本
unsigned int strlen(const char *s)
{
char *p = s;
while (*p != ’\0’) p++;
return (p - s);
}
這樣的版本有什麼問題?雖然看起來精簡,但是因為他一次只檢查 1byte,所以一旦字串很長,他就會處理很久。另外一個問題是,假設是在 32-bit 的 CPU 上,一次是處理 4-byte (32-bit) 大小的資訊,不覺得這樣很浪費嗎?
為了可以思考這樣的程式,我們由已知的計算方式來逆推原作者可能的思考流程,首先先將計算再簡化一點點,將他從 (((X) - 0x01010101) & ~(X) & 0x80808080) 變成
((X) - 0x01) & ~(X) & 0x80
還是看不懂,將以前學過的笛摩根定理套用上去,於是這個式子就變成了
~( ~(X - 0x01) | X ) & 0x80
再稍微調整一下順序
~( X | ~(X - 0x01) ) & 0x80
所以我們就可進行分析
X | ~(X - 0x01)
=> 取得最低位元是否為 0 ,並將其他位元設為 1
上面這兩組組合起來,我們可以得到以下結果
於是我們知道,原來這樣的運算,如果一個 byte 是 0,那經由這個運算得到的結果會是 0x80,反之為 0。
再將這個想法擴展到 32-bit,是不是可以想到說在 32bit 的情況下,0 會得到 0x80808080 這樣的答案?我們只要判斷這個數值是不是存在,就可以找到 ’\0’ 在哪了!
參考資料:
應用:
只能使用位元運算子和遞迴,在C程式中實做兩個整數的加法,可行嗎?
先回顧加法器 的實做,思考以下程式碼:
int add(int a, int b)
{
if (b == 0) return a;
int sum = a ^ b; /* 相加但不進位 */
int carry = (a & b) << 1; /* 進位但不相加 */
return add(sum, carry);
}
延伸閱讀: How to simulate a 4-bit binary adder in C
Fast computing of log2 for 64-bit integers
類似 De Bruijn 演算法
64-bit version
const int tab64[64] = {
63, 0, 58, 1, 59, 47, 53, 2,
60, 39, 48, 27, 54, 33, 42, 3,
61, 51, 37, 40, 49, 18, 28, 20,
55, 30, 34, 11, 43, 14, 22, 4,
62, 57, 46, 52, 38, 26, 32, 41,
50, 36, 17, 19, 29, 10, 13, 21,
56, 45, 25, 31, 35, 16, 9, 12,
44, 24, 15, 8, 23, 7, 6, 5
};
int log2_64 (uint64_t value)
{
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
value |= value >> 32;
return tab64[((uint64_t)((value - (value >> 1 ))*0x07EDD5E59A4E28C2)) >> 58];
}
const int tab32[32] = {
0, 9, 1, 10, 13, 21, 2, 29,
11, 14, 16, 18, 22, 25, 3, 30,
8, 12, 20, 28, 15, 17, 24, 7,
19, 27, 23, 6, 26, 5, 4, 31
};
int log2_32 (uint32_t value)
{
value |= value >> 1;
value |= value >> 2;
value |= value >> 4;
value |= value >> 8;
value |= value >> 16;
return tab32[(uint32_t)(value*0x07C4ACDD) >> 27];
}
gcc 提供 built-in Function:
可用來實做 log2:
#define LOG2(X) ((unsigned) \
(8 * sizeof (unsigned long long) -
__builtin_clzll((X)) - 1))
那該如何實做 clz 呢?
int clz(uint32_t x) {
int n = 32, c = 16;
do {
uint32_t y = x >> c;
if (y) { n -= c; x = y; }
c >>= 1;
} while (c);
return (n - x);
}
int clz(uint32_t x) {
if (x == 0) return 32;
int n = 0;
if (x <= 0x0000FFFF) { n += 16; x <<= 16; }
if (x <= 0x00FFFFFF) { n += 8; x <<= 8; }
if (x <= 0x0FFFFFFF) { n += 4; x <<= 4; }
if (x <= 0x3FFFFFFF) { n += 2; x <<= 2; }
if (x <= 0x7FFFFFFF) { n += 1; x <<= 1; }
return n;
}
int clz(uint32_t x) {
if (x == 0) return 32;
int n = 1;
if ((x >> 16) == 0) { n += 16; x <<= 16; }
if ((x >> 24) == 0) { n += 8; x <<= 8; }
if ((x >> 28) == 0) { n += 4; x <<= 4; }
if ((x >> 30) == 0) { n += 2; x <<= 2; }
n = n - (x >> 31);
return n;
}
0x10000000
,ffs(128)會回傳 80x10000001,
ffs(129) 會回傳 1延伸閱讀: Bit scanning equivalencies
考慮以下 C 程式,解說在 32-bit 架構下具體作用(不是逐行註解),以及能否避開用迴圈?
int func(unsigned int x) {
int val = 0; int i = 0;
for (i = 0; i < 32; i++) {
val = (val << 1) | (x & 0x1);
x >>= 1;
}
return val;
}
簡單來說這段程式碼就是拿來顛倒輸入數字的位元順序,如下面測試所示,顛倒後位元不足 32bit 者,全部補 0
------input number 99--------
2bit= 1100011
val = 11000110000000000000000000000000
------output number -973078528--------
------input number 198--------
2bit= 11000110
val = 1100011000000000000000000000000
------output number 1660944384--------
------input number 297--------
2bit= 100101001
val = 10010100100000000000000000000000
------output number -1803550720--------
------input number 396--------
2bit= 110001100
val = 110001100000000000000000000000
------output number 830472192--------
------input number 4294967281--------
2-bit= 11111111111111111111111111110001
val = 10001111111111111111111111111111
------output number -1879048193--------
參考 Reverse integer bitwise without using loop,將原本的 for 迴圈變更為 bit-wise 操作:
new = num;
new = ((new & 0xffff0000) >> 16) | ((new & 0x0000ffff) << 16);
new = ((new & 0xff00ff00) >> 8) | ((new & 0x00ff00ff) << 8);
new = ((new & 0xf0f0f0f0) >> 4) | ((new & 0x0f0f0f0f) << 4);
new = ((new & 0xcccccccc) >> 2) | ((new & 0x33333333) << 2);
new = ((new & 0xaaaaaaaa) >> 1) | ((new & 0x55555555) << 1);
在不使用迴圈的情況下,可以做到一樣的功能。
上圖左 1 是原圖,左 2 是用 AND 做運算之後,右 2 是用 OR 做運算之後,右 1 是用 XOR,可見使用 XOR 的加密效果最好。
參考資料:Ciphers vs. codes