[关闭]
@cloverwang 2016-05-18T12:03:03.000000Z 字数 7407 阅读 2160

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

Python 正则表达式


声明

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

一. 问题提出

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

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

二. 代码实现

2.1 函数匹配测试

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

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

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

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

  1. import sys, os, re
  2. def ParseFuncHeader():
  3. regPattern = re.compile(r'''(?P<Ret>[\w\s]+[\w*]+) #return type
  4. \s+(?P<Func>\w+) #function name
  5. \s*\((?P<Args>[,/*[\].\s\w]*)\) #args
  6. \s*({?)\s*$''', re.X)
  7. for i in range(0, len(funcList)):
  8. regMatch = regPattern.match(funcList[i])
  9. if regMatch != None:
  10. print '[%d] %s' %(i, regMatch.groups())
  11. print ' %s' %regMatch.groupdict()

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

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

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

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

2.2 插入跟踪语句

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

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

  1. #查找复合语句的一对花括号{},返回右花括号所在行号
  2. def FindCurlyBracePair(lineList, startLineNo):
  3. leftBraceNum = 0
  4. rightBraceNum = 0
  5. #若未找到对应的花括号,则将起始行的下行作为结束行
  6. endLineNo = startLineNo + 1
  7. for i in range(startLineNo, len(lineList)):
  8. #若找到{,计数
  9. if lineList[i].find('{') != -1:
  10. leftBraceNum += 1
  11. #若找到},计数。}可能与{位于同一行
  12. if lineList[i].find('}') != -1:
  13. rightBraceNum += 1
  14. #若左右花括号数目相等且不为0,则表明最外层花括号匹配
  15. if (leftBraceNum == rightBraceNum) and (leftBraceNum != 0):
  16. endLineNo = i
  17. break
  18. return endLineNo

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

  1. FUNC_MIN_LINE = 10
  2. totalFileNum = 0; totalFuncNum = 0; procFileNum = 0; procFuncNum = 0;
  3. def AddFuncTrace(dir, file):
  4. global totalFileNum, totalFuncNum, procFileNum, procFuncNum
  5. totalFileNum += 1
  6. filePath = os.path.join(dir, file)
  7. #识别C文件
  8. fileExt = os.path.splitext(filePath)
  9. if fileExt[1] != '.c':
  10. return
  11. try:
  12. fileObj = open(filePath, 'r')
  13. except IOError:
  14. print 'Cannot open file (%s) for reading!', filePath
  15. else:
  16. lineList = fileObj.readlines()
  17. procFileNum += 1
  18. #识别C函数
  19. lineNo = 0
  20. while lineNo < len(lineList):
  21. #若为注释行或不含{,则跳过该行
  22. if re.match('^.*/(?:/|\*)+.*?(?:/\*)*\s*$', lineList[lineNo]) != None \
  23. or re.search('{', lineList[lineNo]) == None:
  24. lineNo = lineNo + 1; continue
  25. funcStartLine = lineNo
  26. #默认左圆括号与函数头位于同一行
  27. while re.search('\(', lineList[funcStartLine]) == None:
  28. funcStartLine = funcStartLine - 1
  29. if funcStartLine < 0:
  30. lineNo = lineNo + 1; break
  31. regMatch = re.match(r'''^\s*(\w+\s*[\w*]+) #return type
  32. \s+(\w+) #function name
  33. \s*\([,/*[\].\s\w]* #patial args
  34. \)?[^;]*$''', lineList[funcStartLine], re.X)
  35. if regMatch == None \
  36. or 'if' in regMatch.group(2): #排除"else if(bFlag)"之类的伪函数头
  37. #print 'False[%s(%d)]%s' %(file, funcStartLine+1, lineList[funcStartLine]) #funcStartLine从0开始,加1为真实行号
  38. lineNo = lineNo + 1; continue
  39. totalFuncNum += 1
  40. #print '+[%d] %s' %(funcStartLine+1, lineList[funcStartLine])
  41. funcName = regMatch.group(2)
  42. #跳过短于FUNC_MIN_LINE行的函数
  43. funcEndLine = FindCurlyBracePair(lineList, funcStartLine)
  44. #print 'func:%s, linenum: %d' %(funcName, funcEndLine - funcStartLine)
  45. if (funcEndLine - funcStartLine) < FUNC_MIN_LINE:
  46. lineNo = funcEndLine + 1; continue
  47. #花括号{与函数头在同一行时,{后通常无语句。否则其后可能有语句
  48. regMatch = re.match('^(.*){(.*)$', lineList[lineNo])
  49. lineList[lineNo] = '%s{printf("%s() at %s, %s.\\n"); %s\n' \
  50. %(regMatch.group(1), funcName, file, lineNo+1, regMatch.group(2))
  51. print '-[%d] %s' %(lineNo+1, lineList[lineNo]) ###
  52. procFuncNum += 1
  53. lineNo = funcEndLine + 1
  54. #return
  55. try:
  56. fileObj = open(filePath, 'w')
  57. except IOError:
  58. print 'Cannot open file (%s) for writing!', filePath
  59. else:
  60. fileObj.writelines(lineList)
  61. fileObj.close()

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

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

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

  1. def ValidateDir(dirPath):
  2. #判断路径是否存在(不区分大小写)
  3. if os.path.exists(dirPath) == False:
  4. print dirPath + ' is non-existent!'
  5. return ''
  6. #判断路径是否为目录(不区分大小写)
  7. if os.path.isdir(dirPath) == False:
  8. print dirPath + ' is not a directory!'
  9. return ''
  10. return dirPath
  11. def WalkDir(dirPath):
  12. dirPath = ValidateDir(dirPath)
  13. if not dirPath:
  14. return
  15. #遍历路径下的文件及子目录
  16. for root, dirs, files in os.walk(dirPath):
  17. for file in files:
  18. #处理文件
  19. AddFuncTrace(root, file)
  20. print '############## %d/%d functions in %d/%d files processed##############' \
  21. %(procFuncNum, totalFuncNum, procFileNum, totalFileNum)

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

  1. usage = '''Usage:
  2. AddFuncTrace(.py) [options] [minFunc] [codePath]
  3. This program adds trace code to functions in source code.
  4. Options include:
  5. --version : show the version number
  6. --help : show this help
  7. Default minFunc is 10, specifying that only functions with
  8. more than 10 lines will be processed.
  9. Default codePath is the current working directory.'''
  10. if __name__ == '__main__':
  11. if len(sys.argv) == 1: #脚本名
  12. WalkDir(os.getcwd())
  13. sys.exit()
  14. if sys.argv[1].startswith('--'):
  15. option = sys.argv[1][2:]
  16. if option == 'version':
  17. print 'Version 1.0 by xywang'
  18. elif option == 'help':
  19. print usage
  20. else:
  21. print 'Unknown Option.'
  22. sys.exit()
  23. if len(sys.argv) >= 3:
  24. FUNC_MIN_LINE = int(sys.argv[1])
  25. WalkDir(os.path.abspath(sys.argv[2]))
  26. sys.exit()
  27. if len(sys.argv) >= 2:
  28. FUNC_MIN_LINE = int(sys.argv[1])
  29. WalkDir(os.getcwd())
  30. sys.exit()

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

三. 效果验证

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

  1. #include <stdio.h>
  2. /* {{{ Local definitions/variables */
  3. unsigned int test0(int a, int b){
  4. int a0; int b0;}
  5. unsigned int test1 (int a, int b) {
  6. int a1; int b1;
  7. a1 = 1;
  8. b1 = 2;}
  9. int test2 (int a, int b)
  10. {
  11. int a2; int b2;
  12. a2 = 1;
  13. b2 = 2;
  14. }
  15. /* {{{ test3 */
  16. int test3(int a,
  17. int b)
  18. { int a3 = 1; int b3 = 2;
  19. if(a3)
  20. {
  21. a3 = 0;
  22. }
  23. else if(b3) {
  24. b3 = 0;
  25. }
  26. }
  27. /* }}} */
  28. static void test4(A *aaa,
  29. B bbb,
  30. C ccc[]
  31. ) {
  32. int a4; int b4;
  33. }
  34. static void test5(void);
  35. struct T5 {
  36. int t5;
  37. };

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

  1. E:\PyTest>python AddFuncTrace.py 1 test
  2. -[4] unsigned int test0(int a, int b){printf("test0() at test.c, 4.\n");
  3. -[7] unsigned int test1 (int a, int b) {printf("test1() at test.c, 7.\n"
  4. -[13] {printf("test2() at test.c, 13.\n");
  5. -[23] {printf("test3() at test.c, 23.\n"); int a3 = 1; int b3 = 2;
  6. -[37] ) {printf("test4() at test.c, 37.\n");
  7. ############## 5/5 functions in 1/3 files processed##############

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

  1. #include <stdio.h>
  2. /* {{{ Local definitions/variables */
  3. unsigned int test0(int a, int b){printf("test0() at test.c, 4.\n");
  4. int a0; int b0;}
  5. unsigned int test1 (int a, int b) {printf("test1() at test.c, 7.\n");
  6. int a1; int b1;
  7. a1 = 1;
  8. b1 = 2;}
  9. int test2 (int a, int b)
  10. {printf("test2() at test.c, 13.\n");
  11. int a2; int b2;
  12. a2 = 1;
  13. b2 = 2;
  14. }
  15. /* {{{ test3 */
  16. int test3(int a,
  17. int b)
  18. {printf("test3() at test.c, 23.\n"); int a3 = 1; int b3 = 2;
  19. if(a3)
  20. {
  21. a3 = 0;
  22. }
  23. else if(b3) {
  24. b3 = 0;
  25. }
  26. }
  27. /* }}} */
  28. static void test4(A *aaa,
  29. B bbb,
  30. C ccc[]
  31. ) {printf("test4() at test.c, 37.\n");
  32. int a4; int b4;
  33. }
  34. static void test5(void);
  35. struct T5 {
  36. int t5;
  37. };

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

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

  1. -[1619] {printf("bcmGetQuietCrossTalk() at bcm_api.c, 1619.\n");
  2. -[1244] {printf("afeAddressExist() at bcm_hmiLineMsg.c, 1244.\n");
  3. -[1300] {printf("afeAddressMask() at bcm_hmiLineMsg.c, 1300.\n");
  4. -[479] uint32 stpApiCall(uint8 *payload, uint32 payloadSize, uint32 *size) {printf("stpApiCall() at bcm_stpApi.c, 479.\n");
  5. ############## 291/1387 functions in 99/102 files processed##############

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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注