使用constexpr实现更高效、更精简、更安全的代码
随着C++14标准的发布,标准委员会强化了C++最酷的现代特性之一:constexpr。现在,C++开发者可以编写常量表达式并强制在编译时而非每次调用时求值,从而实现更快的执行速度、更小的可执行文件,以及更安全的代码。
未定义行为一直是许多安全漏洞的根源,例如Linux内核权限提升(CVE-2009-1897)以及无数因未定义行为而被移除的整数溢出检查实现。C++标准委员会在设计constexpr时规定,标记为constexpr的代码不能引发未定义行为。
constexpr面对未定义行为
最近在我们的内部Slack频道中,一位同事试图创建一个可利用的二进制文件,其漏洞是未初始化的栈局部变量,但编译器拒绝生成易受攻击的代码。
|
|
使用现代编译器(clang 8.0)编译上述示例代码时,编译器会静默消除易受攻击的情况。如果调用者指定了switch未处理的选择(如0或4),函数将返回handler2。在优化级别高于-O0时会出现这种情况。
原因是未定义行为。由于未定义行为不能存在,编译器可以自由地对代码做出假设——在这种情况下假设handler h永远不会未初始化。
constexpr拯救我们
|
|
constexpr在这里强制产生错误,这正是我们想要的。它对大多数形式的未定义行为都有效,但编译器实现中仍存在差距。
全面应用constexpr!
在深入研究clang源代码后,我意识到可以使用libclang在语义分析期间确定某物是否可以constexpr的相同机制,自动将函数和方法标记为constexpr。
我最初开始编写clang-tidy传递,但在可用API和传递中的上下文方面遇到了问题。我决定创建自己的独立工具:constexpr-everything。它可在我们的GitHub上获取,并应与最近的libclang版本配合使用。
我编写了两个访问器,一个试图识别函数是否可以标记为constexpr。这变得相当简单;我迭代当前翻译单元中的所有clang::FunctionDecls,并使用clang::Sema::CheckConstexprFunctionDecl、clang::Sema::CheckConstexprFunctionBody和clang::Sema::CheckConstexprParameterTypes询问它们是否可以在constexpr上下文中求值。
应用constexpr变量的困难
我们需要将变量标记为constexpr以强制对constexpr函数进行求值。自动将constexpr应用于函数很容易,但对变量这样做相当困难。我遇到了之前未标记为const的变量通过添加constexpr而隐式标记为const的问题。
在尝试尽可能广泛地应用constexpr并与我的测试用例斗争后,我转换策略,采用了更保守的方法:仅标记已经具有const限定符并具有constexpr初始化器或构造函数的变量。
在代码库上尝试它
在运行constexpr-everything之前和之后对测试进行基准测试。不仅代码会更快、更小,而且会更安全。标记为constexpr的代码不易bitrot。
constexpr-everything仍然是一个原型——它还有一些粗糙的边缘。最大的问题是FixIts仅适用于源(.cpp)文件,而不适用于其关联的头文件。此外,constexpr-everything只能将现有的constexpr兼容函数标记为constexpr。我们正在努力使用提供的机制来识别由于未定义行为而无法标记的函数。
代码可在我们的GitHub上获取。要自己尝试,你需要cmake、llvm和libclang。试试看,并告诉我们它在你的项目中的效果如何。