C++代码安全争议专家观点:安全是系统工程,没有银弹

7f2d993b8f2d4d2ef4749100b31a61f8.gif

采访嘉宾 | 吴咏炜       责编 | 梦依丹

出品 | CSDN(ID:CSDNnews)

近日,因为内存安全问题,老牌编程语言 C++ 又被推上风口浪尖。一些人认为 C++ 的内存管理复杂,容易出现漏洞,而另一些人则认为这正是 C++ 的魅力所在,它赋予了程序员更多的控制权。

此时,有人建议转向 Rust,声称它在内存安全方面有着出色的表现。然而在 AI 大模型的加持之下,未来的程序员还有必要学习编程语言吗?上周,世界上第一个 AI 程序员 Devin 诞生,李彦宏说“以后不会存在程序员这种职业了”。

为此,我们采访了拥有近 30 年 C/C++ 系统级软件开发和架构经验的「吴咏炜」,解锁 Just for fun 的程序人生。

6ee78f64f926f4577853a17b707b0eb6.jpeg

吴咏炜,国内知名 C++专家,曾任英特尔亚太研发中心资深系统架构师,近 30 年 C/C++系统级软件开发和架构经验。专注于 C/C++ 语言(包括 C++98/C++11/14/17/20)、软件架构、性能优化、设计模式和代码重用。对于精炼、易于维护的代码和架构有着不懈的追求,对开源平台(GNU/Linux)有深入的理解。长期担任资深技术教练,涉及 C++、软件架构、安全软件开发、开源软件等多方面。

0900a1c38d07088603b4fd6245419405.png

代码安全是个系统工程

无法靠某种银弹就能立即解决

“像学外语一样去学编程语言,优先掌握惯用法,而不是语法”

Q:美国政府近日发布了一份网络安全报告,呼吁开发人员停止使用容易出现内存安全漏洞的编程语言,例如 C 和 C++,转而使用内存安全的编程语言进行开发。CSDN 在公众号上报道了《美国政府督促开发者:停止使用C、C++》获得了近 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 替代,但项目经理目前来看还不行,但未来难说……

d29b950d16032911297ff1c991bedfbb.png

“意外”解锁高难度项目

Q:是否还记得自己第一次接触编程是什么时候,可否分享一下当时的编程过程?

吴咏炜:初一下学期的时候,当时学校里有计算机兴趣班,我一下子就迷上了。第一次写的比较完整的程序是利用随机数出计算题,让用户输入回答,然后进行计分。很具有学生色彩的做法吧。用 BASIC 写的,还调试了好一会儿,因为对很多东西都是一知半解,经过老师指导才算完成了。

Q:在 30 年的 C/C++ 系统级软件开发和架构经验中,能分享一个您认为最具挑战性的项目案例吗?在这个项目中,你的角色和贡献是什么?

吴咏炜:年数有点长……让我想想。还是说两个不同的案例吧,里面我扮演的角色和做的事情非常不一样。

挑战性,或者说难易,都是相对的。所谓难者不会,会者不难。所以,总体来说,挑战性意味着做自己之前没有做过的新的事情。我进英特尔没多久,就碰到了一个这种类型的项目,跟微软合作的 FlexGo,“即用即付费”的计算机。这个项目可以说不太成功,但技术上还是很有意思的。这个项目原本的技术架构主导是我在英特尔的第一个经理,一个非常厉害的德国人。就在我跟他在美国出差的途中,他收到了裁员通知,但那时我们完全没有看出来。他仍然主导了技术方案的讨论,而回国后这个任务就逐渐转到了我的身上。项目里微软主导上层软件,而英特尔负责底层,项目本身牵扯到系统的各个层面,包括硬件、BIOS、驱动程序、操作系统等等,并对加密和安全有很高的要求。我们需要用到特殊高安全级别 System Management Mode(SMM),可能大部分软件开发人员都没听说过吧的,比操作系统的 0 环还要高。有相当一部分资料属于高加密级别,不对外公开。很多东西对我都是新的。不过,那个项目做下来,我不仅站稳了架构师的位置,还成了半个安全专家。回报也很丰厚,我后来主要因为这个项目拿到了全年考评的最高等级 Outstanding。

但这个项目里我只管组织技术讨论和撰写技术方案,代码方面只是找了点没人干的小活练手,不多,难度也不高。从开发角度,近些年的项目有更难的。比如,有一个项目我利用模板和 constexpr 函数开发一些基于静态反射概念的工具,那就非常有摸着石头过河的感觉了。开始写的时候都不确定这条技术道路到底能不能走通,只是觉得应该基本上可行吧。不过,等到做完之后,自己对于编译期编程能做到什么、善于做什么就非常有信心了。现在再做类似的东西就不会觉得有挑战性了,几乎就是按部就班的纯粹工程问题。

Q:在技术领域有着深厚的经验积累,吴老师能否分享一下是如何保持对技术的热情,以及如何适应和掌握不断变化的技术趋势的?

吴咏炜:我觉得对技术的热情纯粹是个人志趣问题。如果不喜欢技术,也就不会这么多年一直做技术。兴趣是最好的老师。拿 Linus 的一本书名来说,Just for Fun。(我的书架上就放着这本书。)

当然,幸运的是,我可以把工作和兴趣结合起来,通过做自己感兴趣的事情挣钱。不是每个人都有这样的幸运的。不管为了兴趣,还是为了生计,我都有对技术进行深研、追踪业界趋势的动力。

e84c760d02212c5853bfe7f978d84587.png

程序员说,能玩转 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++ 特性,而完全不去动老代码——如果不需要对其进行修改的话。

51fc943a05216cbf310bf22d9c132718.png

编程永无止境,丰富别样的程序人生

除了开发工作之外,吴咏炜还是《现代C++实战培训》系列课程的主讲人,谈到角色的转变,他直言,分享是一种非常好的自我总结和提升,本来只是模模糊糊基本认识正确的东西,在需要给别人讲解的过程中,自己就越来越能清晰地理解相关的概念,并能够更深入地理解相关的技术。这对他的开发工作和培训以及咨询,都有着非常大的好处。

 Q:从一名开发者到技术布道者(讲师),你有哪些心得感悟?

吴咏炜:分享本身就是一种非常好的自我总结、自我提升的手段。从一开始为极客时间写专栏开始,我就深深地感到了这一点。本来自己只是模模糊糊基本认识正确的东西,在需要给别人讲解的过程中,自己就越来越能清晰地理解相关的概念,并能够更深入地理解相关的技术。这对我自己的开发工作,以及做培训和咨询,都有很大的好处。

因此,我鼓励任何有志于在技术道路上发展的开发者,都要努力多进行分享。不管是口头的(培训和交流),还是书面的(写博客),都会对己对人都有好处。

 Q:在担任技术教练的过程中,有哪些让您难忘的学员或案例?

吴咏炜:学员……他们彼此的差异真的非常大。我记得参加培训的人当中有资深的开发,公司里的 CTO 之类的,也有入门才几年的,虽然不能算小白,但经验确实差得比较多。有些专攻特定方向的学员,实际上除了 C++ 本身和性能相关问题,其他方面我得向他们学习。比如遇到过编译器方向的学员,我能教他 C++,但对于编译器如何优化某些 C++ 构件的问题,我就没他理解深入了。事实上,他在知乎上发的文章和回答,我感觉我是看不太懂的。

项目则是见招拆招了。在大规模项目里,各种古怪的问题都可能出现,有些还进入了我的培训案例。比如内存使用上 use-after-free 问题,如果套上了一些类封装的话,就不那么明显了。在项目里曾经遇到过一个这样的问题,因为在多线程下才能偶发复现,别人花了两周才定位到问题点,然后找我去看,倒是一下子就看出问题了。但问题在于在当初代码检视的时候评审人员都没看出来呀。也因为如此,我现在很看重工具,希望工具能够尽可能自动找出这类问题。对于这个项目问题而言,在测试的时候使用 Address Sanitizer(ASan)这样的运行期检测工具,或者使用像 cppsafe 这样的静态扫描工具,都是可以自动发现问题的。

有关这门课的详细介绍,欢迎大家扫码下方二维码了解详细信息:

4ddae169324360fe11be2584134c9718.jpeg

  • 24
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值