静态分析C语言生成函数调用关系的利器——cally和egypt

大纲

《静态分析C语言生成函数调用关系的利器------cflow》《静态分析C语言生成函数调用关系的利器------cflow(二)》中,我们介绍了使用cflow直接分析c语言源码导出调用栈的方法。在做实验的过程中,我一直在思考一个问题:cflow能解释C语言?看了下源码后,发现它的确有解析的模块。大家可以看下它的部分代码。

c 复制代码
// parser.c
typedef struct {
     char *name;
     int type_end;
     int parmcnt;
     int line;
     enum storage storage;
} Ident;

void parse_declaration(Ident*, int);
void parse_variable_declaration(Ident*, int);
void parse_function_declaration(Ident*, int);
......
static void
print_token(TOKSTK *tokptr)
{
     switch (tokptr->type) {
     case IDENTIFIER:
     case TYPE:
     case WORD:
     case MODIFIER:
     case STRUCT:
     case PARM_WRAPPER:
     case QUALIFIER:
     case OP:
	  fprintf(stderr, "`%s'", tokptr->token);
	  break;
     case LBRACE0:
     case LBRACE:
	  fprintf(stderr, "`{'");
	  break;
     case RBRACE0:
     case RBRACE:
	  fprintf(stderr, "`}'");
	  break;
     case EXTERN:
	  fprintf(stderr, "`extern'");
	  break;
     case STATIC:
	  fprintf(stderr, "`static'");
	  break;
     case TYPEDEF:
	  fprintf(stderr, "`typedef'");
	  break;
     case STRING:
	  fprintf(stderr, "\"%s\"", tokptr->token);
	  break;
     default:
	  fprintf(stderr, "`%c'", tokptr->type);
     }
}

可以发现它是纯纯的文本解析。这就引发了我的一个担忧:如果C语言的编译器对文件的解释和cflow的解释器对同一份文件的结果解析不同怎么办?这个可能性还是存在的。

本文介绍的cally和egypt就很好的避开了这个问题,因为对文件的解析交给了GCC编译器。它们只是对编译器产生的中间结构化内容(Register transfer language)进行解释和整理,这个难度就比解析C语言源码要简单。产出的DOT (graph description language)文件交给dot程序生成调用栈的图。

我们还是以《静态分析C语言生成函数调用关系的利器------cflow(二)》中的libevent库为例。

准备工作

安装graphviz

bash 复制代码
sudo apt install graphviz

安装cally

cally就是一个python脚本,我们只要把工程代码下载下来即可。

bash 复制代码
git clone https://github.com/chaudron/cally.git

安装egypt

bash 复制代码
wget https://www.gson.org/egypt/download/egypt-1.11.tar.gz .
tar xzf egypt-1.11.tar.gz
rm egypt-1.11.tar.gz
cd egypt-1.11
perl Makefile.PL
make
sudo make install
cd -

简单分析

GCC产生RTL(Register transfer language)文件

libevent库中的test-time程序是通过链接编译完的libevent.a和libevent_core.a生成的。现在我们不能依赖原工程中的cmake来生成,需要自己编写编译指令。(还是需要先把整个工程编译一遍,具体见《静态分析C语言生成函数调用关系的利器------cflow(二)》中坑3:缺失编译时产生的文件)。

bash 复制代码
gcc ./test/test-time.c \
 -I./build/include/ -I./include -I./ \
 -L./build/lib/ -Wl,-Bstatic -levent -levent_core -Wl,-Bdynamic \
 -o test-time-main

上面的脚本可以正确将test-time.c编译成可执行文件。

现在我们只要让它产出RTL文件即可。

bash 复制代码
gcc ./test/test-time.c \
 -I./build/include/ -I./include -I./ \
 -L./build/lib/ -Wl,-Bstatic -levent -levent_core -Wl,-Bdynamic \
 -fdump-rtl-expand

这样就产生了一个名字叫a-test-time.c.245r.expand的RTL文件。

cally

将上一步生成的文件拷贝到cally.py所在的目录,然后执行

bash 复制代码
python3 ./cally.py a-test-time.c.245r.expand |  dot -Grankdir=LR -Tpng -o cally_test_time_call_graph.png

egypt

bash 复制代码
egypt a-test-time.c.245r.expand --include-external |  dot -Grankdir=LR -Tpng -o egypt_test_time_call_graph.pn
g

总结

我们看下test-time.c的部分源码。可以看到egypt的展现更加准确,因为它将time_cb和main进行了关联,而cally则没展现出来这层关系。

c 复制代码
static int
rand_int(int n)
{
	return evutil_weakrand_(&weakrand_state) % n;
}

static void
time_cb(evutil_socket_t fd, short event, void *arg)
{
	struct timeval tv;
	int i, j;

	called++;

	if (called < 10*NEVENT) {
		for (i = 0; i < 10; i++) {
			j = rand_int(NEVENT);
			tv.tv_sec = 0;
			tv.tv_usec = rand_int(50000);
			if (tv.tv_usec % 2 || called < NEVENT)
				evtimer_add(ev[j], &tv);
			else
				evtimer_del(ev[j]);
		}
	}
}

int
main(int argc, char **argv)
{
	struct event_base *base;
	struct timeval tv;
	int i;

#ifdef _WIN32
	WORD wVersionRequested;
	WSADATA wsaData;

	wVersionRequested = MAKEWORD(2, 2);

	(void) WSAStartup(wVersionRequested, &wsaData);
#endif

	evutil_weakrand_seed_(&weakrand_state, 0);

	if (getenv("EVENT_DEBUG_LOGGING_ALL")) {
		event_enable_debug_logging(EVENT_DBG_ALL);
	}

	base = event_base_new();

	for (i = 0; i < NEVENT; i++) {
		ev[i] = evtimer_new(base, time_cb, event_self_cbarg());
		tv.tv_sec = 0;
		tv.tv_usec = rand_int(50000);
		evtimer_add(ev[i], &tv);
	}

	i = event_base_dispatch(base);

	printf("event_base_dispatch=%d, called=%d, EVENT=%d\n",
		i, called, NEVENT);

	if (i == 1 && called >= NEVENT) {
		return EXIT_SUCCESS;
	} else {
		return EXIT_FAILURE;
	}
}

我们看到上面图片并没有展现诸如event_add这类外部函数的底层调用栈。这是因为这些函数是作为静态库提供给test-time进行链接的。且我们并没有生成它们的RTL文件,所以不能分析出完整的调用关系。

为了展现更加具体的调用关系,我们将进行一些改造,以获得更多RTL进行分析。

高级分析

上面问题的根源在于test-time编译依赖于静态库,我们首先要解决这个问题,就是要手撸一条可用的编译指令。

这个实验的主要难点也是在这个指令的正确书写,中间修正的过程我就不表了,直接贴出结果。

bash 复制代码
gcc `find . -regextype posix-extended -regex '^./[^/]*\.c$' ! -name 'wepoll.c' ! -name 'win32select.c' ! -name 'evthread_win32.c' ! -name 'buffer_iocp.c' ! -name 'bufferevent_async.c' ! -name 'arc4random.c' ! -name 'event_iocp.c' ! -name 'bufferevent_mbedtls.c'` \
 ./test/test-time.c \
 -I./build/include/ -I./include -I./ \
 -L./build/lib/ -lcrypto -lssl \
 -DLITTLE_ENDIAN -D__clang__ \
 -UD_WIN32 -UDMBEDTLS_SSL_RENEGOTIATION \
 -fdump-rtl-expand

这样我们得到一堆RTL文件。这些文件我都将其拷贝到cally和egypt测试工程的sample目录下。

cally

bash 复制代码
python3 ./cally.py ../sample/*.expand --caller main |  dot -Grankdir=LR -Tsvg -o cally_full_test_time_call_graph.svg

生成文件非常大,就不展示了。(见https://github.com/f304646673/tools/tree/main/cally

只展示event_add函数的调用栈。

egypt

bash 复制代码
egypt sample/*.expand --include-external --callees main |  dot -Grankdir=LR -Tsvg -o egypt_full_test_time_call_graph.svg

生成文件非常大,就不展示了。(见https://github.com/f304646673/tools/tree/main/egypt

只展示event_add函数的调用栈。

总结

egypt比cally优秀,可以分析出更加复杂的调用关系。

参考资料

相关推荐
Lizhihao_21 分钟前
JAVA-队列
java·开发语言
远望清一色39 分钟前
基于MATLAB边缘检测博文
开发语言·算法·matlab
何曾参静谧1 小时前
「Py」Python基础篇 之 Python都可以做哪些自动化?
开发语言·python·自动化
Prejudices1 小时前
C++如何调用Python脚本
开发语言·c++·python
我狠狠地刷刷刷刷刷1 小时前
中文分词模拟器
开发语言·python·算法
wyh要好好学习1 小时前
C# WPF 记录DataGrid的表头顺序,下次打开界面时应用到表格中
开发语言·c#·wpf
AitTech1 小时前
C#实现:电脑系统信息的全面获取与监控
开发语言·c#
qing_0406031 小时前
C++——多态
开发语言·c++·多态
孙同学_1 小时前
【C++】—掌握STL vector 类:“Vector简介:动态数组的高效应用”
开发语言·c++
froginwe111 小时前
XML 编辑器:功能、选择与使用技巧
开发语言