有没有办法在 GCC 或 cl.exe 的预处理和编译之间插入一个步骤?

Is there a way to insert a step between pre-processing and compilation for GCC or cl.exe?

我正在尝试检测大型 C 代码库。可以使用 GCC 和 MS cl.exe 构建代码库。代码库包含数百万行。我正在尝试对其进行代码覆盖。因为运行时环境比较特殊,所以我必须用特殊的方式进行检测。

我已经编写了一个可以进行检测的转换工具。但是它不能处理宏扩展,头文件包含等。换句话说,它必须在预处理阶段之后工作

我可能没有足够的时间编写 C 预处理器。由于代码库是用 GCC/cl.exe 构建的,我想知道是否可以将我的转换步骤注入 GCC 或 cl.exe 编译过程。像这样:

GCC/cl.exe pre-process  ->  (My transformation) -> GCC/cl.exe compilation

这可能吗?

加 1

到目前为止,所有的答案都围绕着GCC。微软cl.exe怎么样?我尝试了 /P 选项,它将预处理结果发送到文件中。但结果包含很多行,如下所示:

#line 1306 "<some file path>"

我正在尝试解决它。​​

好的,我解决了,。指定 /P/EP 可以抑制 #line 指令。

添加 2

同时为 cl.exe 指定 "/P /EP" 的输出是没有 #line 指令的 *.i 文件。这是一个有效的 C 源文件。所以它可以直接送入cl.exe。我只是重命名原始 C 文件并使用 *.i 文件进行检测,然后进行构建过程。

(注意避免通过 /FI 包含一些头文件。这可能会导致一些重复定义错误。应该删除它们,因为它们的内容已经包含在 *.i 文件中。)

加 3

我只能使用 /P 开关。 #line 指令不会危害编译并且可以被 C 解析器识别。如果没有此类信息,就很难从经过检测的代码回溯到原始 c 源代码,正如 Jonathan Leffler 所指出的。

添加 4

检测并不容易。例如,根据 here(注意块 4),基于块的代码覆盖的块分离很棘手。

是的,有可能;不,这不是特别容易。

通常有一个单独的 C 预处理器,通常称为 cpp。您可以 运行 在原始源上使用适当的参数,然后检测输出,然后使用完整的编译器完成编译——除了第二个预处理器阶段没有什么重要的事情要做,除非您的检测添加额外的material 需要进一步预处理。

类似地,编译器有一些选项(通常是 -E and/or -P)到 运行 预处理器——你可以 post-process 输出然后将结果再次输入编译器。

例如,给定一个起始文件 file1.pp,您可以使用 GCC (gcc):

gcc -E file1.pp …other-options-as-needed… -o file1.i
transformer file1.i file1.c
gcc -c file1.c …more-options-as-needed…
gcc -o instrumented-program file1.o …other-object-files-and-options…

我假设您的程序名为 transformer,它采用任意输入文件名 (file1.i) 并写入任意输出文件 (file1.c)。当然,您可以根据需要向其中添加其他选项。

然后您在 makefile 中调整构建过程以自动处理此问题。在旧的 (POSIX) 规则下,您将向 .SUFFIXES 添加一个后缀 .pp,然后提供将 .pp 编译为 .o 的规则(也许是.c 文件,并且可能直接指向一个可执行文件)。大多数时候,您希望自动移动中间 file1.i 文件,但偶尔可能需要保留它。

考虑是否创建一个 'compiler' shell 脚本,从 .pp 文件中一次性生成经过检测的 .c 文件。请注意,处理此类程序可能会变得相当复杂——但如果您能保持简单,就会非常有帮助。这样一个脚本的一个优点是你可以让它在 Windows 和 Unix 上呈现相同的外部(命令行)界面,并且只安排内部处理 GCC vs Clang vs MSVC vs 任何其他编译器。

您可以从 .c 文件开始(而不是我假设的 .pp 文件),但是您需要一种系统的方式来处理名称——您不会破坏原始文件.c 文件。同样,使用 shell 脚本从 C 源代码创建经过检测的 .o(或 .obj)文件可能更容易 — 它可以处理文件命名的复杂性。

请记住,#line 指令允许您为 C 编译器指定行号和文件名;它旨在协助预处理文件(例如,Yacc/Bison 的输出包含 #line 指令,以识别代码在原始语法 (.y) 文件中的来源)。

当 GCC 预处理文件时,其输出包含 #line 指令的变体。当我预处理一个名为 alloc3d19.c 的文件时,它有前 4 行:

/* SO 4885-6272 */

#include <stdlib.h>
#include <stdio.h>

然后 GCC 生成输出开始:

# 1 "alloc3d19.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "alloc3d19.c"


# 1 "/usr/include/stdlib.h" 1 3 4
# 61 "/usr/include/stdlib.h" 3 4
# 1 "/usr/include/Availability.h" 1 3 4
# 202 "/usr/include/Availability.h" 3 4
# 1 "/opt/gcc/v7.3.0/lib/gcc/x86_64-apple-darwin17.4.0/7.3.0/include-fixed/AvailabilityInternal.h" 1 3 4
# 203 "/usr/include/Availability.h" 2 3 4
# 62 "/usr/include/stdlib.h" 2 3 4

# 之后没有 line 但它的意思基本相同,除了文件名后面的数字。 (两个空行是源代码中的注释和空行;直到输出文件的第 1638 行才到达 stdio.h。73 行源代码,输出为 2091 行,其中292 是 #line 指令。)你的转换器需要处理——可能是忽略——这样的行。您可能会忽略它们,但是 back-tracking 很难找到源代码。您可能需要添加一些 #line 指令来伪装代码的添加位置。您可能需要临时更改文件名,以便与您的检测相关的任何消息与与原始源代码相关的消息分开。

GCC (specifically) you might consider writing your own GCC plugin (which will transform not textual files, but internal GCC representations). You might also consider libclang。但这并不容易(您可能需要花费数周或数月的时间)。

请注意:GCC 是一个复杂的软件(大约一千万行代码),您需要做大量工作才能了解其内部表示形式 (Generic/TREE & GIMPLE)。此外,插件 API 并不完全稳定,因此从 GCC 7 到 GCC 8(将于 spring 2018 年发布)时,您可能需要更改插件代码。

我在我的旧 GCC MELT documentation 页面上收集并写了一些 material(有点旧)关于 GCC 插件。

另一种可能是使用一些 other 预处理器(例如 GPP or m4) and generate some instrumented C or C++ code from some other files. Be aware that generating C or C++ code is a common habit (look into Qt moc, into bison ...)。

无论您采用何种方法,通常都不会很容易(除非您的特定代码库遵循一些一致的约定)。在某些情况下(只有十万行的小代码库)手动转换代码可能更简单。

顺便说一句,如果您使用编译器生成预处理文件,您可以(轻松地)删除发出的 #line# 行,例如一些 grep -v '^#'(但您可能还想保留它们 and/or 解析它们)。

请注意,自动检测代码比您想的要难....(主要问题是不要忽略 # 行)。