C和C++安全编码笔记:总结
《C和C++安全編碼》(原書第2版)這本書是2013年出版的。
這里是基于之前所有筆記的簡單總結,筆記列表如下:
字符串:https://blog.csdn.net/fengbingchun/article/details/105325508
指針詭計:https://blog.csdn.net/fengbingchun/article/details/105458861
動態內存管理:https://blog.csdn.net/fengbingchun/article/details/105921174
整數安全:https://blog.csdn.net/fengbingchun/article/details/106444980
格式化輸出:https://blog.csdn.net/fengbingchun/article/details/106728792
并發:https://blog.csdn.net/fengbingchun/article/details/106962487
文件I/O:https://blog.csdn.net/fengbingchun/article/details/107138261
下面是對每章中關鍵語句的摘記:
1. 字符串:
在獲取一個數組的大小時,不要對一個指針應用sizeof運算符。
每個UTF-8字符由1~4個字節表示。
寬字符串字面值除了以字面L作為前綴外,其它的表示方式與字符串字面值相同。
不要試圖修改字符串字面值。
不要指定一個用字符串字面值初始化的字符數組的界限。
在標準C++的string類中,其內部表示并不一定非得是以空字符結尾的,雖然所有常見的實現都是以空字符結尾的。
存儲在unsigned char類型對象中的值,保證會當作一個純粹的二進制表示法來表示屬性值。
任何類型的非二進制位域(non-bit-field)的對象都可以復制到一個unsigned char數組中(例如,通過memcpy()),并每次1個字節地檢查它們的表示形式。
保證字符串的存儲空間具有容納字符數據和空終結符的足夠空間。
不要從一個無界源復制數據到定長數組。
不要使用廢棄或過時的函數。
如果一個字符串沒有以空字符結尾,程序可能會被欺騙,導致在數組邊界之外讀取或寫入數據。
空終止字符之所以是必要的,是因為前面這些函數以及其它由C標準定義的字符串處理函數,都依賴于它的存在來標記字符串的結尾。
空字符結尾的字符串是用字符數組實現的。
C11附錄K邊界檢查接口:設計目的主要是實現現有函數的更安全的替代品。例如,C11附錄K定義了strcpy_s、strcat_s、strncpy_s和strncat_s函數,分別作為strcpy、strcat、strncpy和strncat的替代品,適用于源字符串長度未知的或保證小于已知目標緩沖區大小的情況。
basic_string類實現了”由被調用者分配,由被調用者釋放”的內存管理策略。
注意std::string的下標成員std::string::operator[](不執行邊界檢查)不會拋出異常。
永遠不要使用gets(),它不對緩沖區溢出進行任何檢測。
如果字符數組不是正確地以空字符結尾的,strlen()函數可能會返回一個錯誤的超大的數值,使用它時,就可能會導致漏洞。
任何到達某個跨越信任邊界的程序接口的數據都需要驗證。
2. 指針詭計(pointer subterfuge):是通過修改指針值來利用程序漏洞的方法的統稱。
atexit()是C標準定義的一個通用工具函數。atexit()可以注冊無參函數,并在程序正常結束后調用該函數。atexit()通過向一個退出時將被調用的已有函數的數組中添加指定的函數完成工作。
防止指針詭計的最佳方式就是消除”允許內存被不正確地覆寫”的漏洞。
3. 動態內存管理:
free(void* p):如果p是一個空指針,則不執行任何操作。
對齊:完整的對象類型有對齊(alignment)要求,這種要求對可以分配該類型對象的地址施加限制。對齊是實現定義的整數值,它表示可以在一個連續的地址之間分配給指定的對象的字節數量。對象類型規定了每一個該類型對象的對齊要求。
每個有效對齊值都是2的一個非負整數冪。
不要假定內存分配函數初始化內存。
不要引用未初始化的內存。
內存分配函數的返回值表示分配失敗或成功。如果請求的內存分配失敗,那么aligned_alloc、calloc、malloc和realloc函數返回空指針。
空指針的解引用通常會導致段錯誤,但并非總是如此。許多嵌入式系統有映射到地址0處的寄存器,因此覆寫它們會產生不可預知的后果。在某些情況下,解引用空指針會導致任意代碼的執行。
不要執行零長度分配。
連續調用分配函數分配的存儲的順序、連續性、初始值都是不確定的。
C++在發起一個為零的請求時行為與C不同,它返回一個非空指針。
通常情況下,分配函數無法分配存儲時拋出一個異常表示失敗,這個異常將匹配類型為std::bad_alloc的異常處理器。
如果用std::nothrow參數調用new,當分配失敗時,分配函數不會拋出一個異常。相反,它將返回一個空指針。
提供給一個釋放函數的第一個參數的值可以是一個空指針值,如果是這樣的話,并且如果釋放函數是標準庫提供的,那么該調用沒有任何作用。
C++的內存分配和釋放函數分配和釋放內存的方式可能不同于C內存分配和釋放函數分配和釋放內存的方式。因此,在同一資源上混合調用C++內存分配和釋放函數及C內存分配和釋放函數是未定義的行為,并可能會產生災難性的后果。
new和operator new():可以直接調用operator new()分配原始內存,但不調用構造函數。
釋放函數必須避免拋出異常。
一個明顯的可以減少C和C++程序中漏洞數量的技術就是在指針所引用的內存被釋放后,將此指針設置為NULL。如果指針被設置為NULL,內存可以被”釋放”多次而不會導致不良后果。
4. 整數安全:
特定于編譯器和平臺的整數極值記錄在<limits.h>頭文件中。牢記這些值都是特定于平臺的。出于可移植性的考慮,在代碼中應該使用具名常量而不是實際的值。
C標準允許的負數表示方法有三種,分別是原碼表示法(sign and magnitude)、反碼表示法(one’s complement)和補碼表示法(two’s complement)。若要取一個原碼的相反數,只要改變符號位。若要取一個反碼的相反數,需要改變每一位(包括符號位)。若要取一個補碼的相反數,首先構造反碼的相反數,然后再加1(在需要時進位)。
用補碼表示的一個給定類型最小負值的相反數不能以那種類型表示。
在把char型用于數值時僅使用明確的signed char或unsigned char型。
整數提升保留值,其中包括符號。如果在所有的原始值中,較小的類型可以被表示為一個int,那么:原始值較小的類型會被轉換成int;否則,它被轉換成unsigned int。
之所以需要整數類型提升,主要是為了防止運算過程中中間結果發生溢出而導致算術錯誤,也為了在該架構中以自然的大小執行操作。
當從一個無符號類型轉換為有符號類型時,應驗證范圍。
當從一個有符號類型轉換到精度較低的有符號類型時,應驗證范圍。從較高精度的有符號類型轉換為較低精度的有符號類型需要同時對上限和下限進行檢查。
從有符號類型轉換為無符號類型時,應驗證取值范圍。
唯一的對所有數據值和所有符號標準的實現都保證安全的整數類型轉換是轉換為符號相同而寬度更寬的類型。
在C中有符號溢出是未定義的行為,允許實現默默地回繞(最常見的行為)、陷阱、飽和(固定在最大值/最小值中),或執行實現選擇的其它任何行為。
使用靜態斷言static_assert來測試一個常數表達式的值。
不要移動一個負的位數或移動比操作數中存在的位數更多的位。
移位運算符和其它位運算符應僅用于無符號整數操作數。
應使用無符號整數表示不可能是負數的整數值,而且應使用有符號整數值表示可以為負的值。在一般情況下,應該使用完全可以代表任何特定變量可能值的范圍的最小的有符號或無符號類型,以節省內存。
5. 格式化輸出:
格式化輸出函數是由一個格式字符串和可變數目的參數構成的。
變參函數是通過使用一個部分參數列表后跟一個省略號進行聲明的。省略號必須出現在參數列表的最后。參數列表的終止條件是函數的實現者和使用者之間的一個契約。
格式字符串:是由普通字符(ordinary character)(包括%)和轉換規范(conversion specification)構成的字符序列。當參數多于轉換規范時,多余的將被忽略,而當參數不足時,則結果是未定義的。
為了消除格式字符串漏洞,推薦在可能的情況下使用iostream代替stdio,在沒有條件的情況下則要盡量使用靜態格式字符串。
6. 并發:
并發是一種系統屬性,它是指系統中幾個計算同時執行,并可能彼此交互。一個并發程序通常使用順序線程和(或)進程的一些組合來執行計算,其中每個線程和進程執行可以在邏輯上并行執行的計算。
多線程不一定是并發的。
所有的并行程序都是并發的,但不是所有的并發程序都是并行的。這意味著并發程序既可以用交錯、時間分片的方式執行又可以并行執行。
對不能緩存的數據使用volatile。當一個變量被聲明為volatile時,就會禁止編譯器對該內存位置的讀取和寫入順序進行重新排列。
在重組程序方面,編譯器具有非常大的自由度。
C和C++都支持幾種不同類型的同步原語,包括互斥變量(mutex variable)、條件變量(condition variable)和鎖變量(lock variable)。
鎖機制導致一個或多個線程等待,直到另一個線程退出臨界區。
一個線程鎖定一個互斥量后,任何后續試圖鎖定該互斥量的線程都將被阻止,直到此互斥量被解鎖為止。
互斥量可以包裝在臨界區,以使它們序列化,從而使程序是線程安全的。互斥量不與任何其它數據關聯。它們只是作為鎖對象。
在用C++編程時,如果發生臨界區拋出異常,或退出時沒有明確地對互斥量解鎖,我們建議使用鎖衛士緩解這些問題。
原子操作是不可分割的。也就是說,一個原子操作不能被任何其它的操作中斷,當正在執行原子操作時,它訪問的內存,也不可以被任何其它機制改變。
原子對象不存在數據競爭,雖然它們仍然可能會受到競爭條件的影響。
為了使一個函數成為線程安全的,它必須同步訪問共享資源。
7. 文件I/O:
特殊文件:包括目錄、符號鏈接、命名管道、套接字和設備文件。
文本流stdin、stdout和stderr是FILE指針類型的表達式。
文件描述符是每一個進程為了文件訪問的目的,用來識別一個打開的文件的唯一的非負整數。文件描述符只是一個標識符或句柄,它實際上并沒有描述什么。
超級UID(root)擁有一個為0的UID,并可以訪問任何文件。
文件權限一般都用八進制值的向量表示。
權限字符串的第一個字符表示文件類型:普通-、目錄d、符號鏈接l、設備b/c、套接字s或FIFO f/p。
目錄內的特殊文件名”.”指的是目錄本身,”..”指的是目錄的父目錄。作為一種特例,在根目錄中,”..”可能指的是根目錄本身。
符號鏈接是特殊的文件,其中包含了實際文件的路徑名。
對同步原語的使用要求我們小心翼翼地將臨界區的大小減到最小。
當宣告一個函數為線程安全的時候,就意味著作者相信這個函數可以被并發線程調用,同時該函數不會導致任何競爭條件問題。
GitHub:https://github.com/fengbingchun/Messy_Test
總結
以上是生活随笔為你收集整理的C和C++安全编码笔记:总结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C和C++安全编码笔记:并发
- 下一篇: Windows/Linux TCP So