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
- Generate Map File -> Yes
- Map Exports -> Yes
- Map Lines -> Yes
我個人的習慣是還會到 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 檔中找出確實的地點。
http://www.cchsu.com/arthur/prg_bg5/map_debug.htm