建議:
用內聯函數或靜態函數代替與函數相似的宏
在宏參數名兩邊加上括號
宏替換列表應該加上括號
應該使用typedef定義編碼類型
不要復用標准頭文件名
理解連接標記或執行字符串化時的宏替換
把頭文件放在包含防護條件中
避免使用連續的問號
保證頭文件名唯一
不要用不安全的函數替換安全函數
在一個do-while循環中包裝多條語句的宏
規則:
不要通過連接創建統一字符名稱
不要在不安全宏的參數中包含賦值、增值、減值、volatile訪問或函數調用
本文地址:http://www.cnblogs.com/archimedes/p/c-security-pretreatment-.html,轉載請注明源地址。
宏是危險的,用法與真正的函數相似,但是具有不同的語義。C99在C中增加了內聯函數,當內聯函數和宏可以互換使用時,應該優先選擇內聯函數,內聯替換並不是文本替換,也沒有創建函數,決定一個函數是否為內聯函數是一個底層的優化細節,編譯器應該不依賴程序換做出這個決定,是否使用內聯函數取決於目標編譯器對它們的支持,它們對系統性能特征所產生的影響以及可移植性問題,靜態函數常常具有與內聯函數相同的優點。
下面的例子中,當傳遞給CUBE宏的參數是一個具有副作用的表達式時,這個宏就具有未定義的行為。
代碼1:
#define CUBE(x) ((x) * (x) * (x)) /*...*/ int i = 2; int a = 81 / CUBE(++i);
在這個例子中,a的初始化表達式展開為: int a = 81/((++i) * (++i) * (++i));
解決方案:
inline int cube(int x) { return x * x *x; } /*...*/ int i = 2; int a = 81 / cube(++i);
代碼2:
#include<stdio.h> size_t count = 0; #define EXEC_BUMP(func) (func(), ++count) void g(void) { printf("Called g, count = %zu.\n", count); } void aFunc(void) { size_t count = 0; while(count++ <10) { EXEC_BUMP(g); } } int main(void){ aFunc(); return 0; }
運行結果:
解決方案:
#include<stdio.h> size_t count = 0; void g(void) { printf("Called g, count = %zu.\n", count); } typedef void(*exec_func)(void); inline void exec_bump(exec_func f) { f(); ++count; } void aFunc(void) { size_t count = 0; while(count++ <10) { exec_bump(g); } } int main(void){ aFunc(); return 0; }
運行結果:
和函數不同,宏的執行可以是交錯的,兩個宏單獨執行時無害,但是它們在同一個表達式中組合在一起時可能導致未定義的行為:
代碼3:
#define F(x) (++operations, ++calls_to_F, 2 * x) #define G(x) (++operations, ++calls_to_G, x + 1) /*...*/ y = F(x) + G(x);
operations變量在同一個表達式中讀取並修改了2次,因此按照某種順序,可能會接收到錯誤的值
解決方案:
inline int f(int x) { ++operations; ++calls_to_f; return 2 * x; } inline int g(int x) { ++operations; ++calls_to_f; return 1 + x; } /*...*/ y = f(x) + g(x);
代碼1:
#define CUBE(I) (I * I * I) int a = 81 / CUBE(2 + 1)
被展開為: int a = 81 / (2 + 1 * 2 + 1 * 2 + 1);
解決方案:
#define CUBE(I) ((I) * (I) * (I)) int a = 81 / CUBE(2 + 1)
例外:當替換文本中的參數名由逗號分隔時,不管實際參數如何復雜,不需要對宏參數加上括號,因為逗號操作符的優先級低於其他任何操作符
#define FOO(a, b, c) bar(a, b, c) /*...*/ FOO(arg1, arg2, arg3);
宏替換列表應該加上括號,以保護表達式中所有優先級較低的操作符
代碼1:
#define CUBE(X) (X) * (X) * (X) int i = 3; int a = 81 / CUBE(i); //被展開為: int a = 81 / i * i * i
解決方案:
#define CUBE(X) ((X) * (X) * (X)) int i = 3; int a = 81 / CUBE(i);
這個方案最好實現為內聯函數
如果需要對類型進行編碼,應該使用類型定義(typedef)而不是宏定義(#define)。類型定義遵循作用域規則,而宏定義卻不遵循
代碼1:
#define cstring char * cstring s1, s2;
其中s1聲明為char *,s2聲明為char
解決方案:
typedef char * cstring; cstring s1, s2;
如果一個文件與標准頭文件同名。並且位於包含源文件的搜索路徑中,其行為是未定義的
建議:不要復用標准頭文件名、系統特定的頭文件名或其他的頭文件名
防止頭文件沒多次包含或是忘記包含,通過一種簡單的技巧:每個頭文件應該用#define指令定義一個符號,表示已經被包含,然後整個頭文件出現在一個包含防護條件中:
#ifndef HEADER_H #define HEADER_H /*....header的內容*/ #endif
兩個連續的問號表示一個三字符序列,據C99標准,在一個源文件中,下列這些3個字符的連續出現被對應的單個字符所替換
??= # ??) ] ??! | ??( [ ??' ^ ??> } ??/ \ ??< { ??- ~代碼1:
//what is the value of a now ??/ a++;
由於??/等價於\,a++相當於被注釋掉
解決方案:
//what is the value of a now? ?/ a++;
文件名中只有前8個字符保證是唯一的
文件名中的點號後面只有1個非數字字符
文件名中字符的大小寫並不保證是區分的
代碼1:
#include<stdio.h> #include “Library.h” #include "library.h" #include "utilities_math.h" #include "utilities_physics.h" #include "my_library.h"
Library.h和library.h可能表示同一個文件,並不清楚utilities_math和utilities_physics能否進行區分
解決方案:
#include<stdio.h> #include “Lib_main.h” #include "lib_2.h" #include "util_math.h" #include "util_physics.h" #include "my_library.h"
宏經常用於修補現有的代碼,用一個標識符對另一個標識符進行全局替換,但是這種做法存在一些風險,當一個函數被一個不夠安全的函數替換時,這種做法就顯得特別的危險
代碼:
#define vsnprintf(buf, size, fmt, list) \ vsprintf(buf, fmt, list)
vsprintf函數並不會檢查邊界,因此size參數將被丟棄,在使用不信任的數據的時候可能會導致潛在的緩沖區溢出問題
解決方案:
#include<stdio.h> #ifndef __USE_ISOC99 /* 重新實現 vsnprintf()*/ #include "my_stdio.h" #endif
參見《C語言中do...while(0)用法小結》
《C安全編碼標准》