我正在尝试设置一个大的C代码库。代码库可以使用GCC和MS cl.exe构建。代码库包含数百万行。我正在尝试将其用于代码覆盖。因为运行时环境很特殊,所以我必须以特殊的方式进行检测。
我编写了一个可以执行检测的转换工具。但是它无法处理宏扩展,头文件包括等等。换句话说,它必须在预处理阶段之后工作。
我可能没有足够的时间来编写C预处理器。由于代码库是使用GCC / cl.exe构建的,我想知道是否可以将我的转换步骤注入GCC或cl.exe编译过程。像这样:
GCC/cl.exe pre-process -> (My transformation) -> GCC/cl.exe compilation
那可能吗?
到目前为止,所有答案都围绕着海湾合作委员会。怎么样的Microsoft cl.exe?我尝试了/P选项,它会将预处理结果发送到文件。但结果包含许多行,如下所示:
#line 1306 "<some file path>"
我想解决它。
好的,我解决了。同时指定/P
和/EP
可以抑制#line
指令。
为"/P /EP"
指定两个cl.exe
的输出是没有*.i
指令的#line
文件。这是一个有效的C源文件。所以它可以直接输入cl.exe
。我只是重命名原始的C文件并使用*.i
文件进行检测,然后使用构建过程。
(注意避免通过/FI
包含一些头文件。这可能会导致一些重复的定义错误。应该删除它们,因为它们的内容已经包含在*.i
文件中。)
我可以使用/P
开关。 #line
指令不会危害编译,并且可以被C解析器识别。如Jonathan Leffler所指出的那样,如果没有这些信息,很难从仪表代码回溯到原始的c
源代码。
仪器仪表并不容易。例如,根据here,基于块的代码覆盖的块分离是棘手的(注意块4)。
对的,这是可能的;不,这不是特别容易。
通常有一个单独的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
指令来伪装添加代码的位置。您可能需要临时更改文件名,以便与您的检测相关的任何消息都与原始源代码相关。
使用GCC(具体而言),您可以考虑编写自己的GCC plugin(它不会转换文本文件,而是内部GCC表示)。你也可以考虑libclang。但这并不容易(你可以花上几周或几个月的工作)。
考虑到:GCC是一个复杂的软件(大约一千万行代码),你需要做大量的工作来学习它的内部表示(Generic/TREE和GIMPLE)。此外,插件API并不完全稳定,因此您可能需要在从GCC 7转到GCC 8(将于2018年春季发布)时更改插件代码。
我在旧的GCC MELT documentation页面上收集并写了一些关于GCC插件的材料(略显陈旧)。
另一种可能性是使用一些其他预处理器(可能是GPP或m4)并从其他一些文件生成一些检测的C或C ++代码。请注意,生成C或C ++代码是一种常见习惯(请查看Qt moc,以bison为例)。
无论你采取什么方法,一般来说都不容易(除非你的特定代码库遵循一些一致的约定)。在某些情况下(只有十万行的小代码库)手动转换代码可能更简单。
顺便说一句,如果您使用编译器生成预处理文件,您可以(轻松地)删除发射的#line
或#
行,例如,一些grep -v '^#'
(但你可能也想保留它们和/或解析它们)。
请注意,自动检测代码比你想象的更难....(主要问题不是忽略#
线)。