用lex分析C源码中数据结构关系拓扑图
2017-07-06 23:14
447 查看
程序=数据结构+算法。最近在看ovs源码时,被其中c源码里面数据结构之间复杂的关系搞的晕头转向,所以强烈想自己写个工具来解析代码内数据结构之间的拓扑。
本来想找个现成工具来的,一直没有找到好用的工具,于是产生自己来写的想法。本文记录下该想法实现的思路及详细过程。
步骤如下:
1)用lex解析出C源码里面的struct体到文件,包括用lex去掉注释并匹配struct结构体的过程。
2)利用python将1)中的到的struct体进行关系处理,并描述成graphviz dot格式文本。
3)利用graphviz 将dot格式进行绘图,得到可视化的结构体关系图。
下面逐步介绍:
1) lex 解析c里面的结构体
我之前虽然看过编译原理,但是对lex yacc基本都没用过。写这个东西,无非就是将C代码里面的结构体全部过滤出来,之后将结构体图关系绘制出来。所以上来就面临lex的用法。在网上找了一些lex的资料,发现都是实验用的,几乎都是解析一个小功能,没有能够一次解析多个功能的,所以都无法直接用。
关于lex还是找官网直接的资料比较好,我基本全部看了一遍,见官方的介绍http://dinosaur.compilertools.net/#lex。
我的用法也比较简单,只需要一次能够解析2个功能1)去掉源码中的所有注释 2)解析出C代码中的结构体。这中间经历了比较多的曲折,别小看这两个小功能,关于lex的正则表达式可是看了不少贴才写出。贴出代码,再废话。
由于lex不能用注释,我试过// /**/在规则中都会导致一定问题,所以代码里面先没写注释。
上面这个代码的功能,是对输入的源码,执行去除所有注释的功能;再将源码中的结构体输出到后缀为-struct-result.txt的文件中。说下一具体思路:
第一遍,只开启BEGIN COMMENT;的规则,这样将输的源码,全部去除注释后,写入到后缀为-1.txt的文件中。
第二遍,只开启BEGIN STRUCT的规则,修改yyin yyout,将第一次的输出-1txt文件作为输入,识别出所有的struct ,写入到-2.txt的文件中。
第三遍,只开启BEGIN TAG的规则, 修改yyin yyout,将第二次的输入-2.txt文件作用输入,将 struct 头与struct中的元素,加上TTT_HEAD/TTT_ELE的标示,最后输出到-struct-result-txt文件中。因为后面我需要用PYTHON来解析结构体,解析处理时不方便识别struct的名称与里面的结构体,所以这个第三边也是必须的。
初写lex时,由于网上都是写基础资料,都是针对一个小功能进行解析,所以不知道怎么在一个lex脚本里面完成多个功能,故当时写了三个lex程序去做上面三个事情。后面突然看到BEGIN XXX这个用法后,逆向思维了下,觉得靠谱,果然写出来了。这个lex的例子比较少,大部分资料都是介绍怎么用正则表达式,感觉目前是牛人不屑于写一般的例子,而初学者只能写简单例子,所以几乎很少看到能用的中等例子。个人的这个例子,可以在一个lex中处理三个小功能。可能有人会说,你搞复杂了,没必要分三次来解析。其实我想目前我还不是非常精通lex框架处理流程,所以只能分三次解析。因为如果不分三次解析的话,lex的一些特点,比如匹配最长的字串,可能会在二次处理注释与结构体时有一些遗漏或互相干扰。所以干脆来个分三次处理,免得干扰。
所有代码在linux下完成,执行如下构建:
lex example
gcc lex.yy.c -o parse -ll
这样就得出了可以去除注释,并且识别struct的parse程序。这个parse file_name 已经可将file_name中的结构体解析到file_name-struct-result.txt中了。
说一下里面的表达式,
comment_t (\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(\/\/.*) 这个我认为是核心,基本上我花了半天多功夫才找到这个去除C注释的表达式。在一个国人写的帖子里面,而且他引用了一个老外的例子,对所有注释格式进行了说明,解释了为什么这个又长又丑陋的表达式是必须的。老外的帖子我暂时不记得了,感兴趣的“必应”下(顺便吐槽下百度搜索,收学术资料基本收不到)。
string_t [a-zA-Z0-9_\-]+
struct[ \t]+.{string_t}[ \t]*[\n]?\{([^}]|[\r\n])*\}\; 是用来识别C结构体的表达式。注意这里\r\n可能又一些遗留,在linux下是没问题的,在windows下,可能对有\n的地方必须有\r,我没有试过windows。
为了让多个表达式直接不互相影响,(前面说了,lex有个最长匹配等混淆),所有我使用了BEGIN 开关,做了三次处理,每次只处理一个类型,防止多个类型直接互相影响。我觉得每次只处理一个类型是比较好的,这样就不会因为lex自身的规则等互相干扰。
基本上,这样后,通过执行 ./parse file_path 就可以再 file_path-struct-result.txt 中得到struct的结构体了。后面我们需要用python对结构体进行二次分析。
2)用python 来解析结构体文件。
这里说点后话,我最终发现利用我的解析来分析ovs代码时,得到了一个很大的关系图,大的无法想象,有近1000个struct。打开一开,乱的很,基本没法看了,因为有太多结构体图了,一度到了900个结构体之多,而且每个结构体直接用线连起来,实在是没法看。而且我发现,绝大部分都是一个孤立的结构体,即这个结构体就它自己,没有与其他结构体有连接关系,这类就不是我们想要的结果。所以果断忽略孤岛结构体,不绘制这不部分。不说了,上python代码。
parse-struct.py -d xxxx_dir1_path:xxxx_dir1_path
多个目录之间用:分隔开,没有就只写一个目录,这个脚本会将目录下面所以C代码中所有struct全部绘制处理,最后的关系图是graph.png。
这里需要在Linux 上yum install graphviz 来安装graphviz工具,其实就是最后调用的那个命令是graphviz提供的。
这个代码,可能写的比较戳,但基本没有啥费力气的。做了如下功能:
1)读取目录,遍历目录,对目录里面的所有文件调用前面的parsy 工具,得到-struct-result.txt的文件。
2)将所有-struct-result.txt里面的结构体,读取出来。然后根据一个结构体,是否里面嵌套了其他结构体,去判断这二者结构体是否有关系。对有关系的结果体,全部都标示出来。
3)利用graphviz的dot语言,对2中所有有关系的结构体,写出符合dot描述的graph文件,最后直接用dot -Tpng graph -o graph.png命令绘制出来。
里面对结构体里面的函数进行了截断出来,或者干脆不显示结果体里面的函数。所有嵌套的结构体,用红色的线画出,对于指针个数的结构体,用箭头前画出。
代码里面有一个draw_pic,这个函数没用。我开始是准备自己来画图的,利用pil。但是后面再网上看另一个开源的graphviz工具,这个dot语音真是太牛逼了,只需要安装一定的格式去描述结构体,他自己都会讲关系图给我画出来。所以我再一次站在了巨人的肩膀上,直接用了这个画图工具。有关graphviz的资料,还是看官网:
http://www.graphviz.org/Documentation.php 这个工具真是太强大了。
来看看几个最终效果图:
最后来一个巨无霸,本来1000多个结构体的,去掉孤岛结构体后,勉强还可以看:)貌似超过2M了,算了。
后话:lex yacc 这两个东西,以前只知道有,没想到功能真是太强大了。现在真的是有这个感觉,学习编程到一个阶段后,发现所有的语言,无论是过程式、面向对象式、函数式等这些,掌握它们都已经不是问题,甚至觉得学习语言本身太浅,反而应该回归到了编译原理上面,可以武断的说,编译原理才是真正的语言之语言。
本来想找个现成工具来的,一直没有找到好用的工具,于是产生自己来写的想法。本文记录下该想法实现的思路及详细过程。
步骤如下:
1)用lex解析出C源码里面的struct体到文件,包括用lex去掉注释并匹配struct结构体的过程。
2)利用python将1)中的到的struct体进行关系处理,并描述成graphviz dot格式文本。
3)利用graphviz 将dot格式进行绘图,得到可视化的结构体关系图。
下面逐步介绍:
1) lex 解析c里面的结构体
我之前虽然看过编译原理,但是对lex yacc基本都没用过。写这个东西,无非就是将C代码里面的结构体全部过滤出来,之后将结构体图关系绘制出来。所以上来就面临lex的用法。在网上找了一些lex的资料,发现都是实验用的,几乎都是解析一个小功能,没有能够一次解析多个功能的,所以都无法直接用。
关于lex还是找官网直接的资料比较好,我基本全部看了一遍,见官方的介绍http://dinosaur.compilertools.net/#lex。
我的用法也比较简单,只需要一次能够解析2个功能1)去掉源码中的所有注释 2)解析出C代码中的结构体。这中间经历了比较多的曲折,别小看这两个小功能,关于lex的正则表达式可是看了不少贴才写出。贴出代码,再废话。
由于lex不能用注释,我试过// /**/在规则中都会导致一定问题,所以代码里面先没写注释。
cat source.txt %{ #include <stdio.h> extern char* yytext; extern FILE *yyin; extern FILE *yyout; %} comment_t (\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(\/\/.*) string_t [a-zA-Z0-9_\-]+ string_head struct[ \t]+.{string_t}[ \t]*[\n]?\{ string_element {string_t}[ \t]+[^;{]+[ \t]*\; %s STRUCT COMMENT TAG %% <STRUCT>struct[ \t]+.{string_t}[ \t]*[\n]?\{([^}]|[\r\n])*\}\; {fprintf(yyout,"%s\n",yytext);} <COMMENT>{comment_t} ; <COMMENT>. fprintf(yyout,"%s",yytext) ; <COMMENT>\n fprintf(yyout,"\n") ; <STRUCT>. ; <STRUCT>\n ; <TAG>{string_head} fprintf(yyout,"TTT_HEAD:%s\n",yytext); <TAG>{string_element} fprintf(yyout,"TTT_ELE:%s\n",yytext); <TAG>. fprintf(yyout,"%s",yytext); <TAG>\n fprintf(yyout,"\n"); %% #include <sys/types.h> #include <stdio.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int yywrap(){ return 1; } int main(int argc, char *argv[]) { int step = 0; char file_dir[1024]={0}; char file_no_comment[1024]={0}; char file_struct[1024]={0}; char file_result[1024]={0}; int size = 1023; if (argc != 2){ printf("please give src file path\n"); return 0; } getcwd(file_dir,size); strcat(file_no_comment, argv[1]); strcat(file_no_comment, "-1.txt"); strcat(file_struct, argv[1]); strcat(file_struct,"-2.txt"); strcat(file_result, argv[1]); strcat(file_result,"-struct-result.txt"); printf("out file is %s\n", file_result); yyin=fopen(argv[1],"r"); if (!yyin) { printf("error file %s\n",argv[1]); return 0; } yyout=fopen(file_no_comment, "w+"); if (!yyout) { printf("error out file\n"); return 0; } BEGIN COMMENT; yylex(); fclose(yyin); fclose(yyout); yyin=fopen(file_no_comment,"r"); if (!yyin) { printf("error file %s\n",file_no_comment); return 0; } yyout=fopen(file_struct, "w+"); if (!yyout) { printf("error out file\n"); return 0; } BEGIN STRUCT; yylex(); fclose(yyin); fclose(yyout); remove(file_no_comment); yyin=fopen(file_struct,"r"); if (!yyin) { printf("error file %s\n",file_no_comment); return 0; } yyout=fopen(file_result, "w+"); if (!yyout) { printf("error out file\n"); return 0; } BEGIN TAG; yylex(); fclose(yyin); fclose(yyout); remove(file_struct); return 0; }
上面这个代码的功能,是对输入的源码,执行去除所有注释的功能;再将源码中的结构体输出到后缀为-struct-result.txt的文件中。说下一具体思路:
第一遍,只开启BEGIN COMMENT;的规则,这样将输的源码,全部去除注释后,写入到后缀为-1.txt的文件中。
第二遍,只开启BEGIN STRUCT的规则,修改yyin yyout,将第一次的输出-1txt文件作为输入,识别出所有的struct ,写入到-2.txt的文件中。
第三遍,只开启BEGIN TAG的规则, 修改yyin yyout,将第二次的输入-2.txt文件作用输入,将 struct 头与struct中的元素,加上TTT_HEAD/TTT_ELE的标示,最后输出到-struct-result-txt文件中。因为后面我需要用PYTHON来解析结构体,解析处理时不方便识别struct的名称与里面的结构体,所以这个第三边也是必须的。
初写lex时,由于网上都是写基础资料,都是针对一个小功能进行解析,所以不知道怎么在一个lex脚本里面完成多个功能,故当时写了三个lex程序去做上面三个事情。后面突然看到BEGIN XXX这个用法后,逆向思维了下,觉得靠谱,果然写出来了。这个lex的例子比较少,大部分资料都是介绍怎么用正则表达式,感觉目前是牛人不屑于写一般的例子,而初学者只能写简单例子,所以几乎很少看到能用的中等例子。个人的这个例子,可以在一个lex中处理三个小功能。可能有人会说,你搞复杂了,没必要分三次来解析。其实我想目前我还不是非常精通lex框架处理流程,所以只能分三次解析。因为如果不分三次解析的话,lex的一些特点,比如匹配最长的字串,可能会在二次处理注释与结构体时有一些遗漏或互相干扰。所以干脆来个分三次处理,免得干扰。
所有代码在linux下完成,执行如下构建:
lex example
gcc lex.yy.c -o parse -ll
这样就得出了可以去除注释,并且识别struct的parse程序。这个parse file_name 已经可将file_name中的结构体解析到file_name-struct-result.txt中了。
说一下里面的表达式,
comment_t (\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(\/\/.*) 这个我认为是核心,基本上我花了半天多功夫才找到这个去除C注释的表达式。在一个国人写的帖子里面,而且他引用了一个老外的例子,对所有注释格式进行了说明,解释了为什么这个又长又丑陋的表达式是必须的。老外的帖子我暂时不记得了,感兴趣的“必应”下(顺便吐槽下百度搜索,收学术资料基本收不到)。
string_t [a-zA-Z0-9_\-]+
struct[ \t]+.{string_t}[ \t]*[\n]?\{([^}]|[\r\n])*\}\; 是用来识别C结构体的表达式。注意这里\r\n可能又一些遗留,在linux下是没问题的,在windows下,可能对有\n的地方必须有\r,我没有试过windows。
为了让多个表达式直接不互相影响,(前面说了,lex有个最长匹配等混淆),所有我使用了BEGIN 开关,做了三次处理,每次只处理一个类型,防止多个类型直接互相影响。我觉得每次只处理一个类型是比较好的,这样就不会因为lex自身的规则等互相干扰。
基本上,这样后,通过执行 ./parse file_path 就可以再 file_path-struct-result.txt 中得到struct的结构体了。后面我们需要用python对结构体进行二次分析。
2)用python 来解析结构体文件。
这里说点后话,我最终发现利用我的解析来分析ovs代码时,得到了一个很大的关系图,大的无法想象,有近1000个struct。打开一开,乱的很,基本没法看了,因为有太多结构体图了,一度到了900个结构体之多,而且每个结构体直接用线连起来,实在是没法看。而且我发现,绝大部分都是一个孤立的结构体,即这个结构体就它自己,没有与其他结构体有连接关系,这类就不是我们想要的结果。所以果断忽略孤岛结构体,不绘制这不部分。不说了,上python代码。
cat parse-struct.py #!/usr/bin/env python import os import sys import string import getopt import copy parse_tool = '/home/data_struct_show/parse' #this is lex tools path, please modify in you os FILE_TYPE=['.c', '.h'] RESULT_STRUCT=[] count = 0 frame_height = 30 frame_weight = 300 frame_space = 100 def file_type_ok(fileName): global FILE_TYPE for x in FILE_TYPE: if fileName.endswith(x): return True return False def do_parse(fileName): if 'struct-result.txt' in fileName: return if file_type_ok(fileName): cmd = '%s %s' %(parse_tool, fileName) os.system(cmd) def filter_struct_name(name): str_head=name str_head=str_head.replace('{', '') str_head=str_head.replace('\t', ' ') str_head=str_head.strip() return str_head def filter_struct_ele(name): str_ele=name str_ele=str_ele.replace('{', '') str_ele=str_ele.replace('\t', ' ') str_ele=str_ele.strip() return str_ele def judge_element_is_fun(name): if '(' in name and ')' in name: return True return False def judge_element_is_strut(name): if ('struct ' in name) and not ('(' in name and ')' in name): return True return False def get_struct_element_name(name): str = name keys =['struct ', 'const ', ] for x in keys: str = str.replace(x,'') str = str.strip() if str.find(' ')!=-1 and str[:str.index(' ')]: str = str[:str.index(' ')] elif str.find('\t')!=-1 and str[:str.index('\t')]: str = str[:str.index('\t')] else: return None return str def get_struct_name(data): ret = {} index = 0 for m in data: name = m[0] name = name.replace('struct ', '') name = name.strip() if name in ret: pass else: ret[name]=[index] index +=1 for n in m[1:]: if 'struct ' in n: #n = n.replace('struct ', '') n = n.replace(';','') n = n.strip() ret[name].append(n) return ret def get_struct(fileName): global RESULT_STRUCT #print fileName if 'struct-result.txt' in fileName: handle = open(fileName) lines = handle.readlines() handle.close() os.remove(fileName) one_struct=[] for line in lines: if 'TTT_HEAD:' in line: if len(one_struct) != 0: RESULT_STRUCT.append(one_struct) one_struct = [] str_head = line[line.index('TTT_HEAD:')+len('TTT_HEAD:'):-1] str_head = filter_struct_name(str_head) one_struct.append(str_head) elif 'TTT_ELE:' in line: str_ele = line[line.index('TTT_ELE:')+len('TTT_ELE:'):-1] str_ele = filter_struct_ele(str_ele) one_struct.append(str_ele) else: continue def walk_dir(dirName, do_work): global count; if(not os.path.isdir(dirName)): #print dirName count +=1 do_work(dirName) return files = os.listdir(dirName) for oneFile in files: temp = os.path.join(dirName, oneFile) if(os.path.isdir(temp)): walk_dir(temp, do_work) else: #print(temp) count += 1 do_work(temp) def draw_graph(data): fobj = open('./graph', 'w+') str_head = 'digraph G { \n \ node [shape=record,height=.1]; \n' fobj.write(str_head) #fobj.write(str) str_ship='' str_filter=set() for x in data: index = 1 for y in data[x][1:]: if judge_element_is_strut(y): ele_struct_name = get_struct_element_name(y) if ele_struct_name is not None and ele_struct_name in data: if '*' not in y: str='node%d: f%d->"node%d":f%d[dir=none color="red"];\n' %(data[x][0],index,data[ele_struct_name][0],0) else: str='node%d: f%d->"node%d":f%d;\n' %(data[x][0],index,data[ele_struct_name][0],0) str_filter.add(data[x][0]) str_filter.add(data[ele_struct_name][0]) str_ship +=str index +=1 print '--list',str_filter str_node='' for x in data: if data[x][0] not in str_filter: continue str='node%d[label ="{<f0>%s' %(data[x][0], x) index = 1 for y in data[x][1:]: if not judge_element_is_fun(y): str+='|<f%d>%s' %(index,y) index +=1 str +='}"];\n' str_node+=str; fobj.write(str_node) fobj.write(str_ship) fobj.write('}\n') fobj.close() def main(): try: opts, args = getopt.getopt(sys.argv[1:], "d:", ["dir="]) except: print("para error" + ' ' + str(sys.argv[1:])) sys.exit(1) dirPath = [] for opt, arg in opts: if opt in ('-d', '--dir'): dirPath = arg.split(':') continue else: print('para error') sys.exit(1) for dir in dirPath: walk_dir(dir, do_parse) walk_dir(dir, get_struct) print 'total file is %d' %count struct_dit = get_struct_name(RESULT_STRUCT) draw_graph(struct_dit) os.system('dot -Tpng graph -o graph.png') #draw_pic(RESULT_STRUCT) if __name__ == '__main__': main()
parse-struct.py -d xxxx_dir1_path:xxxx_dir1_path
多个目录之间用:分隔开,没有就只写一个目录,这个脚本会将目录下面所以C代码中所有struct全部绘制处理,最后的关系图是graph.png。
这里需要在Linux 上yum install graphviz 来安装graphviz工具,其实就是最后调用的那个命令是graphviz提供的。
这个代码,可能写的比较戳,但基本没有啥费力气的。做了如下功能:
1)读取目录,遍历目录,对目录里面的所有文件调用前面的parsy 工具,得到-struct-result.txt的文件。
2)将所有-struct-result.txt里面的结构体,读取出来。然后根据一个结构体,是否里面嵌套了其他结构体,去判断这二者结构体是否有关系。对有关系的结果体,全部都标示出来。
3)利用graphviz的dot语言,对2中所有有关系的结构体,写出符合dot描述的graph文件,最后直接用dot -Tpng graph -o graph.png命令绘制出来。
里面对结构体里面的函数进行了截断出来,或者干脆不显示结果体里面的函数。所有嵌套的结构体,用红色的线画出,对于指针个数的结构体,用箭头前画出。
代码里面有一个draw_pic,这个函数没用。我开始是准备自己来画图的,利用pil。但是后面再网上看另一个开源的graphviz工具,这个dot语音真是太牛逼了,只需要安装一定的格式去描述结构体,他自己都会讲关系图给我画出来。所以我再一次站在了巨人的肩膀上,直接用了这个画图工具。有关graphviz的资料,还是看官网:
http://www.graphviz.org/Documentation.php 这个工具真是太强大了。
来看看几个最终效果图:
最后来一个巨无霸,本来1000多个结构体的,去掉孤岛结构体后,勉强还可以看:)貌似超过2M了,算了。
后话:lex yacc 这两个东西,以前只知道有,没想到功能真是太强大了。现在真的是有这个感觉,学习编程到一个阶段后,发现所有的语言,无论是过程式、面向对象式、函数式等这些,掌握它们都已经不是问题,甚至觉得学习语言本身太浅,反而应该回归到了编译原理上面,可以武断的说,编译原理才是真正的语言之语言。
相关文章推荐
- HDFS源码分析(三)-----数据块关系基本结构
- 数据结构与算法学习(一)顺序存储结构ArrayList源码分析
- 【OpenVswitch源码分析之六】内核空间转发面数据结构与流程
- ffmpeg教程六——源码分析之数据结构
- KVM的vMMU相关数据结构及其影子页表关系分析
- linux源码-TCP/IP协议栈学习预备(1) 数据结构之各socket之间的关系
- [Java数据结构]从源码分析HashMap
- HDFS源码分析心跳汇报之数据结构初始化
- Twemproxy源码分析(三)数据结构(队列)
- HDFS源码分析心跳汇报之数据结构初始化
- redis源码分析之数据结构(一)链表adlist.c
- 源码分析redis的有序集合,学习skiplist跳跃表数据结构
- 数据结构与算法学习(二)链式存储结构LinkedList源码分析
- Twemproxy源码分析(四)数据结构(array和string)
- HDFS源码分析(三)-----数据块关系基本结构
- [Java数据结构]从源码分析HashMap
- linux内核源码分析(内存管理)--之数据结构
- Wordpress源码分析 目录结构-文件调用关系(1)
- <java EE 项目:Musicstore>项目结构分析: 项目的3层结构之间的关系 :(表示层,业务层,数据层)
- android系统源码分析——binder基础数据结构