C 語言的預處理器巨集雖強大,但因其僅為文字替換,缺乏型別檢查與作用域限制,極易引發緩衝區溢位、多重求值、命名衝突等安全漏洞。本文依據《C 安全編碼標準》,整理預處理器使用的建議與強制規則,並附上違規與合規程式碼對照,協助開發者撰寫更健壯的程式碼。
| 類別 | 項目 | 說明 |
|---|---|---|
| 建議 | 以內聯/靜態函數取代函數式巨集 | 避免副作用多重求值,編譯器可優化內聯展開 |
| 巨集參數兩側加括號 | 防止運算子優先級導致邏輯錯誤 | |
| 巨集替換列表整體加括號 | 保護表達式免受外層運算子干擾 | |
| 使用 typedef 定義編碼類型 | 遵循作用域規則,避免巨集污染命名空間 | |
| 勿重用標準頭檔名稱 | 防止未定義行為 | |
| 理解連接標記與字串化的巨集替換順序 | 正確使用 ## 與 # 運算子 | |
| 頭檔加入包含防護 | 防止重複包含導致重複定義錯誤 | |
| 避免連續問號 (??) | 防止被解析為三字元序列 | |
| 確保頭檔名稱唯一性 | 前 8 字元唯一、大小寫不敏感、副檔名單一非數字字元 | |
| 勿以不安全函數取代安全函數 | 如用 vsnprintf 替換 vsprintf 會丟失邊界檢查 | |
| 多語句巨集以 do-while(0) 包裝 | 確保巨集在語法上如單一陳述式般運作 | |
| 規則 | 勿透過連接建立通用字元名稱 | 避免產生非預期的識別碼 |
| 不安全巨集參數中禁用賦值/增減/volatile/函數呼叫 | 防止引數多重求值導致未定義行為 |
違規碼:
#define CUBE(x) ((x) * (x) * (x))
int i = 2;
int a = 81 / CUBE(++i); // 展開為 81/((++i)*(++i)*(++i)),i 被修改三次,未定義行為
合規碼(內聯函數):
inline int cube(int x) { return x * x * x; }
int i = 2;
int a = 81 / cube(++i); // i 僅遞增一次,行為明確
違規碼:
#define CUBE(I) (I * I * I)
int a = 81 / CUBE(2 + 1); // 展開為 81 / (2 + 1 * 2 + 1 * 2 + 1) = 81 / 7
合規碼:
#define CUBE(I) ((I) * (I) * (I))
int a = 81 / CUBE(2 + 1); // 正確展開為 81 / ((2+1)*(2+1)*(2+1)) = 3
違規碼:
#define CUBE(X) (X) * (X) * (X)
int i = 3;
int a = 81 / CUBE(i); // 展開為 81 / i * i * i = (81/3)*3*3 = 243
合規碼:
#define CUBE(X) ((X) * (X) * (X))
int a = 81 / CUBE(i); // 正確為 81 / (3*3*3) = 3
違規碼:
#define cstring char *
cstring s1, s2; // s1 是 char*,s2 卻只是 char
合規碼:
typedef char * cstring;
cstring s1, s2; // 兩者皆為 char*
違規碼:
//what is the value of a now ??/ a++; // ??/ 為 \,導致 a++ 被註解化
合規碼:
//what is the value of a now? ?/ a++; // 空格分隔避免三字元序列
預處理器巨集是把雙面刃,善用可提升效能與可讀性,濫用則埋下安全隱患。遵循「能用函數不用巨集、必用巨集則嚴守括號與作用域規則」原則,配合靜態分析工具檢查,可大幅降低 C 程式碼的安全風險。