深入理解Call Graph

一、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 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) <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);

    vector liblist; 

    Queue q;
    q.enqueue(callgraph->getNode(libname));
    main_log << libname << " ----" << endl;
    while(!q.isEmpty())
    {
        Node *curnode; 
        Node **childnode; 

        curnode = q.dequeue();
        main_log <getName() < ";  
        childnode = curnode->getChildrenList(); 

        for(int i = 0; childnode[i] != NULL; i++)
        {
            main_log <getName() <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 "<<cur_func<<" 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 <getName() << " call " <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 : "<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()<<endl;
            }
        }
    }
}

四、總結

Call Graph是一種強大的工具,可以用於程序分析、測試和維護等領域。無論是靜態構建還是動態構建,Call Graph的構建過程都需要使用算法對程序代碼進行深入分析,從而得出準確的結果。在使用Call Graph進行應用時,我們還需要對函數的調用關係有着深刻的理解,並能夠熟練地使用Call Graph提供的接口進行分析。最後,希望文章對於讀者能夠有所啟發和幫助,加深對於Call Graph的理解。

原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/198303.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
小藍的頭像小藍
上一篇 2024-12-04 10:24
下一篇 2024-12-04 10:24

相關推薦

  • Docker掛載目錄–graph用法介紹

    本文將從如下幾個方面詳細闡述Docker掛載目錄–graph: 一、基本概念 在Docker中,鏡像是由一系列只讀層組成的文件系統。當我們啟動一個容器時,Docker會…

    編程 2025-04-27
  • 深入解析Vue3 defineExpose

    Vue 3在開發過程中引入了新的API `defineExpose`。在以前的版本中,我們經常使用 `$attrs` 和` $listeners` 實現父組件與子組件之間的通信,但…

    編程 2025-04-25
  • 深入理解byte轉int

    一、字節與比特 在討論byte轉int之前,我們需要了解字節和比特的概念。字節是計算機存儲單位的一種,通常表示8個比特(bit),即1字節=8比特。比特是計算機中最小的數據單位,是…

    編程 2025-04-25
  • 深入理解Flutter StreamBuilder

    一、什麼是Flutter StreamBuilder? Flutter StreamBuilder是Flutter框架中的一個內置小部件,它可以監測數據流(Stream)中數據的變…

    編程 2025-04-25
  • 深入探討OpenCV版本

    OpenCV是一個用於計算機視覺應用程序的開源庫。它是由英特爾公司創建的,現已由Willow Garage管理。OpenCV旨在提供一個易於使用的計算機視覺和機器學習基礎架構,以實…

    編程 2025-04-25
  • 深入了解scala-maven-plugin

    一、簡介 Scala-maven-plugin 是一個創造和管理 Scala 項目的maven插件,它可以自動生成基本項目結構、依賴配置、Scala文件等。使用它可以使我們專註於代…

    編程 2025-04-25
  • 深入了解LaTeX的腳註(latexfootnote)

    一、基本介紹 LaTeX作為一種排版軟件,具有各種各樣的功能,其中腳註(footnote)是一個十分重要的功能之一。在LaTeX中,腳註是用命令latexfootnote來實現的。…

    編程 2025-04-25
  • 深入剖析MapStruct未生成實現類問題

    一、MapStruct簡介 MapStruct是一個Java bean映射器,它通過註解和代碼生成來在Java bean之間轉換成本類代碼,實現類型安全,簡單而不失靈活。 作為一個…

    編程 2025-04-25
  • 深入理解Python字符串r

    一、r字符串的基本概念 r字符串(raw字符串)是指在Python中,以字母r為前綴的字符串。r字符串中的反斜杠(\)不會被轉義,而是被當作普通字符處理,這使得r字符串可以非常方便…

    編程 2025-04-25
  • 深入探討馮諾依曼原理

    一、原理概述 馮諾依曼原理,又稱“存儲程序控制原理”,是指計算機的程序和數據都存儲在同一個存儲器中,並且通過一個統一的總線來傳輸數據。這個原理的提出,是計算機科學發展中的重大進展,…

    編程 2025-04-25

發表回復

登錄後才能評論