为C函数自动添加跟踪语句

为C函数自动添加跟踪语句

标签: Python 正则表达式

[TOC]

声明

本文将借助正则表达式,采用Python2.7编写脚本,自动对C代码工程中的函数添加调试跟踪语句。
正则表达式的基础知识可参考《Python正则表达式指南》一文,本文将不再赘述。
本文同时也发布于作业部落。

一. 问题提出

作者复杂的模块包含数十万行C代码,调用关系复杂。若能对关键函数添加调试跟踪语句,运行时输出当前文件名、函数名、行号等信息,会有助于维护者和新手更好地掌握模块流程。

考虑到代码规模,手工添加跟踪语句不太现实。因此,作者决定编写脚本自动实现这一功能。为保证不影响代码行号信息,跟踪语句与函数左花括号{位于同一行。

二. 代码实现

2.1 函数匹配测试

匹配C函数时,主要依赖函数头的定义模式,即返回类型、函数名、参数列表及相邻的左花括号{。注意,返回类型和参数列表可以为空,{可能与函数头同行或位于其后某行。

函数头模式的组合较为复杂。为正确匹配C函数,首先列举疑似函数头的典型语句,作为测试用例:

funcList = [
    ' static unsigned int test(int a, int b) { ',
    'INT32U* test (int *a, char b[]/*names*/) ', ' void test()',
    '#define MACRO(m) {m=5;}',
    'while(bFlag) {',  ' else if(a!=1||!b)',
    'if(IsTimeOut()){', ' else if(bFlag)',
    'static void test(void);'
]

然后,构造恰当的正则表达式,以匹配funcList中的C函数:

import sys, os, re
def ParseFuncHeader():
    regPattern = re.compile(r'''(?P<Ret>[\w\s]+[\w*]+) #return type
 \s+(?P<Func>\w+) #function name
 \s*\((?P<Args>[,/*[\].\s\w]*)\) #args
 \s*({?)\s*$''', re.X)

    for i in range(0, len(funcList)):
        regMatch = regPattern.match(funcList[i])
        if regMatch != None:
           print '[%d] %s' %(i, regMatch.groups())
           print ' %s' %regMatch.groupdict()

为简化正则表达式,未区分函数返回类型的限定符(如const, static等)和关键字(如int, INT32U等)。若有需要,可在初步匹配后进一步细化子串的匹配。注意,args项也可使用排除字符集,如排除各种运算符[^<>&|=)]。但C语言运算符繁多,故此处选择匹配函数参数中的合法字符,即逗号、注释符、指针运算符、数组括号、空白和单词字符等。

执行ParseFuncHeader()函数后,运行结果如下:

[0] (' static unsigned int', 'test', 'int a, int b', '{')
     {'Args': 'int a, int b', 'Ret': ' static unsigned int', 'Func': 'test'}
[1] ('INT32U*', 'test', 'int *a, char b[]/*names*/', '')
     {'Args': 'int *a, char b[]/*names*/', 'Ret': 'INT32U*', 'Func': 'test'}
[2] (' void', 'test', '', '')
     {'Args': '', 'Ret': ' void', 'Func': 'test'}
[7] (' else', 'if', 'bFlag', '')
     {'Args': 'bFlag', 'Ret': ' else', 'Func': 'if'}

可见,除正确识别合法的函数头外,还误将else if(bFlag)识别为函数头。要排除这种组合,可修改上述正则表达式,或在匹配后检查Func分组是否包含if子串。

2.2 插入跟踪语句

构造出匹配C函数头的正则表达式后,稍加修改即可用于实际工程中函数的匹配。

由于工程中C函数众多,尤其是短小的函数常被频繁调用,因此需控制待处理的函数体规模。亦即,仅对超过特定行数的函数插入跟踪语句。这就要求统计函数行数,思路是从函数头开始,向下查找函数体末尾的}。具体实现如下:

#查找复合语句的一对花括号{},返回右花括号所在行号
def FindCurlyBracePair(lineList, startLineNo):
    leftBraceNum = 0
    rightBraceNum = 0
    #若未找到对应的花括号,则将起始行的下行作为结束行
    endLineNo = startLineNo + 1

    for i in range(startLineNo, len(lineList)):
        #若找到{,计数
        if lineList[i].find('{') != -1:
            leftBraceNum += 1
        #若找到},计数。}可能与{位于同一行
        if lineList[i].find('}') != -1:
            rightBraceNum += 1
        #若左右花括号数目相等且不为0,则表明最外层花括号匹配
        if (leftBraceNum == rightBraceNum) and (leftBraceNum != 0):
            endLineNo = i
            break

    return endLineNo

接下来是本文的重头戏,即匹配当前文件中满足行数条件的函数,并为其插入跟踪语句。代码如下:

FUNC_MIN_LINE = 10

totalFileNum = 0; totalFuncNum = 0; procFileNum = 0; procFuncNum = 0;
def AddFuncTrace(dir, file):
    global totalFileNum, totalFuncNum, procFileNum, procFuncNum
    totalFileNum += 1

    filePath = os.path.join(dir, file)

    #识别C文件
    fileExt = os.path.splitext(filePath)
    if fileExt[1] != '.c':
        return

    try:
        fileObj = open(filePath, 'r')
    except IOError:
        print 'Cannot open file (%s) for reading!', filePath
    else:
        lineList = fileObj.readlines()

    procFileNum += 1
    #识别C函数
    lineNo = 0
    while lineNo < len(lineList):
        #若为注释行或不含{,则跳过该行
        if re.match('^.*/(?:/|\*)+.*?(?:/\*)*\s*$', lineList[lineNo]) != None \
            or re.search('{', lineList[lineNo]) == None:
            lineNo = lineNo + 1; continue

        funcStartLine = lineNo
        #默认左圆括号与函数头位于同一行
        while re.search('\(', lineList[funcStartLine]) == None:
            funcStartLine = funcStartLine - 1
            if funcStartLine < 0:
                lineNo = lineNo + 1; break

        regMatch = re.match(r'''^\s*(\w+\s*[\w*]+) #return type
 \s+(\w+) #function name
 \s*\([,/*[\].\s\w]* #patial args
 \)?[^;]*$''', lineList[funcStartLine], re.X)

        if regMatch == None \
            or 'if' in regMatch.group(2): #排除"else if(bFlag)"之类的伪函数头
            #print 'False[%s(%d)]%s' %(file, funcStartLine+1, lineList[funcStartLine]) #funcStartLine从0开始,加1为真实行号
            lineNo = lineNo + 1; continue

        totalFuncNum += 1
        #print '+[%d] %s' %(funcStartLine+1, lineList[funcStartLine])
        funcName = regMatch.group(2)
        #跳过短于FUNC_MIN_LINE行的函数
        funcEndLine = FindCurlyBracePair(lineList, funcStartLine)
        #print 'func:%s, linenum: %d' %(funcName, funcEndLine - funcStartLine)
        if (funcEndLine - funcStartLine) < FUNC_MIN_LINE:
            lineNo = funcEndLine + 1; continue

        #花括号{与函数头在同一行时,{后通常无语句。否则其后可能有语句
        regMatch = re.match('^(.*){(.*)$', lineList[lineNo])
        lineList[lineNo] = '%s{printf("%s() at %s, %s.\\n"); %s\n' \
                            %(regMatch.group(1), funcName, file, lineNo+1, regMatch.group(2))
        print '-[%d] %s' %(lineNo+1, lineList[lineNo]) ###
        procFuncNum += 1
        lineNo = funcEndLine + 1

    #return
    try:
        fileObj = open(filePath, 'w')
    except IOError:
        print 'Cannot open file (%s) for writing!', filePath
    else:
        fileObj.writelines(lineList)
        fileObj.close()

因为实际工程中函数头模式更加复杂,AddFuncTrace()内匹配函数时所用的正则表达式与ParseFuncHeader()略有不同。正确识别函数头并添加根据语句后,会直接跳至函数体外继续向下处理。但未识别出函数头时,正则表达式可能会错误匹配函数体内"else if(bFlag)"之类的语句,因此需要防护这种情况。

注意,目前添加的跟踪语句形如printf("func() at file.c, line.\n")。读者可根据需要自行定制跟踪语句,如添加打印开关。

因为源代码文件可能以嵌套目录组织,还需遍历目录以访问所有文件:

def ValidateDir(dirPath):
    #判断路径是否存在(不区分大小写)
    if os.path.exists(dirPath) == False:
        print dirPath + ' is non-existent!'
        return ''

    #判断路径是否为目录(不区分大小写)
    if os.path.isdir(dirPath) == False:
        print dirPath + ' is not a directory!'
        return ''
    return dirPath

def WalkDir(dirPath):
    dirPath = ValidateDir(dirPath)
    if not dirPath:
        return

    #遍历路径下的文件及子目录
    fileNum = 0
    for root, dirs, files in os.walk(dirPath):
        for file in files:
            #处理文件
            AddFuncTrace(root, file)

    print '############## %d/%d functions in %d/%d files processed##############' \
          %(procFuncNum, totalFuncNum, procFileNum, totalFileNum)

最后,添加可有可无的命令行及帮助信息:

usage = '''Usage:
 AddFuncTrace(.py) [options] [minFunc] [codePath]
 This program adds trace code to functions in source code.
 Options include:
 --version : show the version number
 --help : show this help
 Default minFunc is 10, specifying that only functions with
 more than 10 lines will be processed.
 Default codePath is the current working directory.'''

if __name__ == '__main__':
    if len(sys.argv) == 1: #脚本名
        WalkDir(os.getcwd())
        sys.exit()
    if sys.argv[1].startswith('--'):
        option = sys.argv[1][2:]
        if option == 'version':
            print 'Version 1.0 by xywang'
        elif option == 'help':
            print usage
        else:
            print 'Unknown Option.'
        sys.exit()

    if len(sys.argv) >= 3:
        FUNC_MIN_LINE = int(sys.argv[1])
        WalkDir(os.path.abspath(sys.argv[2]))
        sys.exit()
    if len(sys.argv) >= 2:
        FUNC_MIN_LINE = int(sys.argv[1])
        WalkDir(os.getcwd())
        sys.exit()

上述命令行参数解析比较简陋,也可参考《Python实现Linux命令xxd -i功能》一文中的optionparser解析模块。

三. 效果验证

为验证上节的代码实现,建立test调试目录。该目录下包含test.c及两个文本文件。其中,test.c内容如下:

#include <stdio.h>
/* {{{ Local definitions/variables */

unsigned int test0(int a, int b){
    int a0; int b0;}

 unsigned int test1 (int a, int b) {
    int a1; int b1;
    a1 = 1;
    b1 = 2;}

  int test2 (int a, int b)
{
    int a2; int b2;
    a2 = 1;
    b2 = 2;
}


/* {{{ test3 */
int test3(int a,
          int b)
{   int a3 = 1; int b3 = 2;
    if(a3)
    {
        a3 = 0;
    }
    else if(b3) {
        b3 = 0;
    }
}
/* }}} */

static void test4(A *aaa,
       B bbb,
       C ccc[]
       ) {
    int a4; int b4;
}

static void test5(void);
struct T5 {
  int t5;
};

考虑到上述函数较短,故指定函数最短行数为1,运行AddFuncTrace.py:

E:\PyTest>python AddFuncTrace.py 1 test
-[4] unsigned int test0(int a, int b){printf("test0() at test.c, 4.\n");

-[7]  unsigned int test1 (int a, int b) {printf("test1() at test.c, 7.\n"

-[13] {printf("test2() at test.c, 13.\n");

-[23] {printf("test3() at test.c, 23.\n");    int a3 = 1; int b3 = 2;

-[37]        ) {printf("test4() at test.c, 37.\n");

############## 5/5 functions in 1/3 files processed##############

查看test.c文件内容如下:

#include <stdio.h>
/* {{{ Local definitions/variables */

unsigned int test0(int a, int b){printf("test0() at test.c, 4.\n"); 
    int a0; int b0;}

 unsigned int test1 (int a, int b) {printf("test1() at test.c, 7.\n"); 
    int a1; int b1;
    a1 = 1;
    b1 = 2;}

  int test2 (int a, int b)
{printf("test2() at test.c, 13.\n"); 
    int a2; int b2;
    a2 = 1;
    b2 = 2;
}


/* {{{ test3 */
int test3(int a,
          int b)
{printf("test3() at test.c, 23.\n");    int a3 = 1; int b3 = 2;
    if(a3)
    {
        a3 = 0;
    }
    else if(b3) {
        b3 = 0;
    }
}
/* }}} */

static void test4(A *aaa,
       B bbb,
       C ccc[]
       ) {printf("test4() at test.c, 37.\n"); 
    int a4; int b4;
}

static void test5(void);
struct T5 {
  int t5;
};

可见,跟踪语句的插入完全符合期望。

接着,在实际工程中运行python AddFuncTrace.py 50,截取部分运行输出如下:

-[1619] {printf("bcmGetQuietCrossTalk() at bcm_api.c, 1619.\n"); 

-[1244] {printf("afeAddressExist() at bcm_hmiLineMsg.c, 1244.\n"); 

-[1300] {printf("afeAddressMask() at bcm_hmiLineMsg.c, 1300.\n"); 

-[479] uint32 stpApiCall(uint8 *payload, uint32 payloadSize, uint32 *size) {printf("stpApiCall() at bcm_stpApi.c, 479.\n"); 

############## 291/1387 functions in 99/102 files processed##############

查看处理后的C函数,插入效果也符合期望。

你可能感兴趣的:(为C函数自动添加跟踪语句)