CallGraph文件信息重整

前言

在 RTT 系统下我们通常可以动态地查看剩余栈空间去约算最大栈空间,最近了解到可以通过查看 CallGraph 文件从静态角度分析,尝试结合 CallGraph 文件从另一个角度分析栈大小分配是否合理,实现动静结合。在查看CallGraph 文件时发现了一些不便的地方,然后对文件进行了一些重整理。

文件分析

浏览文件

检查Callgraph是否勾选,勾选后编译一次就可以看到Callgraph的htm文件生成了

image-20240117185218601

MaximumStackUsage

页面最上方给出了计算出最大栈深的一个函数,nrt_rms_entry函数最大使用2116字节的栈空间,那就先检查nrt_rms_entry所在线程分配的栈大小。

nrt_rms_entrynrt_rms线程的入口函数,该线程当前分配了2600字节栈空间,仍有20%的冗余。

image-20240117185645747

MutuallyRecursiveFunctions

然后下面是递归函数

SVC_HandlerADC_IRQHandler,通过全局搜索,这两个函数都没有使用到。

FunctionPoints

往下的内容是函数指针,文件列出了很长的函数列表,但是这里面并没有什么有效信息。

image-20240117191244268

Global Symbols

最后就是Global SymbolsUndefined Global Symbols , Undefined Global Symbols是空的就不需要看了

Global Symbols中查看函数的指令占用,栈占用,最大栈深、调用链等相关信息。目前需要关注的是MaxDeph的值

image-20240117192033258

按照之前介绍的方法,在文本中搜索”Max Depth”,我这个工程搜索到了1178个结果,数据十分之多。而且栈深不同,我们不可能每一个都去看,就算只找较大栈深的来看,在数据量较大的情况下也很困难。

image-20240118090210863

刚刚的MaximumStackUsage一栏里提到线程的入口函数,从函数调用链角度来说,这些入口函数理论都是调用链的链头,即对于该线程是(静态分析下)最大栈深。通过直接搜索各个线程的入口函数,找到线程的Max Depth,根据Max Depth查看栈空间分配是否合理。

但是这样还不够,因为这个调用链并不包含函数指针指向的函数栈深。所以我们还需要看下这些函数栈使用情况。

所以问题还是回来了,数据量大,想只看栈较大的部分函数不容易。

对callgraph信息重整

下面写一个Python脚本,重新处理网页内容后可以更好查看函数信息,不安装额外依赖。callgraph文件中函数格式都是有规律的,由一些标签包围,所以可以很容易地取得所需内容。为了不引入外部依赖,主要使用通过正则取得相应字符串。正则难理解、可读性差,而且性能低,一般是不推荐使用的。

image-20240124211440504

主要做如下处理:

  • 将带[called by]信息和不带此信息的函数分组,然后按最大栈深降序显示到网页上
  • 不带[called by]函数置于网页左侧,按序列出函数名与最大栈深,且可链接到对应函数,并标记没有出现在函数指针列表的函数
  • Call Chain中的函数添加超链接,并在函数后面添加函数的栈大小

根据[called by]分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
N_CALLED_BY_LIST = []
OTHERS_LIST = []

#正则
RE_GROUPS = re.compile(r"<P><STRONG>(.*?)(?=<P>)", re.S)

# 遍历所有函数
groups = RE_GROUPS.findall(html)

# 分离出no_called_by列表
for group in groups:
if "[Called By]" not in group and "Max Depth" in group:
N_CALLED_BY_LIST.append(group)
else:
OTHERS_LIST.append(group)

获取左侧函数列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
N_CALLED_BY_NAV_LIST = []

#正则
RE_MATCH_GROUP = re.compile(r"<a name=\"\[(\w+)]\"></a>(.*?)</STRONG> \(Thumb, \d+ bytes, Stack size (\d+) bytes")
RE_MAX_DEPTH = re.compile(r"Max Depth = (\w+)")

def get_max_depth(group):
match = RE_MAX_DEPTH.search(group)
return int(match.group(1)) if match else 0

def get_nav_string(group):
match = RE_MATCH_GROUP.search(group)
if match:
label = "【==Non Fn Pointer==】</BR>" if match.group(2) not in fn_pointers else ""
return (f"<LI><a href=\"#[{match.group(1)}]\">{label}{match.group(2)} "
f"Max Depth = {get_max_depth(group)}</a>")
else:
return ""

N_CALLED_BY_NAV_LIST.append(get_nav_string(group))

Call Chain中的函数添加超链接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

#正则
RE_CALL_CHAIN = re.compile(r"Call Chain = (.*)")
RE_LI = re.compile(r"<a.*?&gt;&gt;.*")

# 遍历所有函数
groups = RE_GROUPS.findall(html)

# Call Chain的各个函数名称添加超链接
for i, group in enumerate(groups):
match = RE_CALL_CHAIN.search(group)
if match:
call_chain_names = match.group(1).split(" &rArr; ")
links = map(call_chain_link, map(str.strip, call_chain_names))
new_call_chain = "Call Chain = " + " ⇒ ".join(links)
groups[i] = group.replace(match.group(0), new_call_chain)

处理后效果如下图,排序后可以继续根据最大栈深分析了

image-20240123003232598

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
#!/usr/bin/python
# -*- coding:utf-8 -*-
# @time : 2024-01-22
# @license : GNU General Public License v3.0 <https://www.gnu.org/licenses/>
# @description: 用于重新布局callgraph文件
# @version : V1.0.0

import os
import io
import re

# RE_STACK_SIZE = re.compile(r"Stack size (\d+) bytes")
RE_RECURSIVE = re.compile(r"<P>\n<H3>\nMutually(.*?)</UL>", re.S)
RE_GROUPS = re.compile(r"<P><STRONG>(.*?)(?=<P>)", re.S)
RE_MAX_DEPTH = re.compile(r"Max Depth = (\w+)")
RE_MATCH_GROUP = re.compile(r"<a name=\"\[(\w+)]\"></a>(.*?)</STRONG> \(Thumb, \d+ bytes, Stack size (\d+) bytes")
RE_CALL_CHAIN = re.compile(r"Call Chain = (.*)")
RE_LI = re.compile(r"<a.*?&gt;&gt;.*")
RE_FN_POINTERS = re.compile(r"<H3>\nFunction Pointers\n</H3><UL>\n.*?\n</UL>", re.S)

OTHERS_LIST = []
N_CALLED_BY_LIST = []
N_CALLED_BY_NAV_LIST = []
LINK_DICT = {}


def get_max_depth(group):
match = RE_MAX_DEPTH.search(group)
return int(match.group(1)) if match else 0


def get_nav_string(group):
match = RE_MATCH_GROUP.search(group)
if match:
label = "【==Non Fn Pointer==】</BR>" if match.group(2) not in fn_pointers else ""
return (f"<LI><a href=\"#[{match.group(1)}]\">{label}{match.group(2)} "
f"Max Depth = {get_max_depth(group)}</a>")
else:
return ""


def call_chain_link(name):
return (f'<a href="#[{LINK_DICT[name][0]}]">{name}({LINK_DICT[name][1]})</a>'
if name in LINK_DICT else name)


htm_file = input("请输入callgraph文件路径:")
while not os.path.isfile(htm_file):
print("文件不存在,请重新输入!")
htm_file = input("请输入callgraph文件路径:")
output_file = "(layout)" + os.path.basename(htm_file)
output_file = os.path.join(os.path.dirname(htm_file), output_file)
with open(htm_file, "r", encoding="utf-8") as f_in:
html = f_in.read()


# 存在递归的函数
recursive = RE_RECURSIVE.search(html)[0].replace("H3", "H1")

# 函数指针内容
fn_pointers = RE_FN_POINTERS.search(html)[0]

# 遍历所有函数
groups = RE_GROUPS.findall(html)

# 根据函数和函数的超链接生成字典
for group in groups:
match = RE_MATCH_GROUP.search(group)
if match:
href, name, stack = match.group(1, 2, 3)
LINK_DICT[name] = href, stack

# Call Chain的各个函数名称添加超链接
for i, group in enumerate(groups):
match = RE_CALL_CHAIN.search(group)
if match:
call_chain_names = match.group(1).split(" &rArr; ")
links = map(call_chain_link, map(str.strip, call_chain_names))
new_call_chain = "Call Chain = " + " ⇒ ".join(links)
groups[i] = group.replace(match.group(0), new_call_chain)
# a标签移到末尾,全字符串超链接
groups[i] = RE_LI.sub(lambda a: a.group(0).replace("</a>", "") + "</a>", groups[i])

# 分离出no_called_by列表
for group in groups:
if "[Called By]" not in group and "Max Depth" in group:
N_CALLED_BY_LIST.append(group)
N_CALLED_BY_NAV_LIST.append(get_nav_string(group))
else:
OTHERS_LIST.append(group)

# 按Max Depth排序
N_CALLED_BY_LIST.sort(key=get_max_depth, reverse=True)
N_CALLED_BY_NAV_LIST.sort(key=get_max_depth, reverse=True)

# nav的Max Depth用括号表示
N_CALLED_BY_NAV_LIST = [s.replace("Max Depth = ", "</BR>(Max ")[:-4] + ")" + s[-4:]
for s in N_CALLED_BY_NAV_LIST]

my_css = "<style>#button{display:none;position:fixed;bottom:50px;right:50px;z-index:99;\
border:none;outline:none;color:white;cursor:pointer;padding:15px;border-radius:25px;\
background-image:linear-gradient(to right,rgba(120, 167, 181, 0.732), rgb(120, 157, 180));\
}#button:hover{background-image:linear-gradient(to right,rgba(98, 136, 146, 0.835), rgb(103, 137, 164))}\
li{display:block}.main{margin-left:20%}.frame{border:1px solid#dddddd;padding:10px}\
.frame:hover{border:1px solid#81a79e}li a,h1{color:rgb(2,76,100);text-decoration:none}\
li a:hover{color:rgb(1,24,32);font-weight:700}.nav{list-style-type:none;margin:0;padding:0;\
width:19%;background-color:white;position:fixed;height:99%;overflow-y:auto}.nav li a{\
display:block;color:rgb(2,76,100);padding:5px 1px;margin-bottom:10px}.nav li a:hover{\
background-color:rgb(114,141,150);color:rgb(255,255,255)}</style>"

my_button = "<button onclick=\"topFunction()\" id=\"button\">回到顶部</button>"

my_js = "<script>var button=document.getElementById(\"button\");window.onscroll=function()\
{scrollFunction()};function scrollFunction(){if(document.body.scrollTop>10||document.documentElement\
.scrollTop>10){button.style.display=\"block\"}else{button.style.display=\"none\"}}\
function topFunction(){document.body.scrollTop=0;document.documentElement.scrollTop=0}</script>"

with open(output_file, "w", encoding="utf-8") as f_out:
buffer = io.StringIO()
# CSS与标题
buffer.write("<html><head>" + my_css + "<title>Static Call Graph</title></head><body>")

# 左侧NAV
buffer.write("<div class=\"nav\">")
buffer.write("".join(N_CALLED_BY_NAV_LIST))
buffer.write("</div>")

# 中心内容
buffer.write("<div class=\"main\">")
# 置顶按钮
buffer.write(my_button)

# no_called_by
buffer.write(recursive + "</BR>")
buffer.write("<BR><H1><a id=\"top\" href=\"#all\">Extracted and Sorted Data from Global Symbols</a></H1><BR>")
buffer.write("".join(
[f"<div class=\"frame\"><P><STRONG>{no_called_by}</P></div></BR>" for no_called_by in N_CALLED_BY_LIST]))
buffer.write("<BR><BR><BR><BR><BR>")

# others
buffer.write("<BR><H1><a id=\"all\" href=\"#top\">Others</a></H1><BR>")
buffer.write("".join([f"<div class=\"frame\"><P><STRONG>{other}</P></div></BR>" for other in OTHERS_LIST]))
buffer.write("<BR><BR><BR><BR><BR>")

# JS
buffer.write("</div></body>" + my_js + "</html>")

# 写入文件
f_out.write(buffer.getvalue())
print("保存成功!")

扩展资料

参考:callgraph - Arm Developer Document

callgraph文件默认输出命名为工程名,也可以通过修改参数配置输出文件名

1
--callgraph_file=<filename>

<filename> 参数设置你的文件名

callgraph文件默认输出的是.htm,也可以通过修改参数配置输出为.txt

1
--callgraph_output=<fmt>

<fmt> 参数可以为以下选项

  • html

  • text

默认--callgraph_output=html

image-20240123001613811