Profil de Duo一心天堂PhotosBlogListes Outils Aide

Blog


29 octobre

windbg+vmware单机调试内核

    最近玩debug有点走火入魔了,这两天一直在搞内核调试,希望对内核的分析可以帮助找到应用崩溃的线索。试了下用windbg+vmvare虚拟机调试内核,一开始windbg怎么都连不上vmware,最后发现vmware虚拟的串口要设成The end is server,我设成client了,T_T

windbg内核调试

    windbg进行内核调试,一般要双机,很多时候是用来调试驱动,或是操作系统内核的。但是最近查了很多网上资料,本机内核调试应用程序的dmp文件的话,能提供非常多的有用信息。双机调试环境毕竟有点困难,要么用windbg+vware。但windbg非常奇怪,切换到kenerl mode后不能attach process也算了(这是user mode)。但居然连dmp都不让打开。所以如果用windbg进行本机内核调试,要使用.opendump命令,手动加载dmp文件。
24 mars

Include文件时注意

在做一个纯类声明时,例如

class A;

以后的应用,仅限于声明指针,任何包含实际意义的操作,比如调用其下函数和内存分配,都是不允许的

2 novembre

成员函数指针

这几天因为设计上得需要,突然想起用成员函数指针,老实说,基本上没有用过这个,以前都是用函数指针来做回调函数得,所以第一次使用发生了许多问题。

    具体测试代码如下:

#include <iostream>

using namespace std;

class TestA
{
public:
TestA(int a) {m_test = a;}
void test()
{
  cout<<"A的实例打印测试"<<m_test<<endl;
}
int m_test;
};

struct C {
typedef void (C::*pFunc)();
pFunc funcArray[2];
void func1(){}
void func2(){}
C() {
  funcArray[0] = &C::func1;
  funcArray[1] = &C::func2;
}
void func3() {
  (this->* funcArray[0])();
}
};

typedef void (TestA::*MEMBERFUNC)();

class TestB
{
public:
MEMBERFUNC m_pfn;
TestB()
{
  //MEMBERFUNC m_pfn = &TestA::test;
}

void testB()
{
  //m_pfn = &(ptr_a->test);
  (ptr_a->*m_pfn)();
}

TestA* ptr_a;

};

int main()
{
TestA a(0);
TestB b;
TestA c(1);

b.ptr_a = &a;
b.m_pfn = &TestA::test;
b.testB();
system("PAUSE");
}

我原本意图将B类得成员函数指针中保存指向A类得成员函数,但是编译老是出错。查看了MSDN相关得内容和网上一些关于使用成员函数指针得文档后,改成如上得样子。

具体得问题在于成员函数指针必须动态得绑定到具体类实例

因此在B类中声明A类成员函数得成员函数指针变量时,形式如typedef void (A::*func)();

这样得声明在,调用时,因为B类中会传默认得this指针,就成为了B::func,所以无法调用成功,必须在->操作符前指明指向A得指针,我因此加了ptr_a;

但这样一来,我觉得不如直接在B类中保存A类指针,那还需要用成员函数指针吗,白白增加了复杂度

通过几个MSDN例子,发现成员函数指针多用在指向自己本身得成员函数

这样看来,只有在同类,不同实例互相调用时使用会比较方便了,否则貌似没必要,大概这也是为什么它得使用不及函数指针吧

27 juin

关于基于帧的内存分配方式

在研究了目前项目使用的内存分配方式和HAVOK的内存分配方式后,发现:

我们的项目中使用的并非是基于帧的内存分配方式,而是一种很接近于帧的内存分配的方式。和大多数的游戏一样,我们使用自己的内存分配和释放函数(通过对new和delete的重载)。这里先要谈论的是在游戏中的内存管理

游戏中之所以不采用系统标准的内存管理函数,其原因是从效率上考虑的。而采用系统标准的内存管理函数的话,第一是容易产生内存碎片,第二无法添加对内存进行监控和调试的信息,第三,无法将物理内存映射为连续的逻辑内存。

为了解决以上问题(参考Game Gem1中的主要矛盾为内存碎片),发明了基于帧的内存分配方式。“帧”在这里的概念我个人理解为一个节点,而非通常意义上的视频概念里的一帧,也不是Game Gem中解释的句柄(句柄这东西实在不好理解,老手当然懂了,我觉得还是解释为节点一目了然)。所谓的“节点”就是保存对于一个特定时间点上内存操作的信息。比如什么时候分配了内存,分配了多少,什么时候释放了内存等

在基于帧的内存分配里,帧就是保存内存分配时,申请到的内存地址。比如你执行一次new,返回一个0xADD1的地址,在执行一次,返回0xADD2,那么这是我们有一个struct来保存这两个指针,也就是形成了两个帧

基于帧的内存分配的思想是,事先划分一块较大的内存供日后申请,每次申请保存帧信息。并且内存分配采用类似堆栈的方式,也就是先进后出(采用堆栈,我认为是由内存工作方式,和汇编指令工作方式造成的,先占领地位内存地址)

在保存完帧信息后,无论我们对接下来的地址如果做分配,释放。只要在回收该块内存后,使用帧信息就不会产生碎片,比如:帧中记录内存地址为0x00001111,然后对起始地址后的4K空间进行分配,第一次分配结构structA的数组,大小为24B,第二次分配结构structB的数组,数组元素大小37B,这样类似大小不一就容易产生碎片,但如果我们始终按照0x00001111的地址释放其后面的空间,就可以把碎片回收,同时反复使用了我们预先分配的内存

目前,我们的项目中是采用链表方式来完成这一功能,虽然也做到了反复利用预先申请的内存,也使用链表的后向指针来保存了类似帧的信息,但是我们采用判断申请内存是否大于现有Free块,才决定是否分配,所以如果申请内存总是比free块小一点,是会产生碎片的

在HAVOK中的基于堆栈的内存分配,在释放时,将一次性释放其后所有内存地址。同时在一些havok中的步进运算中,确实可以采用先进后出的方式来抛弃指定帧后的无用数据,加快速度和内存使用效率。

即使,在局部方法中,由系统分配堆栈,但是可能UNIX和WINDOW等操作系统将消耗额外CPU时间在逻辑上组合一块连续内存,而对于一些游戏机上无此类高级内存管理功能的情况,有可能就无法请求比较大的内存块。因为我们构建预先使用的一块堆栈用内存,并提供申请释放方式。当在局部方法中使用时,来代替系统分配的堆栈,并在方法结束时释放堆栈,就解决了以上问题。

6 mars

关于宏的一个发现

    根据代码大全,宏应该采用#define macro() ()的格式,这样不容易出错
    但是,我发现如果将一段内联函数,也就是一段语句使用宏来代替,这个格式会引发错误
 
#include <cstdlib>
#include <iostream>
using namespace std;
#define test(a) a = a + a*(a);\  ======》》这里不能加括号,只能用\来连接语句,否则会报错
                a++;
int main(int argc, char *argv[])
{
    int a = 0;
    test(a);
    system("PAUSE");
    return EXIT_SUCCESS;
}
23 novembre

字节对齐

    关于字节对齐,在编译器中可以使用的是/zp开关,或是使用#pragma编译器指令。/zpn,n代表自然数,和#pragma pack(n)的作用一样,支持1,2,4,8,16字节对齐。
    字节对齐的意思就是让数据存储在自然边界上。所谓自然边界就是指,如果是对字来说,处于偶数地址,对于双字就是能被4整除的地址,以此类推。字节对齐的好处可以让访问数据时一次性读入数据。比如,如果8字节数据,处于自然边界,并且使用8字节对齐,那么只需访问一次内存就能取得8字节数据,如果这8个字节没有处于自然边界,比如从0x00000003上开始,那么要存储到0x0000000B,但读内存的时候,需要先访问0x00000001到0x00000008,在读取0x00000009到0x0000000B,两次才能读取到8字节数据,效率低。
    字节对齐的原则我概括为以下几条
   1.第一,字节对齐是以设定的n最为最大内存宽度,并不是一次必须分配n空间来保证对齐,即对齐不是一定要你的倍数存储地址或是空间
   2.第二,如果,你要保存的数据,超过n或是n不足,那么以小的数据存储,如果剩余的空间还能存放下一个成员数据,那么就存放,如果不能存放,则要等下一个8字节空间。剩下的空间能不能存放,要根据变量的自然边界。比如先存放了5字节数据,8字节对齐情况下,long型自然边界为4字节(long的长度为4),所以要存放在下一个8字节空间。
   注:这句话在MSDN翻译的很拗口,我以自己的理解翻译了下
 
struct a
{
long s;
char s1[5];
}
 
这个结构在8字节对齐需要的存储空间为12字节,long s占用4字节(自然边界对齐),s1占8字节,因为5个char不能放在前一个8字节的4字节空间中,分配了下一个8字节空间
 
struct b
{
long s;
char s1[3];
long s3;
char s4
}
 
为16字节
14 novembre

关于虚函数的访问权限

    对虚函数已经习惯了使用public权限访问了,今天整理了一下,虚函数其实是可以用private进行修饰。其规则和用途是这样的
    1.构造函数不允许使用虚函数
    2.析构函数可以使用虚函数,甚至说必须使用虚函数,当你从基类派生时
    3.虚函数可以使用private进行访问权限修饰,但派生类不能调用基类的虚函数,它只能使用自己的虚函数。这里其实和用普通函数是一样的。即不能在外部访问私有成员函数。只能在类使用私有成员函数。当在类中访问一个虚函数时。由于传递的隐藏参数this指向的是自己,所以会调用自己的虚函数。同样,当通过基类指针访问一个派生类对象的虚函数时,传递的this指针,实际是指向派生类对象的,所以调用的还是派生类虚函数。也就是说,虚函数的访问修饰符,起的作用只是在外部调用时。它的意思是,我是虚函数,我可以被继承形成多态,但是只有我的同类知道怎样,并且可以使用我。外部人员无法使用我的多态性。这也就解释析构函数在类继承中必须是虚的,并且public,否则如下,delete c时,如果非虚析构,A就没有释放,如果是private,就无法调用~A(),编译报错。
    4.相对的,构造函数只允许内联
 
class A
{
public:
 A() {printf("虚构造函数被调用\n");}
 virtual ~A() {printf("虚析构函数被调用\n");}
 void aa() {test();}
private:
 virtual void test() {printf("test虚函数,私有在A类中被调用\n");}
};
class B : public A
{
public:
 B() {printf("B类虚构造函数被调用\n");}
 virtual ~B() {printf("B类虚析构函数被调用\n");}
 void bb() {test();}
private:
 virtual void test() {printf("test虚函数,在私有B类中被调用\n");}
};
int _tmain(int argc, _TCHAR* argv[])
{
 A a;
 B b;
 A* c = new B;
 a.aa();
 b.bb();
 b.aa();
 c->aa();
 delete c;
 system("PAUSE");
 return 0;
}
 
    不过,今天看MSDN的官网说,2005的新语法不支持访问私有虚函数,但托管C++却是可以的,因为没有VS2005环境,所以我也没有验证,有空去试试。
    今天先到这,要上班了。晚上继续程序的分析