使用constexpr实现更快、更小、更安全的代码
随着C++14的发布,标准委员会强化了C++最酷的现代特性之一:constexpr。现在,C++开发者可以编写常量表达式,并强制在编译时而非每次调用时求值。这带来了更快的执行速度、更小的可执行文件,以及出人意料的安全性提升。
未定义行为一直是许多安全漏洞的根源,例如Linux内核权限提升(CVE-2009-1897)以及无数因未定义行为而被移除的整数溢出检查实现。C++标准委员会在设计constexpr时规定,标记为constexpr的代码不能引发未定义行为。如需全面分析,请阅读Shafik Yaghmour的精彩博客文章《使用Constexpr探索未定义行为》。
我相信constexpr将演变为C++的一个更安全子集。我们应全心全意拥抱它。为此,我创建了一个基于libclang的工具,用于尽可能多地将代码标记为constexpr,称为constexpr-everything。它会自动将constexpr应用于符合要求的函数和变量。
面对未定义行为时的constexpr
最近在我们的内部Slack频道中,一位同事试图创建一个可利用的二进制文件,其中漏洞是未初始化的栈局部变量,但他与编译器斗争。编译器拒绝生成易受攻击的代码。
|
|
使用现代编译器(clang 8.0)编译示例代码时,编译器会静默消除易受攻击的情况。如果调用者指定了switch未处理的选项(如0或4),函数将返回handler2。这在优化级别高于-O0时成立。在Compiler Explorer上亲自尝试!
我的默认警告集(-Wall -Wextra -Wshadow -Wconversion)在clang上完全不会警告这一点(尝试一下)。在gcc上它会打印警告,但仅在启用优化时(-O0与-O1)!
注意:如果你想打印clang知道的所有警告,在开发时使用clang的-Weverything。
当然,这是因为未定义行为。由于未定义行为不能存在,编译器可以自由地对代码做出假设——在这种情况下假设handler h永远不会未初始化。
目前编译器静默接受这段糟糕代码,并假设我们知道自己在做什么。理想情况下,它应该报错。这就是constexpr拯救我们的地方。
|
|
|
|
constexpr在这里强制了一个错误,这正是我们想要的。它对大多数形式的未定义行为都有效,但编译器实现中仍存在差距。
全面应用constexpr!
在深入研究clang源代码后,我意识到可以使用libclang在其语义分析期间确定某物是否可以constexpr的相同机制,自动将函数和方法标记为constexpr。虽然这不会直接检测更多未定义行为,但会帮助我们尽可能多地将代码标记为constexpr。
最初我开始编写一个clang-tidy传递,但在可用API和传递中的上下文方面遇到了问题。我决定创建自己的独立工具:constexpr-everything。它可在我们的GitHub上获取,并应与最近的libclang版本配合使用。
我编写了两个访问器,一个试图识别函数是否可以标记为constexpr。这变得相当简单;我遍历当前翻译单元中的所有clang::FunctionDecl,并使用clang::Sema::CheckConstexprFunctionDecl、clang::Sema::CheckConstexprFunctionBody和clang::Sema::CheckConstexprParameterTypes询问它们是否可以在constexpr上下文中求值。我跳过已经是constexpr或不能是constexpr的函数(如析构函数或main)。当分析检测到可以constexpr但尚未标记的函数时,它会发出诊断和FixIt:
|
|
FixIt可以通过-fix命令行选项自动应用。
应用constexpr变量的困难
我们需要将变量标记为constexpr以强制constexpr函数的求值。自动将constexpr应用于函数很容易。对变量这样做相当困难。我遇到了变量之前未标记const,通过添加constexpr隐式标记为const的问题。
在尝试尽可能广泛地应用constexpr并与我的测试用例斗争后,我切换了策略,采用了更保守的方法:仅标记已经const限定并具有constexpr初始化器或构造函数的变量。
|
|
虽然这种方法不会在所有可能的情况下应用constexpr,但可以安全地自动应用。
在你的代码库上尝试
在运行constexpr-everything之前和之后对测试进行基准测试。不仅你的代码会更快、更小,而且会更安全。标记为constexpr的代码不易发生代码腐烂。
constexpr-everything仍是一个原型——它还有一些粗糙的边缘。最大的问题是FixIt仅适用于源(.cpp)文件,而不适用于其关联的头文件。此外,constexpr-everything只能将现有的constexpr兼容函数标记为constexpr。我们正在努力使用提供的机制来识别由于未定义行为而无法标记的函数。
代码可在我们的GitHub上获取。要亲自尝试,你需要cmake、llvm和libclang。试试看,并告诉我们它在你的项目中的效果。
如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News