使用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。
周期性公告:-Wall不包括所有警告。 -Wextra也不包括。 在clang上使用-Weverything,但预计它会变化,所以不要将其与-Werror配对— Ryan Stortz (@withzombies) 2019年2月21日
当然,这是因为未定义行为。由于未定义行为不能存在,编译器可以自由地对代码做出假设——在这种情况下假设handler h永远不会未初始化。
目前编译器静默接受这段糟糕的代码,只是假设我们知道我们在做什么。理想情况下,它应该报错。这就是constexpr拯救我们的地方。
|
|
constexpr在这里强制了一个错误,这正是我们想要的。它在大多数形式的未定义行为上有效,但编译器实现中仍然存在差距。
将所有内容标记为constexpr!
在深入研究clang源代码后,我意识到可以使用libclang在其语义分析期间确定某物是否可以constexpr的相同机制,自动将函数和方法标记为constexpr。虽然这不会直接检测更多未定义行为,但它将帮助我们尽可能多地将代码标记为constexpr。
最初我开始编写一个clang-tidy传递,但在可用API和传递中的上下文方面遇到了问题。我决定创建自己的独立工具:constexpr-everything。它可在我们的GitHub上获得,并应与最近的libclang版本配合使用。
我编写了两个访问者,一个试图识别函数是否可以标记为constexpr。这变得相当简单;我迭代当前翻译单元中的所有clang::FunctionDecls,并使用clang::Sema::CheckConstexprFunctionDecl、clang::Sema::CheckConstexprFunctionBody和clang::Sema::CheckConstexprParameterTypes询问它们是否可以在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。尝试一下,并告诉我们它对您的项目如何工作。