Home 如何基于LLVM IR分析全局函数指针的使用情况
Post
Cancel

如何基于LLVM IR分析全局函数指针的使用情况


内核中的全局函数指针大致可以分为两种:一种是全局变量本身是一个函数指针,另一种是全局结构体的某个子域是函数指针,例如:

1
2
3
4
5
struct funcptr {
	void (*foo)();
}s;

void (*fp)(void);

而对这些函数指针的调用,反映到IR上,通常为:

1
2
3
4
5
6
7
8
fp():
  store void ()* @indirect, void ()** @fp, align 8
  %8 = load void ()*, void ()** @fp, align 8
  call void %8()
s.foo():
  %9 = load void (...)*, void (...)** getelementptr inbounds (%struct.funcptr, %struct.funcptr* @s, i32 0, i32 0), align 8
  %10 = bitcast void (...)* %9 to void ()*
  call void %10()

我们可以根据IR的特征来找到对于这些全局变量的使用情况。

如何识别函数指针

LLVM IR对于全局变量和栈上的变量处理是类似的,都将其看作指针。也就是说,即使对于一个int类型的全局变量a,直接访问其type也是指针类型。例如:

1
2
3
4
5
6
C-Level:
	int a = 0;
IR
	@a = dso_local global i32 0, align 4
Type:
	i32*

因此,对于所有的全局变量,只有通过gv.getType()->getPointerElementType()才能得到真正的类型,再在这个类型的基础上对该变量的类型进行分类。函数指针首先其本身类型为Pointer,其PointerElementType为Function。而对于结构体,或结构体类型的指针,需要对其类型进行一个类BFS的算法,如果类型为Pointer,则对其PointerElementType进行判断,如果该类型为Struct/Array,再依次判断Struct的字段类型或Array中的元素类型,对应地有如下代码:

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
bool hasFuncptr(llvm::Type *type, llvm::GlobalVariable *gv)
{
    assert(isa<ArrayType>(type) || isa<StructType>(type));
    assert(gv != nullptr);
    if (isa<ArrayType>(type))
    {
        if (gv->hasInitializer())
            if (isa<FunctionType>(type->getArrayElementType()))
                return true;
    }
    else if (isa<StructType>(type))
    {
        queue<llvm::Type *> que;
        que.push(type);
        set<llvm::Type *> typeset;
        while (!que.empty())
        {
            llvm::Type *cur = que.front();
            que.pop();
            if (typeset.find(cur) != typeset.end())
                continue;
            else
                typeset.insert(cur);
            if (isa<PointerType>(cur))
            {
                if (isa<FunctionType>(cur->getPointerElementType()))
                {
                    return true;
                }
                else
                    que.push(cur->getPointerElementType());
            }
            else if (isa<StructType>(cur))
            {
                if (gv->hasInitializer())
                {
                    for (int i = 0; i < cur->getStructNumElements(); i++)
                    {
                        que.push(cur->getStructElementType(i));
                    }
                }
            }
            else if (isa<ArrayType>(cur))
            {
                if (gv->hasInitializer())
                    que.push(cur->getArrayElementType());
            }
            else
                continue;
        }
    }
    return false;
}

void check_type(llvm::Module *module)
{
    for (auto &gv : module->getGlobalList())
    {
        Type *type = gv.getType()->getPointerElementType();
        if (isa<PointerType>(type))
        {
            if (isa<FunctionType>(type->getPointerElementType()))
            {
	            // 说明是函数指针
            }
            else
            {
                while (isa<PointerType>(type))
                {
	                // 指针指向指针,继续判断
                    type = type->getPointerElementType();
                }
                if (isa<StructType>(type) || isa<ArrayType>(type))
                {
	                // 结构体或数组
                    if (hasFuncptr(type, &gv))
                        // 子域中存在函数指针
                }
                else
                {
	                // 其他类型,必不可能包含函数指针
                }
            }
        }
        else if (isa<StructType>(type) || isa<ArrayType>(type))
        {
            // 全局变量本身是结构体或数组
            if (hasFuncptr(type, &gv))
	            //子域中存在函数指针
        }
        else
        {
            // 普通数据,必不包含函数指针
        }
    }
}

如何追踪函数指针的调用

对于llvm::Value,可以通过users()找到该变量的所有use点,例如下面这段代码就构成了对fp的一条def-use链:

1
2
3
@fp = common dso_local global void ()* null, align 8
%4 = load void ()*, void ()** @fp, align 8
call void %4()

从上面这段IR,我们似乎可以得到一个非常简单的判断方式:从已经判定为指针类型的全局变量开始,顺着def-use链进行DFS,遇到call指令之后检查其是否是间接调用,如果是间接调用,则说明调用了这个函数指针。

然而,上述方法在很大程度上会存在误报和漏报的现象:

  1. 当这个函数指针作为另一个间接调用的参数时,沿着def-use链依然会出现间接调用,但是实际上调用的并不是这个函数指针,从而会出现误报。
  2. 当某个结构体的子域是函数指针时,这个结构体的另一个子域作为作为另一个间接调用的参数,沿着def-use链依然会出现间接调用,但是调用的并不是作为函数指针的这个子域,从而会出现误报
  3. 当函数指针作为函数调用参数时,在callee中通过该参数调用而不是通过该全局变量调用,由于这个参数并不在该全局变量的def-use链上,因此无法将该调用关联到该全局变量本身,从而会出现漏报。 例如:
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
C-Level:
void (*fp)(void);

void test(void (*funcptr)(void)) {
    funcptr();
}

void (*ffp)(void (*)(void));

int main() {
    ffp = test;
    test(fp);
    ffp(fp);
}

IR
@ffp = common dso_local global void (void ()*)* null, align 8
@fp = common dso_local global void ()* null, align 8

; Function Attrs: noinline nounwind optnone
define dso_local void @test(void ()* %0) #0 {
  %2 = alloca void ()*, align 8
  store void ()* %0, void ()** %2, align 8
  %3 = load void ()*, void ()** %2, align 8
  call void %3()
  ret void
}

; Function Attrs: noinline nounwind optnone
define dso_local i32 @main() #0 {
  store void (void ()*)* @test, void (void ()*)** @ffp, align 8
  %1 = load void ()*, void ()** @fp, align 8
  call void @test(void ()* %1)
  %2 = load void (void ()*)*, void (void ()*)** @ffp, align 8
  %3 = load void ()*, void ()** @fp, align 8
  call void %2(void ()* %3)
  ret i32 0
}

那么fp的def-use链为

1
2
3
4
5
6
7
@fp = common dso_local global void ()* null, align 8
%1 = load void ()*, void ()** @fp, align 8
call void @test(void ()* %1)

@fp = common dso_local global void ()* null, align 8
%3 = load void ()*, void ()** @fp, align 8
call void %2(void ()* %3)

对于第一种情况,我们可以通过判断间接调用的calledOperand是否在def-use链上,如果在,那么说明实际调用的很可能就是追踪的这个全局指针。如果不在,那么这个全局变量可能就是作为参数被用于该call指令。

对于第二种情况,也是同样的道理。如果调用的是该全局变量的某个子域,那么在def-use 链上一定存在一条getElementptr指令,将该结构体的子域加载到某个临时变量,随后call该临时变量。而如果只是将全局变量的某个子域作为函数参数传递,那么calledOperand必然不会在def-use链上,所以也不会关联到对该全局变量的调用。

分析代码如下:

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
void checkUsepoint(llvm::Module *module)
{
    for (auto &gv : module->getGlobalList())
    {
        std::string gv_name = gv.getName().str();
        std::set<llvm::Value *> def_chain;
        queue<llvm::Value *> que;
        que.push(&gv);
        def_chain.insert(&gv);
        while (!que.empty())
        {
            llvm::Value *cur = que.front();
            que.pop();
            for (auto user : cur->users())
            {
                if (CallInst *call = dyn_cast<CallInst>(user))
                {
                    if (call->isIndirectCall())
                    {
                        auto called_operand = call->getCalledOperand();
                        if (def_chain.find(called_operand) != def_chain.end()) {
                            //  出现了对该全局变量的调用
                        }   
                    }
                }
                que.push(user);
                def_chain.insert(user);
            }
        }
    }
}

对于第三种情况:除了main函数中对全局变量的调用之外,test函数中的call void %3()实际上也是调用的全局函数指针fp,但是由于数据流的断裂,上述分析方案无法追踪到该调用。那么如何在过程间重建数据流呢?对于不具有可变参数的函数,我们可以根据参数的下标,找到函数形参列表中与该下标对应的下标。例如,全局变量fp的use链上的call void @test(void ()* %1)这条指令,其参数%1在fp的use链上,对应的下标为0,那么根据calledFunction找到test这个函数,通过getArg(0)找到参数列表中的void ()* %0,再在函数test中对%0进行追踪。然而,从%0无法通过 def-use链找到call void %3()这条指令,因为%3与%0实际上是别名关系,其所在的def-use链为:

1
2
3
  %2 = alloca void ()*, align 8
  %3 = load void ()*, void ()** %2, align 8
  call void %3()

因此这种情况需要辅以别名分析,找到该函数内部该参数的别名,对这些别名追踪其use点是否存在间接调用。

上述方法只对direct Call有效,因为对于间接调用,其calledFunction返回为nullptr。根据c++ - How can I get Function Name of indirect call from CallInst in LLVM - Stack Overflow,对于间接调用无法直接获得其对应的函数。KSplit中对于间接调用是通过匹配callsite与每个函数的参数列表和返回值类型,如果能够对应,则将该函数认为是callsite的candidate,随后再对每个candidate进行分析。然而此方法会得到的分析结果最终是overapproximate的,并且开销过高,并不适用。

This post is licensed under CC BY 4.0 by the author.

LeetCode 11. 盛最多水的容器

Linux下的TTBR1_EL1寄存器使用探究