关于代码阅读分析工具的思考
每当阅读逻辑复杂的代码时,首先都想弄清函数之间的调用关系,然后想在适当的位置打上断点(或者移除断点),或者想看看某个函数被调用了多少次。很多代码阅读器、编辑器都已经提供了这些基本的功能,甚至提供的功能比我们想象的要强大很多,下图为SourceInsight和VS2012的函数调用关系图。但有时候总不能满足我们的所有需求。其中有一种情况是代码分析工具都是基于静态分析(有动态的吗?),无法确定函数的调用顺序。所以我乐此不疲的写一个又一个版本的调试代码,如果把VS的调试功能比做大海,我的代码只是浪花,但是浪花也有美的一瞬间。
要实现的功能
1) 能直观的输出程序运行过程中函数的调用关系。主要技术是记录调用栈的信息。
2)统计函数被调用的次数。
3)可以很方便的给某些函数下断点。
4)对源码的修改尽量少。
在上面所述的四点中,第4条尤为重要,如果在函数里面加入过多的调试代码,即增加了维护的工作量,又破坏了代码原来的结构。我以前写过很多个版本的调试代码,有一个技术问题一直困扰着我。为了记录调用栈的信息,我在函数的开头插入一条代码,当函数被调用的时候就会call我们的代码,我们就知道此函数被调用了。但是,我们要如何知道一个函数已经执行完成了呢?因为一个函数有很多个return的地方。最笨的方法是在每个return前插入代码。上述的版本我实现过,效果差强人意,是利用类似词法分析来完成函数和return的检测的,然后自动在相应位置插入相应代码。这样一来源码就面目全非了。现在我完全抛弃这种做法,只需要在每个函数的开始插入一个宏就行了。这是缘于有一次我在一个项目中看到了析构函数的巧妙使用之后的结果,我顿时老泪纵横,喜极而泣。巧妙的利用析构函数可以判断一个函数是否被执行完成,当在函数中声明一个局部变量的结构体之后,退出函数之前会自动调用结构体的析构函数。这是本“项目”唯一的亮点,如果要我再说一个亮点,那就是对Lua的使用,还有__FUNCTION__,至于__FUNCTION__是什么东东,后面的章节为你讲解。
一个函数要输出断点、调用关系或者调用次数,都在Lua脚本中控制。代码如下:
funcTable =
{
dbg_collectargs = 2
,dbg_docall = 2
,dbg_dofile = 2
,dbg_dolibrary = 2
,dbg_dostring = 2
,dbg_dotty = 2
=2为只输出调用次数,=3为同时输出次数和调用关系,=1为出发一个断点。只要我们知道了当前栈的深度信息,那么我们可以很容易的做到输出有格式的函数调用关系,
下面为Lua.c代码的调用关系。怎么样,比VS2012要直观吧?这样我们在阅读代码的时候就可以有的放矢了。
main
{
pmain
{
collectargs{ }
handle_luainit{ }
runargs{ }
handle_script
{
getargs{ }
docall{ }
report{ }
}
}
finalreport{ }
}
如果在一个函数中又调用了其他函数,它的花括号写成两行,如果其中没有别的函数,花括号写在函数命之后。同样上面的格式微调是利用一个Lua脚本完成的,这比起在C中实现要简单很多。如果要在C中实现这样的功能必须要记录每个函数的父函数,如果当前函数被调用的时候,说明它的夫函数是有孩子函数,最后再来选择括号的输出形式。而后期的处理将会大大简化这些复杂的逻辑。所有在C语言的实现中,无论说明情况都将花括号分两行输出。下面是处理格式的Lua脚本:
function TidyOutput()
f = io.open("output.txt", "r")
--[[for line in f:lines() do
print(line)
end]]
text = f:read("*all")
f:close()
match = string.gmatch(text, "[^\n]+")
tlines = {}
for value in match do
tlines[#tlines + 1] = value
end
print(#tlines)
----[[
for i = 2, #tlines do
if ((string.find(tlines[i], "^%s*{%s*$")) and (string.find(tlines[i+1], "^%s*}%s*$")))then
tlines[i-1] = tlines[i-1] .. "{ }"
tlines[i] = ""
tlines[i+1] = ""
end
end
--]]
f = io.open("output.txt", "w")
for index, value in ipairs(tlines) do
if (value ~= "")then f:write(value.."\n")end
end
f:close()
end
#define DEBUGFLAG \
int act = auxdbg.Action(__FUNCTION__); \
DebugClass setdebug(__FUNCTION__, act); \
if(act == 1) __asm int 3;
上面代码是最关键的代码,有了这段代码我们只需要在函数体开始的时候插入“DEBUGFLAG”宏定义了。DEBUGFALG由三部分组成:
1)int act = auxdbg.Action(__FUNCTION__),__FUNCTION__是编译器给予我们的礼物,我们很轻松就能得到函数的名称,__FUNCTION__即当前的函数名。有了函数名称接下来的action就好理解了。根据函数名称在lua脚本中寻找对应的值,觉得是什么action。
2)DebugClass setdebug 是整个工程的精髓,就是我们上面提到的那个局部结构体,声明的时候表示进入了某个函数,析构的时候表明退出了某个函数。
3)当act返回是1的时候,就产生断点。之所以不在Action函数里面产生断点,是因为觉得有点麻烦,调试的时候要多走一层函数,其实效果差不多。
struct AuxDebug
{
lua_State * m_debug_State;
static int s_callStackIndex;
static FILE * s_fp;
int Init_State();
int Action(const char *funName);
void Free_State();
AuxDebug();
~AuxDebug();
static void StackDump(lua_State *L);
static void Error(lua_State *L, const char *fmt, ...);
};
struct DebugClass
{
int m_curAction;
DebugClass(const char *fname, int act);
~DebugClass();
};
上面是程序中两个主要的结构体。记录栈深度的s_callStackIndex是一个静态变量,在DebugClass的构造和析构中使用:构造的时候s_callStackIndex ++,析构的时候s_callStackIndex --,其他几个函数和Lua模块的初始化有关系。
Lua的使用
上面已经把主要的功能讲的七七八八了,随便提一下Lua的使用。我假设读者对里面涉及的Lua知识非常的了解,如果不了解的话,请看看本博客的前面几个章节的内容。Lua知识一个辅助工具,你可以用python,甚至不用脚本语言都可以实现上面的功能。Lua解放了劳动力:
1)函数名称写在Lua的table中,我们可以不用自己做文本的处理,且可以任意的修改格式。
2)函数次数的统计结果存放在Lua的table中,虽然C++的map、数组也可以胜任,但是Lua的几句代码就可以完成了。
3)就是我前面提及的对输出格式的优化。
代码软件的结构
代码一个有三个文件组成:auxdebug.cpp,auxdebug.h, fordebug.lua。只需要将三个文件加入你的工程,包含头文件“auxdebug.h”,然后在函数中插入"DEBUGFLAG",记得要修改lua中的函数名表。
扩展功能
目前,插入函数第一句的宏代码是手动插入的,其实完全可以用代码实现。
总结
本文在编写调试代码的同时,给出了写类似功能代码的时候,需要使用的三个利器:析构函数、编译器的礼物(__FUCNTION__等)、脚本。这比起自己写一个词法解析器来说简单很多。你觉得如何?