静态分析C语言生成函数调用关系的利器——GCC

在《静态分析C语言生成函数调用关系的利器——cally和egypt》中我们介绍了如何使用GCC生成RTL文件,然后再借助cally和egypt来分析出调用关系的方法。GCC自身有命令可以生成代码内部的调用关系,即-fcallgraph-info参数。

Makes the compiler output callgraph information for the program, on a per-object-file basis. The information is generated in the common VCG format.

代码语言:javascript
复制
gcc some.c -fcallgraph-info

它会生成后缀是ci的VCG格式文件。然后我们使用graph-easy将其转换为dot格式,最后使用graphviz将其绘制出来。

我们还是以libevent的为例。

准备工作

graph-easy 用于将vcg文件转换为dot格式

代码语言:javascript
复制
sudo apt install libgraph-easy-perl

因为脚本是Python写的,且会依赖第三方库,于是会使用《管理Python虚拟环境的脚本》介绍的工具构建一个虚拟环境并安装相应依赖。

代码语言:javascript
复制
source env.sh init
soure env.sh enter
source env.sh install pydot

GCC生成单文件调用关系VCG

代码语言:javascript
复制
gcc `find . -regextype posix-extended -regex '^./[^/]*\.c$' ! -name 'wepoll.c' ! -name 'win32select.c' ! -name 'evthread_win32.c' ! -name 'buffer_iocp.c' ! -name 'bufferevent_async.c' ! -name 'arc4random.c' ! -name 'event_iocp.c' ! -name 'bufferevent_mbedtls.c'` \
 ./test/test-time.c \
 -I./build/include/ -I./include -I./ \
 -L./build/lib/ -lcrypto -lssl \
 -DLITTLE_ENDIAN -D__clang__ \
 -UD_WIN32 -UDMBEDTLS_SSL_RENEGOTIATION \
 -fcallgraph-info

将VCG转为Dot

代码语言:javascript
复制
graph-easy a-test-time.ci --as_dot > a-test-time.dot

绘制图片

代码语言:javascript
复制
dot -Grankdir=LR -T png a-test-time.dot -o test_time.png  

绘制全景图

因为GCC生成VCG文件只是针对单个文件的,不能构成全景图。这个时候就需要我们自己手撸一点代码,让这些信息合并。

代码语言:javascript
复制
import pydot

class CallgraphInfoCombiner(object):
def init(self, dot_folder, function_name, output_file) -> None:
self._dot_folder = dot_folder
self._funciont_name = function_name
self._output_file = output_file
self._callee = dict()
self._graph = pydot.Dot("callgraph-info-combiner", graph_type="graph", bgcolor="white")
pass

def analyze(self, include_private=False):
    for file in os.listdir(self._dot_folder):
        self._read_dot(self._dot_folder + "/" + file)
        
    nodes_in_graph = set()
    if self._funciont_name in self._callee:
        self._add_node_and_edge(self._funciont_name, nodes_in_graph, include_private)
    self._graph.write_dot(self._output_file + ".dot")
    self._graph.write_png(self._output_file + ".png")
    
def _add_node_and_edge(self, node_name, nodes_in_graph, include_private=False):
    if include_private and node_name.startswith('"') and node_name.endswith('"'):
        return
    
    if node_name not in nodes_in_graph:
        print("add node: " + node_name)
        self._graph.add_node(pydot.Node(node_name))
        nodes_in_graph.add(node_name)
        
    if node_name in self._callee:
        for callee in self._callee[node_name]:
            if include_private == False and callee.startswith('"') and callee.endswith('"'):
                continue
    
            if callee not in nodes_in_graph:
                self._add_node_and_edge(callee, nodes_in_graph, include_private)
            self._graph.add_edge(pydot.Edge(node_name, callee))
            print("add edge: " + node_name + " -> " + callee)
    

def _read_dot(self, dot_file):
    graphs = pydot.graph_from_dot_file(dot_file)
    for graph in graphs:                    
        for edge in graph.get_edges():
            if edge.get_source() in self._callee:
                self._callee[edge.get_source()].add(edge.get_destination())
            else:
                self._callee[edge.get_source()] = {edge.get_destination()}</code></pre></div></div><p>上面的代码会分析DOT文件,所以在使用前需要将VCG转换成DOT文件。</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">import os

import sys
import subprocess

class Vcg2Dot(object):
def init(self, vcg_file, dot_file):
self.vcg_file = vcg_file
self.dot_file = dot_file

def vcg_to_dot(self):
    print(&#34;graph-easy --input=&#34; + self.vcg_file + &#34; -as=dot --output=&#34; + self.dot_file)
    subprocess.run(&#34;graph-easy --input=&#34; + self.vcg_file + &#34; -as=dot --output=&#34; + self.dot_file, shell=True)

class VcgFiles2Dot(object):
def init(self, vcg_folder, dot_folder):
self.vcg_folder = vcg_folder
self.dot_folder = dot_folder

def vcg_to_dot(self):
    if not os.path.exists(self.dot_folder):
        os.makedirs(self.dot_folder)
    
    for file in os.listdir(self.vcg_folder):
        vcg_to_dot = Vcg2Dot(self.vcg_folder + file, self.dot_folder + file + &#34;.dot&#34;)
        vcg_to_dot.vcg_to_dot()</code></pre></div></div><p>然后我们只要针对这个脚本传vcg文件目录、起始函数和输出的文件名,即可整合出调用关系。</p><div class="rno-markdown-code"><div class="rno-markdown-code-toolbar"><div class="rno-markdown-code-toolbar-info"><div class="rno-markdown-code-toolbar-item is-type"><span class="is-m-hidden">代码语言:</span>javascript</div></div><div class="rno-markdown-code-toolbar-opt"><div class="rno-markdown-code-toolbar-copy"><i class="icon-copy"></i><span class="is-m-hidden">复制</span></div></div></div><div class="developer-code-block"><pre class="prism-token token line-numbers language-javascript"><code class="language-javascript" style="margin-left:0">python callgraph-info-combiner.py ./sample/ci/ main libevent</code></pre></div></div><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:auto"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1722782145759934000.png" /></div></div></div></figure><p> 局部图如下</p><figure class=""><div class="rno-markdown-img-url" style="text-align:center"><div class="rno-markdown-img-url-inner" style="width:auto"><div style="width:100%"><img src="https://cdn.static.attains.cn/app/developer-bbs/upload/1722782146651905000.png" /></div></div></div></figure><h3 id="571658" name="%E4%BB%A3%E7%A0%81">代码</h3><p>https://github.com/f304646673/tools/tree/main/callgraph-info-combiner</p><h3 id="571660" name="%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99">参考资料</h3><ul class="ul-level-0"><li>https://gcc.gnu.org/onlinedocs/gcc/Developer-Options.html</li><li>https://pypi.org/project/pydot/</li></ul>