在使用cmocka单元测试框架进行单元测试的过程中,发现声明为static的函数是不能mock的;为什么这类函数无法被mock呢?需要先分析一下mock函数的实现原理。

通过连接器参数进行mock

很多C语言的单元测试框架,需要通过设置链接器参数来实现函数的mock,例如想要mock open这个系统调用,需要在编译时添加参数:

1
-Wl,--wrap=open

-Wl是指后面的参数是添加给连接器的;指定该参数后,调用open的时候会转而调用__wrap_open,所以我们只需要实现一个__wrap_open函数,就可以屏蔽open系统调用;如果需要调用原来的open函数,可以显式调用__real_open

mock的原理

根据mock的实现方式,我们可以确定C语言的mock实现是连接器在链接程序时,将open符号链接到__wrap_open,并且将__real_open符号链接到系统调用open

不能mock的函数

确定了mock的实现方式,就可以分析一下这些不能mock的函数究竟为什么不能mock

声明为static的函数,特点是只能在当前文件内使用,所以如果被测试函数调用了static的函数,这两个函数必然在同一个文件中,并且不能被其他文件中的函数调用。

为什么

为了确认这个问题,我们准备一份测试用的代码,分为两个.c文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* func.c
*/
#include <stdio.h>

static int function_1() {
return 12;
}

int function_2() {
int x = function_1();
printf("x = %d\n", x);
return x;
}
1
2
3
4
5
6
7
8
9
10
11
12
/*
* main.c
*/
#include <stdio.h>

extern int function_2();

int main(void) {
function_2();

return 0;
}

编译生成.o文件,并链接生成可执行文件

1
2
3
gcc -g -O0 -c func.c
gcc -g -O0 -c main.c
gcc -o main main.o func.o -g -O0

接着我们来分析一下function_2()函数,函数中有两次函数调用,一次调用同文件中的function_1(),另一次调用printf(),所以其对应的汇编代码中,应该存在两个callq指令(x64平台);所以我们来使用objdump来反汇编一下func.o

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
-> % objdump -d func.o

func.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <function_1>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: b8 0c 00 00 00 mov $0xc,%eax
d: 5d pop %rbp
e: c3 retq

000000000000000f <function_2>:
f: f3 0f 1e fa endbr64
13: 55 push %rbp
14: 48 89 e5 mov %rsp,%rbp
17: 48 83 ec 10 sub $0x10,%rsp
1b: b8 00 00 00 00 mov $0x0,%eax
20: e8 db ff ff ff callq 0 <function_1>
25: 89 45 fc mov %eax,-0x4(%rbp)
28: 8b 45 fc mov -0x4(%rbp),%eax
2b: 89 c6 mov %eax,%esi
2d: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 34 <function_2+0x25>
34: b8 00 00 00 00 mov $0x0,%eax
39: e8 00 00 00 00 callq 3e <function_2+0x2f>
3e: 8b 45 fc mov -0x4(%rbp),%eax
41: c9 leaveq
42: c3 retq

注意function_2的汇编代码中,两个callq指令的不同:

1
2
20:    e8 db ff ff ff           callq  0 <function_1>
39: e8 00 00 00 00 callq 3e <function_2+0x2f>

callq指令码为e8,第一个callq调用function_1,地址为ff ff ff dbx86指令集上callq的地址用补码表示,所以这个地址实际上是个负数,对应的十进制为-37callq下一条指令的起始地址,加上这个相对跳转地址,刚好是function_1的地址。

再看一下第二条callq指令,它的地址是00 00 00 00,实际上是等待填充,现在还不知道之后会跳转到哪。

再来反汇编可执行文件看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0000000000001171 <function_2>:
1171: f3 0f 1e fa endbr64
1175: 55 push %rbp
1176: 48 89 e5 mov %rsp,%rbp
1179: 48 83 ec 10 sub $0x10,%rsp
117d: b8 00 00 00 00 mov $0x0,%eax
1182: e8 db ff ff ff callq 1162 <function_1>
1187: 89 45 fc mov %eax,-0x4(%rbp)
118a: 8b 45 fc mov -0x4(%rbp),%eax
118d: 89 c6 mov %eax,%esi
118f: 48 8d 3d 6e 0e 00 00 lea 0xe6e(%rip),%rdi # 2004 <_IO_stdin_used+0x4>
1196: b8 00 00 00 00 mov $0x0,%eax
119b: e8 b0 fe ff ff callq 1050 <printf@plt>
11a0: 8b 45 fc mov -0x4(%rbp),%eax
11a3: c9 leaveq
11a4: c3 retq
11a5: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
11ac: 00 00 00
11af: 90 nop

我们仍然关注function_2中的两条callq指令:

1
2
1182:    e8 db ff ff ff           callq  1162 <function_1>
119b: e8 b0 fe ff ff callq 1050 <printf@plt>

第一条callq的相对跳转地址并未发生任何变化,但是第二条指令中,地址已经被填充,而不再是之前的00 00 00 00

结论

根据前面的分析我们可以确定,static函数被调用时,调用地址再编译时就已经确定,并且在链接时不会再去改变它的相对地址;而我们的mock参数是添加在链接器上的,理所当然无法mock static函数。