用lex分析C源码中数据结构关系拓扑图

程序=数据结构+算法。最近在看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 这两个东西,以前只知道有,没想到功能真是太强大了。现在真的是有这个感觉,学习编程到一个阶段后,发现所有的语言,无论是过程式、面向对象式、函数式等这些,掌握它们都已经不是问题,甚至觉得学习语言本身太浅,反而应该回归到了编译原理上面,可以武断的说,编译原理才是真正的语言之语言。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值