DOOM3代码放出来也好久了,过年的时候抽空看了看,先更新第一篇,主要是编译的一些问题笔记,以及用自动生成的TypeInfo表进行未初始化变量检查的机制分析。
编译的问题
Can’t load default.cfg
需要手动在FileSystem.cpp中把下面这行fs_basepath初始化改成自己的目录:
idCVar idFileSystemLocal::fs_basepath( "fs_basepath", "Z:\\Docs\\Project_X\\Ref_Engines\\Doom3", CVAR_SYSTEM | CVAR_INIT, "" );
这时因为在编译gamex86.dll时,会先执行一次TypeInfo的生成,这时会用默认路径去加载default.cfg。
win_input.cpp里面包含了一些Unicode的字符串,中文系统下的VS2010编译有这些字符的文件时,会把当前系统字符集(比如gb2312)无法表示的Unicode字符自动替换成“?”。导致编译失败,手动修改一下就好了。不过VS这种行为还是很奇怪的
最新代码把移除的阴影渲染代码又加回去了,不过要做一些小修改,才能在VS中编译
1.在glext.h中把__STDC_VERSION__定义为0(非GCC编译器)
2.在TypeInfo.cpp里面会在预编译头前面强行把private和protected重新定义成public,以方便进行变量初始化和检查工作。不过最新代码显然把一些文件漏了,所以会出现无法访问private变量的编译错误,手动把这些类全部改成public即可。。。
// This is real evil but allows the code to inspect arbitrary class variables.
#define private public
#define protected public
#include "../../idlib/precompiled.h"
3.从VS启动游戏的设置
在Debugging面板中
设置Command为:$(OutDir)DOOM3.exe,不能用默认的$(TargetPath)
否则gamex86.dll不能正常加载.这个问题很是奇怪,宏展开两者的结果应该是一样的。
设置Command Line Arguments:
+set fs_basepath "Z:\Docs\Ref_Engines\Doom3" +set com_allowConsole 1 +set si_pure 0
Doom3的内存调试机制分析
C/C++引擎的特点就是内存越界,泄漏,未初始化变量等Bug非常头疼,特别是在游戏逻辑非常复杂之后,加上一些多线程行为,所以一旦出这种bug,是非常难调试的,所以一般会在引擎一开始就引入一套完善的内存统计和调试机制。这样也能精确的控制游戏各个模块的内存使用。对Console来说这个也非常重要。Doom3引入了一套比较复杂的内存调试机制。除了常规的重载内存分配机制。还加入了一套TypeInfo生成机制。下面主要分析一下这套机制。在编译gamex86.dll的过程中,会在game/gamesys下生成一个GameTypeInfo.h文件。这个文件只有在ID_DEBUG_MEMORY宏定义的情况下才会包含进工程。
#ifdef ID_DEBUG_MEMORY
#include "GameTypeInfo.h" // Make sure this is up to date!
#else
#include "NoGameTypeInfo.h"
#endif
//在Debug With Memory的工程设置里面有这个编译条件的定义:
//ID_REDIRECT_NEWDELETE;ID_DEBUG_MEMORY;ID_DEBUG_UNINITIALIZED_MEMORY;%(PreprocessorDefinitions)
//GameTypeInfo.h里面生成的表结构是这样的:
typedef struct {
const char * name;
const char * type;
const char * value;
} constantInfo_t;
typedef struct {
const char * name;
int value;
} enumValueInfo_t;
typedef struct {
const char * typeName;
const enumValueInfo_t * values;
} enumTypeInfo_t;
typedef struct {
const char * type;
const char * name;
int offset;
int size;
} classVariableInfo_t;
typedef struct {
const char * typeName;
const char * superType;
int size;
const classVariableInfo_t * variables;
} classTypeInfo_t;
static constantInfo_t constantInfo[] = {
{ NULL, NULL, NULL }
};
static enumTypeInfo_t enumTypeInfo[] = {
{ NULL, NULL }
};
static classTypeInfo_t classTypeInfo[] = {
{ NULL, NULL, 0, NULL }
};
也就是所有的变量的类型信息,名字,相对对象起始地址的内存偏移量。需要注意的是:所有的模板类是不生成类型信息的,因为模板会在编译期生成多种实例代码。Bit Fields也不生成,因为无法获得其地址偏移,不支持多重继承,只建立单一继承的链表关系。所有的变量类型信息都是带词法信息的,比如类里面包含的constant是这样的:
{ “int”, “idMapPrimitive::TYPE_PATCH”, “1″ },第一个是类型,第二个是名字,第三个是常量值
对于类变量则是这样的:
{ “idList < const function_t * >“, “functions”, (int)(&((idTypeDef *)0)->functions), sizeof( ((idTypeDef *)0)->functions ) },
最复杂的是第三项,这个表达式其实是这个变量在类实例中的内存偏移量。这样,在运行时,只要有类实例的指针,加上这个offset,就是这个变量的内存区域起始地址了。
TypInfo信息是这样使用的:
1.在CreateInstance的时候检查是否有构造函数未初始化的变量存在。类实例创建之后,立即调用FindUninitializedMemory函数,这个函数会检查是否有内存特殊标记值的存在,如果发现,则查询TypeInfo信息表,获得类名和变量名。输出警告。
void idClass::FindUninitializedMemory( void ) {
#ifdef ID_DEBUG_UNINITIALIZED_MEMORY
unsigned long *ptr = ( ( unsigned long * )this ) - 1;
int size = *ptr;
assert( ( size & 3 ) == 0 );
size >>= 2;
for ( int i = 0; i < size; i++ ) {
if ( ptr[i] == 0xcdcdcdcd ) {
const char *varName = GetTypeVariableName( GetClassname(), i << 2 );
gameLocal.Warning( "type '%s' has uninitialized variable %s (offset %d)", GetClassname(), varName, i << 2 );
}
}
#endif
}
const char *GetTypeVariableName( const char *typeName, int offset ) {
static char varName[1024];
int i;
for ( i = 0; classTypeInfo[i].typeName != NULL; i++ ) {
if ( idStr::Cmp( typeName, classTypeInfo[i].typeName ) == 0 ) {
if ( classTypeInfo[i].variables[0].name != NULL && offset >= classTypeInfo[i].variables[0].offset ) {
break;
}
typeName = classTypeInfo[i].superType;
if ( *typeName == '\0' ) {
return "<unknown>";
}
i = -1;
}
}
const classTypeInfo_t &classInfo = classTypeInfo[i];
for ( i = 0; classInfo.variables[i].name != NULL; i++ ) {
if ( offset <= classInfo.variables[i].offset ) {
break;
}
}
if ( i == 0 ) {
idStr::snPrintf( varName, sizeof( varName ), "%s::<unknown>", classInfo.typeName );
} else {
idStr::snPrintf( varName, sizeof( varName ), "%s::%s", classInfo.typeName, classInfo.variables[i-1].name );
}
return varName;
}
2.在创建时的检查没有递归遍历指针对象,所以可能会有一定的遗漏,所以在idRestoreGame::CreateObjects的时候,会对每一个创建出来的对象,执行idTypeInfoTools::InitTypeVariables。将所有变量初始化为一个特殊值。同时检查指针指向的对象里面是否有没有初始化的变量。
void idTypeInfoTools::InitTypeVariables( const void *typePtr, const char *typeName, int value ) {
idTypeInfoTools::fp = NULL;
idTypeInfoTools::initValue = value;
idTypeInfoTools::Write = InitVariable;
WriteClass_r( typePtr, "", typeName, "", "", 0 );
}
递归的查询TypeInfo表,把每一个变量的内存值都填成一个特殊值(比如0xCE)。同时在这个过程中,检查是否有变量还是没有初始化过的值(在分配内存时,会把内存值全部填充成0xCDCDCDCD)。如果有,则报警。
void idTypeInfoTools::WriteClass_r( const void *classPtr, const char *className, const char *classType, const char *scope, const char *prefix, const int pointerDepth ) {
int i;
const classTypeInfo_t *classInfo = FindClassInfo( classType );
if ( !classInfo ) {
return;
}
if ( *classInfo->superType != '\0' ) {
WriteClass_r( classPtr, className, classInfo->superType, scope, prefix, pointerDepth );
}
for ( i = 0; classInfo->variables[i].name != NULL; i++ ) {
const classVariableInfo_t &classVar = classInfo->variables[i];
void *varPtr = (void *) (((byte *)classPtr) + classVar.offset);
WriteVariable_r( varPtr, classVar.name, classVar.type, classType, prefix, pointerDepth );
}
}
//设置对象变量的初始值
void idTypeInfoTools::InitVariable( const char *varName, const char *varType, const char *scope, const char *prefix, const char *postfix, const char *value, const void *varPtr, int varSize ) {
if ( varPtr != NULL && varSize > 0 ) {
// NOTE: skip renderer handles
if ( IsRenderHandleVariable( varName, varType, scope, prefix, postfix, value ) ) {
return;
}
memset( const_cast<void *>(varPtr), initValue, varSize );
}
}
//检查是否有变量是否初始化的代码如下,如果没有初始化,就打印出详细的变量信息
do {
if ( *((unsigned long *)varPtr) == 0xcdcdcdcd ) {
common->Warning( "%s%s::%s%s uses uninitialized memory", prefix, scope, varName, "" );
break;
}
} while( ++i < typeSize );
基本原理就是这样的,遇到指针和数组的时候,情况会更复杂一些,总的来说是提供了一套在运行时动态检查所有变量的机制。在看这个代码的是否我就在想,ID的静态分析工具是怎么用的?后来看到一篇卡马克的博客,发现他们也是静态分析的狂热支持者,不断的寻求更好的静态分析工具,这篇文章非常值得一看。PC-Lint我们也试用过一段时间,但是最后还是没有用起来。其他的几个工具准备好好评估下。毕竟引擎代码库的质量对项目来说非常关键。
No related posts.