为C函数自动添加跟踪语句
为C函数自动添加跟踪语句
标签: Python 正则表达式
声明
本文将借助正则表达式,采用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
#遍历路径下的文件及子目录
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函数自动添加跟踪语句的更多相关文章
- Excel 提供数据 更新或者插入数据 通过函数 自动生成SQL语句
excel 更新数据 ="UPDATE dbo.yt_vehicleExtensionBase SET yt_purchase_date='"&B2&"' ...
- 总结:自动将函数对象添加到字典的bug
介绍 本文以ATM项目为背景,介绍一个比较实用的编程技巧,使用装饰器将项目中的指定函数添加到字典中. 利用字典通过key访问value的特点,实现用户输入编号,通过字典直接获取并调用编号对应的功能函数 ...
- 使用Excel自动生成sql语句
在近一段日子里,进入了新的项目组,由于项目需要,经常要将一些Excel表中的数据导入数据库中,以前并没有过多的接触过数据导入与数据处理,对于我来说比较痛苦,今天下午花了几个小时处理数据,但是同事给我提 ...
- C#实现为类和函数代码自动添加版权注释信息的方法
这篇文章主要介绍了C#实现为类和函数代码自动添加版权注释信息的方法,主要涉及安装文件的修改及函数注释模板的修改,需要的朋友可以参考下 本文实例讲述了C#实现为类和函数代码自动添加版权注释信息的方法 ...
- ADO.NET 根据实体类自动生成添加修改语句仅限Oracle使用
话不多说直接上代码,新手上路,高手路过勿喷,请多多指教. /// <summary> /// 等于号 /// </summary> ) + Convert.ToChar() + ...
- Excel 用row()函数 在Excel中自动添加序号,
1.如图 2.用if条件根据产品名称判断是否有值进而序号自动添加 If(G9="","",Row()-8)
- python logging详解及自动添加上下文信息
之前写过一篇文章日志的艺术(The art of logging),提到了输出日志的时候记录上下文信息的重要性,我认为上下文信息包括: when:log事件发生的时间 where:log事件发生在哪个 ...
- php post get 繁体、日文、韩文时 自动添加 反斜杠 问题
做些二次开发项目,数据库.文件编码没法大规模的修改,比如二次开发一个日文系统,编码是JA16SJIS,$_POST或$_GET的信息中如果“申請”,得到的信息就会变成“申\請”,多出一个反斜杠! 先贴 ...
- VS 自动添加注释
现在大多数公司都规定程序员在程序文件的头部加上版权信息,这样每个人写的文件都可以区分开来,如果某个文件出现问题就可以快速的找到文件的创建人,用最短的时间来解决问题,常常是以下格式: //======= ...
随机推荐
- float:浮点型double:双精度实型decimal:数字型单精度浮点数(Single)双精度浮点数(double)
单精度浮点数(Single) 双精度浮点数(double) Decimal为SQL Server.MySql等数据库的一种数据类型,不属于浮点数类型,可以在定义时划定整数部分以及小 ...
- ThreadLocal 简介 案例 源码分析 MD
Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...
- Java代码常见的十种错误
每一个程序员在编写代码的过程中都免不了出现错误或是小的失误,这些小的错误和失误往往使得程序员还得返工.那么,如何才能尽量避免这些错误的发生呢?笔者总结只有在日常的编写代码中总结出经验,在这篇文章中,笔 ...
- 几种php加速器比较
一.PHP加速器介绍 PHP加速器是一个为了提高PHP执行效率,从而缓存起PHP的操作码,这样PHP后面执行就不用解析转换了,可以直接调用PHP操作码,这样速度上就提高了不少. Apache中使用mo ...
- Online
上线前的准备: 1.是在本地测试的时候,以为页面上没有php的warning代码就是正确的,没有查看apache的error_log中相关的记录,然后就直接上线了.
- 结巴分词和自然语言处理HanLP处理手记
手记实用系列文章: 1 结巴分词和自然语言处理HanLP处理手记 2 Python中文语料批量预处理手记 3 自然语言处理手记 4 Python中调用自然语言处理工具HanLP手记 5 Python中 ...
- PL/SQL学习笔记之数据类型中的标量、LOB
一:标量 标量 即 基本数据类型,主要有4种:数值.字符.布尔类型.日期时间. 1:数值类型 数据类型 描述 PLS_INTEGER 通过2,147,483,647到-2147483648范围内有符号 ...
- dos 批量重命名 bat
@echo off setlocal enabledelayedexpansion echo %var% set /a i = i + var for %%x in (*) do ( if not & ...
- 6种常见的Git错误以及解决的办法
我们都会犯错误,尤其是在使用像Git这样复杂的东西时.如果你是Git的新手,可以学习如何在命令行上开始使用Git.下面介绍如何解决六个最常见的Git错误. Photo by Pawel Janiak ...
- MVC通用控件库展示-MVC4.0+WebAPI+EasyUI+Knockout--SNF快速开发平台3.0
在我们开发中怎么才能提高效率,就是要有大量的公共组件(控件)可以直接使用而不用自己再开发一遍,既然是公共控件那也得简单实用才行.下面就介绍一下SNF-MVC当中的控件库. 总体控件库展示: 1.通用用 ...