在《静态分析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.
gcc some.c -fcallgraph-info
它会生成后缀是ci的VCG格式文件。然后我们使用graph-easy将其转换为dot格式,最后使用graphviz将其绘制出来。
我们还是以libevent的为例。
准备工作
graph-easy 用于将vcg文件转换为dot格式
sudo apt install libgraph-easy-perl
因为脚本是Python写的,且会依赖第三方库,于是会使用《管理Python虚拟环境的脚本》介绍的工具构建一个虚拟环境并安装相应依赖。
source env.sh init
soure env.sh enter
source env.sh install pydot
GCC生成单文件调用关系VCG
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
graph-easy a-test-time.ci --as_dot > a-test-time.dot
绘制图片
dot -Grankdir=LR -T png a-test-time.dot -o test_time.png
绘制全景图
因为GCC生成VCG文件只是针对单个文件的,不能构成全景图。这个时候就需要我们自己手撸一点代码,让这些信息合并。
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("graph-easy --input=" + self.vcg_file + " -as=dot --output=" + self.dot_file)
subprocess.run("graph-easy --input=" + self.vcg_file + " -as=dot --output=" + 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 + ".dot")
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>