使用带有多个 cpp 文件的 clang 的 -dot-callgraph 和一个 sed 命令生成调用图

Generating a call graph with clang's -dot-callgraph with multiple cpp files, and a sed command

我试过Doxygen,但是有点慢,而且生成了很多不相关的单个点文件,所以我正在追求clang方式生成调用图。

这个回答 发布了这个命令:

$ clang++ -S -emit-llvm main1.cpp -o - | opt -analyze -dot-callgraph
$ dot -Tpng -ocallgraph.png callgraph.dot

然后

$ clang++ -S -emit-llvm main1.cpp -o - |
   opt -analyze -std-link-opts -dot-callgraph
$ cat callgraph.dot | 
   c++filt | 
   sed 's,>,\>,g; s,-\>,->,g; s,<,\<,g' | 
   gawk '/external node/{id=}  != id' | 
   dot -Tpng -ocallgraph.png    

我设法获取了 .dot 文件并用 c++filt 对它们进行了整理,但是这些符号是由很多“噪音”组成的,例如:

"{__gnu_cxx::new_allocator<std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >::deallocate(std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >*, unsigned long)}"
"{void std::allocator_traits<std::allocator<std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > >::destroy<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >(std::allocator<std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >&, std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >*)}"
"{void __gnu_cxx::new_allocator<std::_Rb_tree_node<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > >::destroy<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > >(std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >*)}"

doxygen 如何设法“简化”这些符号?除了STLfilt还有别的吗?

如何正确过滤与我的代码无关的符号,例如容器的分配器、构造函数?这个 sed 和 gawk 命令试图做什么?我试过了,但我真的看不出他们做了什么。

我设法做到了,但这并不简单,而且 clang 并没有真正提供过滤“嘈杂”符号的选项。

重要的是要知道 graphviz 不能神奇地优化图表的布局,因此最好为每个目标文件生成一个图表。

这是我想出的 python 过滤器,可以去除很多噪音。有很多东西不属于 std::,比如 sfml 或 nlohmann(一个重型模板化 json 库,会生成很多符号)。我没有使用正则表达式,因为它并不真正相关。这些过滤器应该根据您的代码、您使用的库以及您最终使用的标准库的哪些部分而有很大差异,因为“它一直都是模板”。

def filtered(s):
    return not (
        s.startswith("& std::__get_helper")
        or s.startswith("__cx")
        or s.startswith("__gnu_cxx::")
        or s.startswith("bool nlohmann::")
        or s.startswith("bool std::")
        or s.startswith("decltype")
        or s.startswith("int* std::")
        or s.startswith("int** std::")
        or s.startswith("nlohmann::")
        or s.startswith("sf::")
        or s.startswith("std::")
        or s.startswith("void __gnu_cxx::")
        or s.startswith("void format<")
        or s.startswith("void nlohmann::")
        or s.startswith("void std::")
        or 'std::remove_reference' in s
        or 'nlohmann::' in s
        or '__gnu_cxx::' in s
        or 'std::__copy_move' in s
        or 'std::__niter' in s
        or 'std::__miter' in s
        or 'std::__get_helper' in s
        or 'std::__uninitialized' in s
        or 'sf::operator' in s
        or s == 'sf::Vector2<float>::Vector2()'
        or s == 'sf::Vector2<float>::Vector2(float, float)'
        or s == 'sf::Vector2<float>::Vector2<int>(sf::Vector2<int> const&)'
        or s == 'sf::Vector2<int>::Vector2()'
        or s == 'sf::Vector2<int>::Vector2(int, int)'
        )

其次,我还删除了未调用的符号,以及目标文件中不存在的对节点的调用。具体来说,我只是在生成的 DOT 文件中交叉检查节点和边

# filtering symbols I don't want
nodes_filtered = [(name, label) for (name, label) in nodes if filtered(label)]

# using a set() for further cross checking
nodes_filt_ids = set([name for (name, label) in nodes_filtered])

# we only keep edges with symbols (source and destination) BOTH present in the list of nodes
edge_filtered = [(a,b) for (a,b) in edges if a in nodes_filt_ids and b in nodes_filt_ids]

# we then build a set() of all the nodes from the list of edges
nodes_from_filtered_edges =  set(sum(edge_filtered, ()))

# we then REFILTER AGAIN from the list of filtered edges
nodes_refiltered = [(name, label) for (name, label)
    in nodes_filtered if name in nodes_from_filtered_edges]

第三,我使用了一个makefile来级联步骤。

    object_file.ll: object_file.cpp 2dhelpers.h.gch
        $(CC) -S -emit-llvm $< -o $@ $(args) $(inclflags)
    object_file.ll.callgraph.dot: object_file.ll
        opt $< -std-link-opts -dot-callgraph
    object_file.cxxfilt.dot: object_file.ll.callgraph.dot
        cat $< | llvm-cxxfilt > $@
    object_file.cleaned.dot: object_file.cxxfilt.dot
        python3 dot-parse.py $^
    object_file.final.svg: object_file.cleaned.dot
        dot -Tsvg $^ -o $@

重要的是要注意 llvm-cxxfilt 未损坏的符号比 c++filt 好一点,尽管它在我的情况下并不完美。

opt 写的 DOT 文件解析起来很简单,但你仍然可以使用 pydot,虽然我发现它有点慢,如果你有很多符号(我有 2500 个符号,5000 个调用,减少到 116 和 151)

如果你真的想“组合”多个目标文件并使用一个图形,你绝对可以,使用 llvm-link

# linking llvm IR with this magic
all_objects.ll: file1.ll file2.ll file3.ll file4.ll file5.ll
    llvm-link -S $^ -o all_objects.ll

只需将命令 #2 到 #5 应用于生成的 .ll 文件

在我的例子中,单个大图不容易阅读,因为多条边会交叉并形成“河流”。添加颜色并没有多大帮助。