一、Call Graph的定义
Call Graph (调用图)是通常用于表示程序中各个函数之间调用关系的一种图,其中每个节点代表一个函数,每条有向边则代表函数之间的调用关系。Call Graph在软件维护、分析和测试中有着广泛的应用,例如:代码实现缺陷检测、代码优化以及变化可视化等领域。
二、Call Graph的构建
Call Graph的构建方法通常可以分为两种:静态构建和动态构建。
1. 静态构建
静态分析是在不执行程序的情况下对程序进行分析。一般需要对程序进行编译,在编译阶段生成中间代码或汇编代码,并最终对生成的中间代码或汇编代码进行解析和检测。该方法存在着精度高和适用范围广等优点。静态分析常用的工具如:GCC、LLVM、IDA等。
//静态构建callgraph的代码示例 1.构造callgraph struct CallGraphBuilder { private Hashtable nodesVisited; public CallGraphBuilder() { nodesVisited = new Hashtable(); } public void constructCallGraph() { //do something } } 2. 遍历代码 public class Node { private String name;//代表函数名 private HashSet children; public Node(String name) { this.name = name; children = new HashSet(); } public HashSet getChildren() { return children; } public void addChild(Node child) { children.add(child); } public String getName() { return name; } public String toString() { return getName(); } } public class CallGraph { private Hashtable nodes; public CallGraph() { nodes = new Hashtable(); } public Node getNode(String name) { return (Node)nodes.get(name); } public void addNode(Node node) { nodes.put(node.getName(), node); } public String toString() { String result = ""; for(Enumeration e = nodes.elements(); e.hasMoreElements();) { result += e.nextElement() + "\n"; } return result; } } 3. 遍历函数 private void parseFunctionsInSection(Section section, int sectionLength) { int sectionIndex = 0; int functionSize = sizeof(Function); //获取函数大小 while (sectionIndex < sectionLength)// { Function *func = (Function *) §ion[sectionIndex]; Node *newNode = m_graph->getNode(func->name); if (newNode == NULL) { newNode = new Node(func->name); m_graph->addNode(newNode); } Node *currNode = newNode; for (int j=0; jnumCallees; j++) { Node *calleeNode = m_graph->getNode(func->callees[j]); if (calleeNode == NULL) { calleeNode = new Node(func->callees[j]); m_graph->addNode(calleeNode); } currNode->addChild(calleeNode); } sectionIndex += functionSize; } }
2. 动态构建
动态分析是在程序运行的时候进行分析,通过跟踪用户的输入以及系统调用,动态分析能够检测出更多的可能性错误,如内存泄漏等。但是,由于在运行时必须启动程序的所有逻辑路径以及检测覆盖率,因此分析速度相对较慢,而且可能会存在漏检的情况。动态分析常用的工具如:Valgrind、Gcov等。
//动态构建callgraph的代码示例 1. 重载dlopen函数,实现安装intercepter void *dlopen(const char *filename, int flag) { void *handle; handle = (*orig_dlopen)(filename, flag); if(!filename) return handle; if(string(filename).rfind("mylib.so") == string::npos)//不需要监控库调用。 return handle; ofstream main_log; main_log.open("main.log",ios::out|ios::app); string exefilename = GetExeName(); main_log << "Exe Name is" << exefilename << endl; char buf[256]; sprintf(buf, "hijacked dlopen:%s\n", filename); m_librarystack.push_back(exefilename);//程序开始,将其作为stack底部元素处理 m_librarystack.push_back(string(filename));//程序开始,将其作为stack底部元素处理 main_log << "push lib:" << string(filename) << endl; main_log.close(); callgraph->addNode(string(filename));//CallGraph中添加结点 return handle; } 2. 插桩函数,记录函数调用信息 void callstack_enter(const char*symname) { int tid = (int) pthread_self(); // 栈为空则返回 if(m_librarystack.empty()) return ; if(!IsTargetProcess()) // 记录目标进程相关信息,根据具体项目进行定义 return ; pthread_mutex_lock(&s_mutex); string exename = GetName(); string libname = m_librarystack.back(); // 重入函数则退出 if(symname == NULL || symname[0] == '\0' || strcmp(symname,"callstack_enter") == 0) { pthread_mutex_unlock(&s_mutex); return; } // 忽略一些特定的函数 if(!IsFunctionValid(string(symname[1] == '_' ? symname+2 : symname))) { pthread_mutex_unlock(&s_mutex); return; } // 添加调用边 if(callgraph && libname != exename) { pthread_mutex_lock(&callgraph_mutex); callgraph->addEdge(m_latest_func, symname); pthread_mutex_unlock(&callgraph_mutex); } m_calldepth++; m_latest_func = symname; pthread_mutex_unlock(&s_mutex); } 3. 遍历CallGraph void traverse_callgraph(string libname) { ofstream main_log; main_log.open("main.log",ios::out|ios::app); vectorliblist; Queue q; q.enqueue(callgraph->getNode(libname)); main_log << libname << " ----" << endl; while(!q.isEmpty()) { Node *curnode; Node **childnode; curnode = q.dequeue(); main_log << curnode->getName() << " ------> "; childnode = curnode->getChildrenList(); for(int i = 0; childnode[i] != NULL; i++) { main_log << childnode[i]->getName() << ","; if(CheckInList(liblist,childnode[i]->getName()) == -1) { q.enqueue(childnode[i]); liblist.push_back(childnode[i]->getName()); } } main_log << endl; } main_log << "end---" << endl; mainlog.close(); }
三、Call Graph的应用
1. 检测函数嵌套
通过Call Graph,我们可以清晰地了解到程序中函数的调用关系,从而可以检测出函数嵌套的情况。同时,我们还可以根据规则,对于嵌套层数超过限制的函数进行警告或者修改。
//检测嵌套是否超过限制的代码示例 #include "CallGraph.h" int max_depth = 4; string cur_func; int cur_depth = 0; bool recursion = false; void DetectRecursion(string func_name) { cur_func = func_name; recursion = true; Node *node = callgraph->getNode(func_name); if(node == NULL) { return; } DFS(node,0); if(recursion) { cout<<"ERROR! function "<<<" has Recursion."< max_depth) { return; } Node **children; children = node->getChildrenList(); if(children == NULL) { return; } for(i=0; children[i] != NULL; i++) { if(children[i]->getName() == cur_func) { recursion = true; return ; } DFS(children[i],depth+1); } }
2. 检测函数调用关系
Call Graph允许我们对函数的调用链进行可视化操作,有助于开发者快速定位程序出现的问题。例如:一个函数的返回没有在调用者处进行检查,这就可能导致程序的异常退出;一个函数的形参和实参数量不一致,也可能导致程序的异常退出等。
//检测函数调用关系的代码示例 #include "CallGraph.h" void analysis_function_call_relationship(Node *node) { Node **children; children = node->getChildrenList(); if(children == NULL) { return; } int j = 0; while(children[j] != NULL) { if(callgraph->findEdge(children[j]->getName(), node->getName())) { cout << node->getName() << " call " << children[j]->getName() << endl; } j++; } j = 0; while(children[j] != NULL) { analysis_function_call_relationship(children[j]); j++; } }
3. 检测函数变化
Call Graph的变化可视化功能可以帮助开发者快速捕捉程序的变化情况。例如:在修改了一个函数后,会导致哪些函数的调用链发生了变化?这些变化对于整个程序的功能影响有多大?这些问题都可以通过使用Call Graph进行分析得到解答。
//检测函数变化的代码示例 #include "CallGraph.h" void analysis_code_change(CallGraph *oldGraph, CallGraph *newGraph) { Node **oldNodes = oldGraph->getNodesList(); Node **newNodes = newGraph->getNodesList(); for(int i = 0; oldNodes[i] != NULL; i++) { if(newGraph->findNode(oldNodes[i]->getName()) == NULL) { cout<<"Function removed : "<< oldNodes[i]->getName() <findNode(newNode->getName()) == NULL) { cout<<"Function added : "< getName()< getNode(newNode->getName()); Node **oldChildren = oldNode->getChildrenList(); Node **newChildren = newNode->getChildrenList(); int oldChildNum = 0; int newChildNum = 0; while(oldChildren[oldChildNum] != NULL) { oldChildNum++; } while(newChildren[newChildNum] != NULL) { newChildNum++; } if(oldChildNum != newChildNum) { cout<<"Call relationship changed for: "< getName()< 四、总结
Call Graph是一种强大的工具,可以用于程序分析、测试和维护等领域。无论是静态构建还是动态构建,Call Graph的构建过程都需要使用算法对程序代码进行深入分析,从而得出准确的结果。在使用Call Graph进行应用时,我们还需要对函数的调用关系有着深刻的理解,并能够熟练地使用Call Graph提供的接口进行分析。最后,希望文章对于读者能够有所启发和帮助,加深对于Call Graph的理解。