Symbian开发过程中,如果应用程序碰到了比如空指针,访问错误,活动对象调度错误等时,应用程序会直接Panic,获取的信息顶多只有Panic的Category和Panic的错误号。这种信息虽然能起到一定的提示,但是实在是太少了,空指针这样的错误随时都可能存在。然后,测试人员告诉你今天程序崩溃了,又崩溃了,留下一堆长长的log让你分析。在某些运气不好的情况下,程序死了,但是log没有刷新并写到文件中,这个时候就相当憔悴了,log信息缺失将导致错误的追踪。
如果是按照这种工作方式,无疑开发效率是极其低效的。会让程序员恶心得不行,自然会抱怨程序员是苦力活。既然是个难题,我们就应该想方法去解决它。在Nokia的wiki上关于调试相关的帖子看到这样一篇帖子http://wiki.forum.nokia.com/index.php/Trace_Function_Enter,_Exit_and_Leave。里面提到了利用日志追踪函数的进入与退出,以及异常Leave。这里面利用到了一个局部变量自动析构的概念。无论碰到正常的return,leave,栈中的局部变量就会自动析构,如果是异常退出,则std::uncaught_exception()会返回真。阅读我的这篇帖子前请读懂上面说的这篇帖子。
好了,灵感就来源于这篇帖子。我最终利用Symbian的C/S体系结构,加上Global Chunk构造的栈结构,以及自动化的添加peal脚本和makefile实现自动化宏添加和配置。因为我用的是Carbide C++的开发环境,这些环境都随着开发环境的建立而建立了。
最终,我实现的目标是一个自动化的,跟踪进程崩溃时的调用栈信息保留的工具。该工具实现了源码级别的追踪。
以下是我的实现理论和具体部分代码
1. 工具框架结构

2.Global Chunk的设计
Global Chunk是一段记录Client端运行状况的空间,它用于保留Client端的运行时刻栈中的调用信息。因此,它的数据结构的设计也是模拟系统栈的。按照如下方式组织这段内存:

Chunk 块中的最低的4个字节,记录sp的信息,保存了栈顶元素到top的偏移量;
Chunk块中的第4到第8个字节,记录了栈中元素的个数信息;
注意:栈的结构是由高地址向低地址进行压栈的,这也是系统运行栈的方式。
每个item的前4个字节(地址由低到高),分别记录了item的类型以及item的size,也就是图中我为每个item所花的两个小矩形。其中右边的低地址两个字节记录了item的类型信息,左边的两个字节记录了item的大小。
采用这种方式是由于入栈和弹栈都能很轻易的得到对象信息,同时item支持变长。
3.TTracer
TTracer是一个特殊的类,这个类的设计只是为了追踪函数的进入和退出。Nokia wiki中那篇贴中提到,在这个类的构造函数和析构函数中做一些标志性的事情,比方说简单的打log。
在我的工具架构中,该类的构造函数是像Global Chunk中压入一个字符串元素代表进入的函数;析构函数则是根据是否异常的标志决定是否给Server端发送消息,并将栈顶的元素弹出。
4.自动化
1)TRACER宏的自动化
从解决方案上看,最终落实到开发者的身上时,就是要在每个函数前加上一段TRACER宏。这部分工作其实很固定,就是把每个函数第一句话加入这个TRACER宏。在Nokia wiki上这篇帖子上的perl基本上已经提供了那个功能,但是存在缺陷。就是你每对源文件执行一次这个perl脚本,就会添加一个#include的头文件语句以及在每个函数再重复添加相同的TRACER宏语句。我对这段脚本进行了改写,贴出来给大家用吧:
#perl add_traces.pl
die "Specify an input file!/n" if $ARGV[0] eq "";
die "Specify an output file!/n" if $ARGV[1] eq "";
die "File not found!/n" unless -e $ARGV[0];
die "Incorrect file extension for a C/C++ file!/n"
if ( $ARGV[0] !~ /(.*)/.(c|cpp)$/ );
# Constants
my $INC_TRACER_H = "#include /"TTracer.h/"/n";
my $TRACERVAR = "TRACER";
# Parse output filename from the input filename
my $file = $ARGV[0];
my $origFile = $ARGV[1];
$file =~ s///////g;
$origFile =~ s///////g;
print("file:$file,origFile:$origFile/n");
system( "copy /V /Y $file, $origFile" );
# Reset the input record separator (newline) so the entire file is read at once
undef $/;
# Read the input file
$_ = <>; # All there
# Adds a tracer macro after each function definition
s/
((/b/w*?/b[&*]?)? # Possible function return type
/s+ # One or more empty spaces
(/b/w+?/b) # Class name
/s*? # Possible empty space
:: # ::
/s*? # Possible empty space
(~?/b/w+?/b) # Function name
/s*? # Possible empty space
/( # Opening brace
([^)]*?) # Possible function parameters
/) # Closing brace
/s*? # Possible empty space
(const)? # Possible 'const'
[^{;//]*? # Possible empty space or constructor
# initializer list
/{) # Opening curly brace
(/s*)
($TRACERVAR/(/"(/b/w*?/b[&*]?)? /s*(/b/w+?/b)::(~?/b/w+?/b)/(([^)]*?)/)(const)? /"/))?
/
Parse($&,$1,$2,$3,$4,$5,$6,$7,$8) # Print the match and add the macro
/gxe; # g = repeat, x = split regex to multiple lines, e = evaluate substitution
open OUT, ">$file" or die "Cannot open file $file $!/n";
$pos = index($_, $INC_TRACER_H);
if($pos < 0){
print OUT $INC_TRACER_H;
}
print OUT;
close OUT;
exit 0;
sub Parse {
my $match = shift;
my $wholefunc = shift;
my $ret = shift;
my $class = shift;
my $func = shift;
my $param = shift;
my $const = shift;
my $space = shift;
my $tracer = shift;
foreach ($ret, $class, $func, $param ) {
s/^/s+|/s+$//g;
s//n//g;
}
my $tracerfunc = "";
$tracerfunc .= $ret." " if defined $ret;
$tracerfunc .= $class."::".$func."(";
$tracerfunc .= $param if $param ne "";
$tracerfunc .= ")";
$tracerfunc .= " ".$const if defined $const;
my $output_tracer .= $TRACERVAR."(/"".$tracerfunc."/")";
my $debug = $match;
if(not defined $tracer){ #no TRACER macro
$debug=~s//s*$//;
$debug.="/n ".$output_tracer.$space;
}else{
if($tracer ne $output_tracer){ # TRACER macro is out of date
$debug = $wholefunc;
$debug .= "/n ".$output_tracer;
}
}
return $debug;
}
用法: perl add_traces.pl input_file output_file(output_file:文件的备份)
上面这段脚本解决了Nokia wiki中那篇帖子的代码的缺陷,可以多次重复调用它不会造成重复添加TRACER宏语句,并会根据函数信息的修改而改变TRACER宏中的语句;
2)perl脚本执行到每个源文件
剩下的问题就是如何让这个脚本自动化的执行到每个工程的源文件呢,就是编译前步骤。在这里我采用了makefile的解决方案。
Symbian的bld.inf是工程编译的配置文件,里面支持makefile的选项。例如:资源文件的编译就是利用makefile进行的。
makefile可以干很多事情,配合shell的武力是无比强大,并且自动检查依赖以及执行。
bld.inf中的makefile都会被执行,我将我写的这个makefile调整到bld.inf的第一个选项中,每一次都是第一个被执行。在编译的过程中,MAKMAKE的伪目标将被执行,好,就利用它了。以下是我写的自动话的makefile:
include SOURCES
BACKUP_DIR := ../backup
SRC :=$(foreach DIR,$(SRC_DIR),$(wildcard $(DIR)/*.cpp) $(wildcard $(DIR)/*.c))
SRC_DIR_WITHOUT_UP :=$(patsubst ../%,%,$(SRC_DIR))
BACKUP_DIR_ORDER :=$(foreach DIR,$(SRC_DIR_WITHOUT_UP), $(subst /,/, $(BACKUP_DIR)/$(DIR)))
BACKUP_FILES :=$(patsubst ../%.c, $(BACKUP_DIR)/%.back, /
$(patsubst ../%.cpp, $(BACKUP_DIR)/%.bak, $(SRC)))
MKDIRS := $(BACKUP_DIR_ORDER)
MKDIRS := $(sort $(MKDIRS))
all: $(BACKUP_FILES)
$(BACKUP_FILES): environment
$(BACKUP_DIR)/%.bak: ../%.c
@perl add_traces.pl $< $@
$(BACKUP_DIR)/%.bak: ../%.cpp
@perl add_traces.pl $< $@
environment:
@CMD /C FOR %1 IN ($(BACKUP_DIR_ORDER)) DO CMD /C IF NOT EXIST %1 MKDIR %1
echo SRC:$(SRC)
do_nothing:
@rem do nothing
MAKMAKE : all
BLD : do_nothing
CLEAN :
LIB : do_nothing
CLEANLIB : do_nothing
RESOURCE : do_nothing
FREEZE : do_nothing
SAVESPACE : do_nothing
RELEASABLES :
FINAL : do_nothing
第一句话中的include SOURCES表示包含SOURCES文件,该文件如下:
SRC_DIR := ../src
指定源代码目录,相当于配置文件一样。
这段makefile中并不能很好的使用绝对路径,建议全部使用相对路径,并放在根目下。该makefile文件存放在group目录下。
5. 工具效果演示
工程组织如下:


将其运行之,并故意引发Panic


我认为这个工具是方便的和有力的。由于整个源代码是在工作期间写的,并切会投入到实际的生产中去,不便放出。但是如果有兴趣的同志探讨这个话题,我会很乐意,并期望能够私下交流。当然某些情况下。。。(你懂得!)我的邮箱是:wangli-whu@163.com,请不要发广告,谢谢!
我也是敏捷的爱好者,希望敏捷团队的爱好者能共同交流!
让技术更美好!