寒假捉虫记——从一段损坏的调用栈开始折腾

  放假在家,继续调试《家园》。目前的进度是MinGW上的编译链接都已通过,游戏程序也已经可以跑起来并进入主菜单界面,但加载关卡之后就会闪退。这让我想起了以前上中学时玩盗版游戏的日子。那个年代的单机游戏估计大多是用C/C++写的,一个不小心的内存操作就会让进程崩掉;而且那个年代的操作系统没现在稳定,可能破解技术也不够先进,从电脑城里买来的五六块钱的盗版游戏质量参差不齐。很多游戏跑着跑着就闪退,有的甚至连打都打不开,让人甚为恼火。如今源代码在手,并且我也是程序员了,可以对闪退的原因一探究竟,再也不用怕。

  不过让人失望的是,用MinGW构建出的程序不会像Linux程序那样在崩溃时吐核。还好这回的闪退是可以必现的,所以就在gdb中运行程序,看看它崩在什么地方。

  结果程序如预期崩溃后,调用栈成了下面这个样子:

Program received signal SIGSEGV, Segmentation fault.

0x0ddcf5c0 in ?? ()

(gdb) bt

#0  0x0ddcf5c0 in ?? ()

#1  0xabababab in ?? ()

#2  0x0000abab in ?? ()

#3  0x00000000 in ?? ()

(gdb)

  看样子调用栈已经坏掉了。记得以前在老东家遇到过这种损坏的调用栈,但后来很喜感地发现原来是机器的内存坏了。我相信我家电脑还没有到如此风烛残年的地步。

  在StackOverflow上搜到一篇帖子:http://stackoverflow.com/questions/9809810/gdb-corrupted-stack-frame-how-to-debug,正好是我想问的问题:如何在gdb中调试这种已经损坏的调用栈。帖子里的答案说这种情况99%是因为调用了非法的函数指针。在32位环境中可以用如下方法恢复调用栈:

(gdb) set $pc = *(void **)$esp

(gdb) set $esp = $esp + 4

  可是我检查了一下esp寄存器指向的内存:

(gdb) p $esp

$1 = (void *) 0x22fa34

(gdb) x $esp

0x22fa34:       0x0000007f

(gdb) x/i 0x7f

0x7f:        Cannot access memory at address 0x7f

(gdb)

  0x7f显然不可能是一个合法的指令地址。看来我落到剩下那1%的区间里了=A=。

  把这事情分享到朋友圈里后,主程建议我让程序链接tcmalloc试试,看看能否让程序在应用代码进行非法内存操作时就崩溃,兴许那时调用栈还没损坏。可是试过后情况并无改变。不过这倒是提醒了我,以后不妨让自己的程序都链接tcmalloc,这样可以让很多问题都提前暴露。顺便写下,我的tcmalloc链接选项是-L/local/lib -ltcmalloc_minimal -fno-builtin-malloc -fno-builtin-calloc -fno-builtin-realloc
-fno-builtin-free。

  无奈,最后还是通过打日志和单步调试的方法,通过应用代码本身的逻辑定位到了具体崩溃位置。原来程序崩在了一个OpenGL接口——glDrawElements的调用。多留日志和熟练掌握项目代码逻辑真是重要啊。

  如果是在工作中,我的排查工作一般在这一步也就该结束了。因为我从未接触过OpenGL,所以此时我应该让有OpenGL经验的同事来帮忙处理。不过这回不是在工作而是在玩耍,所以我打算满足下自己用牛刀杀鸡、用导弹打蚊子的癖好,好好研究一番,一是看看能否在gdb中恢复出调用栈,二是研究下glDrawElements这个调用为啥会崩溃,趁机接触下OpenGL。

调用栈的恢复

  先看看崩溃的直接原因是什么,看看崩溃时执行的汇编指令是什么:

(gdb) x/i $pc

=> 0xddcf5c0:   mov    (%esi),%edi

  看来esi寄存器里存了一个非法内存地址。

(gdb) p/x $esi

$3 = 0xfeee4

(gdb) p *(void **)$esi

Cannot access memory at address 0xf00d4

(gdb)

  果然如此。

  再看看这之前还执行了什么指令。

(gdb) x/40i $pc-90

0xc54f7a6:   add    %al,(%eax)

0xc54f7a8:   add    %al,(%eax)

0xc54f7aa:   add    %al,(%eax)

0xc54f7ac:   add    %al,(%eax)

0xc54f7ae:   add    %al,(%eax)

0xc54f7b0:   add    %al,(%eax)

0xc54f7b2:   add    %al,(%eax)

0xc54f7b4:   add    %al,(%eax)

0xc54f7b6:   add    %al,(%eax)

0xc54f7b8:   cmp    $0xff,%bh

0xc54f7bb:   incl   0x550000f7(%eax)

0xc54f7c1:   mov    %esp,%ebp

0xc54f7c3:   push   %ebx

0xc54f7c4:   push   %esi

0xc54f7c5:   push   %edi

0xc54f7c6:   mov    0x8(%ebp),%ebx

0xc54f7c9:   mov    0xc(%ebp),%eax

0xc54f7cc:   mov    0x14(%ebp),%ebp

0xc54f7cf:   mov    %ebp,%edi

0xc54f7d1:   shl    $0x14,%edi

0xc54f7d4:   lea    0x40003640(%edi),%esi

0xc54f7da:   mov    %esi,(%eax)

0xc54f7dc:   add    $0x4,%eax

0xc54f7df:   mov    0x1c(%esp),%edx

0xc54f7e3:   lea    (%edx,%ebp,2),%ebp

0xc54f7e6:   mov    %ebp,0x20(%esp)

0xc54f7ea:   movzwl (%edx),%ecx

0xc54f7ed:   add    $0x2,%edx

0xc54f7f0:   mov    0xc509890,%esi

0xc54f7f6:   mov    0x4(%esi),%esi

0xc54f7f9:   mov    %ecx,%edi

0xc54f7fb:   shl    $0x4,%edi

0xc54f7fe:   add    %edi,%esi

=> 0xc54f800:   mov    (%esi),%edi

0xc54f802:   mov    0x4(%esi),%ebp

0xc54f805:   mov    %edi,(%eax)

0xc54f807:   mov    %ebp,0x4(%eax)

0xc54f80a:   mov    0x8(%esi),%edi

0xc54f80d:   mov    %edi,0x8(%eax)

0xc54f810:   mov    0xc509890,%esi

(gdb)

  看样子在0xc54f7bb附近很可能有一个函数头。函数开头通常由两条汇编指令组成——第一条指令保存当前栈帧的帧底地址,第二条指令将当前的栈顶指为栈帧底,开启新栈帧:

push %ebp

move %esp %ebp

  于是从0xc54f7bc开始,一路用x命令检查:

(gdb) x/40i 0xc54f7bc

...

(gdb) x/40i 0xc54f7bd

...

(gdb) x/40i 0xc54f7be

0xc54f7be:   add    %al,(%eax)

0xc54f7c0:   push   %ebp

0xc54f7c1:   mov    %esp,%ebp

   0xc54f7c3:   push   %ebx

0xc54f7c4:   push   %esi

0xc54f7c5:   push   %edi

0xc54f7c6:   mov    0x8(%ebp),%ebx

0xc54f7c9:   mov    0xc(%ebp),%eax

0xc54f7cc:   mov    0x14(%ebp),%ebp

0xc54f7cf:   mov    %ebp,%edi

0xc54f7d1:   shl    $0x14,%edi

0xc54f7d4:   lea    0x40003640(%edi),%esi

0xc54f7da:   mov    %esi,(%eax)

0xc54f7dc:   add    $0x4,%eax

0xc54f7df:   mov    0x1c(%esp),%edx

0xc54f7e3:   lea    (%edx,%ebp,2),%ebp

0xc54f7e6:   mov    %ebp,0x20(%esp)

0xc54f7ea:   movzwl (%edx),%ecx

0xc54f7ed:   add    $0x2,%edx

0xc54f7f0:   mov    0xc509890,%esi

0xc54f7f6:   mov    0x4(%esi),%esi

0xc54f7f9:   mov    %ecx,%edi

0xc54f7fb:   shl    $0x4,%edi

0xc54f7fe:   add    %edi,%esi

=> 0xc54f800:   mov    (%esi),%edi

0xc54f802:   mov    0x4(%esi),%ebp

0xc54f805:   mov    %edi,(%eax)

0xc54f807:   mov    %ebp,0x4(%eax)

0xc54f80a:   mov    0x8(%esi),%edi

0xc54f80d:   mov    %edi,0x8(%eax)

0xc54f810:   mov    0xc509890,%esi

0xc54f816:   mov    0x54(%esi),%esi

0xc54f819:   mov    %ecx,%edi

0xc54f81b:   shl    $0x4,%edi

0xc54f81e:   add    %edi,%esi

0xc54f820:   mov    (%esi),%edi

0xc54f822:   mov    %edi,0xc(%eax)

0xc54f825:   add    $0x10,%eax

0xc54f828:   cmp    0x20(%esp),%edx

0xc54f82c:   jne    0xc54f7ea

(gdb)

  果真如此。地址0xc54f7c0和0xc54f7c1这两条指令就是典型的函数开头:

0xc54f7c0:   push   %ebp

0xc54f7c1:   mov    %esp,%ebp

  从这之后到崩溃处0xc54f800,有两处修改ebp的指令:

0xc54f7cc:   mov    0x14(%ebp),%ebp

...

0xc54f7e3:   lea    (%edx,%ebp,2),%ebp

  因此在0xc54f800: mov (%esi),%edi 崩溃的时候,寄存器中记录的就是错误的栈帧。也就是说,在gdb中查看的调用栈不正常是因为ebp被篡改了。从这两条指令还可以看出,崩溃时ebp的值取决于传入函数的参数。

  在指令0xc54f7c1: mov %esp,%ebp刚执行之后,esp的值和ebp的值是相等的。在这之后直到崩溃前,只有三条压栈指令(0xc54f7c6至0xc54f7cc)会修改esp。它们会使esp自减3个word,即3*4=12字节。分析到这里,就有办法恢复ebp的值了:

(gdb) set $ebp = $esp + 12

  通过检查esp指向的内存段可以进一步确认:

(gdb) x/8x $esp

0x22fa34:       0x0000007f      0x0c4f0000      0x0cbb6660      0x0022fa70

0x22fa44:       0x69a84ce2      0x0c4f0000      0x0cd3e900      0x0cbb6660

(gdb)

  可见栈在内存段0x22fa??附近,0x002fa70想必就是上一个栈帧的帧底,0x69a84ce2就是函数调用前的指令地址,也就是函数的返回地址。

  现在可以看到正确的调用栈了:

(gdb) bt

#0  0x0c54f800 in ?? ()

#1  0x69a84ce2 in nvoglv32!DrvPresentBuffers () from C:\Windows\system32\nvoglv32.dll

#2  0x69a85ed6 in nvoglv32!DrvPresentBuffers () from C:\Windows\system32\nvoglv32.dll

#3  0x69a86214 in nvoglv32!DrvPresentBuffers () from C:\Windows\system32\nvoglv32.dll

#4  0x695f8988 in ?? () from C:\Windows\system32\nvoglv32.dll

#5  0x0049dd33 in btgRender () at ../../../src/Game/BTG.c:1307

#6  0x00417d0c in rndBackgroundRender (radius=100000, camera=0x9259c0 <universe+32>, bDrawStars=1) at ../../../src/SDL/render.c:1287

#7  0x00419c37 in rndMainViewRenderFunction (camera=0x9259c0 <universe+32>) at ../../../src/SDL/render.c:2440

#8  0x0040d730 in mrRegionDraw (reg=0xa66a0a0) at ../../../src/SDL/mainrgn.c:5509

#9  0x0056508f in regFunctionsDraw () at ../../../src/Game/Region.c:1094

#10 0x0041c47a in rndRenderTask (taskContextPtr=0xa666710) at ../../../src/SDL/render.c:3869

#11 0x005b0bc7 in taskExecuteAllPending (ticks=4) at ../../../src/Game/Task.c:370

#12 0x0042bb26 in utyTasksDispatch () at ../../../src/SDL/utility.c:4721

#13 0x00402df6 in HWSDL_main (argc=4, argv=0x59315d8) at ../../../src/SDL/main.c:2252

#14 0x004013e0 in main (argc=4, argv=0x59315d8) at ../../src/homeworld.c:32

(gdb)

  崩溃的应用程序代码(BTG.c:1307)与通过日志和单步调试分析出的结果完全一致。

  看样子崩在了OpenGL内部。既然是崩在了glDrawElements里面,那就要研究下glDrawElements的使用,想必是API使用不当。

glDrawElements的使用

  从glDrawElements的官方文档来看,这个接口的作用是批量绘制多个基本图元(如点、线、三角形和多边形)。不过这个接口并没有参数可以直接传入顶点数据,那个indices参数只是顶点数据的索引而已。这一点让我花了很长时间琢磨。用glDrawElements作关键词搜了很多文章,基本都能看懂,但还是不知道该如何从零开始用起来。

  还是先把这函数放一放,从基本的OpenGL程序开始吧,先写个Helloworld。从网上的文章得知现代OpenGL和过去的OpenGL 1.x在用法上似乎有很大不同;而《家园》是很老的游戏了,早在1999年就已经发行,即便是HomeworldSDL的代码也非常老,更新很缓慢。所以我恐怕还得学习老式OpenGL的用法。幸运的是从这里搜到了Tutorial:http://en.wikibooks.org/wiki/OpenGL_Programming#Legacy_OpenGL_1.x。根据以下两节教程写出了一个Windows上运行的OpenGL小程序window.c:

http://en.wikibooks.org/wiki/OpenGL_Programming/GLStart/Tut1

http://en.wikibooks.org/wiki/OpenGL_Programming/GLStart/Tut2

#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>

HDC hDC; //device context
HGLRC hglrc; //rendering context

void SetupPixels(HDC hDC)
{
    int pixelFormat;
    PIXELFORMATDESCRIPTOR pfd;
    pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
    pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
    pfd.nVersion = 1;
    pfd.iPixelType = PFD_TYPE_RGBA;
    pfd.cColorBits = 32;
    pfd.cDepthBits = 24;
    pixelFormat = ChoosePixelFormat(hDC, &pfd);
    if(!SetPixelFormat(hDC, pixelFormat, &pfd))
    {
         MessageBox(NULL,"Error setting up Pixel Format","ERROR",MB_OK);
         PostQuitMessage(0);
    }
}

void Resize(int width, int height)
{
    glViewport(0,0,(GLsizei)width,(GLsizei)height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,1.0f,1000.0f);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

void Render()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glTranslatef(0.0f,0.0f,-4.0f);
    glColor3f(0.0f,0.0f,1.0f);
    glBegin(GL_POLYGON);
    glVertex3f(1.0f,1.0f,0.0f);
    glVertex3f(-1.0f,1.0f,0.0f);
    glVertex3f(-1.0f,-1.0f,0.0f);
    glVertex3f(1.0f,-1.0f,0.0f);
    glEnd();
}

LRESULT CALLBACK WinProc(HWND hWnd,
                         UINT msg,
                         WPARAM wParam,
                         LPARAM lParam)
{
    int w,h;
    switch(msg)
    {
    case WM_CREATE:
        hDC = GetDC(hWnd);
        SetupPixels(hDC);
        hglrc = wglCreateContext(hDC);
        wglMakeCurrent(hDC, hglrc);
        break;
    case WM_DESTROY:
        wglMakeCurrent(hDC,NULL);
        wglDeleteContext(hglrc);
        PostQuitMessage(0);
        break;
    case WM_SIZE:
        w = LOWORD(lParam);
        h = HIWORD(lParam);
        Resize(w,h);
        break;
    default: break;
    }
    return DefWindowProc(hWnd,msg,wParam,lParam);
}

int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,
                   int nShowCmd)
{
    HWND hWnd;
    WNDCLASSEX wcex;
    MSG msg;

    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WinProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(NULL,IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL,IDC_ARROW);
    wcex.hbrBackground = (HBRUSH) GetStockObject(GRAY_BRUSH);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = "WinClass";
    wcex.hIconSm = NULL;

    RegisterClassEx(&wcex);

    hWnd = CreateWindow("WinClass","My Window",
        WS_OVERLAPPEDWINDOW,0,0,400,400,NULL,NULL,
        hInstance,NULL);

    if(hWnd == NULL)
    {
        MessageBox(NULL,"Error: Unable to create Window","ERROR",MB_OK);
        return -1;
    }

    ShowWindow(hWnd,nShowCmd);
    UpdateWindow(hWnd);
    hDC = GetDC(hWnd);
    glClearColor(0.0f,0.0f,0.0f,0.0f);

    while(1)
    {
        Render();
        SwapBuffers(hDC);
        if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
        {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return 0;
}

  MinGW上的编译命令:gcc -o window.exe window.c -mwindows -lopengl32 -lglu32

  运行结果如下:

  程序中关键的绘制代码就是Render函数中从glBegin到glEnd的部分。

glBegin(GL_POLYGON);
glVertex3f(1.0f,1.0f,0.0f);
glVertex3f(-1.0f,1.0f,0.0f);
glVertex3f(-1.0f,-1.0f,0.0f);
glVertex3f(1.0f,-1.0f,0.0f);
glEnd();

  这段代码绘制了四个顶点,从而绘制出一个正方形。这种用法是Tutorial 3中提到的Immediate
Mode。看来如果要用glDrawElements,关键就是将这段代码替换为glDrawElements。

  现在关键是要知道如何将顶点数据传给glDrawElements。从HomeworldSDL的代码和glDrawElements相关的资料中,我注意到两个概念:Vertex Array Object(VAO)和Vertex Buffer Object(VBO)。Tutorial
3的末尾
就简略地提及了这两个概念。简要地说,VAO就是我们要传给glDrawElements的顶点数据,这些数据是以数组形式存放的。glDrawElements被调用时可以从内存里拿这个数据,也可以从显存里拿,后一种方式的性能更好。如果是从显存里拿数据,那glDrawElements拿的数据就是VBO。HomeworldSDL崩溃时用的就是VBO。具体说明可以参考OpenGL官方文档中的Vertex
Specification

  于是归纳了一下,采用VBO方式使用glDrawElements的步骤大致如下:

1、初始化,准备好顶点数据

1)用glGenBuffers申请Buffer
Object的名字,也就是为即将分配的显存申请ID。

2)用glBindBuffer绑定Buffer
Object,这样在再次调用glBindBuffer之前,接下来的Buffer Object相关的操作都是针对当前绑定的Buffer Object。target参数需是GL_ARRAY_BUFFER。

3)用glBufferData分配并初始化一段显存,将顶点数据传进显存。

4)再次调用glBindBuffer解绑Buffer Object。

2、准备好索引数组,步骤和1类似,只是调用glBindBuffer时target需是GL_ELEMENT_ARRAY_BUFFER。

3、调用glVertexPointer,指定顶点数据。

4、调用glDrawElements进行绘制。

5、如果所有绘制工作完成,之前的显存不再需使用,就要调用glDeleteBuffers释放显存。

  需要注明的是,以上只是一种简单的使用VBO的方式,并不是说glDrawElements一定要严格按照这个流程。在顶点和索引数据都不会变化的情况下,3~4两步可以反复执行。索引数组也不一定要放在显存里,可以在调用glDrawElements的时候直接通过参数将内存中的数组传进去,这时就不需要第2步。同理如果顶点数据也不用VBO的话,那第1步也省去了,glGenBuffers、glBindBuffer、glBufferData和glDeleteBuffers这几个函数都不用调用。

  很快,我的glDrawElements版本的Helloworld出炉了:

#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>

HDC hDC; //device context
HGLRC hglrc; //rendering context

#ifdef USE_VBO
GLfloat transVerts[] =
{
    1.0f, 1.0f, 0.0f,
    -1.0f, 1.0f, 0.0f,
    -1.0f,-1.0f, 0.0f,
    1.0f, -1.0f, 0.0f
};
GLuint vboTransVerts;
GLushort indices[] =
{
    0, 1, 2,
    3, 4, 5,
    6, 7, 8,
    9, 10, 11
};
GLuint vboIndices;
#endif

void SetupPixels(HDC hDC)
{
    int pixelFormat;
    PIXELFORMATDESCRIPTOR pfd;
    pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
    pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
    pfd.nVersion = 1;
    pfd.iPixelType = PFD_TYPE_RGBA;
    pfd.cColorBits = 32;
    pfd.cDepthBits = 24;
    pixelFormat = ChoosePixelFormat(hDC, &pfd);
    if(!SetPixelFormat(hDC, pixelFormat, &pfd))
    {
         MessageBox(NULL,"Error setting up Pixel Format","ERROR",MB_OK);
         PostQuitMessage(0);
    }
}

void Resize(int width, int height)
{
    glViewport(0,0,(GLsizei)width,(GLsizei)height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,1.0f,1000.0f);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

#ifdef USE_VBO
void InitVBO()
{
    glGenBuffers(1, &vboTransVerts);
    glBindBuffer(GL_ARRAY_BUFFER, vboTransVerts);
    glBufferData(GL_ARRAY_BUFFER, sizeof(transVerts), transVerts, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glGenBuffers(1, &vboIndices);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndices);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
#endif

void Render()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glTranslatef(0.0f,0.0f,-4.0f);
    glColor3f(0.0f,0.0f,1.0f);
#ifdef USE_VBO
    glEnableClientState(GL_VERTEX_ARRAY);
    glBindBuffer(GL_ARRAY_BUFFER, vboTransVerts);
    glVertexPointer(3, GL_FLOAT, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndices);
    glDrawElements(GL_POLYGON, sizeof(transVerts) / sizeof(transVerts[0]), GL_UNSIGNED_SHORT, 0);
#else
    glBegin(GL_POLYGON);
    glVertex3f(1.0f,1.0f,0.0f);
    glVertex3f(-1.0f,1.0f,0.0f);
    glVertex3f(-1.0f,-1.0f,0.0f);
    glVertex3f(1.0f,-1.0f,0.0f);
    glEnd();
#endif  // USE_VBO
}

LRESULT CALLBACK WinProc(HWND hWnd,
                         UINT msg,
                         WPARAM wParam,
                         LPARAM lParam)
{
    int w,h;
    switch(msg)
    {
    case WM_CREATE:
        hDC = GetDC(hWnd);
        SetupPixels(hDC);
        hglrc = wglCreateContext(hDC);
        wglMakeCurrent(hDC, hglrc);
#ifdef USE_VBO
        InitVBO();
#endif
        break;
    case WM_DESTROY:
#ifdef USE_VBO
        glDeleteBuffers(1, &vboTransVerts);
        glDeleteBuffers(1, &vboIndices);
#endif
        wglMakeCurrent(hDC,NULL);
        wglDeleteContext(hglrc);
        PostQuitMessage(0);
        break;
    case WM_SIZE:
        w = LOWORD(lParam);
        h = HIWORD(lParam);
        Resize(w,h);
        break;
    default: break;
    }
    return DefWindowProc(hWnd,msg,wParam,lParam);
}

int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,
                   int nShowCmd)
{
    HWND hWnd;
    WNDCLASSEX wcex;
    MSG msg;

    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WinProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(NULL,IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL,IDC_ARROW);
    wcex.hbrBackground = (HBRUSH) GetStockObject(GRAY_BRUSH);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = "WinClass";
    wcex.hIconSm = NULL;

    RegisterClassEx(&wcex);

    hWnd = CreateWindow("WinClass","My Window",
        WS_OVERLAPPEDWINDOW,0,0,400,400,NULL,NULL,
        hInstance,NULL);

    if(hWnd == NULL)
    {
        MessageBox(NULL,"Error: Unable to create Window","ERROR",MB_OK);
        return -1;
    }

    ShowWindow(hWnd,nShowCmd);
    UpdateWindow(hWnd);
    hDC = GetDC(hWnd);
    glClearColor(0.0f,0.0f,0.0f,0.0f);

    while(1)
    {
        Render();
        SwapBuffers(hDC);
        if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
        {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }

    return 0;
}

  MinGW上的编译命令:gcc -o window.exe window.c -DUSE_VBO -mwindows -lopengl32 -lglu32

  不过先别高兴太早,报错了:

$ gcc -o window.exe window.c -DUSE_VBO -mwindows -lopengl32 -lglu32

window.c: In function ‘InitVBO‘:

window.c:60:18: error: ‘GL_ARRAY_BUFFER‘ undeclared (first use in this function)

glBindBuffer(GL_ARRAY_BUFFER, vboTransVerts);

^

window.c:60:18: note: each undeclared identifier is reported only once for each function it appears in

window.c:61:67: error: ‘GL_STATIC_DRAW‘ undeclared (first use in this function)

glBufferData(GL_ARRAY_BUFFER, sizeof(transVerts), transVerts, GL_STATIC_DRAW);

^

window.c:64:18: error: ‘GL_ELEMENT_ARRAY_BUFFER‘ undeclared (first use in this function)

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndices);

^

window.c: In function ‘Render‘:

window.c:78:18: error: ‘GL_ARRAY_BUFFER‘ undeclared (first use in this function)

glBindBuffer(GL_ARRAY_BUFFER, vboTransVerts);

^

window.c:81:18: error: ‘GL_ELEMENT_ARRAY_BUFFER‘ undeclared (first use in this function)

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndices);

  原来GL/gl.h和GL/glu.h里都没有定义GL_ARRAY_BUFFER、GL_ELEMENT_ARRAY_BUFFER和GL_STATIC_DRAW。从两篇StackOverflow的帖子里得知,GL/gl.h(包括MinGW提供的)只提供了OpenGL 1.1(还是1.2?)的接口声明。不仅如此,通过nm工具,发现libopengl32.a和opengl32.dll并未导出glGenBuffers等VBO接口。

http://stackoverflow.com/questions/11951380/lots-of-undeclared-identifiers-related-to-opengl-using-glfw

http://stackoverflow.com/questions/679113/trouble-porting-opengl-app-to-windows

  于是我只好参考SDL的源代码,写出了可以工作的glDrawElements版Helloworld:

#include <windows.h>
#include <GL/gl.h>
#include <GL/glu.h>

#ifdef USE_VBO  // copied from SDL_opengl.h
#define GL_ARRAY_BUFFER                   0x8892
#define GL_ELEMENT_ARRAY_BUFFER           0x8893
#define GL_STATIC_DRAW                    0x88E4
#endif

HDC hDC; //device context
HGLRC hglrc; //rendering context

#ifdef USE_VBO
typedef ptrdiff_t GLsizeiptr;

//void * (WINAPI *wglGetProcAddress)(const char *proc);  // May need this for VC++
GLAPI void (APIENTRY *glGenBuffers)(GLsizei, GLuint *);
GLAPI void (APIENTRY *glBindBuffer)(GLenum, GLuint);
GLAPI void (APIENTRY *glBufferData)(GLenum, GLsizeiptr, const GLvoid *, GLenum);
GLAPI void (APIENTRY *glDeleteBuffers)(GLsizei, const GLuint *);

GLfloat transVerts[] =
{
    1.0f, 1.0f, 0.0f,
    -1.0f, 1.0f, 0.0f,
    -1.0f,-1.0f, 0.0f,
    1.0f, -1.0f, 0.0f
};
GLuint vboTransVerts;
GLushort indices[] =
{
    0, 1, 2,
    3, 4, 5,
    6, 7, 8,
    9, 10, 11
};
GLuint vboIndices;
#endif

void SetupPixels(HDC hDC)
{
    int pixelFormat;
    PIXELFORMATDESCRIPTOR pfd;
    pfd.nSize = sizeof(PIXELFORMATDESCRIPTOR);
    pfd.dwFlags = PFD_SUPPORT_OPENGL | PFD_DRAW_TO_WINDOW | PFD_DOUBLEBUFFER;
    pfd.nVersion = 1;
    pfd.iPixelType = PFD_TYPE_RGBA;
    pfd.cColorBits = 32;
    pfd.cDepthBits = 24;
    pixelFormat = ChoosePixelFormat(hDC, &pfd);
    if(!SetPixelFormat(hDC, pixelFormat, &pfd))
    {
         MessageBox(NULL,"Error setting up Pixel Format","ERROR",MB_OK);
         PostQuitMessage(0);
    }
}

void Resize(int width, int height)
{
    glViewport(0,0,(GLsizei)width,(GLsizei)height);
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,1.0f,1000.0f);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
}

#ifdef USE_VBO
void InitVBO()
{
#if 0  // May need the following code for VC++
    HMODULE hModule = LoadLibrary("opengl32.dll");
    if (hModule == NULL)
    {
        MessageBox(NULL, "Error: Unable to load opengl32.dll", "ERROR", MB_OK);
        exit(-1);
    }
    wglGetProcAddress = (void * (WINAPI *)(const char *)) GetProcAddress(hModule, "wglGetProcAddress");
#endif
    glGenBuffers = (GLAPI void (APIENTRY *)(GLsizei, GLuint *)) wglGetProcAddress("glGenBuffers");
    glBindBuffer = (GLAPI void (APIENTRY *)(GLenum, GLuint)) wglGetProcAddress("glBindBuffer");
    glBufferData = (GLAPI void (APIENTRY *)(GLenum, GLsizeiptr, const GLvoid *, GLenum)) wglGetProcAddress("glBufferData");
    glDeleteBuffers = (GLAPI void (APIENTRY *)(GLsizei, const GLuint *)) wglGetProcAddress("glDeleteBuffers");
    glGenBuffers(1, &vboTransVerts);
    glBindBuffer(GL_ARRAY_BUFFER, vboTransVerts);
    glBufferData(GL_ARRAY_BUFFER, sizeof(transVerts), transVerts, GL_STATIC_DRAW);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glGenBuffers(1, &vboIndices);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndices);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
#endif

void Render()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();
    glTranslatef(0.0f,0.0f,-4.0f);
    glColor3f(0.0f,0.0f,1.0f);
#ifdef USE_VBO
    glEnableClientState(GL_VERTEX_ARRAY);
    glBindBuffer(GL_ARRAY_BUFFER, vboTransVerts);
    glVertexPointer(3, GL_FLOAT, 0, 0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndices);
    glDrawElements(GL_POLYGON, sizeof(transVerts) / sizeof(transVerts[0]), GL_UNSIGNED_SHORT, 0);
    //glDrawElements(GL_POLYGON, sizeof(transVerts) / sizeof(transVerts[0]), GL_UNSIGNED_SHORT, transIndices);  // equivalent as above
    //glDrawArrays(GL_POLYGON, 0, sizeof(transVerts) / sizeof(transVerts[0]));  // equivalent as above
#else
    glBegin(GL_POLYGON);
    glVertex3f(1.0f,1.0f,0.0f);
    glVertex3f(-1.0f,1.0f,0.0f);
    glVertex3f(-1.0f,-1.0f,0.0f);
    glVertex3f(1.0f,-1.0f,0.0f);
    glEnd();
#endif  // USE_VBO
}

LRESULT CALLBACK WinProc(HWND hWnd,
                         UINT msg,
                         WPARAM wParam,
                         LPARAM lParam)
{
    int w,h;
    switch(msg)
    {
    case WM_CREATE:
        hDC = GetDC(hWnd);
        SetupPixels(hDC);
        hglrc = wglCreateContext(hDC);
        wglMakeCurrent(hDC, hglrc);
#ifdef USE_VBO
        InitVBO();
#endif
        break;
    case WM_DESTROY:
#ifdef USE_VBO
        glDeleteBuffers(1, &vboTransVerts);
        glDeleteBuffers(1, &vboIndices);
#endif
        wglMakeCurrent(hDC,NULL);
        wglDeleteContext(hglrc);
        PostQuitMessage(0);
        break;
    case WM_SIZE:
        w = LOWORD(lParam);
        h = HIWORD(lParam);
        Resize(w,h);
        break;
    default: break;
    }
    return DefWindowProc(hWnd,msg,wParam,lParam);
}
int WINAPI WinMain(HINSTANCE hInstance,
                   HINSTANCE hPrevInstance,
                   LPSTR lpCmdLine,
                   int nShowCmd)
{
    HWND hWnd;
    WNDCLASSEX wcex;
    MSG msg;
    wcex.cbSize = sizeof(WNDCLASSEX);
    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = WinProc;
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(NULL,IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL,IDC_ARROW);
    wcex.hbrBackground = (HBRUSH) GetStockObject(GRAY_BRUSH);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = "WinClass";
    wcex.hIconSm = NULL;
    RegisterClassEx(&wcex);
    hWnd = CreateWindow("WinClass","My Window",
        WS_OVERLAPPEDWINDOW,0,0,400,400,NULL,NULL,
        hInstance,NULL);
    if(hWnd == NULL)
    {
        MessageBox(NULL,"Error: Unable to create Window","ERROR",MB_OK);
        return -1;
    }
    ShowWindow(hWnd,nShowCmd);
    UpdateWindow(hWnd);
    hDC = GetDC(hWnd);
    glClearColor(0.0f,0.0f,0.0f,0.0f);
    while(1)
    {
        Render();
        SwapBuffers(hDC);
        if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
        {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    return 0;
}

  MinGW上的编译命令还是不变:gcc -o window.exe window.c -DUSE_VBO -mwindows -lopengl32 -lglu32

  这下知道glDrawElements怎么用了。再回到HomeworldSDL的代码中,我发现程序没有调用glBufferData开辟显存空间就调用了glDrawElements。想必这就是崩溃的原因。于是我将我的window.c中的glBufferData调用删除,再运行程序,果然就崩溃了,并且崩溃处的汇编代码和之前查看的基本一模一样。

  这虫子算是确确实实地捉到了。

  说实话,作为完全没接触过OpenGL的新手,我到处搜资料,花了好些时间才掌握了glDrawElements的用法。虽然Unity和CUDA的经验让我在相关概念的理解上没什么大碍,但我花了一天左右时间才真正能用这函数写一个OpenGL小程序。这是因为还有好些其它OpenGL的函数要了解;而且到最后一步还发现,为了像崩溃的代码一样,在这函数中使用OpenGL的顶点缓存对象(vertex buffer object,简称vbo),OpenGL的初始化会麻烦很多。正如《关于游戏开发,学校没有教给我的十件事》一文写道:“了解和理解是不一样的……当你坐一个项目时,你可能会想‘我知道怎么做’。然而,除非你之前做过,否则你仅仅是有怎么做的想法。‘我知道’和‘我有一个如何做的想法’是不同的,它们的区别可能会让你头疼好几个小时。”想必这是每个做过实际项目的程序员的感受。

时间: 2024-09-30 00:00:35

寒假捉虫记——从一段损坏的调用栈开始折腾的相关文章

Cocos2d-x 3.8.1+Cocos Studio 2.3.2捉虫记之控制场景文件中的骨骼动画

Cocos2d-x 3.8.1+Cocos Studio 2.3.2捉虫记之控制场景文件中的骨骼动画 引子 这段时间一直努力在把早期版本的拇指接龙游戏(Cocos2d-x 2.2.3+CocoStudio 1.4.0.1)升级到当前相对稳定的高大上环境--Cocos2d-x 3.8.1+Cocos Studio 2.3.2.行程中遇到不少麻烦,时间不知不觉像流水一样悄然逝去(这正是我时不时酸酸地想起"程序猿"三字的主要原因).想之再三,还是努力记录下来,一来为自己可能逝去的记忆,二来为

捉虫记(四)线程安全导致的HighCpu

一个朋友QQ群里说网站启动后会cpu很高,想要帮忙看一下dump. 1.打开windbg加载dump文件后第一个命令lmf,这个命令显示加载的dll以及路径,这样子可以找个dll来帮忙加载sos,(额,我记不住load那么长的路径啊) 0:000> lmf start end module name …… 00007ffb`a3750000 00007ffb`a375a000 version C:\Windows\System32\version.dll 00007ffb`a37e0000 00

MapReduce全局变量之捉虫记

Sublime 2 如何配置Java运行环境 第一步:配置JDK 之前玩过Java的都应该知道,在这里我就不赘述了. 第二步:创建runJava.bat 批处理文件 用记事本创建一个文件命名为runJava.bat 将下面的内容粘贴进记事本即可: @ECHO OFF cd %~dp1 ECHO Compiling %~nx1....... IF EXIST %~n1.class ( DEL %~n1.class ) javac -encoding utf-8 %~nx1 IF EXIST %~n

捉虫记:QT5.2 SSL握手失败问题

最近在测试项目的时候,出现了这样一个bug:在某些win7和 win8主机上,我们的客户端使用paypal进行付款时,出现SSL握手失败的问题. 项目使用QT5.2.1开发,由于QT移植了开源的webkit,我们在项目中内置了一个浏览器,用来完成商品浏览和付款. 问题来了,当然需要进行"捉虫"了. 自从上次OpenSSL爆出"心脏出血"(见wiki),我们也使用了最新的openssl代码. 首先,需要定位问题出现的位置具体在哪里. 好在QT是开源的,方便我们定位问题

PHP网站 “新手”捉虫记

我拖着疲惫的身躯,努力打开眼皮在写...... 昨晚弄到12点,我感觉应该弄好了. 故事开头是这样的:我呢朋友有个网站需要开发,我当时没时间就包给外面的公司了,由于外面公司维护费用比较贵. 那么网站维护就交给我了,我呢做软件开发很多年了,没用过PHP,所以算是新手. 年前开始的某一天网站首页突然就被修改了,点链接都停在首页,且偶尔会打开菲律宾的博彩网站. 这下我开始紧张了,这网站访问量还比较大,不能出乱子啊. 1.爱理不理 那么我赶快请外包公司的帮我看看,可能是开发阶段结束了,没有给维护费用.

捉虫记2:windows程序句柄泄露的上下文环境

作为程序员,开发程序是基本功,而调试程序也是必不可少的技能之一.软件在主体功能开发完成后会经历各个阶段的测试,才会被发布.在测试过程中,出现较多的可能就是内存泄漏,句柄泄漏,异常崩溃等属于非功能型的软件Bug.而Windows作为一个相当成熟的平台,对于软件的调试也支持很到位.今天想要记录的是这次调查的一个模块的句柄泄漏问题. 关于句柄泄漏的文章网上很多,很多关于调试的书籍中也有说明,而且有些也比较详细.之前也解决过这类的问题,所以毫不在意.先介绍一下基本情况:工作机是Windows 7 64b

[原]捉虫记3:_ConectionPtr指针调用open失败

背景 产品使用MySQL来存储报警服务产生的报警.在报警服务的组件中使用ADO接口 客户方有两台计算机,一台计算机A用来组态,且可以对设备进行调试,操作系统是Win7 64bit 专业版,安装了VS2010:另一台计算机B用作验收后生产环境中使用,操作系统是Win 2008 R2 标准版 我个人在公司的工作机的操作环境是win10 64bit 企业版 问题 在客户公司时,组态.开发.调试都是在计算机A上进行的,运行也是在计算机A上.一切都很正常.当调试完后,就从现场回到杭州,可是后续又出现了一些

捉虫记:SHGetSpecialFolderPath返回错误码为2

通常我们想获得系统的一些路径时,都会使用一些Shell函数.比如SHGetSpecialFolderPath,SHGetFolderPath,SHGetKnownFolderPath等,传入我们想要的路径的CSIDL即可.通常情况下都会得到我们想要的结果.但是也存在例外. 目前从事的工业监控软件的研发,一般的监控软件通常都是属于中大型的系统,还包括组态期和运行期,所以复杂度相对来说还是比较高的.上周测试团队报告了一个Bug,在运行期时,监控日志的保存按钮点击没反应.咋一看,就感觉好像是类似Fil

一次MySQL(INNODB存储引擎) 死锁捉虫记

前言 任何系统不管在什么阶段都需要关注生产环境错误日志,最近几个月内,发现偶尔会出现数据库死锁情况.以前碰到的数据库类错误大部分是SQL语法造成的错误,来到新东家之后才第一次碰到死锁情况,以前是搞游戏开发,现在是搞电商类开发,可能是不同的项目不同的业务的原因吧,查阅了各种资料后发现,是我想错了:(.一般业务瓶颈在数据库层,对于数据库层的问题需要重点关注,以为死锁这种情况是很严重的问题,这个要分情况,偶尔死锁对业务不会有太大的影响,我又想错了:(. 虫子发现  第一次发现死锁很惊讶,这个是什么鬼?