請停止使用strncpy


我不斷遇到使用strcpy、sprintf、strncpy、_snprintf(僅限Microsoft)、wcsncpy、swprintf和等效的函數的代碼。請停下來。還有一些更安全的選擇,而且它們實際上需要更少的輸入。


這篇文章的重點是固定大小的字符串緩沖區,但是這種技術適用於任何類型的固定長度緩沖區。如果你不使用固定大小的緩沖區,那么這篇文章與你無關。有些人確實使用它們,有時是出於正當的原因,這篇文章是為他們而寫的。


我希望strcpy和sprintf的危險不需要解釋。這兩個函數都不允許指定輸出緩沖區的大小,因此緩沖區溢出通常是一個風險。使用strcpy從網絡數據包復制數據或將大數組復制到較小的數組中是特別危險的,但是即使您確定字符串適合,也不值得冒這個風險。

“n”函數被認為是危險的

strncpy、unu snprintf和wcsncpy的危險應該是眾所周知的,但顯然它們不是。這些函數允許您指定緩沖區的大小,但這一點非常重要,它們不能保證空終止。如果您要求這些函數寫入的字符數超過將填充緩沖區的字符數,那么它們將停止運行,從而避免緩沖區溢出,但它們不會為null終止緩沖區。為了正確地使用這些函數,你必須做這種無稽之談。

char buffer[5];
strncpy(buffer, “Thisisalongstring”, sizeof(buffer));
buffer[sizeof(buffer)-1] = 0;

C/C++中的非終止字符串是一個定時炸彈,它正等待銷毀代碼。我的理解是strncpy是為在字符串中間插入文本而設計的,然后被重新用於“安全”編碼,盡管它非常適合。同時,snprintf遵循strncpy模式,但snprintf沒有。也就是說,snprintf保證空終止,但strncpy和_snprintf不保證。奇怪的是開發人員會感到困惑嗎?奇怪的是,開發人員經常這樣做:

// Make snprintf available on Windows:
// Don’t ever do this! These two functions are different!
#define snprintf _snprintf

strlcpy and lstrcpy

strlcpy旨在解決空終止問題-它總是空終止。當然,它比strncpy有改進,但是它在VC++中不是本機可用的。


lstrcpy是一個與之類似的微軟設計缺陷,它看起來像strlcpy,但實際上是一個安全缺陷。它使用結構化異常處理來捕獲訪問沖突,然后返回,因此在某些情況下,它將掩蓋崩潰並為您提供一個未終止的緩沖區。令人驚嘆。

 

寬字符更糟?


swprintf是一個無法預測的函數。它的名稱中缺少'n',但是它接受字符計數,但是它不能保證空終止。足以使人的頭爆炸。

其他呢?


如果你發現下面的列表很明顯或者很容易記住,那么你可能是個天才,或者是個騙子:


可能會超出緩沖區:strcpy,sprintf

有時null終止:strncpy、_snprintf、swprintf、wcsncpy、lstrcpy

總是空終止:snprintf,strlcpy


這些函數的文檔(手冊頁,MSDN)通常相當薄弱。我想在頂部加粗的字母告訴我它是否會以null結尾,但通常需要非常仔細的閱讀才能確定。編寫測試程序通常更快。


同樣值得強調的是,在上面列出的七個函數中,只有一個函數是可以安全使用的。也不是很好。

做的多,錯誤多

但是等等,實際上情況更糟。因為事實證明程序員是不完美的人,因此程序員有時會傳遞錯誤的緩沖區大小。不經常——可能不會超過百分之一的時間——但這些錯誤肯定會發生,“小心”並沒有實際幫助。我見過開發人員傳遞硬編碼的常量(錯誤的)、傳遞命名的常量(錯誤的)、使用sizeof(錯誤的緩沖區)或在wchar_t數組上使用sizeof(從而獲得字節計數而不是字符計數)。我甚至看到了一段代碼,其中傳遞的是字符串的地址而不是大小,而且由於模板和強制轉換的混合,它實際上被編譯了!將sizeof()傳遞給一個需要字符計數的函數是最常見的錯誤,但它們都會發生,甚至snprintf和strlcpy也被誤用。使用注釋和/分析可以幫助捕獲這些問題,但是我們可以做得更好。

解決方案

我們是程序員,不是嗎?如果我們處理字符串的函數很難正確使用,那么我們應該編寫新的函數。結果很簡單。這里我向您介紹將字符串復制到數組的最安全方法:

    template <size_t charCount>
    void strcpy_safe(char (&output)[charCount], const char* pSrc)
    {
    YourCopyNFunction(output, pSrc, charCount);
    // Copy the string — don’t copy too many bytes.
    //strncpy(output, pSrc, charCount);
    // Ensure null-termination.
    //output[charCount – 1] = 0;
    }

    // Call it like this:
    char buffer[5];
    strcpy_safe(buffer, “Thisisalongstring”);

這種語法有點奇怪,因為它將對整數值(而不是類型)的模板化與通過引用傳遞數組相結合,這兩種方法對於許多程序員來說都是陌生的。有關通過引用傳遞數組的詳細信息,請參閱這篇堆棧溢出文章。或者,您可以非常有效地使用模板魔術,而不必擔心它是如何工作的細節。


<note>評論人士正確地指出,strncpy后跟空終止不是strcpy_safe的理想實現,因為它效率低下(strncpy會將所有字節清零到緩沖區末尾),並且可能會將UTF-8字符減半。修復這個問題超出了本文的范圍,本篇文章的重點是通過模板魔法自動推斷緩沖區大小。所以,別忘了實現你的copynfunction,也許下次我會發布一個版本。</note>


我要求你不要錯誤地使用這個函數。您可以通過傳遞一個無效的源指針使其崩潰,但在多年的使用該技術的過程中,我從未見過一個緩沖區大小沒有被正確推斷的情況。如果傳遞一個指針作為目標,因為無法推斷大小,代碼將無法編譯。它只使用靜態字符串緩沖區作為目標(沒有std::string或std::vector),但是可以為這些目標類型生成不同的重載。
我認為strcpy_safe是一個完美的功能。它要么使用正確,要么編譯失敗。它。是完美的。只有六行。如果你像K&R那樣縮進五個。
因為strcpy_safe非常小-它只調用strncpy,然后存儲一個0-它將在優化的版本中自動在VC++和gcc中內聯。如果您想進一步減小代碼大小,可以編寫一個非內聯helper函數(strlcpy?)這將執行null終止,並讓strcpy\u safe調用此函數。這取決於你。
人們當然可以討論這個名字——也許你更願意稱之為acme_strcpy,或acme_strncpy_safe。我真的不在乎。你甚至可以稱之為strcpy,讓模板重載神奇地提高代碼的安全性。

Unicode碼

字符串截斷會導致UTF-8編碼出現問題。如果你想在一個字符的邊界處截斷(或者是那個代碼點——我不記得了),那么需要添加一些額外的代碼來向后掃描到字符邊界。這並不復雜,但它超出了本文的討論范圍,這篇文章的重點是使用模板來推斷數組大小。

外推法

顯然可以為您使用的所有字符串函數生成類似的包裝器。你甚至可以發明新的,比如sprintf_cat_safe。事實上,當我編寫一個成員函數時,它需要一個指針和一個大小,我通常把它設為私有的,然后編寫一個模板包裝器來處理這個大小。這是一種多功能的技巧,你應該習慣使用。模板不僅僅用於編寫不可讀的元代碼。

字符串類

是的,說清楚,我知道std::string的存在。不管是好是壞,大多數游戲開發人員都盡量避免動態分配內存,std::string通常就是這樣。使用字符數組有合理的理由(更少的分配,更好的緩存位置),即使這些合理的理由僅僅是因為您已經收到了上百萬行遺留代碼,這些代碼在各個方面都存在安全性和可靠性問題。strcpy_safe和friends的獨特之處在於,它們允許您通過一個簡單的s_strcpy/strcpy_safe來提高代碼的安全性和可靠性。

正如我在上面說的,如果你不需要使用固定長度的緩沖區,那么恭喜你,這篇文章不適用於你。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM