使用 MAP file 除錯的技巧

Last Update: 03/11/2004

主要參考資料:http://www.codeproject.com/debug/mapfile.asp

引言

寫程式除錯時最難除的是什麼?就是寫好後找不到程式當掉的原因。大部份的人不外乎使用除錯器,除錯器找不到時逼不得已得用用 printf TRACE OutputDebugString 等等大法,但這些都不見得管用,真正可靠的還是要靠 MAP file 來指引。另外若程式已經進入 release mode,也安裝在客戶手上了,這時若不幸當掉了想除錯,也只有求助 MAP file。

MAP file 是 linker 將 object files 編譯成 binary (EXE, DLL, ...) 時所產生的對應表,亦即系統的 linker loader 如何載入這個 binary image 的報表。有了這個 map 檔,再加上 compiler 產生的 assembly source code,就成為找程式當機的最佳利器。

設定產生 MAP file

上頭列的超連結,裡頭說的是 VC6 的設定方法,其實 VC7 也是差不多的,在 Project 的 Properties dialog 中,選擇 Linker -> Debugging

我個人的習慣是還會到 C/C++ -> Output Files 中,將 Assembler Output 改成 Assembly, Machine Code, and Source (/FAcs),這樣打線比較全面。

如何使用 MAP file 來除錯

假設有底下的程式:

// main.cpp

void main()
{
    char* pEmpty = NULL;
    *pEmpty = 'x';
}

這個程式編譯後會產生 main.cod 和 main.map 兩個檔。執行時當然會當掉,在 Windows 2K/XP 中會跑出來一個 error reporting screen,按這個對話盒中的連結去看詳細資料,它會告訴你當在 ModName: main.exe,ModVer: 0.0.0.0,Offset 00011cc8,還有個 minidump (但按進去看這個 minidump 卻無法 copy/paste ... 只能到他列出的路徑去將該檔案 copy 起來)。

若是在 VC 中執行,VC 的 IDE 則會告訴你在 0x00411cc8 的地方發生了 Unhandled Exception。

我們有興趣的是如何從 00011cc8 或 00411cc8 這個位址去計算出錯的那一行。

先看 map file 最前面的地方,我們要找出 base address:

map_debug

 Timestamp is 404eb1b2 (Tue Mar 09 22:12:02 2004)

 Preferred load address is 00400000

 Start         Length     Name                   Class
 0001:00000000 00010000H .textbss                DATA
 0002:00000000 00012492H .text                   CODE
 0003:00000000 00002687H .rdata                  DATA
 0003:00002688 0000014aH .rdata$debug            DATA

我們較有興趣的是 load address 00400000 以及 CODE segment 之前所有段落的長度和,在這個例子裡,CODE segment 前面有 0x00010000 的長度,先記下來之後有用處。

 Address         Publics by Value              Rva+Base     Lib:Object

 0000:00000000       __except_list              00000000     
 0000:00000000       ___safe_se_handler_table   00000000     
 0000:00000000       ___safe_se_handler_count   00000000     
 0001:00000000       __enc$textbss$begin        00401000     
 0001:00010000       __enc$textbss$end          00411000     
 0002:00000a50       __RTC_InitBase             00411a50 f   LIBCD:init.obj
 0002:00000a90       __RTC_Shutdown             00411a90 f   LIBCD:init.obj
 0002:00000ab0       _mainCRTStartup            00411ab0 f   LIBCD:crt0.obj
 0002:00000ca0       _main                      00411ca0 f   main.obj

接下來第二段的部份我們要記下自己程式函數的 Offset (Rva+Base 值),通常從 Lib:Object 去查找會較快些。在這個例子中我們只有一個函數,它的 Rva+Base 為 00411ca0。

由於這個例子的程式太小,所以 linker 並不會產生如參考資料中所述的第三段 map file,也就是原始程式行號對應的位址,這並沒有太大的關係,因為我們有更精準的 .cod 檔可用。

Windows report 的是已經算好 Offset 的值,VC report 的則是未經 Offset 的值,所以你可以發現若將 VC report 的值減去 preferred load address,就可得到 Windows report 的值。

00411cc8 - 00400000 = 00011cc8

這個 Offset 是指對於整個 binary image 的 offset,而 CODE segment 之前有一個固定的長度,所以我們要將它減去,才知道對於 CODE segment 而言,它的 offset 是多少

00011cc8 - 00010000 = 00001cc8

接下來算算 main 函數的 offset 值為多少:

00411ca0 - 00400000 - 00010000 = 00001ca0

因此可以求出,發生錯誤的地方距 main 函數的 offset 為:

00001cc8 - 00001ca0 = 28

然後我們就可以把 main.cod 找出來看

; 3    : 	char* pEmpty = 0;

  0001e	c7 45 f8 00 00
	00 00		 mov	 DWORD PTR _pEmpty$[ebp], 0

; 4    : 	*pEmpty = 'x';

  00025	8b 45 f8	 mov	 eax, DWORD PTR _pEmpty$[ebp]
  00028	c6 00 78	 mov	 BYTE PTR [eax], 120	; 00000078H

Offset 28 的地方就是發生問題的指令 (mov BYTE PTR [eax], 120),由 cod 檔的資訊我們可以精準地反推問題是出在程式的第 4 行。

GCC 如何產生 MAP file

使用 gcc 要產生 .cod 檔會稍微麻煩一點:

    g++ -c -g -Wa,-a,-ad main.cpp > main.cod

不過眼尖的你應該發現,有 -g 這個參數,因此它產出的 .o 檔不能拿來 link。我們下了 -Wa,-a,-ad 的命令告知 as 丟棄所有的 debug directives,但若要求保險的話,請另外再用 g++ -S main.cpp 產生 main.s 檔來對照一下 production code 的 assembly 是否相同。本例中產生的 .cod 片段如下:

    31                    main:
  32 0000 55                    pushl %ebp
  33 0001 89E5                  movl %esp,%ebp
  34 0003 83EC18                subl $24,%esp
   1:main.c        **** int main()
   2:main.c        **** {
  36                    .LM1:
   3:main.c        ****         char* p = 0;
  38                    .LM2:
  39                    .LBB2:
  40 0006 C745FC00              movl $0,-4(%ebp)
  40      000000
   4:main.c        ****         *p = 'x';
  42                    .LM3:
  43 000d 8B45FC                movl -4(%ebp),%eax
  44 0010 C60078                movb $120,(%eax)
   5:main.c        **** }

map 檔是由 ld 產生的,不過一般我們還是交由 gcc 來代勞:

    g++ -o main main.o -Wl,-Map,main.map

當程式當掉時,你可用 gdb 看一下 core 的資訊:

    gdb main core.19320

沒有 debug info 的情形下,gdb 只會告訴你 #0 0x080483d0 in main () 而已,但有比沒有好,因為若你的程式用了 strip -name 處理過,則你連 main() 這個東西都看不到!接下來找一下 map file,由於 ELF 格式執行檔要比 Windows 用的 PE 格式簡潔,沒有太多 offset 尋找的問題,你只需要找到 main() 的 base address:

 .text          0x080482d0       0x24 /usr/lib/crt1.o
                0x080482d0                _start
 .text          0x080482f4       0x24 /usr/lib/crti.o
 *fill*         0x08048318        0x8 2425393296
 .text          0x08048320       0xa0 /usr/lib/gcc-lib/i686-pc-linux-gnu/2.95.3/
crtbegin.o
 .text          0x080483c0       0x18 main.o
                0x080483c0                main
 *fill*         0x080483d8        0x8 2425393296

所以已知 main 函數位址 0x080483c0,掛掉的地方是 0x080483d0,因此 offset 為 0x00000010,便很快可由 .cod 檔中找出確實的地點。