从ANSI C代码中获取控制流图

16
我正在构建一个用于测试 ANSI C 应用程序的工具。只需加载代码,查看控制流图,运行测试,标记所有被触及的顶点。我试图通过解析代码自己构建控制流图。不幸的是,如果代码嵌套,它会变得混乱。GCC 提供了从编译代码获取控制流图的能力。我可以为其输出编写解析器,但我需要行号来设置断点。在使用 -fdump-tree-cfg-fdump-tree-vcg 输出控制流图时,是否有办法获取行号?
3个回答

21

对于C程序的控制流程图,您可以查看现有的Python解析器,如:

调用图是与控制流程图密切相关的构造。 有多种方法可用于为C代码创建调用图(函数依赖关系)。 这可能对控制流程图生成的进展有所帮助。 创建C中依赖关系图的方法:

  • 使用cflow

  • cflow + pycflow2dot + dot (GPL,BSD) cflow非常强大,因为它可以处理无法编译的代码,例如缺少包含的代码。如果经常使用预处理器指令,则可能需要--cpp选项来预处理代码。

  • cflow + cflow2dot + dot(GPL v2,GPL v3,Eclipse Public License(EPL)v1)(请注意,在其工作之前,cflow2dot需要一些路径修正)

  • cflow + cflow2dot.bash (GPL v2, ?)

  • cflow + cflow2vcg (GPL v2, GPL v2)

  • 增强版cflow(GPL v2),可排除图中的符号列表

  • 使用cscope

  • cscope (BSD)

  • cscope + callgraphviz +dot +xdot

  • cscope +vim CCTree(C调用树浏览器)

  • cscope + ccglue

  • cscope +CodeQuery用于C、C++、Python和Java

  • cscope + Python html生产工具

  • cscope + calltree.sh

  • 以下工具需要可编译源代码,因为它们依赖于gcc的输出:

    • CodeViz (GPL v2)(弱点:需要可编译源代码,因为它使用gcc转储cdepn文件)
    • gcc+egypt+dot(GPL v*,Perl = GPL |艺术许可证,EPL v1)(egypt使用gcc生成RTL,因此对于任何有缺陷的源代码或者即使你只想关注一个大型项目中的单个文件,都会失败。因此,与更健壮的基于cflow的工具链相比,它不是非常有用。请注意,默认情况下,egypt具有排除图中的库调用的良好支持,使其更清晰)

    此外,可以使用crowfood创建C/C++文件依赖关系图。


    调用图并不是我所需要的。我需要在代码中可视化分支结构。我需要向用户展示代码中所有的循环和决策点。我自己制作了一个用于VCG的解析器,但我会检查你发布的工具。 - Eloar

    8

    我进行了更多的研究,发现获取节点行号并不困难。只需在以下选项之一中添加 lineno 选项即可获取。因此使用 -fdump-tree-cfg-lineno-fdump-tree-vcg-lineno。我花了一些时间检查这些数字是否可靠。在VCG格式的图形中,每个节点的标签包含两个数字。这些数字是表示该节点所代表的代码部分的开始和结束行号。


    2

    动态分析方法

    在这个答案中,我描述了几种动态分析方法。

    动态方法实际上运行程序以确定调用图。

    与动态方法相反的是静态方法,它们试图仅通过源代码而不运行程序来确定调用图。

    动态方法的优点:

    • 捕获函数指针和虚拟C ++调用。这些在任何非平凡软件中都存在大量。

    动态方法的缺点:

    • 您必须运行程序,这可能会很慢,或者需要您没有的设置,例如交叉编译
    • 只有实际调用的函数才会显示。例如,某些函数可以根据命令行参数调用或不调用。

    KcacheGrind

    https://kcachegrind.github.io/html/Home.html

    测试程序:

    int f2(int i) { return i + 2; }
    int f1(int i) { return f2(2) + i + 1; }
    int f0(int i) { return f1(1) + f2(2); }
    int pointed(int i) { return i; }
    int not_called(int i) { return 0; }
    
    int main(int argc, char **argv) {
        int (*f)(int);
        f0(1);
        f1(1);
        f = pointed;
        if (argc == 1)
            f(1);
        if (argc == 2)
            not_called(1);
        return 0;
    }
    

    使用方法:

    sudo apt-get install -y kcachegrind valgrind
    
    # Compile the program as usual, no special flags.
    gcc -ggdb3 -O0 -o main -std=c99 main.c
    
    # Generate a callgrind.out.<PID> file.
    valgrind --tool=callgrind ./main
    
    # Open a GUI tool to visualize callgrind data.
    kcachegrind callgrind.out.1234
    

    您现在位于一个包含许多有趣性能数据的GUI程序中。在右下角,选择“调用图”选项卡。这将显示一个交互式的调用图,当您点击函数时,它与其他窗口中的性能指标相关联。要导出图形,请右键单击它并选择“导出图形”。导出的PNG文件如下所示:链接。从中我们可以看到:
    - 根节点是`_start`,它是实际的ELF入口点,并包含glibc初始化样板。 - `f0`、`f1`和`f2`按预期相互调用。 - `pointed`也被显示出来,即使我们使用了函数指针来调用它。如果我们传递了命令行参数,它可能不会被调用。 - `not_called`没有被显示出来,因为在运行时它没有被调用,因为我们没有传递额外的命令行参数。
    `valgrind`的好处是它不需要任何特殊的编译选项。因此,即使您只有可执行文件而没有源代码,您也可以使用它。`valgrind`通过运行代码通过轻量级“虚拟机”来实现这一点。在Ubuntu 18.04上测试过。`gcc -finstrument-functions` + etrace。`-finstrument-functions`添加回调,etrace解析ELF文件并实现所有回调。然而,我无法让它正常工作。声称的输出格式如下:
    \-- main
    |   \-- Crumble_make_apple_crumble
    |   |   \-- Crumble_buy_stuff
    |   |   |   \-- Crumble_buy
    |   |   |   \-- Crumble_buy
    |   |   |   \-- Crumble_buy
    |   |   |   \-- Crumble_buy
    |   |   |   \-- Crumble_buy
    |   |   \-- Crumble_prepare_apples
    |   |   |   \-- Crumble_skin_and_dice
    |   |   \-- Crumble_mix
    |   |   \-- Crumble_finalize
    |   |   |   \-- Crumble_put
    |   |   |   \-- Crumble_put
    |   |   \-- Crumble_cook
    |   |   |   \-- Crumble_put
    |   |   |   \-- Crumble_bake
    

    除了特定的硬件跟踪支持之外,这可能是最有效的方法,但缺点是您必须重新编译代码。


    1
    原问题陈述了我拥有源代码,并且完全可以重新编译,因为它是用于源代码动态分析的工具。 - Eloar

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接