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

问题描述 投票:0回答:2

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

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

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

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

那可能吗?

添加1

到目前为止,所有答案都围绕着海湾合作委员会。怎么样的Microsoft cl.exe?我尝试了/P选项,它会将预处理结果发送到文件。但结果包含许多行,如下所示:

#line 1306 "<some file path>"

我想解决它。

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

添加2

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

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

添加3

我可以使用/P开关。 #line指令不会危害编译,并且可以被C解析器识别。如Jonathan Leffler所指出的那样,如果没有这些信息,很难从仪表代码回溯到原始的c源代码。

添加4

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

c gcc cl
2个回答
6
投票

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

通常有一个单独的C预处理器,通常称为cpp。您可以在原始源上使用适当的参数运行它,然后检测输出,然后使用完整的编译器完成编译 - 除非您的仪器添加了需要进一步的额外材料,否则第二个预处理器阶段没有任何重要意义。预处理。

类似地,编译器只有选项(通常是-E和/或-P)来运行预处理器 - 您可以对其进行后处理并再次将结果提供给编译器。

例如,给定一个起始文件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)规则下,您可以将后缀.pp添加到.SUFFIXES,然后提供规则来将.pp编译为.o(可能还有.c文件,可能直接指向可执行文件)。您希望在大多数情况下自动移动中间file1.i文件,但您可能需要偶尔保留它。

考虑是否创建一个'编译器'shell脚本,一下子从.c文件生成检测的.pp文件。请注意,处理此类程序可能会变得非常复杂 - 但如果您可以保持简单,那么它可能会非常有用。这样一个脚本的一个优点是你可以在Windows和Unix上使它呈现相同的外部(命令行)接口,并且只安排内部处理GCC vs Clang vs MSVC与任何其他编译器。

你可以从一个.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指令。)你的变压器需要处理 - 可能忽略 - 这样的线路。你可能会省略它们,但是后来跟踪源很难。您可能需要添加一些#line指令来伪装添加代码的位置。您可能需要临时更改文件名,以便与您的检测相关的任何消息都与原始源代码相关。


2
投票

使用GCC(具体而言),您可以考虑编写自己的GCC plugin(它不会转换文本文件,而是内部GCC表示)。你也可以考虑libclang。但这并不容易(你可以花上几周或几个月的工作)。

考虑到:GCC是一个复杂的软件(大约一千万行代码),你需要做大量的工作来学习它的内部表示(Generic/TREEGIMPLE)。此外,插件API并不完全稳定,因此您可能需要在从GCC 7转到GCC 8(将于2018年春季发布)时更改插件代码。

我在旧的GCC MELT documentation页面上收集并写了一些关于GCC插件的材料(略显陈旧)。

另一种可能性是使用一些其他预处理器(可能是GPPm4)并从其他一些文件生成一些检测的C或C ++代码。请注意,生成C或C ++代码是一种常见习惯(请查看Qt moc,以bison为例)。

无论你采取什么方法,一般来说都不容易(除非你的特定代码库遵循一些一致的约定)。在某些情况下(只有十万行的小代码库)手动转换代码可能更简单。

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

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

© www.soinside.com 2019 - 2024. All rights reserved.