Skip to content

Commit 7c23d59

Browse files
author
jason
committed
update
1 parent ecfabb5 commit 7c23d59

File tree

8 files changed

+451
-0
lines changed

8 files changed

+451
-0
lines changed
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
Kernel Visualization Tool
2+
=========================
3+
This tool used to analysize Linux or Solaris kernel.
4+
It can draw callgraphs of your specific function, and help you understand the code.
5+
6+
7+
Quick Start
8+
===========
9+
1. Debian
10+
11+
```
12+
apt-get install -y systemtap linux-image-`uname -r`-dbg linux-headers-`uname -r` graphvize
13+
```
14+
15+
2. Solaris
16+
17+
```
18+
pkg install graphvize
19+
```
20+
21+
Example
22+
=======
23+
Debian
24+
------
25+
1. Run using stap
26+
27+
```
28+
bash stap_base.stp 'module("scsi_mod").function("scsi_request_fn")' 'module("scsi_mod").function("*")' | tee scsi_request_fn.log
29+
```
30+
31+
```
32+
python callee.py scsi_request_fn.log
33+
```
34+
35+
![callgraph of scsi_request_fn](/examples/images/scsi_request_fn.cg.png)
36+
![backtrace of scsi_request_fn](/examples/images/scsi_request_fn.bt.png)
37+
38+
39+
Solaris
40+
-------
41+
1. Run using Dtrace(use `-d` option that stands for dtrace log)
42+
43+
```
44+
./dtrace_base.d sdioctl | tee sdioctl.dtrace.log
45+
```
46+
47+
```
48+
python callee.py sdioctl.dtrace.log -d
49+
```
50+
51+
![callgraph of sdioctl](/examples/images/sdioctl.cg.png)
52+
53+
![callgraph of sdioctl](/examples/images/sdioctl.bt.png)
54+
55+
Details
56+
=======
57+
1. In callgraph, from left to right, it presents the throughout program flow which begins from the most left function. It's only **one program call path** that currently your kernel is running. So this tool is dynamic, not like Doxygen. Doxgen is static analysis. It couldn't give me the whole path.
58+
59+
2. In the same list box of callgraph, it presents its parent call them **step by step**, one and one. If it enters and calls other functions, you will
60+
see that it points to his children.
61+
62+
3. There exists these paths that they have the same root parent, because we are **dynamic**. The same function could have different path in different context. So I have to add some random number in the output name.
63+
64+
Usage
65+
======
66+
67+
```
68+
$ callee.py -h
69+
Usage: callee.py [options] log_file
70+
71+
Generate pngs from Dtrace or Systemtap log
72+
73+
Options:
74+
-h, --help show this help message and exit
75+
-k, --keep-dot keep dot file, default delect it
76+
-o OUTPUT_FORMAT, --output-format=OUTPUT_FORMAT
77+
output file format, could be ["png", "jpg", "svg"],
78+
default is png
79+
-d, --is_dtrace_log default is systemtap log, -d stand for dtrace log
80+
-c THRESHOLD_CG, --threshold_cg=THRESHOLD_CG
81+
only generate call graph when the call link extend to
82+
threshold_cg
83+
-b THRESHOLD_BT, --threshold_bt=THRESHOLD_BT
84+
only generate backtrace graph when the call link
85+
extend to threshold_bt
86+
87+
```
88+
89+
You can go to `example/log/` and play.
90+
91+
Contact
92+
=======
93+
Alex Feng
94+
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright 2015 vonnyfly([email protected])
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
17+
import commands
18+
import sys
19+
import string
20+
import random
21+
import os
22+
import hashlib
23+
from optparse import OptionParser
24+
25+
bt_threshold = 3
26+
callgraph_threshold = 3
27+
keep_dot_files = False
28+
is_dtrace = False
29+
output_format = "png"
30+
31+
def get_random_id():
32+
return ''.join(random.SystemRandom().choice(string.digits) for _ in range(3))
33+
34+
def write_file(basename, suffix, content):
35+
outfile = basename + suffix
36+
if os.path.exists(outfile):
37+
outfile = basename + "_" + get_random_id() + suffix
38+
with open(outfile, "w") as f:
39+
f.write(content)
40+
return outfile
41+
42+
def deduplicate(files):
43+
container = {}
44+
for f in files:
45+
hash = hashlib.sha256(open(f).read()).hexdigest()
46+
if hash in container:
47+
os.remove(f)
48+
else:
49+
container[hash] =f
50+
return container.values()
51+
52+
def draw_backtrace(funcs):
53+
if len(funcs) < bt_threshold:
54+
return None
55+
header = """digraph backtrace{
56+
\tnode [shape="record"];
57+
"""
58+
footer = "\n\t label=\"backtrace of %s\"}\n" %(funcs[0],)
59+
60+
nodes = ""
61+
links = ""
62+
63+
for i, func in enumerate(reversed(funcs)):
64+
nodes += "\t a%d[label=\"%s\"];\n" %(i, func)
65+
66+
links = "\t" + ' -> '.join(["a%d" %(i,) for i in range(0, len(funcs))]) + ";\n"
67+
68+
content = "%s%s%s%s" %(header, nodes, links, footer)
69+
return write_file(funcs[0], ".bt.dot", content)
70+
71+
72+
73+
class Tree:
74+
class Node:
75+
def __init__(self, data, id):
76+
self.data = data
77+
self.parent = None
78+
self.children = []
79+
self.head = self
80+
#self.id =get_random_id()
81+
#self.id = data
82+
self.id = id
83+
self.is_head = False
84+
self.first_child = None
85+
self.is_root = False
86+
87+
def __init__(self, root = None):
88+
self.root = root;
89+
self.core_content = ""
90+
self.id = -1
91+
92+
def create_node(self, data):
93+
self.id += 1
94+
return Tree.Node(data, "a" + str(self.id))
95+
96+
def clean(self):
97+
self.root = None
98+
self.core_content = ""
99+
self.id = -1
100+
101+
def travel_tree(self):
102+
if len(self.root.children) == 1 and self.root.data == self.root.children[0].data:
103+
self.root = self.root.children[0]
104+
self.root.parent = None
105+
self.root.is_root = True
106+
107+
self._travel_tree(self.root)
108+
109+
def _travel_tree(self, node):
110+
if node.first_child:
111+
if node.is_root:
112+
new_root = "\t %s [label=\"<%s>%s\", color=red];\n" %(node.id, node.data, node.data)
113+
self.core_content += new_root
114+
#print new_root
115+
new_node = "\t %s [label=\"%s\"];\n" %(node.first_child.id, " | ".join(["<" + i.data + ">" + i.data for i in node.children]))
116+
self.core_content += new_node
117+
#print new_node
118+
left = "\t %s:%s" % (node.head.id, node.data)
119+
right = "%s:%s" % (node.first_child.id, node.first_child.data)
120+
style = "[dir=both, arrowtail=dot];\n"
121+
new_link = "%s -> %s%s" %(left, right, style)
122+
self.core_content += new_link
123+
#print new_link
124+
125+
for child in node.children:
126+
self._travel_tree(child)
127+
128+
129+
def draw_callgraph(funcs):
130+
if len(funcs) < 2 * callgraph_threshold:
131+
return None
132+
header = """digraph callee {
133+
\tsize="30,40";
134+
\tcenter=true;
135+
\tmargin=0.1;
136+
\tnodesep=2;
137+
\tranksep=0.5;
138+
\trankdir=LR;
139+
\tedge[arrowsize=1.0, arrowhead=vee];
140+
\tnode[shape = record,fontsize=20, width=1, height=1, fixedsize=false];
141+
"""
142+
143+
is_first = True
144+
root = None
145+
index = None
146+
tree = Tree()
147+
for label in funcs:
148+
sign = label[0:2]
149+
func = label[2:]
150+
if sign == "->":
151+
new_node = tree.create_node(func)
152+
if is_first:
153+
root = new_node
154+
index = new_node
155+
is_first = False
156+
index.is_root = True
157+
else:
158+
new_node.parent = index
159+
if len(index.children) == 0:
160+
new_node.is_first = True
161+
index.first_child = new_node
162+
else:
163+
new_node.head = index.first_child
164+
index.children.append(new_node)
165+
#print "%s -> %s\n" %(index.data, new_node.data)
166+
index = new_node
167+
elif sign == "<-":
168+
if not index:
169+
print "[-]except: ", label
170+
return None
171+
#print "from return : %s" %(index.data, )
172+
index = index.parent
173+
if not index:
174+
break
175+
176+
177+
#print "Begin travel:"
178+
tree.root = root
179+
tree.travel_tree()
180+
footer = "\n\t label=\"callgraph of %s\";\n}\n" %(tree.root.data,)
181+
content = "%s%s%s" % (header, tree.core_content, footer)
182+
outfile = write_file(tree.root.data, ".cg.dot", content)
183+
tree.clean()
184+
return outfile
185+
186+
def generate_pngs(dotfiles):
187+
dotfiles = deduplicate(dotfiles)
188+
for f in dotfiles:
189+
cmd = 'filename=%s; dot $filename -T%s >${filename%%.*}.%s' % (f, output_format, output_format)
190+
commands.getstatusoutput(cmd)
191+
if not keep_dot_files:
192+
os.remove(f)
193+
print "Generated %s.%s" % (f[0:-4], output_format)
194+
195+
def main():
196+
global callgraph_threshold, bt_threshold, keep_dot_files, is_dtrace
197+
global output_format
198+
parser = OptionParser(usage='%prog [options] log_file',
199+
description='Generate pngs from Dtrace or Systemtap log')
200+
parser.add_option('-k', '--keep-dot', action = 'store_true',
201+
help = 'keep dot file, default delect it')
202+
parser.add_option('-o', '--output-format', type = "string",
203+
help = 'output file format, could be ["png", "jpg", "svg"], '
204+
' default is png')
205+
parser.add_option('-d', '--is_dtrace_log', action = 'store_true',
206+
help = 'default is systemtap log, -d stand for dtrace log')
207+
parser.add_option('-c', '--threshold_cg', type = "int",
208+
help = 'only generate call graph when the call link'
209+
' extend to threshold_cg')
210+
parser.add_option('-b', '--threshold_bt', type = "int",
211+
help = 'only generate backtrace graph when the call link'
212+
' extend to threshold_bt')
213+
214+
(options, args) = parser.parse_args()
215+
216+
if options.keep_dot:
217+
keep_dot_files = True
218+
if options.is_dtrace_log:
219+
is_dtrace = True
220+
if options.output_format and options.output_format in ["png", "jpg", "svg"]:
221+
output_format = options.output_format
222+
if options.threshold_cg and options.threshold_cg > 0:
223+
callgraph_threshold = options.threshold_cg
224+
if options.threshold_bt and options.threshold_bt > 0:
225+
bt_threshold = options.threshold_bt
226+
227+
if len(args) != 1:
228+
parser.error("incorrect number of arguments")
229+
try:
230+
with open(args[0]) as f:
231+
content = f.readlines()
232+
except:
233+
sys.exit("No file!")
234+
235+
bt_list = []
236+
callgraph_list = []
237+
dotfiles = []
238+
for l in content:
239+
#print l
240+
if '+' not in l:
241+
outfile = draw_backtrace(bt_list)
242+
if outfile:
243+
dotfiles.append(outfile)
244+
bt_list = []
245+
246+
if '->' not in l and '<-' not in l and '|' not in l:
247+
outfile = draw_callgraph(callgraph_list);
248+
if outfile:
249+
dotfiles.append(outfile)
250+
callgraph_list = []
251+
252+
if '+' in l:
253+
if is_dtrace:
254+
func_name = l.split('+')[0].strip().split('`')[-1]
255+
else:
256+
func_name = l.split(':')[1].split('+')[0].strip()
257+
bt_list.append(func_name)
258+
259+
if '|' in l and is_dtrace:
260+
if 'entry' in l:
261+
func_name = "->" + l.strip(' \n\t').split('|')[-1].split(':')[0].strip(' \n\t')
262+
elif ':return' in l:
263+
continue
264+
else:
265+
func_name = l.strip(' \n\t').split('|')[-1].strip(' \n\t')
266+
callgraph_list.append(func_name)
267+
268+
if '->' in l or '<-' in l:
269+
if is_dtrace:
270+
func_name = ''.join(l.strip(' \n\t').split(' ')[-2:])
271+
else:
272+
func_name = l.split(':')[-1].strip().split(' ')[0]
273+
callgraph_list.append(func_name)
274+
outfile = draw_backtrace(bt_list)
275+
if outfile:
276+
dotfiles.append(outfile)
277+
outfile = draw_callgraph(callgraph_list)
278+
if outfile:
279+
dotfiles.append(outfile)
280+
281+
generate_pngs(dotfiles)
282+
283+
284+
if __name__ == "__main__":
285+
main()

0 commit comments

Comments
 (0)