GDB筆記.
loda.chou
Android/Linux Source Code Tags
App BizOrz
BizOrz.COM
BizOrz Blog
http://loda.hala01.com/oldarticles/
記得以前玩電腦遊戲時,Game Buster這套古董級的軟體是筆者玩遊戲時修改的利器,在MS-DOS時代下,這類系統常駐軟體會透過按鍵中斷組合鍵喚醒(記憶無誤的話,應該是連續兩下Ctrl鍵).隨後,開始有機會撰寫MS-Windows上的程式,” Ctrl + D” 喚醒SoftICE相信是很多在Windows下軟體破解或開發驅動程式的人深刻的回憶.對很多初學者而言,這種Console模式的除錯器總讓人覺得距離遙遠,能不碰或少背些指令就不碰.而能製作出Windows上這套經典SoftICE的人想必來頭也不小,其中一位開發者就是Matt Pietrek,也是"Windows 95 System Programming Secrets"這本 Windows 95系統探索經典書籍的作者(台灣譯本為"Windows95系統程式設計-大奧秘"),在Microsoft Systems Journal 對系統開發者深具影響力的年代,Matt的 "Under The Hood" 專欄,是對系統層級開發者很棒的滋補養分.直到2006年4月,NuMega宣布將SoftICE的維護期只延續到隔年的3月,並替這套經典軟體畫下句點. 目前Windows上,首推的Console除錯工具應該就是微軟自己維護的WinDbg(參考網頁http://msdn.microsoft.com/en-us/windows/hardware/gg463009).
每一套Console除錯器,從開始使用到能充分掌握,其實都需要一點時間去熟悉處理器,作業系統與該Console除錯器上各類常用指令.但面對現實,相信很多人都跟我有類似的經驗,在某一個時間點熟悉一個開發工具,但因著開發環境的轉換,時間久了,自己也專注在新的產品開發上,對於過往工具的熟韌度就又一點一滴的流失.
這篇文章的誕生,主要緣於自己希望為GDB寫一篇值得紀念的文章,二者也是把自己對這套工具的使用心得記錄下來,希望能讓有同樣需求的開發者得到助益. 本文的撰寫,會以筆者自身的經驗為主,希望提供的資訊不只是使用GDB,也能了解背後運作的原理,最後,在實務上,如果遇到現場損毀時,能如何透過有限的線索去找出問題發生的原因.
而GDB之所以強大,除了自身體系的完整外,提供給GDB系統層級除錯能力的Linux Kernel PTrace核心,是讓GDB之所以能發揮作用的原因. Linux Kernel 透過ptrace System Call讓GDB可以去Attach運作中的Process,或透過TraceME命令讓應用程式從一開始載入就進入GDB除錯的範圍裡,包括像是GDB支援對除錯程式的跨行程記憶體修改,設定BreakPoint/WatchPoint,單步除錯…等,都有賴於Linux Kernel ptrace System Call的支援.
不免俗的,筆者選擇MS-Windows作為對比的環境,跟Linux Kernel Ptrace一樣,MS Windows也由核心提供Win32 Debug API除錯介面,記得之前的工作,曾有需求要在Windows平台上為產品x86模擬器提供自製的除錯介面,但又以不希望使用者需額外安裝Visual Studio的前提,當時的解決方案就是使用Windows內建的Win32 Debug API(http://msdn.microsoft.com/en-us/library/windows/desktop/ms679303(v=vs.85).aspx ),並藉此搭建屬於自己產品化需求的除錯環境,像是Win32 PDB Debug Symbol解析,可用以把Symbol/位址與行數對應回來,單步除錯,設定Break-Point..等. 這套Win32 Debug API可讓開發者呼叫CreateProcess產生要被除錯的Debug Child,並由Debug Parent透過WaitForDebugEvent接收並處理來自被除錯Debug Child的Debug Event. 跟透過Linux Kernel Ptrace除錯時要由Debug Parent透過fork產生Debug Child,再透過wait去接收來自Debug Child的Notification,實現的概念非常類似.
但兩者還是有一些差異,像是Win32 Debug API有把PDB Symbol Decoder都包裝好了(可藉由QueryInterface 取得__uuidof(IDebugSymbols2)介面,並透過GetOffsetByName/GetLineByOffset/StartSymbolMatch/GetNextSymbolMatch/GetTypeName/GetTypeSize/GetOffsetTypeId進行PDB Symbol的查詢),這對於在Win32環境撰寫除錯介面的開發者而言可省去不少工作量. 反觀GDB本身需額外實作DWARF2 Symbol Decode,以便讓除錯器功能完整,但也基於此,我們可以透過GDB對DWARF2的實現,更進一步的了解除錯機制的設計原理.
GDB 是一套free software並在受GNU General Public License (GPL)的保護下發佈,最早是由Richard Stallman (http://stallman.org/) 在1990年代中期投入開發,並以GDB(GNU symbolic debugger)為命名,讓這套Unix環境下的經典除錯工具正式誕生.
而這樣一套強大的除錯工具也絕非一人之力所能完成,像是Michael Tiemann主要貢獻了GNU C++ 在GDB中的支援, David Johnson貢獻了GDB原本的COFF檔案格式支援, Pace Willison則支援encapsulated COFF檔案格式, Adam de Boor 與 Bradley Davis貢獻了ISI Optimum V的支援, Per Bothner,Noboyuki Hikichi與Alessandro Forin 貢獻了對 MIPS 的支援, Jean-Daniel Fekete貢獻了Sun 386i的支援, Chris Hanson貢獻了HP9000的支援, Noboyuki Hikichi與 Tomoyuki貢獻了Sony/News OS3的支援, David Johnson貢獻了對Umax的支援, Jyrki Kuoppala貢獻了對Altos 3068的支援, Je Law貢獻了對HP PA與SOM的支援,Keith Packard 貢獻了對NS32K 支援. Doug Rabson 貢獻了對Acorn Risc Machine 支援. Bob Rusk 貢獻了對Harris Nighthawk CX-UX 支援. Chris Smith 貢獻了對Convex support (and Fortran debugging)支援. Jonathan Stone 貢獻了對Pyramid 支援. Michael Tiemann 貢獻了對SPARC 支援. Tim Tucker 貢獻了對 Gould NP1 與 Gould Powernode支援. Pace Willison 貢獻了對Intel 386 支援. Jay Vosburgh 貢獻了對Symmetry支援. Rich Schaefer 與 Peter Schauer 貢獻了對SunOS shared libraries支援…等.並在GDB 4.x進一步支援了BFD函式庫,並藉此可以支援多種LE-Object檔案格式.
總結前述討論,GDB的誕生確實很不容易,由於它功能的強大,與跨平台的彈性,熟悉這套工作對於開發者而言,相信會是有其價值與意義的!!
使用GDB開始除錯前
GDB是如何知道編譯後的程式碼與Source Code的關係呢? 跟前面提到的Windows PDB格式一樣,ELF檔案在透過GCC編譯時,我們可以透過-g的參數,讓GCC為編譯的執行檔案加上 DWARF 2的除錯資訊內容,
我們可以從Code Sourcery下載編譯好的Toolchain 網址是http://www.codesourcery.com/sgpp/lite/arm/portal/package7851/public/arm-none-linux-gnueabi/arm-2010.09-50-arm-none-linux-gnueabi-i686-pc-linux-gnu.tar.bz2 (網站http://www.codesourcery.com/sgpp/lite/arm/portal/release1600) 下載ARM eabi gcc Cross Compiler工具.
接下來我們以如下TestA.c程式碼為例,來說明編譯後是如何透過DWARF 2資訊,去把記憶體位址與行數給對應回來,並會驗證在有無DWARF 2資訊下,對應到GDB操作TestA的結果
#include <stdio.h>
int FuncB(int X) { int Y; Y=X+40; Y*=X+20; printf("FuncB:%ld\n",Y); return Y; } int FuncA(int X) { int Y; int Z; Y=X+10; Y*=X+30; Z=FuncB(Y); printf("FuncA:%ld\n",Z); return Y; } int main() { int Y=99; Y=FuncA(Y); printf("main:%ld\n",Y); return 0; } |
如下所示以ARM GCC編譯TestA程式碼
arm-none-linux-gnueabi-gcc TestA.c -g -o TestA
然後透過 ARM ObjDump進行反組譯與透過readelf判讀 DWARF 2的資訊,
arm-none-linux-gnueabi-objdump -x -D TestA > TestA.asm
readelf TestA –debug-dump
結果如下所示,參考DWARF 2的Section “.debug_line”,筆者以函式 main為例來說明
行數 | Source Code |
21
22 23 24 25 26 27 | int main()
{ int Y=99; Y=FuncA(Y); printf("main:%ld\n",Y); return 0; } |
在筆者的ARM處理器上,應用程式載入到記憶體後的起點為0×00008000,以這次驗證的TestA來說,參考ELF檔頭start address 落在.text section的起點0×00008380,也正好是在執行到應用程式進入點main以前的前置函式_start所在位置,並由_start函式把main函式位址當做參數,帶入呼叫__libc_start_main , 而__libc_start_main原型為如下,
int __libc_start_main(int *(main) (int, char * *, char * *), int argc, char * * ubp_av, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void (* stack_end));
以testA來說,main函式最後被擺放的記憶體位址為0x84e0,會在前置函式執行後才被正式執行到,如下所示,參考DWARF 2中的 .debug_line section中的內容,可以看到每一行Source Code都會有對應的描述,並可對應到實際ARM機械碼所在的記憶體範圍,
.debug_line | 反組譯內容 |
Special opcode 119: advance Address by 16 to 0x84e0and Line by 2 to 22
Special opcode 90: advance Address by 12 to 0x84ec and Line by 1 to 23 Special opcode 62: advance Address by 8 to 0x84f4 and Line by 1 to 24 Special opcode 90: advance Address by 12 to 0×8500and Line by 1 to 25 Special opcode 118: advance Address by 16 to 0×8510and Line by 1 to 26 Special opcode 34: advance Address by 4 to 0×8514 and Line by 1 to 27 | 000084e0 <main>:
84e0: e92d4800 push {fp, lr} 84e4: e28db004 add fp, sp, #4 84e8: e24dd008 sub sp, sp, #8 84ec: e3a03063 mov r3, #99 ; 0×63 84f0: e50b3008 str r3, [fp, #-8] 84f4: e51b0008 ldr r0, [fp, #-8] 84f8: ebffffe0 bl 8480 <FuncA> 84fc: e50b0008 str r0, [fp, #-8] 8500: e59f3018 ldr r3, [pc, #24] ; 8520 <main+0×40> 8504: e1a00003 mov r0, r3 8508: e51b1008 ldr r1, [fp, #-8] 850c: ebffff98 bl 8374 <_init+0×44> 8510: e3a03000 mov r3, #0 8514: e1a00003 mov r0, r3 8518: e24bd004 sub sp, fp, #4 851c: e8bd8800 pop {fp, pc} 8520: 00008610 andeq r8, r0, r0, lsl r6 |
基於上述反組譯main函式內容與 .debug_line的DWARF2資訊,我們可以進一步用GDB指令 (gdb) disas /m main 來得到一個綜合的結果檢視,如下所示,可以看到基於 .debug_info的內容,能把C程式Source Code對應到編譯後ARM指令集的對應位址. (隨著編譯器優化程度的不同,對應結果可能會有差異.)
(gdb) disas /m main
Dump of assembler code for function main: 22 { 0x000084e0 <+0>: push {r11, lr} 0x000084e4 <+4>: add r11, sp, #4 0x000084e8 <+8>: sub sp, sp, #8 23 int Y=99; 0x000084ec <+12>: mov r3, #99 ; 0×63 0x000084f0 <+16>: str r3, [r11, #-8] 24 Y=FuncA(Y); 0x000084f4 <+20>: ldr r0, [r11, #-8] 0x000084f8 <+24>: bl 0×8480 <FuncA> 0x000084fc <+28>: str r0, [r11, #-8] 25 printf("main:%ld\n",Y); 0×00008500 <+32>: ldr r3, [pc, #24] ; 0×8520 <main+64> 0×00008504 <+36>: mov r0, r3 0×00008508 <+40>: ldr r1, [r11, #-8] 0x0000850c <+44>: bl 0×8374 <printf> 26 return 0; 0×00008510 <+48>: mov r3, #0 27 } 0×00008514 <+52>: mov r0, r3 0×00008518 <+56>: sub sp, r11, #4 0x0000851c <+60>: pop {r11, pc} 0×00008520 <+64>: andeq r8, r0, r0, lsl r6 End of assembler dump. |
雖然透過GDB能檢視編譯後的ARM指令集與Source Code對應關係,但在實務操作上,經過編譯器更深度的優化後,上述優化程度較低,容易識別的.debug_line與機械碼的對應關係就可能會有所差異,在產品化階段,解決問題最棒的不二法門,還是要了解你所使用的處理器與指令集,並基於透過GDB的操作得到的資訊,加速問題收斂的效率與準確度.
對筆者來說,GDB是一個在產品化階段,可以進行事後分析問題的好工具,只要有問題發生的Core-Dump,帶有DWARF資訊的ELF執行檔與.so動態函式庫檔案,一個基本的 GDB問題分析流程就可展開.
但實務上,除錯的過程不總是完美的,例如Core-Dump檔案損毀,GDB Back Trace因為編譯的ARM代碼對SP暫存器有額外操作或是有Stack覆蓋問題,以致無法順利回推.此外像是牽涉到ARM處理器的ARMv32/Thumb Mode轉態問題…等,都需要有額外的系統知識進行現場狀況排除.筆者會建議避免過度仰賴除錯器的結果,在問題分析上,可看待GDB為處理繁瑣解析過程的 ”工具”,但系統問題發生的真正原因, 還是需仰賴問題分析者對系統問題的理解深度,來做最後的判定與解決.
GDB的原理
首先,以下所列舉的Linux Kernel Source Code實作會以3.0.8為依據,而所參考的GDB Source Code版本為 7.4(http://www.gnu.org/software/gdb/download/ ),若有不同版本間實作上的差異,還請以各位所使用的開發環境為依據. 有關GDB在ARM Linux的實作,主要可以參考的代碼為gdb-7.4/gdb/arm-linux-nat.c(Linux ARM native support )與gdb-7.4/gdb/arm-linux-tdep.c(Linux ARM target support).
Linux 提供讓 Ptrace Debug Parent對Debug Child進行除錯機制的ptrace函式,是一個 Linux Kernel的System Call介面 (sys_ptrace 編號為 26.),可讓Debug Parent控制Debug Child的執行,檢視與改變Debug Child的記憶體內容與暫存器.
在Linux環境下,ptrace函式的C原型為
long ptrace(enum __ptrace_request request, pid_t pid,void *addr, void *data);
Ptrace Debug Parent接上Debug Child可以有兩種方式,第一個是Process已經被執行後, 開發者希望在Run-Time接上進行除錯,此時可以透過PTRACE_ATTACH命令去Attach指定的Process ID.第二個類情況是Process尚未被執行,就可以透過PTRACE_TRACEME命令,去Fork出一個Debug Child Process,然後在該Process行程空間中再透過exec指令去執行外部希望被載入除錯的應用程式.
當行程處於被除錯的狀態,除了SIGKILL有預設的行為外,Debug Child會在收到每一個Signal時就暫停執行,並由Debug Parent透過wait回傳Notification,檢視當下Debug Child的狀態. 當Debug Parent完成除錯工作後,可以選擇透過PTRACE_KILL結束Debug Child的執行,或透過PTRACE_DETACH解除目前除錯的狀態.
若一個正在執行的 Process已經有一個Parent Process會透過wait去接收它的Notification,當這個Child Process被Debug Parent透過 PTRACE_ATTACH給接上,成為被除錯的Debug Child Process後,這個Child Process原本的Parent Process就無法再透過wait去收到原本Child Process的Notification.
有關Debug Parent與Debug Child透過Linux Kernel所提供的ptrace System Call進行除錯的運作概念,可參考下圖所示.
Debug Parent可透過wait函式接收來自Debug Child的Notification,例如Debug Parent收到wait傳回的WIFSTOPPED狀態,就可以判斷是否為剛才設定的BreakPoint/WatchPoint被觸發,以檢視Debug Child當下的執行狀態,或透過PTRACE_SINGLESTEP以Single Step執行Debug Child時,每執行完一個指令,也會讓Debug Child進入Stop狀態.
如下演示一個基本的Ptrace Debug Parent與Debug Child互動的範例程式,可以看到當除錯程式fork出要被除錯的Process後,會先透過PTRACE_TRACEME去Enable Debug Child處於被除錯的狀態,而一個Debug Child在執行過PTRACE_TRACEME後,在第一個System Call執行前,會進入Stop狀態,等待來自Debug Parent所下的ptrace Command,以進行後續除錯流程.
#include <stdio.h>
#include <stdlib.h> #include <string.h> #include <errno.h> #include <unistd.h> #include <signal.h> #include <sys/ptrace.h> #include <sys/user.h> #include <sys/reg.h> int main() { int vPid; int vGetChildStatus; unsigned long vInstCount; //Keep the record of how many inst. child executed. vInstCount=0; if((vPid = fork()) == 0) { //In Child ptrace(PTRACE_TRACEME, 0, 0, 0); printf("In Child, try to call /bin/ls command.\n"); execl("/bin/ls", "", 0); } else { //In Parent //Keep Child tracing through forks, vforks, execs and clones ptrace(PTRACE_SETOPTIONS, vPid, 0, PTRACE_O_TRACEFORK|PTRACE_O_TRACEVFORK|PTRACE_O_TRACECLONE|PTRACE_O_TRACEEXEC); while(1) {//Busy 1 Loop //Wait for Child to Stop at Next Inst. if(wait(&vGetChildStatus)==-1) { printf("Failed to wait.\n"); break; } if (WIFSTOPPED(vGetChildStatus)) {//Child is under Stop-Stat. vInstCount++; if(ptrace(PTRACE_SINGLESTEP, vPid, 0, 0)!=0) perror("Singl Step Error"); //Continue to enanle child under Single-Step Mode. } else if (WIFEXITED(vGetChildStatus)) { printf("Child exited with status %d\n",WEXITSTATUS(vGetChildStatus)); break; } if (WIFSIGNALED(vGetChildStatus)) { printf("Child was terminated by signal %d. ",WTERMSIG(vGetChildStatus)); break; } }//End of Busy 1 Loop printf("Total Instrution Count:%ld\n",vInstCount); } return 0; } |
一般來說BreakPoint 可以分為以下幾類
1,Software BreakPoint : 在ARM中可以特定的指令集(例如:0xffffffff 就可以觸發Undefined Instruction Exception.),放在要觸發軟體中斷的記憶體位址,以便在該指令被執行到時,透過觸發Undefined Instruction Exception以作為軟體中斷機制
2,Hardware BreakPoint : ARM透過CoProcessor 14支援的硬體BreakPoint,當ARM處理器要去Fetch被硬體中斷點設定的記憶體位址指令時,就會觸發這個硬體中斷點,以進行後續的處理程序.
3,Conditional BreakPoint: 條件式中斷點,可用以設定條件是否滿足,例如像是GDB指令 ‘break xxx if yyy > zzz’,用以表示當yyy > zzz時,若是執行到xxx位置,就觸發該條件式中斷點,以進行後續的處理程序.
4,Temporary Software/Hardware BreakPoint: 暫時性的軟/硬體中斷點,當這中斷點被觸發後,就會失效,而不像是前述的中斷點,可以重複觸發中斷.
硬體的BreakPoint通常會有數量上的限制(像是Cortex A9支援6個Hardware BreakPoint). 但軟體的BreakPoing,例如透過x86的0xcc (=int 13h=Debug Interrupt),或是使用ARM的Undefined Instruction Exception機制,讓程式碼執行區間的軟體BreakPoint配置,可不受到Hardware BreakPoint數目限制,能依據軟體開發的實際需求,去操作BreakPoint的配置.
同樣的,Debug Parent也要在收到wait Notification後,確認目前Debug Child Stop的狀態是因為剛才置入的除錯指令或真的是一般性的Undefined Instruction所觸發的(例如: Stack覆蓋或Function Pointer指向錯誤的地址),若是屬於因為置換指令所觸發的軟體BreakPoint,亦可透過把置換掉的指令集內容置換回原本的指令集內容,以便讓Debug Child可以執行原本的指令,就像是沒受到任何影響的執行下去.
參考arm-linux-tdep.c中,有關 GDB 支援ARM Linux軟體BreakPoint的Undefined Instruction指令定義如下
/* Under ARM GNU/Linux the traditional way of performing a breakpoint is to execute a particular software interrupt, rather than use a particular undefined instruction to provoke a trap. Upon exection of the software interrupt the kernel stops the inferior with a SIGTRAP, and wakes the debugger. */
static const char arm_linux_arm_le_breakpoint[] = { 0×01, 0×00, 0x9f, 0xef }; static const char arm_linux_arm_be_breakpoint[] = { 0xef, 0x9f, 0×00, 0×01 }; /* However, the EABI syscall interface (new in Nov. 2005) does not look at the operand of the swi if old-ABI compatibility is disabled. Therefore, use an undefined instruction instead. This is supported as of kernel version 2.5.70 (May 2003), so should be a safe assumption for EABI binaries. */ static const char eabi_linux_arm_le_breakpoint[] = { 0xf0, 0×01, 0xf0, 0xe7 }; static const char eabi_linux_arm_be_breakpoint[] = { 0xe7, 0xf0, 0×01, 0xf0 }; /* All the kernels which support Thumb support using a specific undefined instruction for the Thumb breakpoint. */ static const char arm_linux_thumb_be_breakpoint[] = {0xde, 0×01}; static const char arm_linux_thumb_le_breakpoint[] = {0×01, 0xde}; /* Because the 16-bit Thumb breakpoint is affected by Thumb-2 IT blocks, we must use a length-appropriate breakpoint for 32-bit Thumb instructions. See also thumb_get_next_pc. */ static const char arm_linux_thumb2_be_breakpoint[] = { 0xf7, 0xf0, 0xa0, 0×00 }; static const char arm_linux_thumb2_le_breakpoint[] = { 0xf0, 0xf7, 0×00, 0xa0 }; |
更進一步參考 Linux Kernel 3.0.8中的/arch/arm/kernel/ptrace.c ,可以知道在ARMv32或是Thumb Mode下,目前在Linux Kernel中建議的Undefined Instruction如下所示
/*
* New breakpoints – use an undefined instruction. The ARM architecture * reference manual guarantees that the following instruction space * will produce an undefined instruction exception on all CPUs: * * ARM: xxxx 0111 1111 xxxx xxxx xxxx 1111 xxxx * Thumb: 1101 1110 xxxx xxxx */ #define BREAKINST_ARM 0xe7f001f0 #define BREAKINST_THUMB 0xde01 |
所支援的ARMv32指令集為ARM EABI所定義的Undefined Instruction指令編碼,並且ARM也確保這些指令編碼可在所有的ARM平台上產生Undefined Instruction Exception,以便相容於不同的ARM處理器方案,使軟體BreakPoint順利運作.
更進一步來看,並非所有的Linux Kernel Undefined Instruction Exception都是跟GDB/Ptrace除錯機制有關,也有很大機會是軟體因為遇到Stack Overflow,或因為軟體邏輯上的缺陷而導致程式執行到錯誤的位置,以致於讀取到錯誤的指令集,也同樣的觸發了Undefined Instruction Exception,也因此Kernel在arch/arm/kernel/traps.c中的函式 asmlinkage void __exception do_undefinstr(struct pt_regs *regs) 中有支援Undefined Hook介面,會在每次進入到Undefined Instruction Exception時會呼叫函式call_undef_hook去確認有哪些核心模組有註冊Undefined Instruction OpCode. 也因為有這樣的Undefined Instruction支援介面存在,其它ARM Linux指令集的模擬,也會透過同樣的介面支援平台上不支援的處理器指令集與行為.
由核心所支援的Undefined Instruction Hook使用前會先透過register_undef_hook進行註冊,所以我們可以檢視在ARM Linux中有哪些部分使用到register_undef_hook,就可以清楚目前到底還有哪些核心機制有使用到Undefined Instruction機制,筆者會在下一段文章中,舉Linux Kernel透過Undefined Instruction Exception以ARMv6之後開始提供的ldrex與strex指令取代SWP/SWPB指令集的例子來說明Linux Kernel對於Undefined Instruction Exception的延伸應用.
在透過ptrace撰寫Debug Parent時,另一個重要的課題就是有哪些Debug Client的wait Event可以用來讓Parent判別目前Child的狀態,筆者舉 include/sys/wait.h中的巨集簡要說明如下
1, WIFEXITED: 用在Debug Parent中,用來判別wait所接收回來的Debug Child狀態,該Debug Client 是不是已經正常執行結束,並可透過 WEXITSTATUS 巨集取得Debug Child的Exit Code.
2, WIFSIGNALED: 用在Debug Parent中判別 Debug Child是否因為收到Signal而導致不正常的結束執行,並可透過 WTERMSIG巨集取得Debug Child所收到的Signal號碼(例如:可以故意透過kill(PID,12) 送Signal 12給Debug Child,而在這發生不正常結束的情況下,就可以透過WTERMSIG由wait確認最後Debug Child所收到的Signal為12.
3, WIFSTOPPED:用在Debug Parent中判別 Debug Child是否因為收到Signal而導致執行暫停,一般來說為 SIGTRAP (Trace trap),也就是Signal 5. 可透過WSTOPSIG巨集取得Debug Child所收到導致執行暫停的Signal號碼.
4, WIFCONTINUED:用在Debug Parent中判別 Debug Child是否收到Signal SIGCONT以繼續執行下去.
有關Linux Kernel 支援的Ptrace命令, 包括在kernel/ptrace.c 所實作跟平台無關,或在arch/arm/kernel/ptrace.c所實作跟ARM處理器平台有關Ptrace Command的列表可參考以下介紹
Ptrace 命令 | 說明 | |
PTRACE_TRACEME | 用以通知應用程式被它的Parent Process進行除錯,除了SIGKILL以外的Signal都會被送給這個除錯中的Process,並會導致該被除錯的Process Stop,如果Debug Parent這時有透過wait函式等待的話,這時Debug Parent就會收到通知的訊息.一般來說Debug Parent會收到 SIGTRAP(Signal 5),用以暫停Debug Child,並讓Debug Parent可以去設定相關的BreakPoint/WatchPoint,確認當下的除錯狀況等等. 在Debug Parent確認所預備的環境無誤後,就可以透過SIGCONT讓Debug Child繼續執行下去.
可以參考如下實作範例,會由Parent process fork出一個Child Process,並由Parent Process下達Continue指令同意Child Process可以往後繼續執行. #include <sys/ptrace.h> #include <unistd.h> #include <stdio.h> int main() { int pid; if((pid = fork()) == 0) { ptrace(PTRACE_TRACEME, 0, 0, 0); printf("In Child, In Child, try to call ls command.\n"); execl("/bin/ls", "", 0); } else { //Child is under Stop-State now. printf("In Parent,Stop…5 second.\n"); sleep(5); printf("Continue the Child process.\n"); ptrace(PTRACE_CONT, pid, 0, 0); } return 0; } 執行結果為 In Parent,Stop…5 second. In Child, call the outside command. Continue the Child process. GroupA GroupA.txt test test3.c test5 TestA.A TestA.txt GroupA.asm GroupB test2 test4 test5.c TestA.asm TestB.c GroupA.c GroupB.c test2.c test4.c TestA TestA.c test.c [root@localhost test]# | |
PTRACE_KILL | 用以送出SIGKILL給Debug Child,並使其結束執行.
如下所示 child->exit_code = SIGKILL; wake_up_state(child, __TASK_TRACED); | |
PTRACE_SETOPTIONS | 用以透過ptrace_setoptions函式設定ptrace debug child的Option參數,可以選擇包括PT_TRACESYSGOOD,PT_TRACE_FORK,PT_TRACE_VFORK,PT_TRACE_CLONE,PT_TRACE_EXEC,PT_TRACE_VFORK_DONE,PT_TRACE_EXIT.
(since Linux 2.4.6; see BUGS for caveats) 可用以設定Debug Parent執行ptrace時的除錯選項,每個選項都會對應到一個Bit Mask,目前支援以下選項 1,PTRACE_O_TRACESYSGOOD (since Linux 2.4.6): 可設定PTRACE_O_TRACESYSGOOD 讓Debug Child在Stop狀態時,由Debug Parent判斷SIGTRAP 的bit 7是否為1,若為真就是透過System Call所導致的System Call Trap,可用以監控Debug Child所有呼叫的System Call行為. (例如 strace指令的實作一樣),如下範例程式.
2,PTRACE_O_TRACEFORK (since Linux 2.5.46): 可支援在Debug Child中呼叫到fork時,觸發SIGTRAP暫停Debug Child執行,並讓Debug Parent可以收到 (SIGTRAP | PTRACE_EVENT_FORK << 8)的Notification.而fork所新產生的Process也會進入被除錯的模式中,並在新的Process開始執行後,便隨著產生 SIGSTOP,讓新的Process暫停執行.對於Debug Parent Process來說,則可以透過 PTRACE_GETEVENTMSG命令取得這個新產生的Process Id. 3,PTRACE_O_TRACEVFORK (since Linux 2.5.46): 可支援在Debug Child中呼叫到 vfork時,觸發SIGTRAP暫停Debug Child執行,並讓Debug Parent可以收到 (SIGTRAP | PTRACE_EVENT_VFORK << 8 )的Notification.而vfork所新產生的Process也會進入被除錯的模式中,並在新的Process開始執行後,便隨著產生 SIGSTOP,讓新的Process暫停執行.對於Debug Parent Process來說,則可以透過 PTRACE_GETEVENTMSG命令取得這個新產生的Process Id. 4, PTRACE_O_TRACECLONE (since Linux 2.5.46): 可支援在Debug Child中呼叫到clone時,觸發SIGTRAP暫停Debug Child執行,並讓Debug Parent可以收到 (SIGTRAP |PTRACE_EVENT_CLONE << 8 )的Notification.而clone所新產生的Process也會進入被除錯的模式中,並在新的Process開始執行後,便隨著產生 SIGSTOP,讓新的Process暫停執行.對於Debug Parent Process來說,則可以透過 PTRACE_GETEVENTMSG命令取得這個新產生的Process Id. 5,PTRACE_O_TRACEEXEC (since Linux 2.5.46): 可支援在Debug Child中呼叫到execve時,觸發SIGTRAP暫停Debug Child執行,並讓Debug Parent可以收到 (SIGTRAP |PTRACE_EVENT_EXEC << 8)的Notification. 6,PTRACE_O_TRACEVFORKDONE (since Linux 2.5.60) 可支援在Debug Child中呼叫vfork結束後,觸發SIGTRAP暫停Debug Child執行,並讓Debug Parent可以收到 (SIGTRAP | PTRACE_EVENT_VFORK_DONE << 8)的Notification. 7, PTRACE_O_TRACEEXIT (since Linux 2.5.60) 會在Debug Child結束執行時進行暫停,並觸發SIGTRAP暫停Debug Child執行,並讓Debug Parent可以收到 (SIGTRAP | PTRACE_EVENT_EXIT << 8)的Notification.由於是在Debug Child結束執行時進行暫停,所以有關的暫存器與記憶體內容都還是可以正確的讀取.對於Debug Parent Process來說,則可以透過 PTRACE_GETEVENTMSG命令取得Debug Child的Exit Status. | |
PTRACE_CONT | 重啟執行處於Stop狀態的Process,會透過函式ptrace_resume,由Debug Parent對Debug Child執行Continue的動作,讓Debug Child能繼續執行. | |
PTRACE_PEEKTEXT, PTRACE_PEEKDATA | 用以讀取指定的Debug Child記憶體空間的資料內容,可一次傳回32bits,PTRACE_PEEKTEXT與PTRACE_PEEKDATA原意上是要分別取得屬於程式碼的Text與資料節區的Data,但Linux本身並沒有把這兩塊的記憶體空間各自獨立,因此使用者只需指定所需讀取的記憶體位址,就可以依序把兩者資料讀取回來.
由於透過這機制讀取記憶體內容的效率較差,因此也可以看到在GDB中會改以透過/proc/%d/mem的機制,以檔案系統Shared Memory File Mapping機制進行效率較高的讀取動作. | |
PTRACE_POKETEXT, PTRACE_POKEDATA | 用以把數值內容寫入到Debug Child的記憶體空間,如上所述,使用者只需指定所需寫入的記憶體位址,就可以依序對Debug Child的記憶體空間進行寫入的動作. | |
PTRACE_PEEKUSER | 用以讀取Debug Child的User Data,所讀取的資料在32-bits Linux環境下會以32-bits Alignment的方式排列,主要包括當下Debug Child的暫存器與跟Process有關的資訊,參考x86 Linux環境下的sys/user.h可知在x86平台上,這資訊包括
struct user { struct user_regs_struct regs; int u_fpvalid; struct user_fpregs_struct i387; unsigned long int u_tsize; unsigned long int u_dsize; unsigned long int u_ssize; unsigned long start_code; unsigned long start_stack; long int signal; int reserved; struct user_regs_struct* u_ar0; struct user_fpregs_struct* u_fpstate; unsigned long int magic; char u_comm [32]; unsigned long int u_debugreg [8]; }; 或像是在筆者ARM Linux編譯器的環境中,這資訊包括 struct user { struct user_regs regs; /* General registers */ int u_fpvalid; /* True if math co-processor being used. */ unsigned long int u_tsize; /* Text segment size (pages). */ unsigned long int u_dsize; /* Data segment size (pages). */ unsigned long int u_ssize; /* Stack segment size (pages). */ unsigned long start_code; /* Starting virtual address of text. */ unsigned long start_stack; /* Starting virtual address of stack. */ long int signal; /* Signal that caused the core dump. */ int reserved; /* No longer used */ struct user_regs *u_ar0; /* help gdb to find the general registers. */ unsigned long magic; /* uniquely identify a core file */ char u_comm[32]; /* User command that was responsible */ int u_debugreg[8]; struct user_fpregs u_fp; /* Floating point registers */ struct user_fpregs *u_fp0; /* help gdb to find the FP registers. */ }; | |
PTRACE_POKEUSER | 用以把數值回寫到Debug Child的User Data,所寫入的資料在32-bits Linux環境下,會是 32-bits Alignment. | |
PTRACE_GETEVENTMSG | 用以讓Debug Parent可獲取剛才發生的ptrace Event,可由Debug Parent呼叫put_user(child->ptrace_message, datalp)取得.如果Debug Parent透過wait notification 所取得的Event (event = (Nitification >> 16) & 0xffff;),等於PTRACE_EVENT_FORK, PTRACE_EVENT_VFORK 或PTRACE_EVENT_CLONE,則可以透過PTRACE_GETEVENTMSG取得新Process的ID,若該Event等於PTRACE_EVENT_EXIT,則可以透過PTRACE_GETEVENTMSG取得新Debug Child的Exit Status. | |
PTRACE_ATTACH | 主要用以針對已經執行的Process進行除錯,例如,可指定Process Id並執行
ptrace(PTRACE_ATTACH, vPID,0,0); 就可以使一個Process成為一個被除錯的Debug Child Process,並會讓這個被除錯的Process就像是透過執行 PTRACE_TRACEME的Process一樣,由呼叫端成為它的Debug Parent Process,可以透過wait函式收到來自Debug Child Process的Notification(以進行就像是PTRACE_TRACEME一樣的除錯流程).雖然彼此有Debug Parent/Child的架構在,但Debug Child Process透過getppid時,還是會得到原本的Parent Process ID. | |
PTRACE_DETACH | 主要用以把被Attach成為Debug Child Process的行程,進行Detach的動作,例如,可帶入目前成為Debug Child Process的ID,並執行
ptrace(PTRACE_DETACH,vPID,0,0); 可針對的Process Id包括 1,透過Fork與PTRACE_TRACEME命令的行程 2,或是正在運作的行程藉由PTRACE_ATTCH接上成為Debug Child Process 都可以透過這個命令,讓被除錯的行程可以脫離除錯的狀態,一路執行下去 | |
PTRACE_SETSIGINFO | 用以把Debug Parent的 siginfo_t structure複製到Debug Child的行程記憶體空間中. | |
PTRACE_GETSIGINFO | 把Debug Child行程記憶體空間中的siginfo_t structure複製到Debug Parent的行程記憶體中. | |
PTRACE_SYSC
ALL, PTRACE_SINGLESTEP | 這兩個命令主要用以把Debug Child從Stop狀態重新往後執行,但基於PTRACE_SINGLESTEP的命令會在執行下個指令時讓Debug Child重新進入Stop狀態,也就是說如果我們希望Debug Child執行每一個ARM指令時,都會觸發SIGTRAP,讓Debug Parent可以有機會檢視執行狀態以進行調整的話,就可以使用這個ptrace 命令.
反之,PTRACE_SYSCALL命令則會在System Call呼叫前先觸發SIGTRAP,讓Debug Parent可以檢視Debug Child呼叫該System Call時所帶入的變數.並在該System Call呼叫結束時觸發第二次的SIGTRAP,讓Debug Parent取得Debug Child執行System Call的返回值.離開時觸發該Event.也就是說對於PTRACE_SYSCALL命令來說,每個System Call呼叫都會觸發兩次SIGTRAP,一個是在呼叫前,一個是在呼叫後.基於這樣的使用行為,其實我們也可以很容易的去撰寫自己的System Call Trace工具. | |
PTRACE_SYSEMU, PTRACE_SYSEMU_SINGLESTEP | 主要用在像是User-Mode Linux這樣的環境中,以PTRACE_SYSEMU來說,會模擬System Call的呼叫,但並不會真的去執行Native Linux Kernel的System Call,而是會讓Debug Parent去執行出對應的System Call行為.
而PTRACE_SYSEMU_SINGLESTEP也是類似的目的,只是這個命令模擬的是PTRACE_SINGLESTEP單步執行的行為. 這兩個特別的ptrace命令主要用來讓應用可以模擬User-Space Linux Kernel環境的執行,若各位開發上有此需求,可以進一步的加以深究. | |
PT_GETFPREGS | 用以取得Debug Child所在處理器(例如:ARM)的 Floating Point暫存器 | |
PTRACE_SETFPREGS | 用以修改Debug Child所在處理器(例如:ARM) Floating Point暫存器值 | |
PTRACE_GETREGS, PTRACE_GETFPREGS | 用以取得Debug Child所在處理器(例如:ARM) 一般暫存器值 | |
PTRACE_SETREGS, PTRACE_SETFPREGS | 用以修改Debug Child所在處理器(例如:ARM) 一般暫存器值 | |
PTRACE_GETWMMXREGS | 用以取得Debug Child所在處理器(例如:ARM) WMMX暫存器值 | |
PTRACE_SETWMMXREGS | 用以修改Debug Child所在處理器(例如:ARM) WMMX暫存器值 | |
PTRACE_GETVFPREGS | 用以取得Debug Child所在處理器(例如:ARM) VFP暫存器值 | |
PTRACE_SETVFPREGS | 用以修改Debug Child所在處理器(例如:ARM)VFP暫存器值 | |
PTRACE_GET_THREAD_AREA | Fetch the thread-local storage pointer for libthread_db | |
PTRACE_GETHBPREGS | 用以透過Linux Kernel ptrace System Call來取得屬於平台相關的 Hardware Breakpoing與Watchpoint資訊 | |
PTRACE_SETHBPREGS | 用以透過Linux Kernel ptrace System Call來設定屬於平台相關的 Hardware Breakpoing與Watchpoint. |
以上介紹了Ptrace的命令,行為以及軟體BreakPoint的概念,接下來重要的議題就是由ARM支援的Hardware BreakPoints 與 WatchPoints (可參考網頁資訊http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0446d/BEICGIFG.html ). ARM上的硬體BreakPoints與WatchPoints會偵測指令的Fetch與資料Access動作,當條件滿足時,就會讓處理器進入Debug State.
硬體的BreakPoint 可讓開發者設定ARM執行到特定的記憶體位址時,中斷處理器的執行,以便進行除錯分析的工作.而硬體的WatchPoint 主要用以讓開發者能監控ARM去存取特定資料的記憶體位址時,可觸發中斷點,讓開發者可以找出在開發過程中所遇到的變數污染問題,或是可以透過設定變數的條件式,在指定的變數滿足該條件時 (例如, gCount>100)觸發WatchPoint中斷,以便開發者可以找出程式設計資料處理邏輯上的問題.一般來說WatchPoints可以分為以下幾類
1,當指定的記憶體位址資料倍存取時,就會觸發WatchPoints.
2,當指定的記憶體位址資料滿足條件時,,就會觸發WatchPoints.
在基於MMU的環境下,ARM的硬體BreakPoint/WatchPoint都是依據Virtual Address虛擬地址來做配置的,也因為在有MMU的多工環境下,每個Process都會各自擁有屬於自己的虛擬地址空間,因此雖然硬體BreakPoint/WatchPoint都是依據虛擬地址來做配置,但ARM可透過把BreakPoint 4與BreakPoint 5拿來當做Context Id使用,也就是說讓虛擬位址空間的BreakPoint/WatchPoint可以綁定一個Context Id(例如:對應到 Process ID),藉此就可以讓硬體BreakPoint/WatchPoint的配置是一個虛擬位址空間加上一個Context ID.可支援多工環境下,針對虛擬位址空間所配置的硬體BreakPoint/WatchPoint會在錯誤的Process空間觸發的問題.
由於一般區域變數是放在Stack中,而且若不在該執行函式的Scope內,也無法使用該區域變數,因此ARM硬體的WatchPoint只能支援Global與Static變數的Symbol,因為這兩者所在的Symbol記憶體位址在載入後就會確定下來,沒有執行Scope的問題.但若是開發者已經知道哪一塊記憶體要透過WatchPoint監看,透過鎖定記憶體的方式操作也是可以的.此外,由於ARM處理器PipeLine的行為影響,有機會導致硬體WatchPoint觸發時,其實當下的PC暫存器的記憶體位址並非去修改該變數的真正指令記憶體位址,此時還是會需要開發者根據當下的指令集執行狀態判斷,給予正確的分析.
在Linux上透過Ptrace提供的PTRACE_GETHBPREGS/PTRACE_SETHBPREGS命令,就能透過對ARM CoProcessor 14的設定,使用ARM上運作的Linux GDB能支援Hardware BreakPoint/WatchPoint .硬體的BreakPoints與WatchPoints可讓ARM硬體在特定的Event或條件觸發時暫停執行,以便讓開發者可以檢視記憶體,暫存器,變數內容,或採取任何可幫助除錯的動作,之後則讓程式繼續執行以觸發所設定的相關除錯條件.
以既有的ARM架構來說,ARM926支援兩個Hardware WatchPoint,兩者可依需求定義為用以偵測指令Fetch到特定位址觸發的BreakPoint(for Instruction Fetch),或用以偵測資料存取特定位址變數而進行觸發的WatchPoint(for Data Access).而像是 ARM1176則支援6個Hardware BreakPoint,2個Hardware WatchPoint. Cortex M3則支援6個Hardware BreakPoint與4個Hardware WatchPoint .
而除了前面提到的軟體中斷透過Undefined Instruction Exception實現外,在ARMv5架構之後,也支援新的ARM指令集 “BKPT (Breakpoint)”,可用以透過置入該指令的方式,增加Software BreakPoint的個數,解決Hardware BreakPoint個數限制的問題.
最後,用下面的圖示,簡單呈現Debug Parent兩種接上Debug Child的機制,以及在Debug流程結束後,兩種結束Debug Child除錯狀態的方式.
ARM 硬體 BreakPoint/WatchPoint
參考ARM文件”Cortex-A9 Technical Reference Manual”,每個硬體的BreakPoint操作都有兩個配對的暫存器,例如BVR0(Breakpoint Value Register #0)與BCR0(Rreakpoint Control Register #0),並依序到BVR4與BCR4,BVR5與BCR5. 而儲存在Breakpoint Value Register中的值可以為
1,一個指令所在的虛擬記憶體位址 (IVA,Instruction Virtual Address)
2,一個Process Context ID
3,一對指令所在的虛擬記憶體位址(IVA )與Process Context ID 的組合 (需要有兩個BVR配對使用)
如果希望鎖定特定Process Id的虛擬記憶體空間進行除錯配置,就必須要有一個BVR0-BVR3的BVR搭配一個BVR4或BVR5作為Context Id,也就是說要有兩個BVR暫存器成對,前者用來指定虛擬記憶體空間指令執行所在的記憶體位址,後者用來指定所要鎖定的Context ID (Process ID+ASPD),Context Id的32-bits組成為 Process ID[31:8] + ADID [7:0], Process Id為目前系統給當前執行Process唯一的識別碼,可用以讓Trace/Debug Logic識別目前正在執行的Process.而ASID (Address Space Identifier)而對應到目前Process所在的記憶體空間識別碼,主要是供Memory Management有關的識別之用.一旦有指定Context ID,在執行階段就會依據 CP15 Context ID Register(c13)與Instruction Address Bus的內容進行觸發的依據.
以目前的Cortex A9來說,只有BPR4與BPR5可以使用BVR4與BVR5來當做Context Id的使用,換句話說,如果要考慮到支援應用程式Process Id層級的BreakPoint除錯機制,那硬體的BreakPoint最多就只能同時允許對兩個應用程式的使用者空間使用BVR4與BVR5進行Context ID設定.
從ARM針對這Feature的支援來看,Context Id是在ARMv6的架構之後才支援的機制,其中ASID是用以識別系統每個執行行程的虛擬記憶體空間唯一個識別碼 .而ASID還可用以Tag處理器的Cache/TLB Entry. Context Id對於硬體支援的Debugger/Trace Logic可供識別系統目前Process執行的資訊,且這個ID必須要是整個系統中唯一的識別碼.
參考cpu_v7_switch_mm的實作 (in arch/arm/mm/proc-v7.S),在進行Context-Switch時,會透過CoProcess 15 c13的命令,把Context Id設定為0 (=r2),
mcr p15, 0, r2, c13, c0, 1
隨後設定新Process的Page Table Entry給TTBR0,
mcr p15, 0, r0, c2, c0, 0
最後才把最新Process的 Context Id (=r1)透過CoProcess 15 c13命令進行設定.
mcr p15, 0, r1, c13, c0, 1
如果沒有指定Context ID,該BVR的虛擬記憶體位址執行BreakPoint觸發就無針對特定行程的記憶體空間進行虛擬位址的BreakPoint除錯機制,但實際上由於是藉由Undefined Instruction Exception來實現這機制,以目前Linux Kernel對Context Id的處理來說,ASID是透過cpu_last_asid (in arch/arm/mm/context.c)進行配置, cpu_last_asid初值為ASID_FIRST_VERSION (=0×100)會從0×100依序遞增為0×101,0×102..並往上持續累加到0xffffffff,最後為0,然後又從0×100開始累加,也就是說對硬體而言ASID只有8 its,但對Linux Kernel而言CoProcess 15的Context Id以其32bits長度來看,足以充分的表示Context bits Process id + 8bits ASID組合所代表的意義,能確保Context Id在系統中的唯一性. 但這樣的實作,其實與ARM 原本對於Context Id設計的本意是不一樣的.例如參考ARM的文件,Context Id中的Process Id應該要對應到作業系統中唯一的Process ID,但根據目前Linux Kernel的實作,Context Id會等於cpu_last_asid隨著應用程式產生的累加值,而跟實際的Process Id對應兩者是拖勾的.
其實,對於核心來說,只要記得哪個Process有設定BreakPoint,並記住它使用的BreakPoint資訊,一旦在特定的記憶體位址中觸發BreakPoint,就可以透過Kernel所記住的這些BreakPoint與Task之間的關係,更直接一點來說,就是透過核心所記錄的相關Task BreakPoint資訊,用軟體手段達到使用者端虛擬記憶體與對應執行Task行程的關聯BreakPoint觸發行為.從效率而言,當然是透過硬體BreakPoint的效率最高,但以對所有ARM處理器(ARMv5或以前沒有支援ARM Context Id的處理器)來說,用核心Undefined Instruction Exception的操作,可以讓BreakPoint個數沒有上限,又能不被ARM處理器世代的差異而限制住實作彈性與平台支援的豐富性.
相對的,如果沒有Context ID的話,硬體 BreakPoint就無法針對特定Process ID所屬的Virtual Address進行執行,的指令集BreakPoint使用,這也是因為屬於Kernel Space的區塊,在Linux Kernel中,可以確保不管上層的應用程式如何切換,大家所共用的Kernel Space虛擬記憶體空間都是一致的,而在面對多工的環境下,會有不同的Process彼此切換與Context Id的變動,也因此如果要特別指定在特別的應用程式下的特別的記憶體位址的硬體BreakPoints功能有效的發揮作用,既需要透過成對的硬體BreakPoint Value Registers,讓其中一個暫存器指定Instruction Virtual Address而另一個暫存器用以指定Context ID(也就是等於當下執行或指定要設定硬體BreakPoint的Process ID),基於 IVA與Context Id所Link起來的硬體BreakPoint條件,就會在IVA與CP15中的Context Id與我們指定的BreakPoint Context Id兩個條件都滿足時,透過ARM處理器觸發Debug Event以進行後續的除錯流程.
從cpu_last_asid與對CP15 Context Id的配置來看,我們可以知道雖然ARMv6之後對Context Id的設計為Process ID+ ASID所搭配的32-bits組合,但在目前的ARM Linux Kernel中,並沒有如此的實現,ASID仍舊會隨著應用程式的載入與消滅而隨次遞增,每一次都會有所差異,而在超過0xff 之後的數值就會往Process Id的部份遞增,例如會累計至 0×101,0×400..etc,也就是說對ASID原本的用意來說也並沒有違背,只是Linux Kernel變成會依據應用程式載入的先後順序來做ASID的累加動作,就算Process Id跟Linux Kernel上真正的Peocess Id不是真正的對應,但卻也不影響ASID對TLB/Cache所能發揮的影響. 畢竟,都隨著應用程式的產生而累加了.
如下所示為參考自 ”Cortex-A9 Technical Reference Manual”的BVR暫存器說明
Breakpoint Value Registers | |
Bits | Description |
[31:0] | Breakpoint value. The reset value is 0. |
如下則為BCR暫存器的說明(SBZP= Should-Be-Zero-or-Preserved on Writes, RAZ=Appear as Zero on Reads).
Breakpoint Control Register | |
Bits | Description |
[31:29] | RAZ on reads, SBZP on writes. |
[28:24] | Breakpoint address mask.
RAZ/WI b00000 = no mask |
[23] | RAZ on reads, SBZP on writes. |
[22:20] | Meaning of BVR:
b000 = instruction virtual address match b001 = linked instruction virtual address match b010 = unlinked context ID b011 = linked context ID b100 = instruction virtual address mismatch b101 = linked instruction virtual address mismatch b11x = reserved.s |
[19:16] | Linked BRP number. The binary number encoded here indicates another BRP to link this one with. |
[15:14] | Secure state access control. This field enables the breakpoint to be conditional on the
security state of the processor. b00 = breakpoint matches in both Secure and Non-secure state b01 = breakpoint only matches in Non-secure state b10 = breakpoint only matches in Secure state b11 = reserved. |
[13:9] | RAZ on reads, SBZP on writes. |
[8:5] | Byte address select. For breakpoints programmed to match an IVA, you must write a
word-aligned address to the BVR. You can then use this field to program the breakpoint so it hits only if you access certain byte addresses. If you program the BRP for IVA match: b0000 = the breakpoint never hits b0011 = the breakpoint hits if any of the two bytes starting at address BVR & 0xFFFFFFFC +0 is accessed b1100 = the breakpoint hits if any of the two bytes starting at address BVR & 0xFFFFFFFC +2 is accessed b1111 = the breakpoint hits if any of the four bytes starting at address BVR & 0xFFFFFFFC +0 is accessed. If you program the BRP for IVA mismatch, the breakpoint hits where the corresponding IVA breakpoint does not hit, that is, the range of addresses covered by an IVA mismatch breakpoint is the negative image of the corresponding IVA breakpoint. If you program the BRP for context ID comparison, this field must be set to b1111. Otherwise, breakpoint and watchpoint debug events might not be generated as expected. |
[4:3] | RAZ on reads, SBZP on writes. |
[2:1] | Supervisor access control. The breakpoint can be conditioned on the mode of the processor.
b00 = User, System, or Supervisor b01 = privileged b10 = User b11 = any. |
[0] | Breakpoint enable:
0 = breakpoint disabled, reset value 1 = breakpoint enabled. |
以cortex A9為例來說,WatchPoints的操作會有兩個配對的暫存器,主要為WVR(Watchpoint Value Register)與WCR(Watchpoint Control Register),每個硬體WatchPoint操作都需要對應配對的WVRn與WCRn. WVR中會儲存所要監控的資料基於MMU的虛擬記憶體位置DVA(Data Virtual Address). 在多工的環境下,也可以是一個DVA與 Context Id(可以為BVR 4或BVR5)的配對,當所在的Context ID與 DVA都吻合,也就是在多工環境下指定的Process被切換到執行,且去存取該Process記憶體空間對應的DVA虛擬記憶體位址資料時,才會觸發Debug Event.
如下則為WVR暫存器的說明
Watchpoint Value Registers | |
Bits | Description |
[31:2] | Watchpoint address |
[1:0] | RAZ on reads, SBZP on writes |
如下則為WCR暫存器的說明
Watchpoint Control Register | |
Bits | Description |
[31:29] | RAZ on reads, SBZP on writes. |
[28:24] | Watchpoint address mask. |
[23:21] | RAZ on reads, SBZP on writes. |
[20] | Enable linking bit:
0 = linking disabled 1 = linking enabled. When this bit is set, this watchpoint is linked with the context ID holding BRP selected by the linked BRP field. |
[19:16] | Linked BRP number. The binary number encoded here indicates a context ID holding BRP to
link this WRP with. If this WRP is linked to a BRP that is not configured for linked context ID matching, it is Unpredictable whether a watchpoint debug event is generated |
[15:14] | Secure state access control. This field enables the watchpoint to be conditioned on the security
state of the processor. b00 = watchpoint matches in both Secure and Non-secure state b01 = watchpoint only matches in Non-secure state b10 = watchpoint only matches in Secure state b11 = reserved. |
[13] | RAZ on reads, SBZP on writes. |
[12:9] | RAZ/WI |
[8:5] | Byte address select. The WVR is programmed with word-aligned address. You can use this
field to program the watchpoint so it only hits if certain byte addresses are accessed. |
[4:3] | Load/store access. The watchpoint can be conditioned to the type of access being done.
b00 = reserved b01 = load, load exclusive, or swap b10 = store, store exclusive or swap b11 = either. SWP and SWPB trigger a watchpoint on b01, b10, or b11. A load exclusive instruction triggers a watchpoint on b01 or b11. A store exclusive instruction triggers a watchpoint on b10 or b11 only if it passes the local monitor within the processor |
[2:1] | Privileged access control. The watchpoint can be conditioned to the privilege of the access
being done: b00 = reserved b01 = privileged, match if the processor does a privileged access to memory b10 = User, match only on nonprivileged accesses b11 = either, match all accesses. |
[0] | Watchpoint enable:
0 = watchpoint disabled, reset value 1 = watchpoint enabled. |
參考arch/arm/include/asm/hw_breakpoint.h,有關struct arch_hw_breakpoint_ctrl ctrl 的結構定義如下
struct arch_hw_breakpoint_ctrl {
u32 __reserved : 9, mismatch : 1, : 9, len : 8, type : 2, privilege : 2, enabled : 1; }; |
我們可以知道除了Enable/Disable,Privileged access control,Read/Write Type,Byte address select與Mismatch外, 在ARMv6與ARMv7中 BreakPoint有關 Context Id配置與Link/UnLink Context Id的機制並沒有在Linux Kernel中被完全的實現.因此,有關ARMv6與ARMv7的BreakPoint/WatchPoint透過Context Id硬體支援BreakPoint/WatchPoint,並不包含在筆者Linux Kernel 3.0.8的環境中,但基於Undefined Instruction Exception與Kernel對Process Id的紀錄,Linux Kernel所支援的Ptrace對特定應用程式的資料WatchPoint與指令BreakPoint操作上並不受影響.
如下所示,為Linux Kernel初始化Control Register的部份代碼.
static inline u32 encode_ctrl_reg(struct arch_hw_breakpoint_ctrl ctrl)
{
return (ctrl.mismatch << 22) | (ctrl.len << 5) | (ctrl.type << 3) |
(ctrl.privilege << 1) | ctrl.enabled;
}
再來簡要說明,GDB是如何透過 ptrace的PTRACE_GETHBPREGS/PTRACE_SETHBPREGS去設定硬體的BreakPoints與WatchPoint,參考Linux Kernel 3.0.8 中的Source Code arch/arm/kernel/ptrace.c,
long arch_ptrace(struct task_struct *child, long request,unsigned long addr, unsigned long data)
統一的路口為 kernel/ptrace.c,然後會透過函式arch_ptrace呼叫進入arch/arm/kernel/ptrace.c中,有關屬於Linux在ARM平台上的ptrace實作. 在 kernel/ptrace.c中的ptrace函式,為Linux Kernel提供給上層應用程式ptrace System Call的實作介面,如下所示,也可以看到在這的ptrace函式中會再呼叫進入arch/arm/kernel/ptrace.c中屬於ARM平台的arch_ptrace實作,
SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
unsigned long, data) { struct task_struct *child; long ret; if (request == PTRACE_TRACEME) { ret = ptrace_traceme(); if (!ret) arch_ptrace_attach(current); goto out; } child = ptrace_get_task_struct(pid); if (IS_ERR(child)) { ret = PTR_ERR(child); goto out; } if (request == PTRACE_ATTACH) { ret = ptrace_attach(child); /* * Some architectures need to do book-keeping after * a ptrace attach. */ if (!ret) arch_ptrace_attach(child); goto out_put_task_struct; } ret = ptrace_check_attach(child, request == PTRACE_KILL); if (ret < 0) goto out_put_task_struct; ret = arch_ptrace(child, request, addr, data); out_put_task_struct: put_task_struct(child); out: return ret; } |
之後進入到arch_ptrace中執行跟ARM處理器有關的ptrace command,
long arch_ptrace(struct task_struct *child, long request,
unsigned long addr, unsigned long data) { int ret; unsigned long __user *datap = (unsigned long __user *) data; switch (request) { case PTRACE_PEEKUSR: ret = ptrace_read_user(child, addr, datap); break; case PTRACE_POKEUSR: ret = ptrace_write_user(child, addr, data); break; case PTRACE_GETREGS: ret = copy_regset_to_user(child, &user_arm_view, REGSET_GPR, 0, sizeof(struct pt_regs), datap); break; case PTRACE_SETREGS: ret = copy_regset_from_user(child, &user_arm_view, REGSET_GPR, 0, sizeof(struct pt_regs), datap); break; case PTRACE_GETFPREGS: ret = copy_regset_to_user(child, &user_arm_view, REGSET_FPR, 0, sizeof(union fp_state), datap); break; case PTRACE_SETFPREGS: ret = copy_regset_from_user(child, &user_arm_view, REGSET_FPR, 0, sizeof(union fp_state), datap); break; #ifdef CONFIG_IWMMXT case PTRACE_GETWMMXREGS: ret = ptrace_getwmmxregs(child, datap); break; case PTRACE_SETWMMXREGS: ret = ptrace_setwmmxregs(child, datap); break; #endif case PTRACE_GET_THREAD_AREA: ret = put_user(task_thread_info(child)->tp_value, datap); break; case PTRACE_SET_SYSCALL: task_thread_info(child)->syscall = data; ret = 0; break; #ifdef CONFIG_CRUNCH case PTRACE_GETCRUNCHREGS: ret = ptrace_getcrunchregs(child, datap); break; case PTRACE_SETCRUNCHREGS: ret = ptrace_setcrunchregs(child, datap); break; #endif #ifdef CONFIG_VFP case PTRACE_GETVFPREGS: ret = copy_regset_to_user(child, &user_arm_view, REGSET_VFP, 0, ARM_VFPREGS_SIZE, datap); break; case PTRACE_SETVFPREGS: ret = copy_regset_from_user(child, &user_arm_view, REGSET_VFP, 0, ARM_VFPREGS_SIZE, datap); break; #endif #ifdef CONFIG_HAVE_HW_BREAKPOINT //這就是用GDB使用ptrace支援Hardware Breakpoint/WatchPoint 的編譯時define選項 case PTRACE_GETHBPREGS: if (ptrace_get_breakpoints(child) < 0) return -ESRCH; ret = ptrace_gethbpregs(child, addr, (unsigned long __user *)data); ptrace_put_breakpoints(child); break; case PTRACE_SETHBPREGS: if (ptrace_get_breakpoints(child) < 0) return -ESRCH; ret = ptrace_sethbpregs(child, addr, (unsigned long __user *)data); ptrace_put_breakpoints(child); break; #endif default: ret = ptrace_request(child, request, addr, data); break; } return ret; } |
若不屬於跟ARM平台有關的ARM ptrace command,就會走到default選項,然透走回屬於Linux Kernel一般性ptrace命令的實作ptrace_request (kernel/ptrace.c)
int ptrace_request(struct task_struct *child, long request,
unsigned long addr, unsigned long data) { int ret = -EIO; siginfo_t siginfo; void __user *datavp = (void __user *) data; unsigned long __user *datalp = datavp; switch (request) { case PTRACE_PEEKTEXT: case PTRACE_PEEKDATA: return generic_ptrace_peekdata(child, addr, data); case PTRACE_POKETEXT: case PTRACE_POKEDATA: return generic_ptrace_pokedata(child, addr, data); #ifdef PTRACE_OLDSETOPTIONS case PTRACE_OLDSETOPTIONS: #endif case PTRACE_SETOPTIONS: ret = ptrace_setoptions(child, data); break; case PTRACE_GETEVENTMSG: ret = put_user(child->ptrace_message, datalp); break; case PTRACE_GETSIGINFO: ret = ptrace_getsiginfo(child, &siginfo); if (!ret) ret = copy_siginfo_to_user(datavp, &siginfo); break; case PTRACE_SETSIGINFO: if (copy_from_user(&siginfo, datavp, sizeof siginfo)) ret = -EFAULT; else ret = ptrace_setsiginfo(child, &siginfo); break; case PTRACE_DETACH: /* detach a process that was attached. */ ret = ptrace_detach(child, data); break; #ifdef CONFIG_BINFMT_ELF_FDPIC case PTRACE_GETFDPIC: { struct mm_struct *mm = get_task_mm(child); unsigned long tmp = 0; ret = -ESRCH; if (!mm) break; switch (addr) { case PTRACE_GETFDPIC_EXEC: tmp = mm->context.exec_fdpic_loadmap; break; case PTRACE_GETFDPIC_INTERP: tmp = mm->context.interp_fdpic_loadmap; break; default: break; } mmput(mm); ret = put_user(tmp, datalp); break; } #endif #ifdef PTRACE_SINGLESTEP case PTRACE_SINGLESTEP: #endif #ifdef PTRACE_SINGLEBLOCK case PTRACE_SINGLEBLOCK: #endif #ifdef PTRACE_SYSEMU case PTRACE_SYSEMU: case PTRACE_SYSEMU_SINGLESTEP: #endif case PTRACE_SYSCALL: case PTRACE_CONT: return ptrace_resume(child, request, data); case PTRACE_KILL: if (child->exit_state) /* already dead */ return 0; return ptrace_resume(child, request, SIGKILL); #ifdef CONFIG_HAVE_ARCH_TRACEHOOK case PTRACE_GETREGSET: case PTRACE_SETREGSET: { struct iovec kiov; struct iovec __user *uiov = datavp; if (!access_ok(VERIFY_WRITE, uiov, sizeof(*uiov))) return -EFAULT; if (__get_user(kiov.iov_base, &uiov->iov_base) || __get_user(kiov.iov_len, &uiov->iov_len)) return -EFAULT; ret = ptrace_regset(child, request, addr, &kiov); if (!ret) ret = __put_user(kiov.iov_len, &uiov->iov_len); break; } #endif default: break; } return ret; } |
有關ptrace System Call運作流程,可以參考如下圖所示,統一的入口為ptrace System Call(實作在kernel/ptrace.c),之後呼叫跟平台有關的arch_ptrace實作(檔案在 arch/arm/kernel/ptrace.c),最後才是由跟平台有關的arch_ptrace在發現有無法處理的ptrace command時,就會透過ptrace_request函式(實作在kernel/ptrace.c)執行跟處理器無關的ptrace command.
如下,我們舉由GDB對被除錯的Process虛擬位址空間的WatchPoint進行設定的流程為例,說明如下,
1, main (gdb/gdb.c)
2,captured_main (gdb/main.c)
3,captured_command_loop (gdb/main.c)
4,current_interp_command_loop (gdb/interps.c)
5,cli_command_loop (gdb/event-top.c)
6,start_event_loop (gdb/event-loop.c)
=>主要的輸入Event Loop迴圈,會不斷在此等待使用者輸入命令
7,gdb_do_one_event(gdb/event-loop.c)
=>會處理每一個輸入的按鍵Event.
8,process_event(gdb/event-loop.c)
=>會判斷使用者輸入的Command,把處理對應Command的函式指標"proc = event_ptr->proc;"設定給區域變數”event_handler_func *proc;”,並進行後續呼叫 “(*proc) (data);”
9,print_mention_watchpoint (gdb/breakpoint.c)
=>執行 ” watch 變數” 時,會把每次使用者透過 “watch 變數” 所設定的 watchpoint 列出來.例如: "Hardware watchpoint 2: MyVariable”.
10,之後執行指令 run,
11,insert_bp_location (gdb/breakpoint.c)
=>透過這函式對指定記憶體進行BreakPoint/WatchPoint設定.
12.insert_watchpoint(gdb/breakpoint.c)
=>由於筆者是以WatchPoint為例,所以在函式insert_bp_location中會判斷這是一個Hardware WatchPoint的Type (=bp_loc_hardware_watchpoint),之後透過 ”bl->owner->ops->insert_location”進入到函式insert_watchpoint中. 會根據該變數所在的記憶體位址與該變數Type所對應的記憶體Size(例如 int 為4 bytes) 進行函式target_insert_watchpoint的呼叫
13,target_insert_watchpoint (gdb/target.h)
=>可以參考 gdb/target.h中的宣告, “#define target_insert_watchpoint(addr, len, type, cond) (*current_target.to_insert_watchpoint) (addr, len, type, cond)”,得知GDB會依據平台與作業系統的不同,而對target_insert_watchpoint有不同的實作,進一步參考current_target的宣告”struct target_ops current_target;”,我們可以在ARM Linux GDB初始化過程中在函式_initialize_arm_linux_nat(gdb/arm-linux-nat.c)中看到對應的初始化動作 "t->to_insert_watchpoint = arm_linux_insert_watchpoint;", 也因此我們知道從target_insert_watchpoint 的呼叫之後會進入函式arm_linux_insert_watchpoint .
14, arm_linux_insert_watchpoint (gdb/arm-linux-nat.c)
=>依據平台的不同,例如x86的電腦則會呼叫函式i386_insert_watchpoint (gdb/i386-nat.c),而在ARM平台則是呼叫函式arm_linux_insert_watchpoint . 在這函式流程中,首先會透過函式arm_linux_get_hw_watchpoint_count確認平台上Hardware WatchPoint的數目,若為0就直接結束.再來就是透過函式arm_linux_hw_watchpoint_initialize初始化結構"struct arm_linux_hw_breakpoint *p" 以便之後設定WatchPoint初始值給 WVR(Watchpoint Value Register) 與 WCR (Watchpoint Control Register) 對應的兩個暫存器.WCR 所設定的初值包括
1 | Watchpoint enable[0]=1 |
2 | Privileged access control[2:1]=b11 (Kernel/User存取都能觸發) |
3 | Load/store access[4:3]=觸發的可讀寫條件. (例如:b01讀取才觸發,b10寫入才觸發,b11讀寫都觸發) |
4 | Byte address select[8:5]=由於ARM都是32-bits進行Fecth的動作,但同時包括ARM32bits,Thumb16bits或Thumb2 16/32bits混合模式,可在此設定比對的位址是要
(a)b1111:32-bits對齊位址的4bytes都吻合, (b)b0011:32-bits對齊位址的前2bytes吻合, (c)b1100:32-bits對齊位址的後2bytes吻合 |
15,arm_linux_insert_hw_breakpoint1 (gdb/arm-linux-nat.c)
=>透過arm_linux_get_hw_watchpoint_count取得Hardware WatchPoint個數與透過arm_hwbp_control_is_enabled找出目前可以使用的Hardware WatchPoint,並透過兩次ptrace PTRACE_SETHBPREGS命令,帶入Thread ID依序設定 WVR(Watchpoint Value Register) 與 WCR (Watchpoint Control Register) 所要使用的兩個WatchPoint Address/Control暫存器.值得參考的是,在這屬於WatchPoint的Request dir參數會為負值,對Kernel提供的ptrace System Call來說,這個值會對應到Num參數,而若這值為負值就表示為判斷讀寫(RW)動作的WatchPoint,反之如果為正值,就表示為判斷執行動作的BreakPoint.
16,之後便透過ptrace System Call進入到Kernel Mode.
17,SYSCALL_DEFINE4(ptrace, long, request, long, pid, unsigned long, addr,
unsigned long, data) (kernel/ptrace.c)
=>如ptrace System Call進入到Kernel Space的入口,會依據pid透過函式ptrace_get_task_struct取得Task Struct,與進入屬於平台相關的arch_ptrace函式中 .
18,arch_ptrace (arch/arm/kernel/ptrace.c)
=>由於Request Type為PTRACE_SETHBPREGS,便會呼叫函式ptrace_sethbpregs,而帶入的addr會等於函式ptrace_sethbpregs的num,data則為User Space的"unsigned long __user *data" 對應到上層User Space記憶體空間,就是要設定給WatchPoint的WVR或WCR的User Space數值位址 (&bpt->address for WVR或 &bpt->control for WCR).
19,ptrace_sethbpregs (arch/arm/kernel/ptrace.c)
=>對watchPoint來說num為負值,所以implied_type會為HW_BREAKPOINT_RW,也就表示為WatchPoint的記憶體數值讀寫操作.在這函式中,會透過get_user把User Space的WatchPoint Address與 Control所在記憶體中的數值拷貝到Kernel Space給區域變數”u32 user_val”. 若目前Task對應這個Hardware WatchPoint是第一次產生的,此時"tsk->thread.debug.hbp[idx]"為0,會透過函式ptrace_hbp_create,產生給這個Task.
20,modify_user_hw_breakpoint (kernel/events/hw_breakpoint.c)
=>把使用者WatchPoint的調整,修改到對應的資料結構中,以便之後程式重新執行時,可以致能發揮作用.
接下來,讓我們針對Linux Kernel Undefined Instruction Exception的延伸應用加以說明,以供相關開發者可以在後續其它產品中參考之用.
Linux Kernel 對 Undefined Instruction Exception 應用的延伸
在GDB軟體BreakPoint以外,筆者覺得比較經典的案例為Linux Kernel模擬User-Space ARMv32的SWP/SWPB指令集處理函式swp_handler (in arch/arm/kernel/swp_emulate.c),透過註冊如下的 undef_hook swp_hook,我們可以知道這個SWP/SWPB模擬指令集在運作時,是必須在Thumb Bit與Jazzel Bit都不為1的ARMv32 Mode的情況下(也就是非Thumb/ThumbEE Mode)才會成立.
/*
* Only emulate SWP/SWPB executed in ARM state/User mode.
* The kernel must be SWP free and SWP{B} does not exist in Thumb/ThumbEE.
*/
static struct undef_hook swp_hook = {
.instr_mask = 0x0fb00ff0,
.instr_val = 0×01000090,
.cpsr_mask = MODE_MASK | PSR_T_BIT | PSR_J_BIT,
.cpsr_val = USR_MODE,
.fn = swp_handler
};
簡要說明這個SWP/SWPB實作的背景,在多工尤其是多核心的環境下,會有多個Task共用同樣的系統資源,但這些共用的資源如何進行同步(例如使用Mutex or Semaphore),以確保正確性,就是在軟體設計上重要的議題. 基於此,若能有處理器層級的指令集支援,確保共用的資源在多核心多工的架構下,同一時間只有一個Task可以存取更動,將會是最有效率的作法.若無這樣的指令可供使用,通常作法會是把中斷Disable,確保沒有其它中斷或Task有機會插入執行.但這樣的作法對系統效能來說,卻並非最好的選擇.
ARM在ARMv5或之前的架構,支援了SWP(Swap)與SWPB(Swap Byte)兩個指令, 讓有同步需求的環境,可以不必要透過關閉中斷來確保資料的同步無誤,當處理器在進行Swap的過程中收到中斷,ARM處理器會確保整個Swap所包含的Load與Store過程是Atomic的,處理器會延遲中斷的處理,等Swap動作結束才會去進行中斷的執行. 對ARM指令集來說,Load與Store是兩種不同行為的指令,且在多核心的架構下,如果有多個不同頻率的處理器,共用同一個外部記憶體,若要進行Swap行為就會對整體系統效能造成較大的影響.因此Swap指令在ARMv6的架構後,便不建議使用,在開發上ARM則建議開發者改以Ldrex (Load Exclusive)與Strex (Store Exclusive)兩個指令取代SWP指令.
有關SWP指令的操作可以參考ARM的網頁 “A.1.1. SWP and SWPB”(http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/CJHBGBBJ.html ),有一個簡要的範例如下,可示範如何透過 SWP指令實現 Mutex Lock/Unlock (通常Linux上的Lock值為1表示鎖住,而0表示解鎖,以下例來說,會把1寫入r0所在記憶體代表這個Lock已經鎖上),
EXPORT lock_mutex_swp
lock_mutex_swp PROC LDR r2, =locked SWP r1, r2, [r0] ; Swap R2 with location [R0], [R0] value placed in R1 CMP r1, r2 ; Check if memory value was ‘locked’ BEQ lock_mutex_swp ; If so, retry immediately BX lr ; If not, lock successful, return ENDP EXPORT unlock_mutex_swp unlock_mutex_swp LDR r1, =unlocked STR r1, [r0] ; Write value ‘unlocked’ to location [R0] BX lr ENDP |
在ARMv7的架構下,針對SWP/SWPB也提供了額外的彈性,可藉由設定CP15 (CoProcessor 15) 的 System Control Register的SW bit為1去決定是否支援SWP或SWPB指令. 更進一步的拓展,就是在Linux Kernel下,基於對於已經Phase-Out的SWP/SWPB進行支援,考量到還是有部分User Mode的應用程式有使用到SWP/SWPB指令(例如GLibc 2.8版本以前所編譯的函式庫),在ARMv7多核心架構下,如果希望避免透過設定CP15 System Control Register的SW bit 為1去支援SWP/SWPB指令,讓使用SWP/SWPB指令的應用程式時,對多核心環境造成影響.Linux Kernel支援了額外的編譯選項”SWP_EMULATE”,會在ARMv7多核心Linux Kernel初始化時把CP15 System Control Register的SW bit 設定為0,讓所運作的ARMv7多核心架構在遇到採用SWP/SWPB的User-Space應用程式時,會因為執行到這不支援的指令集而觸發Undefined Instruction Exception,讓Linux Kernel透過函式swp_handler (in arch/arm/kernel/swp_emulate.c),把SWP/SWPB指令改用__user_swpX_asm中的ldrex與strex兩個指令來實現.
簡單來說,就是透過Undefined Instruction Exception的觸發,基於軟體實現了一個原本不支援的ARM指令.
GDB 指令
進入GDB後,可以執行如下的環境變數,或是透過 ’gdb script-file ‘ 外部執行Script 檔案,減少要在GDB中配置環境變數的不便.
file builtin-symbol-execution-file
set solib-search-path builtin-symbol-library-file-path
set solib-absolute-prefix builtin-symbol-library-filepath-prefix
core core-dump-file
首先, GDB指令繁多,不論是書籍 (像是"The Art of Debugging with GDB and DDD"),或是網路上的參考網頁,都已經很豐富,在此僅舉筆者認為值得進一步說明的指令,其它的部份,都請自行參閱其它GDB指令的介紹,或在GDB下透過 help指令尋求解答也是很棒的方式.
Break Point
指令 | 說明 |
break | 1,對函式起點 設定break point
=>(gdb) b main Breakpoint 1 at 0×400557: file test.c, line 23. 2,對目前Source Code第27行, 設定break point =>(gdb) b 27 Breakpoint 2 at 0×400567: file test.c, line 27. 3,對記憶體位址0×400567 , 設定break point =>(gdb) b *0×400567 Note: breakpoint 2 also set at pc 0×400567. Breakpoint 3 at 0×400567: file test.c, line 27. 4,對特定行數的 break point,加上條件判斷 如下所示,想在函式 thread_func中的區域變數i為30時,觸發斷點,就可以在 break 指令後,加上 if 條件式的判斷 (gdb) list 10 5 void thread_func() 6 { 7 int i=0; 8 while(1) 9 { 10 i++; 11 printf("Thread:%ld\n",i); 12 if(i>99) 13 gCount=-1; 14 sleep(1); (gdb) b 12 if i==30 Breakpoint 1 at 0x4005de: file test2.c, line 12. 或像是 (gdb) b 12 if (i==30 && X=5) (gdb) b 12 if (X%50==0)
5,對指定檔案與指定行數, 設定break point (gdb) break TestA.c:23 Breakpoint 2 at 0×400557: file TestA.c, line 23. 6,對指定檔案的函式, 設定break point (gdb) break TestA.c:main Breakpoint 1 at 0×400557: file TestA.c, line 23. (gdb) |
command | command 指令可以讓每個break point暫停的位置去執行預設想要執行的Script,包括可以透過printf列印出變數,或透過 if/else 條件判斷式,while/for 條件式的還圈,可以讓command執行GDB Script讓除錯的流程與機制更加便利
(gdb) b main Breakpoint 1 at 0×400557: file TestA.c, line 23. (gdb) command 1 Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". >printf "I am break point 1\n" >end (gdb) b 26 Breakpoint 2 at 0×400582: file TestA.c, line 26. (gdb) command 2 Type commands for when breakpoint 2 is hit, one per line. End with a line saying just "end". >printf "I am break point 2\n" >end 已經設定Command內容給BreakPoint 1與 2,因此接下來可以透過Run來觸發BreakPoint以便可以執行Command內容. (gdb) r Starting program: /home/loda/test/TestA Breakpoint 1, main () at TestA.c:23 warning: Source file is more recent than executable. 23 int Y=99; I am break point 1 Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.25.el6_1.3.x86_ 64 (gdb) c Continuing. FuncB:198556181 FuncA:198556181 main:14061 Breakpoint 2, main () at TestA.c:26 26 return 0; I am break point 2 (gdb) 如果希望command不要顯示額外的訊息,可以在command一開始加上silent. |
define | 透過define把要在command指令中定義的命令包裝起來,作為自己往後操作Command指令時的前置宣告巨集使用
(gdb) define show_arg_and_go_to_continue Type commands for definition of "show_arg_and_go_to_continue". End with a line saying just "end". >printf $arg0,$arg1,$arg2 >continue >end (gdb) b main Breakpoint 1 at 0×400557: file TestA.c, line 23. 設定Command 1並使用剛才定義好的 show_arg_and_go_to_continue指令串 (gdb) command 1 Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". >silent >show_arg_and_go_to_continue "It is my test:%ld %s\n" Y "Test la" >end 執行Run指令以便啟動程式執行 (gdb) r Starting program: /home/loda/test/TestA It is my test:0 Test la FuncB:198556181 FuncA:198556181 main:14061 Program exited normally. (gdb) |
info break | 用以顯示目前所有配置的Break Point位置與行號
=>(gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0×0000000000400557 in main at test.c:23 breakpoint already hit 1 time 2 breakpoint keep y 0×0000000000400567 in main at test.c:27 3 breakpoint keep y 0×0000000000400567 in main at test.c:27 (gdb) |
clear | 刪除位於指定行數的Break Point配置
=>(gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0×0000000000400557 in main at test.c:23 breakpoint already hit 1 time 2 breakpoint keep y 0×0000000000400567 in main at test.c:27 3 breakpoint keep y 0×0000000000400567 in main at test.c:27 (gdb) clear 27 Deleted breakpoints 2 3 (gdb) |
delete | 刪除指定的Break Point編號
=>(gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0×0000000000400557 in main at test.c:23 breakpoint already hit 1 time 4 breakpoint keep y 0×0000000000400567 in main at test.c:27 5 breakpoint keep y 0×0000000000400567 in main at test.c:27 (gdb) delete 1 (gdb) info b Num Type Disp Enb Address What 4 breakpoint keep y 0×0000000000400567 in main at test.c:27 5 breakpoint keep y 0×0000000000400567 in main at test.c:27 (gdb) |
disable | 關閉指定的Break Point編號
=>(gdb) info b Num Type Disp Enb Address What 4 breakpoint keep y 0×0000000000400567 in main at test.c:27 5 breakpoint keep y 0×0000000000400567 in main at test.c:27 6 breakpoint keep y 0×0000000000400557 in main at test.c:23 (gdb) disable 5 (gdb) info b Num Type Disp Enb Address What 4 breakpoint keep y 0×0000000000400567 in main at test.c:27 5 breakpoint keep n 0×0000000000400567 in main at test.c:27 6 breakpoint keep y 0×0000000000400557 in main at test.c:23 (gdb) |
enable | 重新致能指定的Break Point編號
=>(gdb) info b Num Type Disp Enb Address What 4 breakpoint keep y 0×0000000000400567 in main at test.c:27 5 breakpoint keep n 0×0000000000400567 in main at test.c:27 6 breakpoint keep y 0×0000000000400557 in main at test.c:23 (gdb) enable 5 (gdb) info b Num Type Disp Enb Address What 4 breakpoint keep y 0×0000000000400567 in main at test.c:27 5 breakpoint keep y 0×0000000000400567 in main at test.c:27 6 breakpoint keep y 0×0000000000400557 in main at test.c:23 (gdb) |
Back Trace
指令 | 說明 |
bt | Backtrace可以用來依據目前的PC/LR與r13 Sp暫存器,顯示當下的Call Stack Back Trace,用以讓開發者可以了解目前的函式呼叫流程,以便可以分析最後問題的現場與發生的原因.
如下所示為Backtrcae指令的演示 (gdb) bt #0 FuncB (X=14061) at TestA.c:6 #1 0×0000000000400530 in FuncA (X=99) at TestA.c:17 #2 0×0000000000400568 in main () at TestA.c:24 (gdb) 也可以透過Backtrace把整個Call Stack Back Trace把有關的函式變數與區域變數值給顯示出來,如下所示為Backtrace full指令的演示 (gdb) bt full #0 FuncB (X=14061) at TestA.c:6 Y = 0 #1 0×0000000000400530 in FuncA (X=99) at TestA.c:17 Y = 14061 Z = 0 #2 0×0000000000400568 in main () at TestA.c:24 Y = 99 (gdb) |
frame | 假設現在Back Tree有如下3層
(gdb) bt #0 FuncB (X=14061) at TestA.c:6 #1 0×0000000000400530 in FuncA (X=99) at TestA.c:17 #2 0×0000000000400568 in main () at TestA.c:24 frame 0 既是現在所在的位置, 可以透過 print 查看當下的 X與Y變數的值 (gdb) list FuncB 1 #include <stdio.h> 2 3 int FuncB(int X) 4 { 5 int Y; 6 Y=X+40; 7 Y*=X+20; 8 printf("FuncB:%ld\n",Y); 9 return Y; 10 } (gdb) p X $1 = 14061 (gdb) p Y $2 = 0 frame 1 是在FuncA呼叫進入FuncB時的狀態, (gdb) frame 1 #1 0×0000000000400530 in FuncA (X=99) at TestA.c:17 17 Z=FuncB(Y); (gdb) list 16 11 int FuncA(int X) 12 { 13 int Y; 14 int Z; 15 Y=X+10; 16 Y*=X+30; 17 Z=FuncB(Y); 18 printf("FuncA:%ld\n",Z); 19 return Y; 20 } (gdb) p X $4 = 99 (gdb) p Y $5 = 14061 (gdb) p Z $6 = 0 (gdb) frame2 是在main呼叫進入FuncA時的狀態, (gdb) frame 2 #2 0×0000000000400568 in main () at TestA.c:24 24 Y=FuncA(Y); (gdb) list 26 21 int main() 22 { 23 int Y=99; 24 Y=FuncA(Y); 25 printf("main:%ld\n",Y); 26 return 0; 27 } (gdb) p Y $7 = 99 (gdb) |
down | 可以讓開發者在位於Call Stack後端的Frame,透過 down指令依序往Frame #0方向移動.基本的演示如下所示
(gdb) bt #0 FuncB (X=14061) at TestA.c:6 #1 0×0000000000400530 in FuncA (X=99) at TestA.c:17 #2 0×0000000000400568 in main () at TestA.c:24 (gdb) frame 2 #2 0×0000000000400568 in main () at TestA.c:24 24 Y=FuncA(Y); (gdb) down #1 0×0000000000400530 in FuncA (X=99) at TestA.c:17 17 Z=FuncB(Y); (gdb) down #0 FuncB (X=14061) at TestA.c:6 6 Y=X+40; (gdb) |
up | 可以讓開發者在位於Call Stack前端的Frame,透過 up指令依序往Frame #2方向移動.基本的演示如下所示
(gdb) bt #0 FuncB (X=14061) at TestA.c:6 #1 0×0000000000400530 in FuncA (X=99) at TestA.c:17 #2 0×0000000000400568 in main () at TestA.c:24 (gdb) up #1 0×0000000000400530 in FuncA (X=99) at TestA.c:17 17 Z=FuncB(Y); (gdb) up #2 0×0000000000400568 in main () at TestA.c:24 24 Y=FuncA(Y); (gdb) |
檢查變數
指令 | 說明 |
watch | watch x
可以用來在變數x值被改變時,觸發Break Point,進行分析 watch x > ,< ,== 可以針對watch 動作加上條件判斷,例如 watch x>100 ,就表示當變數x值被改為 > 100時,就觸發Break Point,進行分析 |
Print指令可以用來顯示變數當下的數值內容,如下所示
(gdb) n 24 Y=FuncA(Y); (gdb) p Y $2 = 99 也可以設定要顯示的變數Type,例如筆者演示為16進位方式顯示 (gdb) p /x Y $3 = 0×63 (gdb) 也可以用來顯示struct,例如 int main() { struct timeval Now; unsigned long P; gettimeofday(&Now, NULL); P=(unsigned long)&Now; ………………….. } 可以透過如下的 Print指令,搭配Type Casting,把struct內容顯示出來 (gdb) p Now $1 = {tv_sec = 1331821430, tv_usec = 857355} (gdb) p (struct timeval *)P $4 = (struct timeval *) 0x7fffffffe570 (gdb) p ((struct timeval *)P)->tv_usec $5 = 857355 (gdb) p ((struct timeval *)P)->tv_sec $6 = 1331821430 (gdb) p /x ((struct timeval *)P)->tv_usec $8 = 0xd150b (gdb) p /x ((struct timeval *)P)->tv_sec $9 = 0x4f61fb76 也可以做邏輯上的運算 (gdb) p (((struct timeval *)P)->tv_usec + ((struct timeval *)P)->tv_sec) $3 = 1332083044 | |
ptype | 可以透過ptype顯示變數的原始宣告型態
(gdb) ptype Y type = int 另一個演示的例子 (gdb) ptype P type = long unsigned int 或是也可以顯示結構的內容, (gdb) ptype Now type = struct timeval { __time_t tv_sec; __suseconds_t tv_usec; } |
流程操作
指令 | 說明 |
run | 執行程式,若該程式有參數 例如,test 1 2 3,在這就可以執行run 1 2 3 讓應用程式main的 argv與argc操作可以執行. |
finish | 等於step out,也就是把函式執行完畢後return暫停 |
next | 等於step over,也就是在同一層函式中執行,呼叫到副函式時,不會進入執行,會等該副函式執行完畢後,繼續往下一行執行 |
step | 等於step in,也就是在同一層函式中執行,呼叫到副函式時,會一路跟進下一層執行,若發現跟進下一層後,路徑太長而希望回到上一層的話,就可以透過finish把目前函式執行完畢後,回到上一層進入這函式入口的下一個指令. |
until | 執行程式碼,直到執行到for/while下一個循環程式碼的下一行Source Code. |
continue | 這指令式在程式執行run後,因為break point觸發,或使用者自己透過ctrl+c讓正在執行中的程式中斷執行後,等使用者把相關要加上的break point加上,或是要觀察的變數與Source Code確認完畢後,就可以透過continue指令,讓程式在剛才中斷的位置繼續執行下去. |
查閱記憶體內容
指令 | 說明 |
dump | 可以透過如下指令把記憶體內容Dump到外部檔案中
dump memory /path start-address end-address 例如可以把已知是一個Mpeg Video檔案格式的記憶體內容Dump出來,作為除錯的目的之用. dump memory ~/video.mpg 0×41000000 0×42000000 |
X | 列印十六進位的資料
x/8xw $sp 把PC記憶體內容反組譯為指令 x/8iw $pc 或是把 main函式記憶體內容反組譯為指令 (gdb) x /8iw main 0x842c <main>: push {r11, lr} 0×8430 <main+4>: add r11, sp, #4 0×8434 <main+8>: ldr r0, [pc, #12] ; 0×8448 <main+28> 0×8438 <main+12>: bl 0×8374 <sleep> 0x843c <main+16>: mov r3, #0 0×8440 <main+20>: mov r0, r3 0×8444 <main+24>: pop {r11, pc} 0×8448 <main+28>: addseq r9, r8, pc, ror r6 |
GDB Script
基本的環境變數組成
file builtin-symbol-execution-file
set solib-search-path builtin-symbol-library-file-path
set solib-absolute-prefix builtin-symbol-library-filepath-prefix
core core-dump-file
GDB本身除了大家熟知的指令外,還包括對於GDB Script的支援,像是一般常見的變數宣告(set),if/else,while這些條件判斷與迴圈的支援,都可讓使用GDB環境的開發者,基於GDB Script建構一個強大的分析環境.
如下筆者列出自己常用到的Script寫法
寫出檔案
Set logging file output.txt
開啟Logging
Set logging on
設定變數
Set $xxx=0
Set $i=0
列印字串 print
Print “xxxx”,$xxx,”yyyy\n”
讀取全域變數的資料結構
Set $abc = gTable->ItemIndex
設定 while
While($i<1024)
{
}
設定變數結構的讀取
Set $String=($abc)[$i]
if/else條件判斷
if
…
End
設定print out格式
x /16xw $addr
關閉Logging
Set logging off
網路上有很多關於GDB Script語法介紹的網站,在此就不累述,有興趣的開發者請自行參閱即可.
參考幾個 GDB 處理實例
案例 #A: 如果應用程式 Stack 被覆蓋
通常遇到這樣的問題,會導致GDB BackTrace無法解析出正確的結果,例如下面的例子,我們可以透過bt full去查看目前掛掉應用程式Core Dump的Back Trace內容,但因為有Stack記憶體區塊損壞的問題,所以導致Back Trace的內容不正確
如果根據現場的行為判定,Stack可能是部分損壞,我們可以先透過info registers確認目前r12,r13與14的內容
然後透過UltraEdit去搜尋r12,r13與14三個暫存器連續的12 bytes值 (0x68231940A84874559F361440),如下圖所示
找到後,我們透過指令 x/8xw $sp 確認可能有效的Stack起點,如下所示,筆者根據問題的判斷,選擇把Stack起點從0x557448a8設為0x557448b8
如下所示透過UltraEdit修改,並進行儲存Core Dump的動作
重新載入有被修改內容的Core Dump,如下所示SP(r13)暫存器被設定為0x557448b8,
基於新的Stack起點,重新透過 bt 指令,可以看到正確的 Back Trace被解析出來,藉此我們就可以去推敲是因為怎樣的原因導致最後的Stack內容被覆蓋,以及在怎樣的情況下,會讓ARM Program Counter執行到最後問題發生的所在位置
案例 #B: 程式執行過程 , 被資料溢位覆蓋
通常在軟體整合過程中,包括來自Linux Kernel中撰寫不適當的Driver,或是硬體DMA的值錯誤,或在操作所配置的記憶體區塊時,因為沒有考慮到邊界問題,導致有大面積的資料溢位覆蓋時,通常可以透過GDB把這整塊覆蓋過去的記憶體內容 dump出來,以便透過格式的分析找出可能是被哪類大量搬移的資料所覆蓋.
dump memory /path start-address end-address
dump memory /loda/xxxx.raw 0×41000000 0×42000000
通常如果是儲存裝置的DMA搬移,在這塊記憶體中就會有相關檔案的內容甚至是明確的檔頭,又或者是因為被多媒體的資料內容所覆蓋,在這塊記憶體範圍內就會有16-bits左右聲道的Audio資料,或YUV16資料,或32bits ARGB資料.
找出疑似覆蓋的資料內容格式後,就可以把問題的追查限縮在有限的範圍內.
案例 #C: 多執行緒下 , 全域變數值被不預期的修改
通常在多工環境下,如果有一些資料結構或是變數會不預期的被修改,而又希望可以找出是誰去修改的,就可以透過GDB的watch指令,使用ARM上的Hardware WatchPoint來監視資料是否有被透過ARM處理器的操作而被修改到或滿足特定的條件下,進行除錯斷點的觸發.
但需要注意的是,Hardware WatchPoint只限於透過ARM處理器的存取監控,如果這個變數是被硬體DMA或其它共用外部DRAM的處理器所修改的,就無法透過watch指令進行監控.
如下舉一個簡單的例子來加以說明,
int gCount=0;
void thread_func() { int i=0; while(1) { i++; printf("Thread:%ld\n",i); if(i>99) gCount= -1; sleep(1); } } int main() { int i; pthread_t tid; pthread_create(&tid,0,(void *)thread_func,0); sleep(9999999); return 0; } |
執行GDB後,透過Watch指令去查看全域變數何時被修改為負值
(gdb) watch gCount<0 =>設定當gCount 為負值時觸發watchpoint中斷點
Hardware watchpoint 1: gCount<0
(gdb) run
Starting program: /home/loda/test/test2
[Thread debugging using libthread_db enabled]
[New Thread 0x7ffff7831700 (LWP 7860)]
Thread:1
Thread:2
Thread:3
Thread:4
Thread:5
Thread:6
……………
Thread:97
Thread:98
Thread:99
Thread:100
[Switching to Thread 0x7ffff7831700 (LWP 7860)]
Hardware watchpoint 1: gCount<0 =>在這因gCount<0 而觸發硬體的watchpoint中斷點.
Old value = 0
New value = 1
thread_func () at test2.c:14
14 sleep(1);
(gdb) p gCount
$1 = -1
(gdb) list 14
9 {
10 i++;
11 printf("Thread:%ld\n",i);
12 if(i>99)
13 gCount=-1;
14 sleep(1); =>最後是在這行之後,符合 gCount<0.
15 }
16 }
17 int main()
18 {
如上述例子,就可在某一條執行緒把全域變數改為負值時,透過watch指令觸發中斷點,暫停程式執行,讓開發者可以去檢視自己程式設計上的缺陷為何.
若是有一個變數,會被不預期寫入任意值,則透過watch 指令,但不加上判斷式,如下所示
(gdb) watch gCount
Hardware watchpoint 1: gCount
(gdb) run
Starting program: /home/loda/test/test2
[Thread debugging using libthread_db enabled]
[New Thread 0x7ffff7fe4700 (LWP 7799)]
Thread:1
Thread:2
Thread:3
…….
Thread:98
Thread:99
Thread:100
[Switching to Thread 0x7ffff7fe4700 (LWP 7799)]
Hardware watchpoint 1: gCount =>同前例,由於gCount值被改變了,而觸發Break Point.
Old value = 0
New value = -1
thread_func () at test2.c:14
14 sleep(1);
(gdb)
案例 #D: 如果 CoreDump 中的 Stack 污染 , 導致回推 Back Trace 被截斷 , 但又想試著把剩下有限的資訊 Debug 出來
如下所示,是筆者在遇到bt指令無法Decode出有意義資訊,或不想手動再透過UltraEdit修改Core Dump中的SP暫存器時,會採用的Stack Decode Script (相信一定有更好的寫法,以下只供參考.).
file builtin-symbol-execution-file
set solib-search-path builtin-symbol-library-file-path
set solib-absolute-prefix builtin-symbol-library-filepath-prefix
core core-dump-file
set print symbol-filename on =>設定Print Symbol時,把檔案位置與行數也印出
set logging file stack-out.txt =>把結果寫出到檔案中
set logging on
set $stack_addr=$sp
printf "Start to decode tack\n"
set $i=0
while($i<256) =>設定依據SP暫存器,往上把256*4 也就是1024 bytes,依序進行Print Symbol
printf "i:%ld stack_addr:%xh\n",$i,$stack_addr
print /a *(int *) $stack_addr
set $stack_addr = $stack_addr +4
set $i = $i + 1
end
printf "End\n"
set logging off
總結上述範例,其實最重要的還是對於系統與處理器的了解,再透過熟悉GDB的命令,就可以在這些平台上,讓GDB成為我們最棒的除錯工具.
結語
在Linux平台上開發,使用GDB無疑是除錯工具的首選,不論是在Run-Time分析或是發生Core Dump的事後分析工作,GDB都會是讓事情事半功倍的最佳工具.
除錯的技巧,其實只是熟悉而已,真正對解決問題有幫助的還是這些基於處理器與作業系統的抽象概念,與內部細節的理解,希望本文可以帶給大家對於GDB除錯器的進一步認識,也期待在我們周圍可以有更多開發者熟悉這套工具,進而對產品與問題的解決上有更進一步深入探索.當然,如果可能的話,基於分享的理念,時間許可也能參與撰寫技術文章的行列,讓這些技術資訊的普及可以更為無遠弗屆.
本文如果有未盡完善之處,還請不吝指教.