整數算術 (Integer Arithmetic)
Overview Table
| 小節 | 主題 | 核心結論 |
|---|---|---|
| 2.3.1 (p.120) | Unsigned 加法 | s < x |
| 2.3.2 (p.126) | Two's-complement 加法 | 三種情況(正溢位/正常/負溢位);位元層級與 unsigned 加法完全相同 |
| 2.3.3 (p.131) | Two's-complement 取負 | TMin 的加法反元素是自己;-x == ~x + 1 |
| 2.3.4 (p.132) | Unsigned 乘法 | |
| 2.3.5 (p.133) | Two's-complement 乘法 | 截斷後位元層級與 unsigned 乘法相同;XDR 漏洞案例 |
| 2.3.6 (p.137) | 乘以常數 | 編譯器用 shift + add/sub 取代乘法(Form A / Form B) |
| 2.3.7 (p.139) | 除以 2 的冪 | unsigned 用 logical shift;signed 用 arithmetic shift + bias 修正捨入 |
| 2.3.8 (p.143) | 總結 | 電腦整數算術本質是 modular arithmetic,有限字長必然可能 overflow |
全節最核心的洞察:同一個位元層級運算(加、減、乘、左移)同時實現了 unsigned 與 two's-complement 兩種算術。這正是大多數機器用同一條指令做有號/無號加法與乘法的原因;差異只出現在「解讀結果」與「右移方向、比較、除法」上。
2.3.1 Unsigned 加法 (p.120-126)
兩個 w-bit 非負整數
公式(Eq. 2.11):
真實和 x+y w-bit unsigned 和
2^(w+1) ─┐
│ Overflow ──── 減去 2^w ────┐
2^w ─┤ ├─ 2^w
│ Normal ──── 原值不變 ────┤
0 ─┘ └─ 0
- 範例(w=4):
9 + 12 = 21 = [10101],丟棄最高位得[0101] = 5 = 21 mod 16。 - Overflow 偵測原理:令
,則 overflow 發生 若且唯若 (等價於 )。推導:未溢位時 ;溢位時 ,因為 。 - C 語言中 unsigned overflow 不會發出任何錯誤訊號,程式須自行檢查。
- Unsigned 取負(Eq. 2.12):模加法構成 abelian group(交換、結合、單位元素 0、每個元素有加法反元素):
getpeername (2002, p.122)
copy_from_kernel(void *user_dest, int maxlen) 用 int len = KSIZE < maxlen ? KSIZE : maxlen; 計算複製長度。攻擊者傳入負的 maxlen,len 為負,但傳入 memcpy 的參數 n 型別是 size_t(unsigned),負數被隱式轉換成極大正數(至少 maxlen、len 與回傳值統一宣告為 size_t,消除 signed/unsigned 型別不一致。
2.3.2 Two's-Complement 加法 (p.126-131)
有號數
真實和 x+y w-bit two's-complement 和
+2^w ─┐
│ Positive overflow ── 減 2^w ──┐
+2^(w-1)─┤ ├─ +2^(w-1)
│ │
0 ─┤ Normal ────── 原值不變 ──────┤─ 0
│ │
-2^(w-1)─┤ ├─ -2^(w-1)
│ Negative overflow ─ 加 2^w ──┘
-2^w ─┘
- 位元層級:two's-complement 和與 unsigned 和的位元表示完全相同,形式化為
(Eq. 2.14)。因此多數 CPU 用同一條加法指令處理兩者。 - 4-bit 範例(Figure 2.25):
5 + 5 = 10→ 截斷得-6(positive overflow,少 16);-8 + (-5) = -13→ 得3(negative overflow,多 16);-8 + 5 = -3(normal,不溢位)。 - Overflow 偵測原理:令
: - Positive overflow ⟺
x > 0 && y > 0 && s <= 0 - Negative overflow ⟺
x < 0 && y < 0 && s >= 0 - 一正一負相加絕不會溢位。
- Positive overflow ⟺
sum - x == y 檢查加法溢位是錯的(Practice 2.31, p.130)
Two's-complement 加法構成 abelian group,(x+y)-x 恆等於 y(溢位效果在減回時完全抵銷),所以該檢查對任何輸入都回傳 1,無法偵測 overflow。正確做法是檢查符號模式(如上)。
tsub_ok(x, y) 寫成 tadd_ok(x, -y) 的漏洞(Practice 2.32, p.130)
當 y = TMin 時 -y 仍是 TMin(見 2.3.3),判斷結果錯誤。正確版本須另行處理 y == TMin 的情況。
2.3.3 Two's-Complement 取負 (p.131-132)
公式(Eq. 2.15):
TMin是自己的加法反元素:造成 negative overflow,截斷後為 0。這是「取負」最重要的例外。 - 位元技巧一:
-x == ~x + 1(逐位取反再加一),對所有整數值成立。例:0xfffffffa逐位取反得0x00000005,加 1 得 6,故原值為 −6。 - 位元技巧二:設 k 為最右邊的 1 的位置(x ≠ 0),則取負 = 位置 k 左邊的位元全部取反、位置 k 與其右邊保持不變。例:
[0101](5)→[1011](−5)。 - 觀察(Practice 2.28/2.33):unsigned 取負與 two's-complement 取負對同一位元模式產生的結果位元模式相同。
2.3.4 Unsigned 乘法 (p.132-133)
真實乘積
2.3.5 Two's-Complement 乘法 (p.133-137)
有號乘積範圍
- 位元層級等價原理:
——同一組位元向量,不論當作有號或無號相乘,截斷後的位元結果完全相同(完整 2w-bit 乘積則可能不同)。推導關鍵: ,mod 後含 、 權重的項全部消失(Eq. 2.18)。 - 3-bit 範例(Figure 2.27):
[101]×[011],unsigned 為 5×3=15=[001111],signed 為 −3×3=−9=[110111],截斷後皆為[111]。 - 溢位偵測(Practice 2.35):
!x || p/x == y可行——乘法可用除法驗證(因且 ⟺ 溢位;除法無截斷、結果唯一),這與「加法不能用減法驗證」形成對比。也可改用更寬型別: int64_t p = (int64_t) x * y;再檢查p == (int) p(Practice 2.36)。
malloc(ele_cnt * ele_size):攻擊者傳 ele_cnt = 2^20 + 1、ele_size = 2^12(32-bit 程式),乘積 uint64_t 仍不夠(Practice 2.37)——malloc 參數是 32-bit size_t,傳入時再被截斷一次;必須先檢查 64-bit 乘積是否超出 size_t 範圍再呼叫。calloc 的許多實作也有同樣問題。
2.3.6 乘以常數 (p.137-139)
整數乘法指令歷史上需 10+ clock cycles(參考機 Intel Core i7 Haswell 上需 3 cycles),而 add/sub/shift 只需 1 cycle,因此編譯器把「乘以常數」改寫成 shift + add/sub 組合。
- 乘以 2 的冪 = 左移:對 unsigned 值 x 與
, x << k;對 two's-complement 值同理 x << k。即使溢位,左移與乘法結果仍逐位相同。例: [1011](11)左移 2 得[101100](44),截斷 4 bits 得[1100](12 = 44 mod 16)。 - 任意常數 K:把 K 的二進位視為 0 串與 1 串交替;對每段從 bit n 到 bit m 的連續 1(n ≥ m)有兩種展開:
- Form A:
(x<<n) + (x<<(n-1)) + ... + (x<<m) - Form B:
(x<<(n+1)) - (x<<m)
- Form A:
- 範例:
x*14,因,Form A 為 (x<<3)+(x<<2)+(x<<1)(3 shifts + 2 adds);Form B(利用)為 (x<<4)-(x<<1)(2 shifts + 1 sub),更佳。兩者對 unsigned/signed、甚至溢位時皆與乘法結果一致。 - 選擇準則(Practice 2.41):run 長度 = 1 用 Form A(單一 shift);run 長度 ≥ 2 用 Form B;划算與否取決於各指令相對速度(machine dependent),多數編譯器只在少量 shift/add/sub 即可取代時才做此最佳化。
- x86-64 的
lea指令可單條完成(a<<k) + b(k ∈ {0,1,2,3}、b 為 0 或程式值),故 a 的 1、2、3、4、5、8、9 倍都能一條指令算出(Practice 2.38)→ 見 03-Machine-Level-Programs/02-Data-Movement-and-Arithmetic。
2.3.7 除以 2 的冪 (p.139-143)
整數除法更慢(30+ clock cycles)。除以
-
捨入定義:C 整數除法一律向零捨入 (round toward zero)——結果為正時向下取整
,為負時向上取整 。floor: 、 ;ceiling: 、 。 -
Unsigned:
x >> k(logical shift),直接正確(非負數向下取整即向零捨入)。例(Figure 2.28):12340 >> 8 = 48(= ⌊48.203⌋)。 -
Signed(直接算術右移):
x >> k(arithmetic shift)——對負數是向下捨入而非向零,不整除時比 C 除法小 1。例(Figure 2.29):−12340 >> 4 = −772,但 C 要求 −12340/16 = −771。 -
Bias 修正(向零捨入):利用恆等式
(y > 0;設 , ,加 後向下取整,r=0 得 q、r>0 得 q+1),先加 bias 再右移: (x + (1 << k) - 1) >> k -
完整的 C 慣用式(編譯器對 signed 除以
的實際產出):
x
│
┌─────┴─────┐
x < 0 ? │
│ yes │ no
▼ │
x += (1<<k)-1 │ ← bias = 2^k - 1
│ │
└─────┬─────┘
▼
x >> k (arithmetic) → x / 2^k(向零捨入)
即 (x < 0 ? x + (1<<k) - 1 : x) >> k。無分支版本(Practice 2.42,k=4):(x + ((x >> 31) & 15)) >> 4——用符號位 mask 產生 bias。
- Practice 2.43 逆向範例:
x*M + y/N被編譯成(x<<5) - x與if (y<0) y += 7; y >>= 3,故 M = 31、N = 8。
加 bias 後,若低 k 位(將被移掉的位)原本全 0(整除,無需捨入),進位不影響高位;若任一位非 0,bias 使高位 +1,達成向零捨入(Figure 2.30)。
與乘法不同,除以任意常數 K 無法一般化——「除以 2 的冪」的 shift 技巧無法組合出除以任意常數(p.143)。此為本節「乘除對稱」直覺的重要例外。
2.3.8 整數算術總結 (p.143-144)
- 電腦的「整數」算術實際上是 modular arithmetic:有限字長限制值域,運算可能 overflow,且 C 不會產生任何錯誤訊號。
- Two's-complement 的巧妙之處:同時表示正負數,而加、減、乘、甚至除法的位元層級行為與 unsigned 相同或非常相似。
- unsigned 型別雖然概念簡單,卻常以意想不到的方式出現(整數常數、
size_t等函式庫型別、隱式轉換),是難以察覺的 bug 與安全漏洞來源;Java 等多數語言乾脆不提供 unsigned(其>>保證為 arithmetic shift、>>>為 logical shift)(p.120)。 - unsigned 的正當用途:把 word 視為純位元集合(Boolean 旗標打包)、位址、模運算與 multiprecision 算術套件(p.120)。
- Practice 2.44 精選判斷(w=32):
(x*x) >= 0非恆真(乘法溢位可為負);x+y == uy+ux恆真(加法位元層級相同);x < 0 || -x <= 0恆真(TMin 取負仍 TMin ≤ 0);x > 0 || -x >= 0非恆真(x = TMin 時兩邊皆偽)。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
判斷 unsigned 加法 s = x + y 是否溢位 |
s < x(或 s < y)即溢位;uadd_ok: return s >= x; |
| 判斷 signed 加法是否溢位 | 正溢位:x>0 && y>0 && s<=0;負溢位:x<0 && y<0 && s>=0;一正一負必不溢位 |
用 (x+y)-x == y 驗證加法溢位? |
永遠為真(abelian group,溢位效果可逆),無法偵測 |
tsub_ok(x,y) 寫成 tadd_ok(x,-y) |
y = TMin 時錯誤(-TMin == TMin) |
4-bit signed:5 + 5 = ? |
−6(positive overflow,10 − 16) |
| unsigned 加法反元素 |
x=0 時為 0;否則 |
-x 的位元算法 |
~x + 1;或「最右邊 1 左側全部取反」 |
-TMin 等於多少 |
仍是 TMin(自己的加法反元素) |
| signed 與 unsigned 乘法的截斷結果 | 位元層級完全相同(完整 2w-bit 乘積可能不同) |
用除法驗證乘法溢位 !x || p/x == y |
可行(對比:加法不能用減法驗證) |
malloc(cnt * size) 溢位攻擊 |
XDR 漏洞:32-bit 乘積 mod |
| kernel copy 函式收到負的長度參數 | getpeername 漏洞:負 int 轉 size_t 成巨大正數 → 越權讀取 |
x * 14 最少運算 |
(x<<4) - (x<<1)(Form B,14 = 16 − 2) |
lea 一條指令能算的倍數 |
(a<<k)+b,k∈{0..3}, b∈{0,a} → 1, 2, 3, 4, 5, 8, 9 倍 |
負數 x >> k 直接算術右移 |
|
| signed 除以 |
(x<0 ? x+(1<<k)-1 : x) >> k;bias |
div16(x) 不用分支/比較/乘除 |
(x + ((x>>31) & 15)) >> 4 |
(x*x) >= 0 恆真? |
否,乘法可溢位成負值 |
x+y == uy+ux 恆真? |
是,signed/unsigned 加法位元層級相同 |
| 為何機器提供兩種右移 | logical 給 unsigned 除法、arithmetic 給 signed 除法 |
Related Notes
- 02-Information-Representation/02-Integer-Representations
- 02-Information-Representation/01-Information-Storage-and-Bits
- 02-Information-Representation/04-Floating-Point
- 03-Machine-Level-Programs/02-Data-Movement-and-Arithmetic
- 03-Machine-Level-Programs/06-Buffer-Overflow-and-Pointer-Safety
- 02-Information-Representation/Practice-Information-Representation