整數算術 (Integer Arithmetic)

Overview Table

小節 主題 核心結論
2.3.1 (p.120) Unsigned 加法 x+wuy=(x+y)mod2w;overflow 判斷: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 乘法 xwuy=(xy)mod2w
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
Important

全節最核心的洞察:同一個位元層級運算(加、減、乘、左移)同時實現了 unsigned 與 two's-complement 兩種算術。這正是大多數機器用同一條指令做有號/無號加法與乘法的原因;差異只出現在「解讀結果」與「右移方向、比較、除法」上。

2.3.1 Unsigned 加法 (p.120-126)

兩個 w-bit 非負整數 0x,y<2w 相加,真實總和範圍是 0x+y2w+12,可能需要 w+1 bits。若持續保留完整結果會造成「word size inflation」(字長無限膨脹),故 C 等語言採固定字長算術:把總和截斷 (truncate) 為 w bits,等價於 mod 2w 的模運算(Lisp 等語言則支援任意精度整數,為例外)。

公式(Eq. 2.11):

x+wuy={x+y,x+y<2w(Normal)x+y2w,2wx+y<2w+1(Overflow)
 真實和 x+y                     w-bit unsigned 和
 2^(w+1) ─┐
          │ Overflow ──── 減去 2^w ────┐
 2^w     ─┤                           ├─ 2^w
          │ Normal  ──── 原值不變 ────┤
 0       ─┘                           └─ 0
wux={x,x=02wx,x>0
安全漏洞:FreeBSD 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),負數被隱式轉換成極大正數(至少 231),導致 kernel 記憶體被越權讀取。修法:把 maxlenlen 與回傳值統一宣告為 size_t,消除 signed/unsigned 型別不一致。

2.3.2 Two's-Complement 加法 (p.126-131)

有號數 2w1x,y2w11 的真實和範圍是 2wx+y2w2,同樣截斷成 w bits。結果分三種情況(Eq. 2.13):

x+wty={x+y2w,2w1x+y(Positive overflow)x+y,2w1x+y<2w1(Normal)x+y+2w,x+y<2w1(Negative overflow)
 真實和 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    ─┘
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):

wtx={TMinw,x=TMinwx,x>TMinw

2.3.4 Unsigned 乘法 (p.132-133)

真實乘積 xy 範圍可達 (2w1)2=22w2w+1+1,最多需要 2w bits。C 定義 unsigned 乘法取 2w-bit 乘積的低 w 位,等價於模運算:

xwuy=(xy)mod2w(Eq. 2.16)

2.3.5 Two's-Complement 乘法 (p.133-137)

有號乘積範圍 22w2+2w122w2,同樣截斷為低 w 位再以 two's-complement 解讀:

xwty=U2Tw((xy)mod2w)(Eq. 2.17)
安全漏洞:Sun XDR library (2002, CERT CA-2002-25, p.136)

malloc(ele_cnt * ele_size):攻擊者傳 ele_cnt = 2^20 + 1ele_size = 2^12(32-bit 程式),乘積 232+212 溢位後只剩 4096,malloc 只配置 4 KB,後續迴圈複製時越界寫入 (buffer overrun),破壞其他資料結構。僅把乘法改成 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.3.7 除以 2 的冪 (p.139-143)

整數除法更慢(30+ clock cycles)。除以 2k 可用右移實現,但 unsigned/signed 需不同種類的 shift——這正是機器同時提供 logical / arithmetic right shift 兩種右移的原因。

             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。

Tip

加 bias 後,若低 k 位(將被移掉的位)原本全 0(整除,無需捨入),進位不影響高位;若任一位非 0,bias 使高位 +1,達成向零捨入(Figure 2.30)。

Warning

與乘法不同,除以任意常數 K 無法一般化——「除以 2 的冪」的 shift 技巧無法組合出除以任意常數(p.143)。此為本節「乘除對稱」直覺的重要例外。

2.3.8 整數算術總結 (p.143-144)

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 加法反元素 wux x=0 時為 0;否則 2wx
-x 的位元算法 ~x + 1;或「最右邊 1 左側全部取反」
-TMin 等於多少 仍是 TMin(自己的加法反元素)
signed 與 unsigned 乘法的截斷結果 位元層級完全相同(完整 2w-bit 乘積可能不同)
用除法驗證乘法溢位 !x || p/x == y 可行(對比:加法不能用減法驗證)
malloc(cnt * size) 溢位攻擊 XDR 漏洞:32-bit 乘積 mod 232 變小 → 越界寫入;須以 64-bit 先檢查範圍
kernel copy 函式收到負的長度參數 getpeername 漏洞:負 intsize_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 直接算術右移 x/2k 向下捨入,不整除時比 C 除法(向零)小 1
signed 除以 2k 向零捨入 (x<0 ? x+(1<<k)-1 : x) >> k;bias =2k1
div16(x) 不用分支/比較/乘除 (x + ((x>>31) & 15)) >> 4
(x*x) >= 0 恆真? 否,乘法可溢位成負值
x+y == uy+ux 恆真? 是,signed/unsigned 加法位元層級相同
為何機器提供兩種右移 logical 給 unsigned 除法、arithmetic 給 signed 除法