[正文内容]

2025年4月10日 David Malcolm 相关主题: CC++CompilersLinuxSecure Coding 相关产品: Red Hat Enterprise Linux

目录:

我在 Red Hat 工作,负责 GCC, the GNU Compiler Collection。 过去一年,我主要致力于改进 GCC 发出的诊断信息(错误和警告),希望能使其更易于使用。让我们来看看即将到来的 GCC 15 中的 6 项改进。

1. 更友好的执行路径

我于 GCC 10 中向 GCC 添加了一个静态分析器,它会打印预测的、通过用户代码的执行路径的可视化信息,展示其预测的每个问题。

下面是一个示例,展示了我对 GCC 15 中此功能所做的一些改进:

infinite-loop-linked-list.c: In function ‘while_loop_missing_next’:
infinite-loop-linked-list.c:30:10: warning: infinite loop [CWE-835] [-Wanalyzer-infinite-loop]
  30 |  while (n)
   |     ^
 ‘while_loop_missing_next’: events 1-3
  30 |  while (n)
   |     ^
   |     |
   |     (1) ⚠️ infinite loop here
   |     (2) when ‘n’ is non-NULL: always following ‘true’ branch... ─>─┐
   |                                     │
   |                                     │
   |┌────────────────────────────────────────────────────────────────────────┘
  31 |│  {
  32 |│   sum += n->val;
   |│       ~~~~~~
   |│       |
   |└─────────────>(3) ...to here
 ‘while_loop_missing_next’: event 4
  32 |    sum += n->val;
   |    ~~~~^~~~~~~~~
   |      |
   |      (4) looping back... ─>─┐
   |                 │
 ‘while_loop_missing_next’: event 5
   |                 │
   |┌─────────────────────────────────┘
  30 |│ while (n)
   |│     ^
   |│     |
   |└────────>(5) ...to here

我添加了一个警告 emoji (⚠️) 到路径中出现问题的事件(上述示例中的事件 1),并且我添加了 "ASCII art" 来显示控制流,例如连接事件 2 和 3 的线条,以及连接事件 4 和 5 的线条(与 GCC 14 输出 相比)。

另一个执行路径的示例可以在这个新的 -fanalyzer warning -Wanalyzer-undefined-behavior-ptrdiff 中看到,它会警告涉及不同内存块的指针减法:

demo.c: In function ‘test_invalid_calc_of_array_size’:
demo.c:9:20: warning: undefined behavior when subtracting pointers [CWE-469] [-Wanalyzer-undefined-behavior-ptrdiff]
  9 |  return &sentinel - arr;
   |          ^
 events 1-2
  │
  │  3 | int arr[42];
  │   |   ~~~
  │   |   |
  │   |   (2) underlying object for right-hand side of subtraction created here
  │  4 | int sentinel;
  │   |   ^~~~~~~~
  │   |   |
  │   |   (1) underlying object for left-hand side of subtraction created here
  │
  └──> ‘test_invalid_calc_of_array_size’: event 3
      │
      │  9 |  return &sentinel - arr;
      │   |          ^
      │   |          |
      │   |          (3) ⚠️ subtraction of pointers has undefined behavior if they do not point into the same array object
      │

左侧有一条线可视化堆栈深度,以突出显示调用和返回。 从 GCC 15 开始,这现在使用 unicode 盒形绘图字符(如果区域设置支持),对于像 -Wanalyzer-infinite-loop 这样的纯粹的过程内情况,我们现在完全省略它,这可以节省一些视觉 "噪音"。

2. C++模板错误的新外观

涉及 C++ 模板的编译器错误出了名的难以阅读。

考虑以下无效的 C++ 代码:

struct widget {};
void draw (widget &);
struct diagram {};
void draw (diagram &);
template <class W>
concept drawable = requires(W w) { w.draw (); };
template <drawable T>
void draw(T);
int main ()
{
 draw (widget ());
}

尝试使用 -fconcepts -fconcepts-diagnostics-depth=2GCC 14 中编译它 会产生 34 行输出,就这些错误而言,这相对简单,但即使这样也很难理解。 我将在此处发布以供参考,但我承认当我尝试阅读时,我的眼睛会变得迟钝:

<source>: In function 'int main()':
<source>:15:8: error: no matching function for call to 'draw(widget)'
  15 |  draw (widget ());
   |  ~~~~~^~~~~~~~~~~
<source>:2:6: note: candidate: 'void draw(widget&)' (near match)
  2 | void draw (widget &);
   |   ^~~~
<source>:2:6: note:  conversion of argument 1 would be ill-formed:
<source>:15:9: error: cannot bind non-const lvalue reference of type 'widget&' to an rvalue of type 'widget'
  15 |  draw (widget ());
   |     ^~~~~~~~~
<source>:5:6: note: candidate: 'void draw(diagram&)'
  5 | void draw (diagram &);
   |   ^~~~
<source>:5:12: note:  no known conversion for argument 1 from 'widget' to 'diagram&'
  5 | void draw (diagram &);
   |      ^~~~~~~~~
<source>:11:6: note: candidate: 'template<class T> requires drawable<T> void draw(T)'
  11 | void draw(T);
   |   ^~~~
<source>:11:6: note:  template argument deduction/substitution failed:
<source>:11:6: note: constraints not satisfied
<source>: In substitution of 'template<class T> requires drawable<T> void draw(T) [with T = widget]':
<source>:15:8:  required from here
  15 |  draw (widget ());
   |  ~~~~~^~~~~~~~~~~
<source>:8:9:  required for the satisfaction of 'drawable<T>' [with T = widget]
<source>:8:20:  in requirements with 'W w' [with W = widget]
<source>:8:43: note: the required expression 'w.draw()' is invalid, because
  8 | concept drawable = requires(W w) { w.draw (); };
   |                  ~~~~~~~^~
<source>:8:38: error: 'struct widget' has no member named 'draw'
  8 | concept drawable = requires(W w) { w.draw (); };
   |                  ~~^~~~

问题之一是这些消息具有分层结构,但我们以 "平面" 列表的形式打印它们,这掩盖了含义。

我一直在试验一种新的信息呈现方式,从 Sy Brand 的优秀论文 "Concepts Error Messages for Humans" 中汲取灵感。 默认情况下,它尚未准备好在 GCC 中为所有 C++ 用户启用,但可以通过命令行选项在 GCC 15 中使用,供想要试用的人使用:-fdiagnostics-set-output=text:experimental-nesting=yes

以下是我们刚刚看到的示例,添加了 -fdiagnostics-set-output=text:experimental-nesting=yes "秘籍代码":

demo.cc: In function ‘int main()’:
demo.cc:19:8: error: no matching function for call to ‘draw(widget)’
  19 |  draw (widget ());
   |  ~~~~~^~~~~~~~~~~
 • there are 3 candidates
  • candidate 1: ‘void draw(widget&)’ (near match)
   demo.cc:6:6:
     6 | void draw (widget &);
      |   ^~~~
   • conversion of argument 1 would be ill-formed:
   • error: cannot bind non-const lvalue reference of type ‘widget&’ to an rvalue of type ‘widget’
    demo.cc:19:9:
      19 |  draw (widget ());
       |     ^~~~~~~~~
  • candidate 2: ‘void draw(diagram&)’
   demo.cc:9:6:
     9 | void draw (diagram &);
      |   ^~~~
   • no known conversion for argument 1 from ‘widget’ to ‘diagram&’
    demo.cc:9:12:
      9 | void draw (diagram &);
       |      ^~~~~~~~~
  • candidate 3: ‘template<class T> requires drawable<T> void draw(T)’
   demo.cc:15:6:
     15 | void draw(T);
      |   ^~~~
   • template argument deduction/substitution failed:
    • constraints not satisfied
     • demo.cc: In substitution of ‘template<class T> requires drawable<T> void draw(T) [with T = widget]’:
     • required from here
      demo.cc:19:8:  
        19 |  draw (widget ());
         |  ~~~~~^~~~~~~~~~~
     • required for the satisfaction of ‘drawable<T>’ [with T = widget]
      demo.cc:12:9:  
        12 | concept drawable = requires(W w) { w.draw (); };
         |     ^~~~~~~~
     • in requirements with ‘W w’ [with W = widget]
      demo.cc:12:20:  
        12 | concept drawable = requires(W w) { w.draw (); };
         |          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
     • the required expression ‘w.draw()’ is invalid, because
      demo.cc:12:43:
        12 | concept drawable = requires(W w) { w.draw (); };
         |                  ~~~~~~~^~
      • error: ‘struct widget’ has no member named ‘draw’
       demo.cc:12:38:
         12 | concept drawable = requires(W w) { w.draw (); };
          |                  ~~^~~~

这种错误呈现方式使用缩进和嵌套的 bullet points 来显示编译器正在执行的操作的逻辑结构,在可能的情况下消除了冗余的 "视觉噪音",并澄清了措辞,清楚地说明编译器尝试了 3 个不同的函数调用候选项,并阐明了每个候选者不合适的原因。

我尝试为我的日常 C++ 工作启用它,感觉这是一个巨大的改进。 我希望我们能够在 GCC 16 中默认启用它; 你可以在这里自己尝试一下。

3. 机器可读的诊断信息

SARIF 是一种文件格式,旨在以机器可读的、可互换的格式存储静态分析工具的结果; 因此,它非常适合编译器诊断。 我在 GCC 13 中添加了对以 SARIF 形式写出 GCC 诊断信息的支持,但这是一个全有或全无的交易:你可以选择 stderr 上的 GCC 经典文本输出,或者 SARIF,但不能同时选择两者。

对于 GCC 15,我重做了我们处理诊断信息的内部方式,以便可以有多个 "输出接收器",并添加了一个新的命令行选项 -fdiagnostics-add-output=ARGS,用于添加新的接收器。 例如,使用 -fdiagnostics-add-output=sarif 将使诊断信息以文本形式在 stderr 上以及以 SARIF 形式在文件中发出。

有各种子选项可用; 例如,-fdiagnostics-add-output=sarif:version=2.2-prerelease 将为该接收器选择 SARIF 2.2 输出(尽管鉴于我们仍在开发 SARIF 2.2 规范,它使用该规范的非官方草案,并且可能会发生更改)。

我还改进了 GCC 发出的 SARIF。 输出现在捕获与诊断信息关联的所有位置和带标签的源代码范围。 例如,对于:

PATH/missing-semicolon.c: In function 'missing_semicolon':
PATH/missing-semicolon.c:9:12: error: expected ';' before '}' token
  9 |  return 42
   |      ^
   |      ;
  10 | }
   | ~

GCC SARIF 输出现在捕获辅助位置(尾随右括号的位置),以及缺少的分号的位置。 类似地,对于:

bad-binary-ops.c: In function ‘bad_plus’:
bad-binary-ops.c:64:23: error: invalid operands to binary + (have ‘S’ {aka ‘struct s’} and ‘T’ {aka ‘struct t’})
 64 |  return callee_4a () + callee_4b ();
   |     ~~~~~~~~~~~~ ^ ~~~~~~~~~~~~
   |     |       |
   |     |       T {aka struct t}
   |     S {aka struct s}

SARIF 输出捕获那些带下划线的范围及其标签。

GCC 的 SARIF 输出现在捕获命令行参数 (§3.20.2)、编译开始和结束的时间戳 (§§3.20.7-8) 和工作目录 (§3.20.19)。 它现在还为 SARIF artifact 对象设置 roles 属性 (§3.24.6),捕获消息文本中的任何嵌入式 URL (§3.11.6)。 对于与头文件相关的诊断信息,SARIF 输出现在捕获导致诊断信息位置的 #include 指令链(使用 SARIF locationRelationship 对象, §3.34)。

除了改进 GCC 生成的 SARIF 之外,我还向 GCC 15 添加了一个 使用 SARIF 的工具:sarif-replay。 这是一个简单的命令行工具,用于查看 .sarif 文件,以文本形式显示("重放").sarif 文件中找到的任何诊断信息,就像它们是 GCC 诊断信息一样,支持诸如引用源代码、带下划线的范围、修复提示和诊断路径等详细信息。

例如,这是重放由 GCC 的 -fanalyzer 静态分析选项发出的 .sarif 文件的示例:

$ sarif-replay signal-warning.sarif
In function 'custom_logger':
signal.c:13:3: warning: call to ‘fprintf’ from within signal handler [-Wanalyzer-unsafe-call-within-signal-handler]
  13 |  fprintf(stderr, "LOG: %s", msg);
   |  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 'main': events 1-2
  │
  │  21 | int main(int argc, const char *argv)
  │   |   ^~~~
  │   |   |
  │   |   (1) entry to ‘main’
  │......
  │  25 |  signal(SIGINT, handler);
  │   |  ~~~~~~~~~~~~~~~~~~~~~~~
  │   |  |
  │   |   (2) registering ‘handler’ as signal handler
  │
 event 3
  │
  │GNU C17:
  │ (3): later on, when the signal is delivered to the process
  │
  └──> 'handler': events 4-5
      │
      │  16 | static void handler(int signum)
      │   |       ^~~~~~~
      │   |       |
      │   |       (4) entry to ‘handler’
      │  17 | {
      │  18 |  custom_logger("got signal");
      │   |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~
      │   |  |
      │   |   (5) calling ‘custom_logger’ from ‘handler’
      │
      └──> 'custom_logger': events 6-7
         │
         │  11 | void custom_logger(const char *msg)
         │   |   ^~~~~~~~~~~~~
         │   |   |
         │   |   (6) entry to ‘custom_logger’
         │  12 | {
         │  13 |  fprintf(stderr, "LOG: %s", msg);
         │   |  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         │   |  |
         │   |   (7) call to ‘fprintf’ from within signal handler
         │

4. 更容易过渡到C23

在编译 C 代码时,GCC 14 及更早版本默认为 -std=gnu17 (即 C 标准的 "C17" 版本,加上扩展)。 GCC 15 现在默认为 -std=gnu23(基于 C23),因此如果你的构建系统未指定要使用的 C 版本,你可能会遇到 C17 与 C23 的不兼容性

我尝试使用 GCC 15 重新构建 Fedora 的大部分子集,因此默认设置为 C23,以尝试消除可能出现的问题,在这样做时,我发现各种需要改进的诊断信息。 例如,booltruefalse 是 C23 中的关键字,因此我调整了在 旧代码 上发生的错误消息,例如:

typedef int bool;

以便你立即知道这是一个 C23 兼容性问题:

<source>:1:13: error: 'bool' cannot be defined via 'typedef'
  1 | typedef int bool;
   |       ^~~~
<source>:1:13: note: 'bool' is a keyword with '-std=c23' onwards

类似地,C17 和 C23 对没有参数的函数声明(例如 int foo();)的处理方式不同。 在 C23 中,它等效于 int foo(void);,而在早期版本的 C 中,这种声明与类型系统进行快速而松散的交互——本质上意味着,“我们不知道此函数采用多少个参数或它们的类型; 让我们希望你的代码是正确的!”。 在我的测试中,这导致旧代码上出现大量错误,例如在此示例中:

#include <signal.h>
void test()
{
 void (*handler)();
 handler = signal(SIGQUIT, SIG_IGN);
}

因此,我澄清了这些错误消息,以便它们告诉你函数的类型(或函数指针)并向你展示相关的 typedef 所在的位置:

<source>: In function 'test':
<source>:6:11: error: assignment to 'void (*)(void)' from incompatible pointer type '__sighandler_t' {aka 'void (*)(int)'} [-Wincompatible-pointer-types]
  6 |  handler = signal(SIGQUIT, SIG_IGN);
   |      ^
In file included from <source>:1:
/usr/include/signal.h:72:16: note: '__sighandler_t' declared here
  72 | typedef void (*__sighandler_t) (int);
   |        ^~~~~~~~~~~~~~

类似地,我澄清了 C 前端对于错误调用站点的错误消息:

struct p { int (*bar)(); };
  
void baz() {
  struct p q;
  q.bar(1);
}
t.c: In function 'baz':
t.c:7:5: error: too many arguments to function 'q.bar'; expected 0, have 1
  7 |   q.bar(1);
   |   ^   ~
t.c:2:15: note: declared here
  2 |     int (*bar)();
   |        ^~~

显示了预期参数计数与实际参数计数,在调用站点用下划线标记了第一个多余的参数,并显示了回调的相关字段声明。

5. 改进的配色方案

当 GCC 在合适的现代终端上在 stderr 上发出其文本消息时,它将使用颜色,使用一些在许多不同的终端主题中似乎效果很好的颜色——但是用于选择哪种颜色用于输出的每个方面的确切规则一直相当随意。

对于 GCC 15,我已经完成了 C 和 C++ 的错误,寻找源代码中的两个不同事物正在进行对比的地方,例如类型不匹配。 这些诊断现在使用颜色来以视觉方式突出显示和区分差异。

例如,此错误(图 1)显示由于两个操作数是结构(通过 typedef ST),而不是数字类型,因此错误地尝试使用二进制 + 运算符。

来自 GCC 15 的 C 类型错误的屏幕截图,显示了颜色的使用 图 1:GCC 15 中错误的新配色方案。

这里两个重要的事情是类型,因此 GCC 15 使用两种颜色来始终如一地突出显示不同的类型:在消息本身中、在引用的源中以及标签中。 在这里,左侧类型 (typedef struct s S;) 始终以绿色显示,右侧类型 (typedef struct t T;) 以深蓝色显示。 我希望这种方法能更好地将消息的文本与源代码联系起来,并使此类错误更容易理解。

6. libgdiagnostics

上面的图 1 展示了 GCC 诊断子系统的一些功能:颜色代码、引用源代码、标记源代码范围、修复提示、执行路径、SARIF 输出等。 以前,此代码隐藏在 GCC 内部,只能由 GCC 本身使用。

对于 GCC 15,我已经将此功能作为共享库提供给其他项目使用:libgdiagnostics。 有一个 C API,以及 C++ 和 Python 绑定。 例如,我能够使用 Python 绑定为我们的测试套件编写一个 "linting" 脚本,并且 "免费" 获得了源代码引用、颜色化和修复提示。 向此脚本添加输出为 SARIF 的能力将是一行代码,而不是编写大量 JSON 处理代码。

尝试 GCC 15

我们仍在修复错误,但我们希望 GCC 15 将在今晚晚些时候正式发布(作为 15.1)。 戴上我的 "下游" 帽子,我们已经在 Fedora 42 Beta 中使用预发布版本(GCC 15.0)。

最后,你可以使用优秀的 Compiler Explorer 站点 来使用新的编译器。 玩得开心!