9

(我发现这个问题相似但不重复: 如何检查 C 编程语言中的头文件的有效性

我有一个函数实现,以及一个头文件中的不匹配原型(同名,不同类型)。头文件包含在使用函数的 C 文件中,但不包含在定义函数的文件中。

这是一个最小的测试用例:

标头.h:

void foo(int bar);

文件1.c:

#include "header.h"
int main (int argc, char * argv[])
{
    int x = 1;
    foo(x);
    return 0;
}

文件 2.c:

#include <stdio.h>

typedef struct {
    int x;
    int y;
} t_struct;

void foo (t_struct *p_bar)
{
    printf("%x %x\n", p_bar->x, p_bar->y);
}

我可以用 VS 2010 编译它,没有错误或警告,但不出所料,当我运行它时它会出现段错误。

  • 编译器很好(我理解)
  • 链接器没有捕捉到它(这让我有点惊讶)
  • 静态分析工具(Coverity)没有捕捉到它(这让我很惊讶)。

我怎样才能捕捉到这些类型的错误?

[编辑:我意识到如果我也#include "header.h"加入file2.c,编译器会抱怨。但是我有一个庞大的代码库,并不总是可能或不适合保证函数原型的所有头文件都包含在实现文件中。]

4

6 回答 6

5

-Werror-implicit-function-declaration-Wmissing-prototypes或您支持的编译器之一上的等效项。那么如果声明没有在全局定义之前,它将出错或抱怨。

以某种形式的严格 C99 模式编译程序也应该生成这些消息。GCC、ICC 和 Clang 都支持此功能(不确定 MS 的 C 编译器及其当前状态,因为 VS 2005 或 2008 是我用于 C 的最新版本)。

于 2013-02-28T14:35:47.037 回答
5

Have the same header file included in both file1.c and file2.c. This will pretty much prevent a conflicting prototype.

Otherwise, such a mistake cannot be detected by the compiler because the source code of the function is not visible to the compiler when it compiles file1.c. Rather, it can only trust the signature that has been given.

At least theoretically, the linker could be able to detect such a mismatch if additional metadata is stored in the object files, but I am not aware if this is practically possible.

于 2013-02-28T14:18:43.023 回答
2

您可以使用http://frama-c.com上提供的 Frama-C 静态分析平台。

在你的例子中,你会得到:

$ frama-c 1.c 2.c
[kernel] preprocessing with "gcc -C -E -I.  1.c"
[kernel] preprocessing with "gcc -C -E -I.  2.c"
[kernel] user error: Incompatible declaration for foo:
                     different type constructors: int vs. t_struct *
                     First declaration was at  header.h:1
                     Current declaration is at 2.c:8
[kernel] Frama-C aborted: invalid user input.

希望这可以帮助!

于 2013-03-30T17:39:39.867 回答
1

每个文件中定义的每个(非静态)函数都foo.c应该在相应的foo.h文件中有一个原型,并且foo.c应该有#include "foo.h". (main是唯一的例外。)foo.h不应包含任何未在foo.c.

每个函数都应该原型化一次

如果文件不包含任何原型,您可以拥有.h没有相应文件的文件。.c唯一.c没有对应.h文件的文件应该是包含main.

您已经知道这一点,而您的问题是您有一个庞大的代码库,但没有遵循此规则。

那么你如何从这里到那里呢?这就是我可能会这样做的方式。

第 1 步(需要一次通过您的代码库):

  • 对于每个文件,如果它不存在,则foo.c创建一个文件。在顶部附近foo.h添加。如果您对文件的存放位置有约定(在同一目录中或与目录并行),请遵循它;如果没有,请尝试引入这样的约定)。"#include "foo.h"foo.c.h.cincludesrc
  • 对于 中的每个函数,如果它不存在,则将foo.c其原型复制到。foo.h使用复制和粘贴来确保一切保持一致。(参数名称在原型中是可选的,在定义中是强制性的;我建议在这两个地方都保留名称。)
  • 进行完整构建并修复出现的任何问题。

这不会抓住你所有的问题。对于某些功能,您仍然可以有多个原型。但是您会发现任何情况,即两个标头对于同一函数的原型不一致,并且两个标头都包含在同一个翻译单元中。

一旦一切都构建得很干净,您应该拥有一个至少与您开始使用的系统一样正确的系统。

第2步:

  • 对于每个文件foo.h,删除任何未定义的函数原型foo.c.
  • 进行完整构建并修复出现的任何问题。如果bar.c调用定义在 中的函数foo.c,则bar.c需要一个#include "foo.h".

对于这两个步骤,“修复出现的任何问题”阶段可能会很漫长且乏味。

如果您不能一次完成所有这些,您可能可以逐步完成很多。从一个或几个.c文件开始,清理它们的.h文件,并删除在其他地方声明的任何额外原型。

每当您发现调用使用了不正确的原型的情况时,请尝试找出执行该调用的情况,以及它如何导致您的应用程序行为异常。创建一个错误报告并向你的回归测试套件添加一个测试(你有一个,对吧?)。您可以向管理层证明,由于您所做的所有工作,测试现在通过了;你真的不只是在胡闹。

可以解析 C 的自动化工具可能很有用。Ira Baxter 有一些建议。ctags也可能有用。根据您的代码格式,您可能可以将一些不需要完整 C 解析器的工具放在一起。例如,您可以使用grepsedperlfoo.c文件中提取函数定义列表,然后手动编辑列表以删除误报。

于 2013-02-28T15:58:36.080 回答
1

看起来这对于 C 编译器是不可能的,因为它的函数名称映射到符号对象名称的方式(直接,不考虑实际签名)。

但这在 C++ 中是可能的,因为它使用依赖于函数签名的名称修饰。所以在 C++ 中void foo(int)void foo(t_struct*)链接阶段会有不同的名称,链接器会引发错误。

当然,将庞大的 C 代码库依次切换到 C++ 并不容易。但是您可以使用一些相对简单的解决方法 - 例如,将单个.cpp文件添加到您的项目中并将所有 C 文件包含在其中(实际上是使用一些脚本生成它)。

TestCpp.cpp以您的示例和我添加到项目中的 VS2010 为例:

#include "stdafx.h"

namespace xxx
{
#include "File1.c"
#include "File2.c"
}

结果是链接器错误 LNK2019:

TestCpp.obj : error LNK2019: unresolved external symbol "void __cdecl xxx::foo(int)" (?foo@xxx@@YAXH@Z) referenced in function "int __cdecl xxx::main(int,char * * const)" (?main@xxx@@YAHHQAPAD@Z)
W:\TestProjects\GenericTest\Debug\GenericTest.exe : fatal error LNK1120: 1 unresolved externals

当然,对于庞大的代码库来说,这并不容易,可能还有其他问题导致编译错误,如果不更改代码库就无法修复。.cpp您可以通过有条件地保护文件内容#ifdef并仅用于定期检查而不是用于常规构建来部分缓解它。

于 2013-02-28T16:03:24.823 回答
0

很明显(“我有一个庞大的代码库”)你不能手动完成。

您需要的是一个自动化工具,它可以在编译器看到它们时读取您的源文件,收集所有函数原型和定义,并验证所有定义/原型是否匹配。我怀疑你会发现这样的工具。

当然,这种匹配多检查签名,这需要编译器前端之类的东西来比较签名。

考虑

     typedef int T;
     void foo(T x);

在一个编译单元中,并且

      typedef float T;
      void foo(T x);

在另一个。您不能只比较签名“行”是否相等;你需要一些可以在检查时解析类型的东西。

如果您使用的是 C 的 GCC 方言,GCCXML 可能会有所帮助;它从源文件中提取顶级声明作为 XML 块。不过,我不知道它是否会解析 typedef。您显然必须构建(相当多的)支持以在中心位置(数据库)收集定义并进行比较。比较等效的 XML 文档至少相当简单,如果它们以常规方式格式化,则相当容易。这可能是您最容易的选择。

如果这不起作用,您需要具有可以自定义的完整 C 前端的东西。众所周知,GCC 是可用的,并且难以定制。Clang 可用,并且可能会为此服务,但 AFAIK 仅适用于 GCC 方言。

我们的 DMS Software Reengineering Toolkit 具有适用于许多 C 方言(GCC、MS、GreenHills 等)的 C 前端(具有完整的预处理能力),并使用完整的类型信息构建符号表。使用 DMS,您可能能够(取决于应用程序的实际规模)简单地处理所有编译单元,并仅为每个编译单元构建符号表。检查符号表条目是否“匹配”(根据编译器规则兼容,包括使用等效的 typedef)是内置在 C 前端中的;所有需要做的就是编排读取,并在各个编译单元的全局范围内为所有符号表条目调用匹配逻辑。

无论您使用 GCC/Clang/DMS 执行此操作,拼凑一个自定义工具都是相当多的工作。因此,与构建这种定制工具的精力相比,您已经决定了减少惊喜的重要性。

于 2013-02-28T15:24:57.160 回答