linux debug : addr2line追蹤出錯地址


原文鏈接

調 試zSeries上的Linux應用程序類似於調試其他體系結構上的Linux應用程序。對於有經驗的Linux開發人員,最大的挑戰是理解新的系統體系 結構。對於剛接觸Linux的大型機開發人員,掌握新的調試工具似乎是一項令人畏懼的任務。不要害怕。本文將提供一些有用的提示來幫助您入門。

學問來自實踐,但是對於調試工具,在沒有出現問題而迫使您去修復它們之前,“實踐”是不會發生的。考慮到這點,下面將提供讓您入門的“速成”指南。

User Debug 日志記錄

調試一個崩潰的程序的第一步是弄清哪里出了錯。zSeries 上的Linux內核具有這樣一個內置特性,它在用戶進程崩潰時記錄一些基本的調試信息。要啟用這個特性,請以 root 用戶身份執行如下命令:

<ccid_nobr>
<ccid_code>echo 1 >> /proc/sys/kernel/userprocess_debug

當某個進程崩潰時,日志文件(/var/log/messages)中就會給出附加的信息,包括程序終止原因、故障地址,以及包含程序狀態字(PSW)、通用寄存器和訪問寄存器的簡要寄存器轉儲。

<ccid_nobr>
<ccid_code>Mar 31 11:34:28 l02 kernel: User process fault: interruption code 0x10
Mar 31 11:34:28 l02 kernel: failing address: 0
Mar 31 11:34:28 l02 kernel: CPU: 1
Mar 31 11:34:28 l02 kernel: Process simple (pid: 30122, stackpage=05889000)
Mar 31 11:34:28 l02 kernel:
Mar 31 11:34:28 l02 kernel: User PSW: 070dc000 c00ab738
Mar 31 11:34:28 l02 kernel: task: 05888000 ksp: 05889f08 pt_regs: 05889f68
Mar 31 11:34:28 l02 kernel: User GPRS:
Mar 31 11:34:28 l02 kernel: 00000000 004019a0 004019a0 00000000
Mar 31 11:34:28 l02 kernel: 00000003 c00ab732 004008f8 00400338
Mar 31 11:34:28 l02 kernel: 40018ffc 0040061c 40018e34 7ffff800
Mar 31 11:34:28 l02 kernel: 00400434 80400624 8040066e 7ffff800
Mar 31 11:34:28 l02 kernel: User ACRS:
Mar 31 11:34:28 l02 kernel: 00000000 00000000 00000000 00000000
Mar 31 11:34:28 l02 kernel: 00000001 00000000 00000000 00000000
Mar 31 11:34:28 l02 kernel: 00000000 00000000 00000000 00000000
Mar 31 11:34:28 l02 kernel: 00000000 00000000 00000000 00000000
Mar 31 11:34:28 l02 kernel: User Code:
Mar 31 11:34:28 l02 kernel: 44 40 50 00 07 fe a7 4a 00 01 18 54 18 43 18 35
a8 24 00 00
圖 1

圖 1 表明程序(名為“simple”)以一個程序中斷代碼 0x10 終止(操作系統原理表明這是一個段轉換錯誤),而故障地址為 0。毫無疑問,有人使用了空指針。現在我們知道發生了什么,下面需要弄清它發生在何處。

基本的診斷

User Debug日志條目所提供的信息可用於確定程序的崩潰位置。一些可用的工具可幫助解決您可能會遇到的各種程序終止問題。我們將在本文中逐步介紹那些工具。

首 先,讓我們檢查一下該日志條目中的用戶 PSW。該 PSW 包含指令地址、狀態碼以及關於機器狀態的其他信息。眼下,我們僅關心指令地址(第33至第63位)。為簡化起見,讓我們假設用戶PSW是 070dc000 80400618。記住,我們是在考察一個 ESA/390(31 位尋址)PSW。第32位不是指令地址的一部分,它是指示 31 位尋址模式的標志,但是在研究 PSW 值時必須處理它。為了獲得實際的指令指針,可把PSW的第二個字減去 0x80000000。結果是一個指令地址 0x400618。為了定位代碼,您需要可執行文件中的一些信息。首先使用readelf來打印一些程序頭信息。

<ccid_nobr>
<ccid_code>Elf file type is EXEC (Executable file)
Entry point 0x400474
There are 6 program headers, starting at offset 52

Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x00400034 0x00400034 0x000c0 0x000c0 R E 0x4
INTERP 0x0000f4 0x004000f4 0x004000f4 0x0000d 0x0000d R 0x1
[Requesting program interpreter: /lib/ld.so.1]
LOAD 0x000000 0x00400000 0x00400000 0x00990 0x00990 R E 0x1000
LOAD 0x000990 0x00401990 0x00401990 0x000fc 0x00114 RW 0x1000
DYNAMIC 0x0009ac 0x004019ac 0x004019ac 0x000a0 0x000a0 RW 0x4
NOTE 0x000104 0x00400104 0x00400104 0x00020 0x00020 R 0x4

Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version
.gnu.version_r .rela.got .rela.plt .init .plt .text .fini .rodata
03 .data .eh_frame .dynamic .ctors .dtors .got .bss
04 .dynamic
05 .note.ABI-tag
圖 2

圖 2 顯示了readelf -l simple的結果(記住“simple”是我們的測試程序的名稱)。在Program Headers部分,第一個 LOAD 行提供了關於程序從哪里加載的信息。在 Flg 列,該段被標記為 R(read)E(executable)。VirtAddr是程序開始加載的地址。MemSiz是正在被加載到這個段中的代碼長度。把它加到 VirtAddr上,這個程序的基本地址范圍就是0x400000-0x400990。程序發生崩潰的指令地址為0x400618,在程序的加載范圍之 內。現在我們知道了問題直接發生在代碼中。

如果可執行文件包括調試符號,那么確定哪一行代碼導致了問題是可以做到的。對該地址和可執行文件使用addr2line 程序,如下所示:

<ccid_nobr>
<ccid_code>addr2line -e simple 0x400618

將返回:

<ccid_nobr>
<ccid_code>/home/devuser/simple.c:34

要研究該問題,可以檢查第 34 行。

對 於圖1中原始的程序崩潰,PSW 為070dc000 c00ab738。要獲得指令地址,可減去0x80000000。結果為0x400ab738。這個地址並不准確地落在我們的小程序之內。那么,它是什么 呢?是來自共享庫的代碼。如果對可執行文件運行ldd 命令(ldd simple),將會返回程序運行所需的共享對象的列表,以及該庫在那里可用的地址。

<ccid_nobr>
<ccid_code>libc.so.6 => /lib/libc.so.6 (0x40021000)
/lib/ld.so.1 => /lib/ld.so.1 (0x40000000)

該指令地址對應於加載libc.so.6的地址。在我們的簡單測試案例中,只需要兩個共享對象。其他應用程序可能需要更多共享對象,這使得ldd的輸出更加復雜。我們將以perl作為例子。 輸入:

<ccid_nobr>
<ccid_code>ldd /usr/bin/perl

將得到:

<ccid_nobr>
<ccid_code>libnsl.so.1 => /lib/libnsl.so.1 (0x40021000)
libdl.so.2 => /lib/libdl.so.2 (0x40039000)
libm.so.6 => /lib/libm.so.6 (0x4003d000)
libc.so.6 => /lib/libc.so.6 (0x40064000)
libcrypt.so.1 => /lib/libcrypt.so.1 (0x4018f000)
/lib/ld.so.1 => /lib/ld.so.1 (0x40000000)

所需要的一切都在那里了,但是我發現對於這個進程,下面的內容讀起來更快一點:

<ccid_nobr>
<ccid_code>ldd /usr/bin/perl | awk ‘{print? $4 “ “ $3 }’ | sort
(0x40000000) /lib/ld.so.1
(0x40021000) /lib/libnsl.so.1
(0x40039000) /lib/libdl.so.2
(0x4003d000) /lib/libm.so.6
(0x40064000) /lib/libc.so.6
(0x4018f000) /lib/libcrypt.so.1

現 在我們來確定崩潰發生在libc中的何處。假設libc.so.6的加載地址是0x40021000,從指令地址 0x400ab738減去它,結果為0x8a738。這是進入libc.so.6 的偏移。使用nm命令,從libc.so.6轉儲符號,然后嘗試確定該地址位於哪個函數中。對於libc.so.6,nm將生成7,000多行輸出。通過 對計算得出的偏移部分執行 grep(正則表達式查找程序)可以削減必須檢查的數據量。輸入:

<ccid_nobr>
<ccid_code>nm /lib/libc.so.6 | sort | grep 0008a

將返回 66 行,在該輸出的中間,我們會發現:

<ccid_nobr>
<ccid_code>0008a6fc T memcpy
0008a754 t _wordcopy_fwd_aligned

該 偏移落在memcpy中的某個位置。在此例中,一個空指針被當作目標地址傳遞給了memcpy。我們在何處調用的memcpy呢?問得好。我們可以通過檢 查輸出在日志文件中的寄存器轉儲來確定目標區域。寄存器14包含執行某個函數調用時的返回地址。根據圖1,R14是0x8040066e,它在截去高位之 后產生一個地址 0x40066e。這個地址落在我們的程序范圍之內,因此可以運行addr2line來確定該地址在何處。輸入:

<ccid_nobr>
<ccid_code>addr2line -e simple 0x40066e

將返回:

<ccid_nobr>
<ccid_code>/home/devuser/simple.c:36

這是我們調用memcpy之后的那一行。關於addr2line的一點補充:如果可執行文件中沒有包括調試符號,您將獲得??:0 作為響應。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM