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

参考资料

相关推荐
Fairy_sevenseven8 分钟前
【二十八】【QT开发应用】模拟WPS Tab
开发语言·qt·wps
蜡笔小新星16 分钟前
Python Kivy库学习路线
开发语言·网络·经验分享·python·学习
凯子坚持 c16 分钟前
C语言复习概要(三)
c语言·开发语言
无限大.28 分钟前
c语言200例 067
java·c语言·开发语言
余炜yw29 分钟前
【Java序列化器】Java 中常用序列化器的探索与实践
java·开发语言
无限大.30 分钟前
c语言实例
c语言·数据结构·算法
篝火悟者31 分钟前
问题-python-运行报错-SyntaxError: Non-UTF-8 code starting with ‘\xd5‘ in file 汉字编码问题
开发语言·python
Death20033 分钟前
Qt 中的 QListWidget、QTreeWidget 和 QTableWidget:简化的数据展示控件
c语言·开发语言·c++·qt·c#
六点半88835 分钟前
【C++】速通涉及 “vector” 的经典OJ编程题
开发语言·c++·算法·青少年编程·推荐算法
惜.己35 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5