C 和 C++ 编译器选项强化指南
C 和 C++ 编译器选项强化指南
由Open Source Security Foundation (OpenSSF) Best Practices Working Group, 2025-03-28 发布
本文档是关于编译器和链接器选项的指南,这些选项有助于使用 C 和 C++ 的原生(或交叉)工具链来交付可靠且安全的代码。编译器选项强化的目标是生成具有安全机制的应用程序二进制文件(可执行文件),以防御潜在的攻击和/或错误行为。
强化的编译器选项还应该生成能够与现代操作系统 (OS) 中现有的平台安全功能良好集成的应用程序。有效配置编译器选项在开发过程中也有几个好处,例如增强的编译器警告、静态分析和调试工具。
本文档适用于:
- 编写 C 或 C++ 代码的人员,帮助他们确保生成的代码可以与强化的选项一起使用,包括嵌入式设备、物联网设备、智能手机和个人计算机。
- 构建 C 或 C++ 代码以在生产环境中使用的人员,包括 Linux 发行版、设备制造商以及为其本地环境编译 C 或 C++ 的人员。
本文档重点介绍 GNU Compiler Collection (GCC) 和 Clang/LLVM 的推荐选项,我们希望这些建议适用于基于 GCC 和 Clang 技术的其他编译器1。未来,我们的目标是扩展指南,以涵盖其他编译器,例如 Microsoft MSVC。
TL;DR:我应该使用哪些编译器选项?
在 GCC 和 Clang 等编译器上编译 C 或 C++ 代码时,在所有情况下都启用以下标志,以便在编译时检测漏洞并启用运行时保护机制:
-O2 -Wall -Wformat -Wformat=2 -Wconversion -Wimplicit-fallthrough \
-Werror=format-security \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3 \
-D_GLIBCXX_ASSERTIONS \
-fstrict-flex-arrays=3 \
-fstack-clash-protection -fstack-protector-strong \
-Wl,-z,nodlopen -Wl,-z,noexecstack \
-Wl,-z,relro -Wl,-z,now \
-Wl,--as-needed -Wl,--no-copy-dt-needed-entries
此外,在下表中任何情况下编译代码时,添加相应的附加选项:
| 何时 | 附加选项标志 |
| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 使用 GCC | -Wtrampolines
|
| 使用 GCC 且源代码中只有从左到右的文字 | -Wbidi-chars=any
|
| 用于可执行文件 | -fPIE -pie
|
| 用于共享库 | -fPIC -shared
|
| 用于 x86_64 | -fcf-protection=full
|
| 用于 aarch64 | -mbranch-protection=standard
|
| 用于生产代码 | -fno-delete-null-pointer-checks -fno-strict-overflow -fno-strict-aliasing -ftrivial-auto-var-init=zero
|
| 用于将过时的 C 结构视为错误 | -Werror=implicit -Werror=incompatible-pointer-types -Werror=int-conversion
|
| 用于使用 GNU C 库 pthreads 的多线程 C 代码 | -fexceptions
|
| 在开发过程中,但_不_在分发源代码时 | -Werror
|
请注意,不同编译器对某些选项的支持可能有所不同,例如对 -D_FORTIFY_SOURCE
的支持取决于编译器2和 C 标准库的实现。有关背景和每个选项的详细讨论,请参见下面的讨论。
我们建议开发人员额外使用通用的 -Werror
,将开发期间的所有警告都视为错误。但是,在分发源代码时,不应以这种通用形式使用 -Werror
,因为这样使用 -Werror
会对特定的工具链供应商和版本产生依赖性。选择性的形式 -Werror=
_<warning-flag>
_,可以在开发和分发源代码时使用,它会将特定的警告提升为错误,以防代码中出现不应该发生的情况。例如,我们鼓励开发人员将有关 1999 C 标准删除的过时 C 结构的警告提升为错误(请参见上表中的“用于禁用过时的 C 结构”)。这些选项通常无法由独立构建软件的人员添加,因为这些选项可能需要对源代码进行重要的更改。
在本指南中,我们使用术语 生产代码 来表示旨在在现实世界中使用并产生实际效果的可执行代码;它应该具有最高的可靠性和性能。我们使用术语 instrumented test code 来表示用于改进缺陷检测和可调试性的可执行代码,因此,通常会崩溃更多且速度较慢。测试过程应同时使用检测测试代码和生产代码。
开发人员应确保他们的生产代码和检测测试代码都通过了具有所有相关选项的自动化测试套件。如果程序无法使用这些选项进行编译,我们鼓励开发人员将其视为错误。如果程序仅处理受信任的数据,则构建生产代码的人员可以选择省略一些会降低性能的强化选项,但请记住,部署不安全且会迅速出错的程序是没有任何帮助的。现有的程序可能需要随着时间的推移进行修改,才能使用其中的某些选项。
背景
为什么我们需要编译器选项强化?
遗憾的是,当今的攻击者每天都在攻击我们使用的软件。许多编程语言的编译器都有一些选项,可以在编译时检测潜在的漏洞和/或插入运行时保护,以防御潜在的攻击。这些在任何语言中都很重要,但在 C 和 C++ 中,这些选项尤其重要。
用 C 和 C++ 编程语言编写的应用程序容易出现一类称为内存安全错误(又名内存错误)的软件缺陷。这类缺陷包括缓冲区溢出、解引用空指针和使用后释放错误等错误。内存错误可能发生,因为 C 和 C++ 中的底层内存管理没有提供任何语言级别的规定来确保指针运算或直接内存访问等操作的内存安全。相反,它们要求软件开发人员在执行常见操作时编写正确的代码,而这已被证明难以大规模实现。内存错误有可能导致内存漏洞,威胁参与者可以利用这些漏洞通过运行时攻击获得对计算机系统的未授权访问。Microsoft 发现,2006-2018 年所有安全缺陷中有 70% 是内存安全故障3,Chrome 团队也同样发现,其所有漏洞中有 70% 是内存安全问题。4
大多数高级编程语言都是 “内存安全” 的,并且默认情况下可以防止此类缺陷。许多这些语言允许程序在特殊情况下暂时中止内存安全保护,例如在调用用 C 编写的操作系统 API 时,但此类中止旨在仅限于几行代码,而不是整个程序。有人呼吁用内存安全的语言重写 C 和 C++ 程序。这种情况在某些情况下已经发生5;但是,这种重写成本高昂且耗时,具有自身的风险,并且在今天有时是不切实际的,特别是对于不常见的 CPU。即使达成普遍共识,重写所有 C 和 C++ 代码也需要数十年时间,并产生巨大的金钱成本。对这种重写的一个粗略估计表明成本为 2.4 万亿美元6,这将使重写 C 和 C++ 成为一个与将全球气候变化目标控制在可实现范围内的规模(在所需资金投入方面)相似的问题。7 因此,并非所有 C 和 C++ 都可以修订或丢弃8。例如,Google 预计 “在可预见的未来,仍然会残留一些成熟且稳定的内存不安全代码”9。
因此,重要的是要接受 C 和 C++ 将继续被使用,并采取 其他 措施来降低风险。为了降低风险,我们必须降低缺陷成为漏洞的可能性,或者降低此类缺陷的影响。积极使用编译器选项有时可以检测漏洞或帮助抵消其运行时影响。
运行时攻击不同于传统的恶意软件,后者通过专用的程序可执行文件执行其恶意程序操作,而运行时攻击会影响良性程序以恶意方式运行。利用未缓解的内存漏洞的运行时攻击可以被威胁参与者用作初始攻击向量,从而使他们能够在系统上立足,例如,通过将恶意代码注入到正在运行的程序中。
现代的、具有安全意识的 C 和 C++ 软件开发实践,例如,诸如 SEI CERT C10 和 C++11 等安全编码标准以及程序分析旨在主动避免将内存错误(和其他软件缺陷)引入应用程序。然而,在实践中,完全消除生产 C 和 C++ 软件中的内存错误已被证明几乎是不可能的。
因此,现代操作系统(包括其 C 和 C++ 编译器及其运行时基础架构)部署各种运行时机制来防止潜在的安全漏洞。此类机制的主要目的是缓解潜在的可利用内存漏洞,以防止威胁参与者利用它们来获得代码执行能力。在缓解措施到位后,如果触发了内存错误,受影响的应用程序可能仍然会崩溃。但是,如果替代方案是损害系统的运行时环境,那么这样的结果仍然是更可取的。
使用时,这些运行时机制 可以 防止攻击,降低其可能性或降低其影响。12
为了受益于操作系统提供的保护机制,必须在构建时准备应用程序二进制文件,使其与缓解措施兼容。通常,这意味着在构建软件时为编译器或链接器启用特定的选项标志。
某些机制可能需要额外的配置和微调,例如,由于某些不太可能出现的边缘情况下可能出现的编译问题,或者缓解措施为某些程序结构增加的性能开销。某些编译器安全功能依赖于程序的数据流分析和启发法,其结果可能因程序源代码的详细信息而异。因此,这些功能实现的保护机制可能并不总是提供完整的覆盖。
在依赖于过时版本的开源软件 (OSS) 编译器的项目中,这些问题会加剧。通常,安全缓解措施更有可能在 Linux 发行版附带的现代编译器版本中默认启用。请注意,上游 GCC 项目使用的默认值不会启用其中一些缓解措施。
如果在构建时忽略或忽略了编译器选项强化,则可能无法向已经分发的可执行文件添加强化。因此,最好评估应用程序应支持哪些缓解措施,并在不启用缓解措施会削弱应用程序防御姿态时做出有意识的、知情的决策。确保使用尽可能多的选项对软件进行_测试_,以确保可以这样运行它。
一些组织要求选择强化规则。例如,美国政府的 NIST SP 800-218 实践 PW.6 要求配置“编译、解释器和构建过程,以提高可执行文件的安全性”13。卡内基梅隆大学 (CMU) 的“十大安全编码实践”建议“使用编译器可用的最高警告级别编译代码,并通过修改代码来消除警告。”14 本指南可以帮助您做到这一点。
应如何应用本指南?
您如何应用本指南取决于您的情况:
- 新的或接近全新的项目(“绿地”):如果您要启动一个新项目,请尽快启用所有内容,最好是在为其编写任何代码之前。这样,您会立即收到任何有问题的结构的通知,并在将来避免它。
- 现有的非平凡项目(“棕地”):通常不可能一次启用所有选项。首先,警告的数量可能会让人不知所措。其次,虽然运行时保护机制通常不会导致正确运行的程序失败,但它们仍然可能导致问题(例如,由于二进制文件大小增加)。相反,一次启用一个或几个选项,评估它们的影响,解决任何问题,并随着时间的推移重复此过程。某些标志(如
-Wall
) 是其他标志的集合;考虑将它们分解并一次启用其中的几个特定标志。
应用程序应努力实现无警告编译。这需要时间,但警告表明存在潜在问题。完成后,任何新的警告都表明存在潜在问题。
编译器选项强化不能做什么?
编译器选项强化不是万能药;仅仅依靠安全功能和功能来获得安全的软件是不够的。安全性是整个系统的一个涌现属性,它依赖于正确构建和集成所有部件。但是,如果使用得当,安全编译器选项将补充现有的流程,例如静态和动态分析、安全编码实践、负面测试套件、分析工具,最重要的是:安全卫生,作为可靠的设计和体系结构的一部分。
在大多数情况下,强化的编译器选项仅在使用强化的选项编译的代码中生效。因此,大多数编译器选项强化并不能使在采用强化选项之前预构建的软件受益。对于包含预构建(可能是第三方)库或其他组件的项目,这尤其令人担忧。在这种情况下,重要的是要了解项目要链接到哪些组件,以及这些组件又是如何构建的,以确定哪些组件可以从编译器选项强化中受益。
我们的威胁模型、目标和目的是什么?
我们的威胁模型是所有软件开发人员都会犯错误,有时这些错误会导致漏洞。此外,一些恶意开发人员可能会故意创建_看起来_是无意漏洞的代码,或者_看起来_正确但故意欺骗审阅者(也称为隐蔽代码15) 的代码。
我们的首要目标是抵御_看起来_是无意的漏洞(无论它们是否是故意的)。我们的次要目标是抵御恶意代码,其中其源代码的外观旨在欺骗审阅者。
许多漏洞是由常见错误引起的。因此,在实现这些目标时,我们的大部分重点是检测和抵御_常见_错误,无论它们在特定情况下是否是漏洞。我们特别(但不完全)侧重于抵御内存安全问题,因为如上所述,内存安全问题会导致 C 和 C++ 代码中的大多数漏洞。
我们_不_试图抵御源代码明显编写为恶意的软件。编译器通常无法抵御这一点,而其他对策(例如源代码同行评审)是更有效的对策。
鉴于这些目标,本指南具有以下目的:
- 最小化 在生产代码中发布的漏洞的可能性和/或影响。
- 最大化 在编译或测试期间(特别是在使用检测测试代码时)检测漏洞,以便可以在发布之前修复它们。
- 检测隐蔽代码15(尤其是特洛伊木马源代码16),在可行的情况下,使同行评审更有效。
本指南不能保证这些结果。但是,当与其他措施结合使用时,它们可以提供显着帮助。
推荐的编译器选项
本节介绍编译器和链接器选项标志的建议,这些标志 1) 启用编译时检查,以警告开发人员源代码中存在的潜在缺陷(表 1),以及 2) 启用运行时保护机制,例如旨在检测应用程序中内存漏洞何时被利用的检查(表 2)。
表 1 和表 2 中的建议主要适用于使用 GCC 和 Binutils 工具链或 Clang / LLVM 工具链在 GNU/Linux 环境中编译用户空间代码,并且已包含在本文档中,因为它们是:
- 在至少一些主要的 Linux 发行版(包括 Debian、Ubuntu、Red Hat 和 SUSE Linux)中广泛部署并默认启用,以用于预构建的软件包。请参阅 Voisin 等人对发行版使用的编译器选项的持续调查17。
- 由 GCC 和 Clang / LLVM 工具链都支持。
- 跨平台且在(至少)Intel 和 AMD 64 位 x86 架构以及 64 位版本的 ARM 架构 (AArch64) 上受支持。
由于历史原因,GCC 编译器和 Binutils 上游项目默认情况下不启用优化或安全强化选项。虽然从源代码构建 GCC 和 Binutils 时可以更改默认选项的某些方面,但 GNU/Linux 发行版附带的工具链中使用的默认值各不相同。发行版还可以附带具有不同默认值的多个工具链版本。因此,开发人员需要注意编译器和链接器选项标志,并根据项目优化的需求、警告和错误检测的级别以及安全强化来管理它们。
要确定系统上 GCC 或 Clang 使用的默认标志,您可以检查 cc -v
_<sourcefile.c>
_的输出,并查看编译器用于构建指定源文件的完整命令行。此信息有两个主要目的:了解系统上编译器的设置,并深入了解发行版的维护人员选择的选项。此外,它对于诊断与选项相关的问题或解决软件编译过程中可能出现的问题非常有价值。例如,某些选项标志依赖于它们出现的顺序;当多次设置参数时,稍后出现的参数通常优先。通过分析已使用的标志的完整列表,可以更轻松地解决由对顺序敏感的标志之间的交互引起的问题。
同样,运行 cc -O2 -dM -E - < /dev/null
将生成宏定义的常量的完整列表。此输出对于解决与通过特定宏定义启用的编译器或库功能相关的问题非常有用。
重要的是要注意,从第三方供应商处获取 GCC 可能会导致您的 GCC 实例预配置为启用或禁用某些默认标志。这些标志会严重影响编译代码的安全性。因此,如果 GCC 是通过软件包管理器、Linux 发行版或其他方式获取的,则必须查看默认标志。我们建议在构建脚本或构建系统配置中显式启用所需的编译器标志,而不是依赖于工具链默认值。如果您正在为 Linux 发行版创建软件包,则发行版维护人员可能有自己的推荐的合并构建标志的方法。在这种情况下,请参阅相应的发行版文档,例如,Debian18、Gentoo19、Fedora20、OpenSUSE21 或 Ubuntu22。
典型的编译器配置不会报告来自系统头文件的警告,因为应用程序开发人员通常不控制这些头文件。在 GCC 中,这是因为默认情况下启用了 -Wno-system-headers
,并且 Clang 通常也会禁止显示来自系统头文件的警告 23。您可能还希望将第三方包含文件标记为系统头文件,以便大大提高警告级别。使用命令行选项 -isystem
添加的目录被 GCC 24 和 Clang 25 视为系统头文件目录。在 Cmake 配置文件中,您可以通过在参数前添加 SYSTEM
来使用 include_directories
来执行此操作 26。这其中存在权衡。禁止显示来自系统头文件和第三方库的警告可能会隐藏其中影响应用程序的漏洞。另一方面,_不_禁止显示它们会将精力集中在开发人员通常无法控制的问题上,在使用 CI 作业中的 -Werror
时会阻碍进度,并且通常很难支持使用旧版本的第三方代码进行构建,从而使增量升级变得困难。
表 1 中选项启用的编译时检查不会影响编译器生成的二进制代码,因此不会在性能或其他运行时特性方面产生任何权衡。相反,它们仅发出警告(如果启用了 -Werror
选项,则发出错误),以告知在源代码中发现的潜在缺陷。
启用此类附加警告后,开发人员应花时间了解编译器标记的底层问题并解决它们。
表 2 中的选项可以分为两种类型:
- 使编译器使用旨在检测内存错误的运行时检查来扩充生成的二进制文件的选项,以及 2) 指示编译器调整生成的二进制文件或代码的属性以确保生成的二进制文件与操作系统强制执行的保护机制兼容的选项。
测试对于验证启用表 2 中列出的任何选项的影响至关重要,因为它们会影响编译器生成的二进制文件。下面描述的某些编译器选项可能会影响软件的性能。但是,这种性能影响通常是特定于上下文的,并且在大多数情况下,要么是最小的,要么是收益大于开销。有关何时可能发生显着性能影响的更多信息,您可以在本文档的后面找到对这些选项的详细说明。
在处理性能至关重要的软件时,开发人员应仔细评估启用更安全选项与观察到的性能测试数据之间的权衡,同时考虑到其软件的特定用例。在生产环境中实施任何更改之前,必须进行彻底的基准测试和测试。这将深入了解编译器选项如何影响软件的性能和安全方面。请记住,对于用户来说,快速工作但容易受到攻击的系统可能难以接受。基准测试应考虑任何相关的性能特征,例如执行期间的平均时间、最坏情况时间和内存使用情况。此外,生成二进制文件的大小也可能是一个问题,特别是对于嵌入式系统。
表 1:推荐的启用严格编译时检查的编译器选项。
| 编译器标志 | 支持版本 | 描述 |
| -------------------------------------------------------------------------------------------------------------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -Wall
-Wextra
| GCC 2.95.3 Clang 4.0.0 | 启用通常与缺陷相关的结构的警告 |
| -Wformat
-Wformat=2
| GCC 2.95.3 Clang 4.0.0 | 启用其他格式函数警告 |
| -Wconversion
-Wsign-conversion
| GCC 2.95.3 Clang 4.0.0 | 启用隐式转换警告 |
| -Wtrampolines
| GCC 4.3.0 | 启用有关需要可执行堆栈的 trampoline 的警告 |
| -Wimplicit-fallthrough
| GCC 7.0.0 Clang 4.0.0 | 警告 switch case 何时失败 |
| -Wbidi-chars=any
| GCC 12.0.0 | 启用有关注释、字符串字面量、字符常量和标识符中可能具有误导性的 Unicode 双向控制字符的警告 |
| -Werror
-Werror=
_<warning-flag>
_ | | 将所有或选定的编译器警告视为错误。仅在开发期间使用通用形式 -Werror
,不要在源代码分发中使用。 |
| -Werror=format-security
| GCC 2.95.3 Clang 4.0.0 | 将不是字符串字面量且未带参数使用的格式字符串视为错误 |
| -Werror=implicit
-Werror=incompatible-pointer-types
-Werror=int-conversion
| GCC 2.95.3 Clang 2.6.0 | 将过时的 C 结构视为错误 |
表 2:推荐的启用运行时保护机制的编译器选项。
| 编译器标志 | 支持版本 | 描述 |
| ---------------------------------------------------------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| -D_FORTIFY_SOURCE=3
| GCC 12.0.0 Clang 9.0.02 | 与 -D_FORTIFY_SOURCE=2
中的检查相同,但使用明显更多的调用来强化,这可能会在某些罕见情况下影响性能。需要 -O1
或更高版本,可能需要预先添加 -U_FORTIFY_SOURCE
。 |
| -D_GLIBCXX_ASSERTIONS
| libstdc++ 6.0.0 | C++ 标准库调用的前提条件检查。可能会影响性能。 |
| -fstrict-flex-arrays=3
| GCC 13.0.0 Clang 16.0.0 | 仅当声明为 []
时,才将结构体中的尾随数组视为灵活数组。 |
| -fstack-clash-protection
| GCC 8.0.0 Clang 11.0.0 | 启用可变大小堆栈分配有效性的运行时检查。可能会影响性能。 |
| -fstack-protector-strong
| GCC 4.9.0 Clang 6.0.0 | 启用基于堆栈的缓冲区溢出的运行时检查。可能会影响性能。 |
| -fcf-protection=full
| GCC 8.0.0 Clang 7.0.0 | 在 x86_64 上启用控制流保护,以防御面向返回编程 (ROP) 和面向跳转编程 (JOP) 攻击 |
| -mbranch-protection=standard
| GCC 9.0.0 Clang 8.0.0 | 在 AArch64 上启用分支保护,以防御 ROP 和 JOP 攻击 |
| -Wl,-z,nodlopen
| Binutils 2.10.0 | 将 dlopen(3)
调用限制为共享对象 |
| -Wl,-z,noexecstack
| Binutils 2.14.0 | 通过将堆栈内存标记为不可执行来启用数据执行保护 |
| -Wl,-z,relro
-Wl,-z,now
| Binutils 2.15.0 | 将在加载时解析的重定位表条目标记为只读。-Wl,-z,now
会影响启动性能。 |
| -fPIE -pie
| Binutils 2.16.0 Clang 5.0.0 | 构建为与位置无关的可执行文件。可能会影响 32 位架构上的性能。 |
| -fPIC -shared
| < Binutils 2.6.0 Clang 5.0.0 | 构建为与位置无关的代码。可能会影响 32 位架构上的性能。 |
| -fno-delete-null-pointer-checks
| GCC 3.0.0 Clang 7.0.0 | 强制保留空指针检查 |
| -fno-strict-overflow
| GCC 4.2.0 | 定义有符号整数和指针算术溢出的行为 |
| -fno-strict-aliasing
| GCC 2.95.3 Clang 2.9.0 | 不要假定严格别名 |
| -ftrivial-auto-var-init
| GCC 12.0.0 Clang 8.0.0 | 执行简单的自动变量初始化 |
| -fexceptions
| GCC 2.95.3 Clang 2.6.0 | 启用异常传播以强化多线程 C 代码 |
| -fhardened
| GCC 14.0.0 | 在 GCC 中启用预先确定的强化选项集 |
| -Wl,--as-needed
-Wl,--no-copy-dt-needed-entries
| Binutils 2.20.0