轉換GDB調用棧到流程圖
如果你想在GDB調試時把調用堆棧保存下來歸檔,那下面這個腳本就方便你了。原理是將調用堆棧的函數抽取出來,再完成調用關係就可以了。
首先你要安裝dot (Mac OS下安裝Graphviz), 如果你想轉為文本格式,就可安裝Perl的Graph::Easy包(命令行:sudo perl -MCPAN -e 'install Graph::Easy', Ubuntu下直接安裝libgraph-easy-perl)。
然後按需要執行腳本就可以了, 假定如下的調用棧:
- stack.txt
#0 WebCore::FrameLoader::FunctionA (this=0x2a7d91f8) at /FrameLoader.cpp
#1 0x4efd2514 in WebCore::FrameLoader::FunctionB (this=0x2a7d91f8) at /FrameLoader.cpp:553
#2 0x4efd1918 in FunctionC ()at /mainFile.cpp:100
1. 轉為圖片
python convertStackToDot.py stack.txt|dot -Tpng>output.png
默認由上到下排列,如果你想改變,可以通過在腳本的參數中增加dot設定調整,比如: python convertStackToDot.py stack.txt 'rankdir=LR;'|dot -Tpng>output.png 就會變成橫排:
2. 轉為文本
有時你希望轉為文本,還好有Graph Easy包,不然你就要使用asciio自己畫了。 python convertStackToDot.py stack.txt 『rankdir=LR;』|graph-easy -as_ascii>output.txt
效果如下:
+-----------+ +------------------------+ +------------------------+
| FunctionC | --> | FrameLoader::FunctionB | --> | FrameLoader::FunctionA |
+-----------+ +------------------------+ +------------------------+
3. 多個調用棧的解析
如果有多個調用棧一起組成完整的流程,只要在各個調用棧中間加入空格就可以了。比如下面的堆棧:
#0 WebCore::FrameLoader::FunctionF (this=0x2a7d90f8,at /FrameLoader.cpp
#1 WebCore::FrameLoader::FunctionA (this=0x2a7d90f8,at /FrameLoader.cpp
#2 0x4ffd2514 in WebCore::FrameLoader::FunctionB (this=0x2a7d90f8, at /FrameLoader.cpp:553
#3 0x4ffd1918 in FunctionC (this=0x2a7d90f8 at /mainFile.cpp:100
#0 WebCore::FrameLoader::FunctionE (this=0x2a7d90f8,at /FrameLoader.cpp
#1 0x4ffd2514 in WebCore::FrameLoader::FunctionB (this=0x2a7d90f8, at /FrameLoader.cpp:553
#2 0x4ffd1918 in FunctionC (this=0x2a7d90f8 at /mainFile.cpp:100
輸出結果如下, 線段上的數字代碼的堆棧序號:
再次修改了一下,對於重複的調用,可以使用-d選項選擇是否全部顯示出來,同時可以允許指定一個正則表達式將某部分節點高亮顯示,如允許顯示重複調用路徑的效果: python convertStackToDot.py -d -l "A|PolicyChecker" stack.txt|dot -Tpng>output.png
- convertStackToDot.py
#!/usr/bin/python
#coding=utf-8
#python convertStackToDot.py stack.txt|dot -Tpng>output.png
#To generate ascii flow chart, graph_easy should be installed:
# sudo apt-get install libgraph-easy-perl
#The use below command line:
#python convertStackToDot.py stack.txt|graph-easy -as_ascii>output.txt
import sys
import re
import argparse
import os
function_name_str = r"[a-zA-Z][a-zA-Z0-9]*::[~a-zA-Z0-9\_\-<: >=]*\(|[a-zA-Z0-9_\-<>]* \("
class_name_str = r"::[a-zA-Z][a-zA-Z0-9]*::"
known_namespaces = []
libCName="/system/lib/libc.so"
Colors=["#000000","#ff0000","#00aa00","#0000ff","#800080","#daa520","#ff00b4","#d2691e","#00bfff",
"#D0A020","#006000","#305050","#101870"]
Styles=["solid","dashed","dotted"]
MAX_COLOR_COUNT = len(Colors)
MAX_LINE_WIDTH_COUNT = 4
MAX_STYLE_COUNT = len(Styles)
FirstNodeAttr = ' style="rounded,filled" fillcolor=\"#00bb0050\"'
HighlightAttr = ' style="rounded,filled" fillcolor=\"yellow\"'
blockNum = 0
nodeNo = 0
nodeList={}
nodeOrderList={}
firstNodeList={}
nodeAttr={}
outputText = ''
callingStack = ''
newBlock=True
willCommit=False #For filtering purpose
blockBackTrace = ''
blockNodeList={}
def getTextOfBlockNodeList(lastNodeName,lastNodeLabel):
global firstNodeList
strBlockNodeText = ''
for key in blockNodeList.keys():
if not nodeList.has_key(key):
name = blockNodeList[key]
strBlockNodeText = strBlockNodeText + name + nodeAttr[name]+'\n'
nodeList[key] = name
#Replace the attribute of the last node
if len(lastNodeName)>0 and not firstNodeList.has_key(lastNodeName):
oldStr = lastNodeName+'[label="'+lastNodeLabel+'" shape=box ];';
newStr = lastNodeName+'[label="'+lastNodeLabel+'" shape=box '+FirstNodeAttr+' ];'
strBlockNodeText = strBlockNodeText.replace(oldStr,newStr,1)
firstNodeList[lastNodeName] = True
return strBlockNodeText
def submitAndResetForNewBlock(args,lastNodeName,lastNodeLabel):
global blockBackTrace,newBlock,callingStack
global blockNodeList,willCommit,outputText
newBlock = True
if willCommit and len(blockBackTrace)>0:
callingStack = blockBackTrace + '\n' + callingStack
blockNodeText = getTextOfBlockNodeList(lastNodeName,lastNodeLabel)
outputText = outputText+blockNodeText
blockNodeList = {}
blockBackTrace = ''
willCommit = (len(args.filter)==0)
def getClassName(text):
m = re.search(class_name_str,text)
if m:
className=text[0:m.end()-2]
elif not text[:text.find('::')] in known_namespaces:
className = text[:text.find('::')]
else:
className = text
return className
def getNodeName(text,nodeNo,args):
global willCommit,blockNodeList,newBlock
processText = text
if len(args.ignore)>0 and re.search(args.ignore,text):
return ''
if args.onlyClass:
processText = getClassName(text)
if nodeList.has_key(processText):
nodeName = nodeList[processText]
elif blockNodeList.has_key(processText):
nodeName = blockNodeList[processText]
else:
nodeName = 'Node'+str(nodeNo)
blockNodeList[processText]=nodeName
extraAttr = ''
try:
if len(args.highlight)>0 and re.search(args.highlight,processText):
extraAttr = HighlightAttr
except:
extraAttr = ''
nodeAttr[nodeName] = '[label="'+processText+'" shape=box '+extraAttr+'];'
if len(args.filter)>0 and re.search(args.filter,text):
willCommit = True
return nodeName
def createNewRelation(nodeName,lastNodeName,blockNum):
global blockBackTrace
tempKey = "%s_%s"%(nodeName,lastNodeName)
if args.duplicate or not nodeOrderList.has_key(tempKey):
lineColor = Colors[(blockNum-1)%MAX_COLOR_COUNT]
linePenWidth = str((int((blockNum-1)/MAX_COLOR_COUNT)%MAX_LINE_WIDTH_COUNT)+1)
lineStyle = Styles[((blockNum-1)/(MAX_COLOR_COUNT*MAX_LINE_WIDTH_COUNT))%MAX_STYLE_COUNT]
if nodeOrderList.has_key(tempKey):
linePenWidth = '1'
lineColor = lineColor+'50' #Set alpha value
blockBackTrace = nodeName+'->'+lastNodeName+'[label='+str(blockNum)+ \
',color=\"'+lineColor+'\"'+ \
',style=\"'+lineStyle+'\"'+ \
',penwidth='+linePenWidth+']\n'+ \
blockBackTrace
nodeOrderList[tempKey] = True
def combineOutputText():
global outputText,callingStack
if len(callingStack)>0:
outputText = outputText+callingStack+"\n}"
return outputText
else:
return ''
def initialize(args):
global outputText,callingStack
outputText = "digraph backtrace{ \nnode [style=rounded fontname=\"Helvetica Bold\"];\n" + args.extraDotOptions +"\n"
def convertToDot(file,args):
global willCommit,outputText,newBlock,blockNum,nodeNo
global outputText,callingStack,blockBackTrace
lastNodeName = ''
lastNodeLabel = ''
willCommit = (len(args.filter)==0) #To specify the initial value according to the filter.
f = open(file, 'r')
for line in f:
line = line.strip()
if(len(line)==0) or line.startswith("#0 ") or line.startswith("#00 "):
if not newBlock:
#Start with new block here.
submitAndResetForNewBlock(args, lastNodeName, lastNodeLabel)
if(len(line.strip())==0):
continue
if not line.startswith("#"):
continue
text = ""
m = re.search(function_name_str, line)
if m:
nodeNo = nodeNo+1
text=m.group(0).strip()
text = text[:-1]
text = text.strip()
elif line.find(libCName)>0:
nodeNo = nodeNo+1
text='FunctionInLibC'
if(len(text)==0):
continue
#Get the existing node or create new one. Anyway, just ask for the name.
nodeName = getNodeName(text,nodeNo,args)
#To throw it away if no valid name was returned according to your arguments.
if(len(nodeName)==0):
continue
if newBlock:
newBlock = False
blockNum = blockNum + 1
else:
createNewRelation(nodeName,lastNodeName,blockNum)
lastNodeName = nodeName
lastNodeLabel = text
if args.onlyClass:
lastNodeLabel = getClassName(text)
if len(blockBackTrace)>0:
#Wow, one block was built successfully, sumit it.
submitAndResetForNewBlock(args, lastNodeName, lastNodeLabel)
f.close()
if __name__=="__main__":
parser = argparse.ArgumentParser()
parser.add_argument('file', type=str, help='The text file which contains GDB call stacks.')
parser.add_argument('-e','--extraDotOptions', default='', help='Extra graph options. For example: rankdir=LR; That means to show functions in horizontal.')
parser.add_argument('-l','--highlight', default='', help='Regular Expression Pattern. Nodes are highlighted whose name match the pattern.')
parser.add_argument('-f','--filter', default='', help='Regular Expression Pattern. The calling stack are shown only if which include the matched nodes.')
parser.add_argument('-d','--duplicate', action='store_true', default=False, help='Leave duplicated callings.')
parser.add_argument('-i','--ignore', default='', help='To hide some nodes, try this.')
parser.add_argument('-c','--onlyClass', action='store_true', default=False, help='To simplify the output with less nodes, only Class node will be listed.')
if len(sys.argv)<=1:
parser.print_help()
print " Any comment, please feel free to contact horky.chen@gmail.com."
quit()
args = parser.parse_args()
if args.file is None:
quit()
initialize(args)
if os.path.isfile(args.file):
convertToDot(args.file,args)
else:
filenames = os.listdir(args.file)
for filename in filenames:
convertToDot(os.path.join(args.file,filename),args)
resultDotString = combineOutputText()
if len(resultDotString)>0:
print(resultDotString)
參考:
- GDB擴展之Command File - 提高調試效率
- [WebKit]C++類的數據結構及在反彙編上的應用
- 使用LLDB腳本簡化打印複雜數據的操作