您的位置:首页 > 理论基础 > 数据结构算法

用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不能用注释,我试过// /**/在规则中都会导致一定问题,所以代码里面先没写注释。
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 这两个东西,以前只知道有,没想到功能真是太强大了。现在真的是有这个感觉,学习编程到一个阶段后,发现所有的语言,无论是过程式、面向对象式、函数式等这些,掌握它们都已经不是问题,甚至觉得学习语言本身太浅,反而应该回归到了编译原理上面,可以武断的说,编译原理才是真正的语言之语言。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  lex linux struct 结构体