静态分析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优秀,可以分析出更加复杂的调用关系。

参考资料

相关推荐
阿蒙Amon4 分钟前
C#扩展方法全解析:给现有类型插上翅膀的魔法
开发语言·c#
尘浮72828 分钟前
60天python训练计划----day59
开发语言·python
Chef_Chen1 小时前
从0开始学习R语言--Day39--Spearman 秩相关
开发语言·学习·r语言
不学会Ⅳ1 小时前
Mac M芯片搭建jdk源码环境(jdk24)
java·开发语言·macos
好开心啊没烦恼2 小时前
Python 数据分析:计算,分组统计1,df.groupby()。听故事学知识点怎么这么容易?
开发语言·python·数据挖掘·数据分析·pandas
lljss20203 小时前
Python11中创建虚拟环境、安装 TensorFlow
开发语言·python·tensorflow
Python×CATIA工业智造6 小时前
Frida RPC高级应用:动态模拟执行Android so文件实战指南
开发语言·python·pycharm
我叫小白菜7 小时前
【Java_EE】单例模式、阻塞队列、线程池、定时器
java·开发语言
森焱森7 小时前
水下航行器外形分类详解
c语言·单片机·算法·架构·无人机
狐凄7 小时前
Python实例题:基于 Python 的简单聊天机器人
开发语言·python