from
How Debugger Works核心是系統(tǒng)調(diào)用
ptrace
坷备,ptrace
可以讓一個process窺探另一個process的內(nèi)部,甚至控制另一個process舅踪。首先創(chuàng)建兩個process辩恼,parent運行debugger,child運行target鱼鼓。target程序調(diào)用ptrace系統(tǒng)調(diào)用。參數(shù)PTRACE_TRACEME告訴內(nèi)核可以讓parent跟蹤自己该编。
void run_target(const char* programname)
{
procmsg("target started. will run '%s'\n", programname);
/* Allow tracing of this process */
if (ptrace(PTRACE_TRACEME, 0, 0, 0) < 0) { perror("ptrace"); return; }
/* Replace this process's image with the given program */
execl(programname, programname, 0);
}
Indicates that this process is to be traced by its parent. Any signal (except SIGKILL) delivered to this process will cause it to stop and its parent to be notified via wait(). Also, all subsequent calls to exec() by this process will cause a SIGTRAP to be sent to it, giving the parent a chance to gain control before the new program begins execution. A process probably shouldn't make this request if its parent isn't expecting to trace it. (pid, addr, and data are ignored.)
- 也就是說在child運行execl之前停止運行迄本,通知parent,發(fā)送一個signal课竣。此時parent
等到了信號發(fā)生嘉赎,并且用WIFSTOPPED
判斷是否是child是否停止。如果是就調(diào)用ptrace但是參數(shù)是PTRACE_SINGLESTEP
于樟,讓child單步運行公条。單步運行意味著child每運行一個指令都要通知parent,這里的while循環(huán)就是這個意思迂曲,直到parent收到停止的信號靶橱。注意這里的icounter就是指令計數(shù)器,每收到一個信號就計數(shù)一次。
void run_debugger(pid_t child_pid)
{
int wait_status;
unsigned icounter = 0;
procmsg("debugger started\n");
/* Wait for child to stop on its first instruction */
wait(&wait_status);
while (WIFSTOPPED(wait_status)) {
icounter++;
/* Make the child execute another instruction */
if (ptrace(PTRACE_SINGLESTEP, child_pid, 0, 0) < 0) { perror("ptrace"); return;}
/* Wait for child to stop on its next instruction */
wait(&wait_status);
}
procmsg("the child executed %u instructions\n", icounter);
}
- 另外关霸,還有其他的ptrace參數(shù)可以幫助獲得child的各種信息传黄,包括寄存器,指令等谒拴。
struct user_regs_struct regs; ptrace(PTRACE_GETREGS, child_pid, 0, ®s); unsigned instr = ptrace(PTRACE_PEEKTEXT, child_pid, regs.eip, 0); procmsg("icounter = %u. EIP = 0x%08x. instr = 0x%08x\n", icounter, regs.eip, instr);
結(jié)果如下:
$ simple_tracer traced_helloworld
[5700] debugger started
[5701] target started. will run 'traced_helloworld'
[5700] icounter = 1. EIP = 0x08048080. instr = 0x00000eba
[5700] icounter = 2. EIP = 0x08048085. instr = 0x0490a0b9
[5700] icounter = 3. EIP = 0x0804808a. instr = 0x000001bb
[5700] icounter = 4. EIP = 0x0804808f. instr = 0x000004b8
[5700] icounter = 5. EIP = 0x08048094. instr = 0x01b880cd
Hello, world!
[5700] icounter = 6. EIP = 0x08048096. instr = 0x000001b8
[5700] icounter = 7. EIP = 0x0804809b. instr = 0x000080cd
[5700] the child executed 7 instructions
attach也并不難尝江,只用給ptrace一個參數(shù)即可,PTRACE_ATTACH
問題是每次單步實在是太費勁了英上,動輒幾千條指令炭序,這個時候breakpoint就是一個好的選擇了。
debugger的兩個基石苍日,一個是breakpoint惭聂,另一個是能夠探測debugged process的memory。
breakpoint就是一個軟中斷相恃,x86上面的3號軟中斷就是專門用來trap to debugger的
The INT 3 instruction generates a special one byte opcode (CC) that is intended for calling the debug exception handler. (This one byte form is valuable because it can be used to replace the first byte of any instruction with a breakpoint, including other one byte instructions, without over-writing other code).
Linux接收到int 3的時候會想process發(fā)送SIGTRAP辜纲,聯(lián)系到之前的child會自動向parent發(fā)送停止信號,就是因為先收到SIGTRAP拦耐。
假設由如下匯編代碼耕腾,這個代碼先打印hello,再打印world杀糯,我們的目的是在打印hello之后暫停進程扫俺。我們只需要將指令
mov edx, len2
替換成int 3
就行了。
section .text
; The _start symbol must be declared for the linker (ld)
global _start
_start:
; Prepare arguments for the sys_write system call:
; - eax: system call number (sys_write)
; - ebx: file descriptor (stdout)
; - ecx: pointer to string
; - edx: string length
mov edx, len1
mov ecx, msg1
mov ebx, 1
mov eax, 4
; Execute the sys_write system call
int 0x80
; Now print the other message
mov edx, len2 <---- replaced by int 3
mov ecx, msg2
mov ebx, 1
mov eax, 4
int 0x80
; Execute sys_exit
mov eax, 1
int 0x80
section .data
msg1 db 'Hello,', 0xa
len1 equ $ - msg1
msg2 db 'world!', 0xa
len2 equ $ - msg2
- 實際上debugger要做兩件事:
- 記住原先的指令
- 將原先指令的第一個byte替換成int 3
- 當debugger讓程序按照PTRACE_CONT運行固翰,運行到int 3的時候收到信號狼纬,然后通知debugger child已經(jīng)停止。debugger要做:
- 恢復原來的指令
- ip - 1
- 可以peek child的狀態(tài)骂际,比如寄存器疗琉,變量等
- 恢復breakpoint,因為用戶沒有要求delete breakpoint
/* Obtain and show child's instruction pointer */
ptrace(PTRACE_GETREGS, child_pid, 0, ®s);
procmsg("Child started. EIP = 0x%08x\n", regs.eip);
/* Look at the word at the address we're interested in */
unsigned addr = 0x8048096;
unsigned data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("Original data at 0x%08x: 0x%08x\n", addr, data);
[13028] Child started. EIP = 0x08048080
[13028] Original data at 0x08048096: 0x000007ba
/* Write the trap instruction 'int 3' into the address */
unsigned data_with_trap = (data & 0xFFFFFF00) | 0xCC;
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data_with_trap)
;
/* See what's there again... */
unsigned readback_data = ptrace(PTRACE_PEEKTEXT, child_pid, (void*)addr, 0);
procmsg("After trap, data at 0x%08x: 0x%08x\n", addr, readback_data);
[13028] After trap, data at 0x08048096: 0x000007cc
/* Let the child run to the breakpoint and wait for it to** reach it*/
ptrace(PTRACE_CONT, child_pid, 0, 0);
wait(&wait_status);
if (WIFSTOPPED(wait_status)) {
procmsg("Child got a signal: %s\n", strsignal(WSTOPSIG(wait_status)));
}else {
perror("wait"); return;
}
/* See where the child is now */
ptrace(PTRACE_GETREGS, child_pid, 0, ®s);
procmsg("Child stopped at EIP = 0x%08x\n", regs.eip);
This prints:
Hello,[13028] Child got a signal: Trace/breakpoint trap[13028] Child stopped at EIP = 0x08048097
/* Remove the breakpoint by restoring the previous data** at the target address, and unwind the EIP back by 1 to** let the CPU execute the original instruction that was** there.*/
ptrace(PTRACE_POKETEXT, child_pid, (void*)addr, (void*)data);
regs.eip -= 1;
ptrace(PTRACE_SETREGS, child_pid, 0, ®s);
/* The child can continue running now */
ptrace(PTRACE_CONT, child_pid, 0, 0);
int 3只占用一個字節(jié)是故意為之的歉铝,否則可能損毀后面的指令
假設有下面的代碼盈简,我想在do_stuff的入口break。首先可以先差找到入口指令的地址太示,這里因為是個循環(huán)我們要使breakpoint能夠在觸發(fā)后恢復柠贤。
#include <stdio.h>
void do_stuff()
{
printf("Hello, ");
}
int main()
{
for (int i = 0; i < 4; ++i)
do_stuff();
printf("world!\n");
return 0;
}
080483e4 <do_stuff>:
80483e4: 55 push %ebp
80483e5: 89 e5 mov %esp,%ebp
80483e7: 83 ec 18 sub $0x18,%esp
80483ea: c7 04 24 f0 84 04 08 movl $0x80484f0,(%esp)
80483f1: e8 22 ff ff ff call 8048318 <puts@plt>
80483f6: c9 leave
80483f7: c3 ret
void run_debugger(pid_t child_pid)
{
procmsg("debugger started\n");
/* Wait for child to stop on its first instruction */
wait(0);
procmsg("child now at EIP = 0x%08x\n", get_child_eip(child_pid));
/* Create breakpoint and run to it*/
debug_breakpoint* bp = create_breakpoint(child_pid, (void*)0x080483e4);
procmsg("breakpoint created\n");
ptrace(PTRACE_CONT, child_pid, 0, 0);
wait(0);
/* Loop as long as the child didn't exit */
while (1) {
/* The child is stopped at a breakpoint here. Resume its
** execution until it either exits or hits the
** breakpoint again.
*/
procmsg("child stopped at breakpoint. EIP = 0x%08X\n", get_child_eip(child_pid));
procmsg("resuming\n");
int rc = resume_from_breakpoint(child_pid, bp);
if (rc == 0) {
procmsg("child exited\n");
break;
}
else if (rc == 1) {
continue;
}
else {
procmsg("unexpected: %d\n", rc);
break;
}
}
cleanup_breakpoint(bp);
}
int resume_from_breakpoint(pid_t pid, debug_breakpoint* bp)
{
struct user_regs_struct regs;
int wait_status;
ptrace(PTRACE_GETREGS, pid, 0, ®s);
/* Make sure we indeed are stopped at bp */
assert(regs.eip == (long) bp->addr + 1);
/* Now disable the breakpoint, rewind EIP back to the original instruction
** and single-step the process. This executes the original instruction that
** was replaced by the breakpoint.
*/
regs.eip = (long) bp->addr;
ptrace(PTRACE_SETREGS, pid, 0, ®s);
disable_breakpoint(pid, bp);
if (ptrace(PTRACE_SINGLESTEP, pid, 0, 0)) {
perror("ptrace");
return -1;
}
wait(&wait_status);
if (WIFEXITED(wait_status))
return 0;
/* Re-enable the breakpoint and let the process run.
*/
enable_breakpoint(pid, bp);
if (ptrace(PTRACE_CONT, pid, 0, 0) < 0) {
perror("ptrace");
return -1;
}
wait(&wait_status);
if (WIFEXITED(wait_status))
return 0;
else if (WIFSTOPPED(wait_status)) {
return 1;
}
else
return -1;
}