寒假捉蟲記——從一段損壞的調用棧開始折騰

  放假在家,繼續調試《家園》。目前的進度是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的初始化會麻煩很多。正如《關於遊戲開發,學校沒有教給我的十件事》一文寫道:“瞭解和理解是不一樣的……當你做一個項目時,你可能會想‘我知道怎麼做’。然而,除非你之前做過,否則你僅僅是有怎麼做的想法。‘我知道’和‘我有一個如何做的想法’是不同的,它們的區別可能會讓你頭疼好幾個小時。”想必這是每個做過實際項目的程序員的感受。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章