當前位置: 妍妍網 > 碼農

C++程式碼安全爭議專家觀點:安全是系統工程,沒有銀彈

2024-03-19碼農

采訪嘉賓 | 吳詠煒 責編 | 夢依丹

出品 | CSDN(ID:CSDNnews)

近日,因為記憶體安全問題,老牌程式語言 C++ 又被推上風口浪尖。 一些人認為 C++ 的記憶體管理復雜,容易出現漏洞,而另一些人則認為這正是 C++ 的魅力所在,它賦予了程式設計師更多的控制權。

此時,有人建議轉向 Rust,聲稱它在記憶體安全方面有著出色的表現。然而在 AI 大模型的加持之下,未來的程式設計師還有必要學習程式語言嗎? 上周,世 界上第一個 AI 程式設計師 Devin 誕生, 李彥宏說 「以後不會存在程式設計師這種職業了」。

為此,我們采訪了擁有近 30 年 C/C++ 系統級軟體開發和架構經驗的「吳詠煒」,解鎖 Just for fun 的程式人生。

吳詠煒,國內知名 C++專家,曾任英特爾亞太研發中心資深系統架構師,近 30 年 C/C++系統級軟體開發和架構經驗。專註於 C/C++ 語言(包括 C++98/C++11/14/17/20)、軟體架構、效能最佳化、設計模式和程式碼重用。對於精煉、易於維護的程式碼和架構有著不懈的追求,對開源平台(GNU/Linux)有深入的理解。長期擔任資深技術教練,涉及 C++、軟體架構、安全軟體開發、開源軟體等多方面。

程式碼安全是個系統工程

無法靠某種銀彈就能立即解決

像學外語一樣去學程式語言,優先掌握慣用法,而不是語法

Q:美國政府近日釋出了一份網路安全報告,呼籲開發人員停止使用容易出現記憶體安全漏洞的程式語言,例如 C 和 C++,轉而使用記憶體安全的程式語言進行開發。CSDN 在公眾號上報道了【 】獲得了近 33 萬的閱讀,您如何看待使用 C++ 就記憶體不安全問題?

吳詠煒: 在報告裏:

  1. 重點是關註網路安全,記憶體安全的程式語言是其中的一小部份內容。

  2. 確實建議大家避免使用記憶體不安全的語言。但問題是,如果不是對效率有極致追求的場景,大家本來就不會選用 C 和 C++(如在企業套用裏,本來 Java 就是主流)。而用到 C 和 C++ 的,基本都是確有需要。在這篇報告裏,作為一個案例,也提到了空間系統裏仍然使用 C 和 C++,並描述了如何使用其他技術手段來規避不安全語言可能帶來的問題。

這篇報告裏的內容本身並不是什麽新東西,目前對相關問題的討論有點誇張了。

對於 C 和 C++ 的安全性,也有必要做幾點陳述:

  • C 和 C++ 不是一回事。在現代 C++ 程式碼裏,因為有很多好的語言構件(如容器和智慧指標)可以用,犯記憶體錯誤的可能性要比 C 低得多。C 的固定大小陣列是很多安全問題的根源。

  • 已經存在很多工具,可以幫助檢查程式碼的安全性,如 clang-tidy、cppsafe 和 address sanitizer(ASan)。

  • C++ 本身也在發展,像 lifetime profile(生存期規格配置)等方面的工作就是為了能提前檢查出安全問題。

  • 記憶體安全是程式碼安全的一部份,不是全部。

  • 程式碼安全問題是個系統工程,不是靠某種銀彈就能立即解決的。培訓、語言、工具等等都是解決方案的一部份。

    Q:Rust 一直被外界稱作是記憶體安全的程式語言,您覺得 C++ 開發者有必要轉向 Rust 嗎?

    吳詠煒: 單純從技術層面上分析,Rust 比 C++ 安全,這是事實。但仔細比較兩者的話,Rust 也有缺點:

    1. 開發的心智負擔比 C++ 要重;

    2. 程式碼因為要顯式表達生存期,常常比 C++ 更啰嗦,可讀性往往更糟糕;

    3. 表達能力在某些場景仍不如 C++,如沒有樣版顯式特化和變參樣版;

    4. 編譯速度慢;

    5. 工具鏈和生態相比 C++ 仍有不足。

    總體而言,Rust 以安全為核心理念,比 C++ 在型別系統上更為復雜。雖然 Rust 不像 C++ 一樣有 C 相容性這個歷史包袱,但卻仍不能算好寫。

    C++ 在龐雜之中給了開發者相當大的自由度:它給了使用者強有力的抽象,但又允許使用者深入底層細節,控制記憶體排布之類的細節。對於熟稔 C++ 的人來說,C++ 的強大和靈活性是任何其他語言無法相比的。記憶體安全性是一柄雙刃劍:雖然我們不能說記憶體安全的語言不好(當然是好的),但它有著自己的代價。

    另外,選用什麽語言,說到底這不是個體程式設計師能夠自由決定的。總體上仍然需要看計畫的需求。Rust 已經是較成熟的程式語言,不可能像另外一些程式語言一樣悄無聲息地死去(或半死不活),但要能達到或超過 C++ 的地位,現在討論恐怕還為時過早。——也許 AI 在那之前就把程式設計師這個崗位都基本滅了……

    退一萬步,如果一定要轉 Rust 的話,一個 C++ 程式設計師也肯定比 Java 程式設計師上手要快得多。

    Q:隨著大模型的發展,像 Copilot 這些 AI 助手確實降低了編程的門檻,那麽,程式設計師是否應該更註重演算法和數據結構的理解,而不僅僅是具體的程式語言?

    吳詠煒: 也對也不對。

    說演算法和數據結構是不是比 C++ 或 Rust 重要,那是對的。但任何演算法或數據結構都要落實到程式語言上的——能夠不透過程式語言、純抽象地理解和運用演算法和數據結構的人,不是天才就是怪胎吧?……反正不是常人。大部份人的學習過程,一定是從具象到抽象:直接去看那些抽象的概念,就如無源之水、無根之木,能學好才是怪事。

    從另外一個角度,至少就目前的 AI 水平,AI 還只是助手,還需要人來把關。程式設計師一定需要有把關的能力。我不相信程式設計師在缺乏底層的具體知識和經驗的情況下,能夠做好這些工作。打個比方,一個技術部門的領導一定需要對部門裏做的具體工作有經驗和了解,而不能拿著書本上學來的知識紙上談兵地判斷誰幹得好誰幹得差——那就真成了技術人員最痛恨的外行領導了。雖然 AI 不會「恨」你,這個部門的產出也同樣不容樂觀。

    對於這個觀點,我能同意的地方是絕大部份程式設計師不需要成為語言律師,不應該鉆到程式語言的細枝末節裏去。我們要成為作家,需要對語言有很好的把握;但我們不需要成為語言學家。這也是我對程式語言學習的一貫態度:像學外語一樣去學,優先掌握慣用法,而不是語法。

    Q:您覺得 AI 會取代程式設計師嗎?以後產品經理只需要跟 AI 交流就能開發出想要的套用了嗎?

    吳詠煒: 「碼農」是會被 AI 替代,但計畫經理目前來看還不行,但未來難說……

    「意外」解鎖高難度計畫

    Q:是否還記得自己第一次接觸編程是什麽時候,可否分享一下當時的編程過程?

    吳詠煒: 初一下學期的時候,當時學校裏有電腦興趣班,我一下子就迷上了。第一次寫的比較完整的程式是利用隨機數出計算題,讓使用者輸入回答,然後進行計分。很具有學生色彩的做法吧。用 BASIC 寫的,還偵錯了好一會兒,因為對很多東西都是一知半解,經過老師指導才算完成了。

    Q: 在 30 年的 C/C++ 系統級軟體開發和架構經驗中,能分享一個您認為最具挑戰性的計畫案例嗎?在這個計畫中,你的角色和貢獻是什麽?

    吳詠煒: 年數有點長……讓我想想。還是說兩個不同的案例吧,裏面我扮演的角色和做的事情非常不一樣。

    挑戰性,或者說難易,都是相對的。所謂難者不會,會者不難。所以,總體來說,挑戰性意味著做自己之前沒有做過的新的事情。我進英特爾沒多久,就碰到了一個這種型別的計畫,跟微軟合作的 FlexGo,「即用即付費」的電腦。這個計畫可以說不太成功,但技術上還是很有意思的。這個計畫原本的技術架構主導是我在英特爾的第一個經理,一個非常厲害的德國人。就在我跟他在美國出差的途中,他收到了裁員通知,但那時我們完全沒有看出來。他仍然主導了技術方案的討論,而回國後這個任務就逐漸轉到了我的身上。計畫裏微軟主導上層軟體,而英特爾負責底層,計畫本身牽扯到系統的各個層面,包括硬體、BIOS、驅動程式、作業系統等等,並對加密和安全有很高的要求。我們需要用到特殊高安全級別 System Management Mode(SMM),可能大部份軟體開發人員都沒聽說過吧的,比作業系統的 0 環還要高。有相當一部份資料屬於高加密級別,不對外公開。很多東西對我都是新的。不過,那個計畫做下來,我不僅站穩了架構師的位置,還成了半個安全專家。報酬也很豐厚,我後來主要因為這個計畫拿到了全年考評的最高等級 Outstanding。

    但這個計畫裏我只管組織技術討論和撰寫技術方案,程式碼方面只是找了點沒人幹的小活練手,不多,難度也不高。從開發角度,近些年的計畫有更難的。比如,有一個計畫我利用樣版和 constexpr 函式開發一些基於靜態反射概念的工具,那就非常有摸著石頭過河的感覺了。開始寫的時候都不確定這條技術道路到底能不能走通,只是覺得應該基本上可行吧。不過,等到做完之後,自己對於編譯期編程能做到什麽、善於做什麽就非常有信心了。現在再做類似的東西就不會覺得有挑戰性了,幾乎就是按部就班的純粹工程問題。

    Q: 在技術領域有著深厚的經驗積累,吳老師能否分享一下是如何保持對技術的熱情,以及如何適應和掌握不斷變化的技術趨勢的?

    吳詠煒: 我覺得對技術的熱情純粹是個人誌趣問題。如果不喜歡技術,也就不會這麽多年一直做技術。興趣是最好的老師。拿 Linus 的一本書名來說,Just for Fun。(我的書架上就放著這本書。)

    當然,幸運的是,我可以把工作和興趣結合起來,透過做自己感興趣的事情掙錢。不是每個人都有這樣的幸運的。不管為了興趣,還是為了生計,我都有對技術進行深研、追蹤業界趨勢的動力。

    程式設計師說,能玩轉 C++ 程式語言的都是牛人

    「高效」與「復雜」經常與 C++ 程式語言同時出現。在一些 C++ 的文章下面,時常會看到這樣的評論: 「C++ 程式語言的語法過於復雜,學習 C++ 新版 本/標準像是在學習一門新程式語言。」

    因此,有程式設計師驚呼:能玩轉 C++ 的,都是高手!

    Q: 您如何看待每一次新版本都像是在學一門新的程式語言,對於那些還在使用較舊版本的 C++ 程式設計師來說,該如何平滑過渡到最新版本?

    吳詠煒: 就如 Bjarne Stroustrup 老爺子常說的,C++ 有成功語言的煩惱。有三種互相矛盾的需求:

  • 簡化語言;

  • 加入現代的新特性;

  • 保留對老程式碼的回溯相容性。

  • 有一部份人想要語言簡單,有一部份人渴求其他語言裏已經有的新特性,絕大部份人都不希望去改動已有的能工作的程式碼。三種需求但凡去掉一點,事情都會簡單很多。

    C++ 設計成為具有高回溯相容性的語言——在 99.9% 的情況裏,老的程式碼拿到新的標準下都可以直接工作。C++ 在加入新的關鍵字時都非常謹慎,唯恐影響現有程式碼,因此我們有 co_await 和 co_yield,而不是 await 和 yield。如果你的程式碼裏正好用到了這種關鍵字,那當然程式碼需要修改。除此之外,C++ 只在某一特性存在較多問題(特別是安全性問題)時才會對其進行修改或移除,典型如 C++17 移除 auto_ptr,及 C++20 廢棄使用「=」在 lambda 運算式中捕獲 this。

    所以程式設計師遷移到 C++ 標準的新版本一般而言就應該是非常平滑的,可以漸進地采用 C++ 新標準裏對當前計畫有用的新特性,而不用急著采用其他還用不著的新特性,或對程式碼進行不必要的覆寫。事實上,升級編譯器本身可能會更加復雜,因為有可能影響二進制相容性,或者因為最佳化方式的變化而使得已有程式碼的行為發生變化(通常也是程式碼中的 bug 引起的)。

    Q: 在最新版本的 C++ 23 新功能新特性中,如靜態operator()、多維陣列支持、擴充套件浮點字面量等,您覺得哪些特性直接改善了開發人員對工作流程和編程效率?

    吳詠煒: C++23 原本就不被視為一個大版本升級,我覺得這些修改還是比較小和局部性的,對某些領域的開發人員潛在會發生影響(在編譯器支持這些新特性並且計畫使用這些新編譯器之後)。我沒看到像 C++20 裏的那種革命性大特性。

    我個人覺得,會比較快能夠用得上的,是使用上比較簡單的特性。比如可以用 import std; 直接匯入整個標準庫,比如可以使用時隔多年終於標準化了的 std::expected,比如可以使用第三方庫中早就已經存在類似功能的 std::views::zip 和 std::print。

    Q: 在【現代 C++ 實戰培訓】這門課中,會有一些具體特性或者高效能工具方面的分享嗎?

    吳詠煒: 這門課程某種程度上是我在 C++ 和效能最佳化的結合點上的經驗分享。我會討論很多 C++ 特性的效能特點,並討論測試效能的個人工具和第三方工具。要寫出高效能的 C++ 程式碼,我們需要理解 C++ 標準庫提供的標準構件的效能特點,並能使用樣版這樣的機制寫出零開銷、甚至負開銷的高效能抽象。——是的,C++ 使用樣版可以寫出比 C 效能更高的程式碼,最常被人參照的典型例子就是 std::sort 比 qsort 通常效能要高,還不是高出一點點。

    Q: 記憶體管理一直是 C++ 開發者的一大難題,其中以 C++ 記憶體管理模型包括棧記憶體和堆記憶體為例,雖然棧記憶體具有自動管理的優點,但它的容量是有限的。堆記憶體提供了更大的容量,但需要手動管理。您認為在開發過程中,如何平衡棧記憶體和堆記憶體的使用,以實作最佳效能和可維護性?

    吳詠煒: 對於使用現代 C++ 的開發者來說,實際上記憶體管理不算一件很麻煩的事。使用智慧指標和容器,很多工作已經相當簡單了。從棧記憶體和堆記憶體的角度來說,大塊的數據顯然適合放到堆上,這些我們才能方便地使用移動語意等 C++ 機制,來高效地轉移數據(的所有權)。透過堆上物件,使用 RAII 慣用法來管理堆上或其他重要資源,本來就是 C++ 的基本用法。對於所有權唯一的情況,實際上已經沒什麽需要平衡的了,這就是很成熟的用法。

    如果用到了共享所有權,那倒是需要斟酌了。我們一需要考慮共享所有權(即參照計數)是否真正需要,二需要考慮怎麽盡可能減少參照計數的增減操作。這些都有成熟的用法。

    Q:在【 現代 C++ 實戰培訓 】這門課中,會有針對記憶體管理方面的具體分享嗎?可以先提前簡單聊聊嗎?

    吳詠煒: 確實會有很多地方會跟記憶體管理相關。我們會討論堆記憶體和棧記憶體的基本用法,會討論使用 RAII 慣用法來簡化對包括堆記憶體在內的資源的管理,會討論能自動管理記憶體的容器和智慧指標,也會討論使用記憶體池和分配器來客製/最佳化對記憶體的使用。當然,記憶體的局部性原則、緩存的使用註意事項等也是不得不討論的問題。

    Q: 由於 C++ 是一門歷史悠久的程式語言,對開發者來說,對早期計畫的維護可能是比較頭疼的問題,也有人形象的稱之為「屎山程式碼」,對於維護遺留計畫,有哪些建議和經驗分享給大家?

    吳詠煒: 這方面實際上已經有很多人討論過了,如 Michael Feathers 專門寫過一本書討論如何維護遺留程式碼,名字就叫 Working Effectively with Legacy Code(中文版名字是【修改程式碼的藝術】,劉未鵬譯)。這裏面討論得已經很深入了。個人並沒有什麽特別要補充的。硬要說說要點的話,我覺得單元測試很重要,SOLID 原則很重要,尤其是 SRP(單一職責原則)。

    話說,C++ 的演進方式實際上是非常照顧遺留程式碼的。你完全可以只在新程式碼裏引入新的 C++ 特性,而完全不去動老程式碼——如果不需要對其進行修改的話。

    編程永無止境,豐富別樣的程式人生

    除了開發工作之外,吳詠煒還是【現代C++實戰培訓】系列課程的主講人,談到角色的轉變,他直言,分享是一種非常好的自我總結和提升,本來只是模模糊糊基本認識正確的東西,在需要給別人講解的過程中,自己就越來越能清晰地理解相關的概念,並能夠更深入地理解相關的技術。這對他的開發工作和培訓以及咨詢,都有著非常大的好處。

    Q: 從一名開發者到技術布道者(講師),你有哪些心得感悟?

    吳詠煒: 分享本身就是一種非常好的自我總結、自我提升的手段。從一開始為極客時間寫專欄開始,我就深深地感到了這一點。本來自己只是模模糊糊基本認識正確的東西,在需要給別人講解的過程中,自己就越來越能清晰地理解相關的概念,並能夠更深入地理解相關的技術。這對我自己的開發工作,以及做培訓和咨詢,都有很大的好處。

    因此,我鼓勵任何有誌於在技術道路上發展的開發者,都要努力多進行分享。不管是口頭的(培訓和交流),還是書面的(寫部落格),都會對己對人都有好處。

    Q: 在擔任技術教練的過程中,有哪些讓您難忘的學員或案例?

    吳詠煒: 學員……他們彼此的差異真的非常大。我記得參加培訓的人當中有資深的開發,公司裏的 CTO 之類的,也有入門才幾年的,雖然不能算小白,但經驗確實差得比較多。有些專攻特定方向的學員,實際上除了 C++ 本身和效能相關問題,其他方面我得向他們學習。比如遇到過編譯器方向的學員,我能教他 C++,但對於編譯器如何最佳化某些 C++ 構件的問題,我就沒他理解深入了。事實上,他在知乎上發的文章和回答,我感覺我是看不太懂的。

    計畫則是見招拆招了。在大規模計畫裏,各種古怪的問題都可能出現,有些還進入了我的培訓案例。比如記憶體使用上 use-after-free 問題,如果套上了一些類封裝的話,就不那麽明顯了。在計畫裏曾經遇到過一個這樣的問題,因為在多執行緒下才能偶發復現,別人花了兩周才定位到問題點,然後找我去看,倒是一下子就看出問題了。但問題在於在當初程式碼檢視的時候評審人員都沒看出來呀。也因為如此,我現在很看重工具,希望工具能夠盡可能自動找出這類問題。對於這個計畫問題而言,在測試的時候使用 Address Sanitizer(ASan)這樣的執行期檢測工具,或者使用像 cppsafe 這樣的靜態掃描工具,都是可以自動發現問題的。

    有關這門課的詳細介紹,歡迎大家掃碼下方二維碼了解詳細資訊: