造輪子-strace(二)實現

東北碼農發表於2022-01-05

這一篇文章會介紹strace如何工作,再稍微深入介紹一下什麼是system call。再介紹一下ptrace、wait(strace依賴的system call)。最後再一起來造個輪子,動手用程式碼實現一個strace。聊天框回覆“strace”,可以獲取本文原始碼。


上一篇,我們介紹了strace工具,strace是非常實用的除錯、分析工具,可以記錄process呼叫system call的引數、返回值。

效果展示
下面展示一下我們實現簡化版的strace的效果,每行列印一個system call。只不過沒有根據system call的序號轉換引數型別來列印,畢竟我們實現的目的是學習。

root@xxx:~/code/case/case20_ptrace/tracer$ ./xx_strace /usr/bin/ls

brk(0) = 0x5626edd99000
arch_prctl() = -22
access(0x7f5b2155f9e0, 4) = -2
openat(0xffffff9c, 0x7f5b2155cb80, 524288, 0) = 3
fstat(3, 0x7ffc965a7fe0) = 0
mmap(0, 33585, 1, 2, 3, 0) = 0x7f5b2152e000
close(3) = 0
openat(0xffffff9c, 0x7f5b21566e10, 524288, 0) = 3
read(3, 0x7ffc965a8188, 832) = 832
fstat(3, 0x7ffc965a8030) = 0
mmap(0, 8192, 3, 34, 0xffffffff, 0) = 0x7f5b2152c000
mmap(0, 174600, 1, 2050, 3, 0) = 0x7f5b21501000
mprotect(0x7f5b21507000, 135168, 0) = 0
mmap(0x7f5b21507000, 102400, 5, 2066, 3, 24576) = 0x7f5b21507000
mmap(0x7f5b21520000, 28672, 1, 2066, 3, 126976) = 0x7f5b21520000
mmap(0x7f5b21528000, 8192, 3, 2066, 3, 155648) = 0x7f5b21528000
mmap(0x7f5b2152a000, 6664, 3, 50, 0xffffffff, 0) = 0x7f5b2152a000
close(3) = 0
openat(0xffffff9c, 0x7f5b2152c4e0, 524288, 0) = 3
read(3, 0x7ffc965a8168, 832) = 832

1、system call

上一篇,我們簡單介紹了系統呼叫(system call)。strace就是記錄system call的工具,我們需要深入瞭解一下system call。

1.1、system call序號

每個system call都有一個序號,記錄在
/usr/include/x86_64-linux-gnu/asm/unistd_64.h檔案中。我們常見的read、write、open都在其中定義。

#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1

#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
...

1.2、使用syscall直接呼叫system call

我們可以呼叫glibc封裝的system call(例如close、connect、bind等),還可以使用syscall直接呼叫。glibc中的封裝最終也是呼叫了syscall。

 long syscall(long number, ...);

例如我們呼叫tgkill時

int tgkill(int tgid, int tid, int sig);

我們可以使用glibc封裝的

tgkill(getpid(), tid, SIGHUP);

也可以使用syscall直接傳system call 序號和對於引數來呼叫。

syscall(SYS_tgkill, getpid(), tid, SIGHUP);

1.3、syscall引數、返回值

我們需要了解一下呼叫syscall時,使用者層與核心是互動互動返回值和引數的。


根據man syscall手冊。不同的cpu通過不同暫存器來傳遞。

返回值:

Arch/ABI    Instruction           System  Ret  Ret  Error    Notes
                                  call #  val  val2
───────────────────────────────────────────────────────────────────
arm64       svc #0                x8      x0   x1   -
x86-64      syscall               rax     rax  rdx  -        5
x32         syscall               rax     rax  rdx  -        5

x86-64位下,返回值在rax暫存器。


引數列表:

Arch/ABI      arg1  arg2  arg3  arg4  arg5  arg6  arg7  Notes
──────────────────────────────────────────────────────────────
arm64         x0    x1    x2    x3    x4    x5    -
x86-64        rdi   rsi   rdx   r10   r8    r9    -
x32           rdi   rsi   rdx   r10   r8    r9    -

x86-64位下,引數依次是rdi rsi rdx r10 r8 r9。

2、strace工作流程

首先介紹我們把tracer和tracee的概念:我們把跟蹤者(strace)叫做tracer,被跟蹤process叫做tracee。

strace整體工作流程如下:

造輪子-strace(二)實現

 

  • 藍色部分:建立trace關係。
  • 橙色部分:子程式執行指令。
  • 綠色部分:迴圈跟蹤tracee的system call。

3、ptrace && wait

磨刀不誤砍柴工,我們也來介紹一下strace工作時兩個重要函式。

3.1、ptrace

通過上面流程圖,可以看出strace在建立trace關係、跟蹤system call時都依賴ptrace。

long ptrace(enum __ptrace_request request, pid_t pid,
            void *addr, void *data);

man ptrace

The ptrace() system call provides a means by which 
one process (the "tracer") may observe and control the execution of another process (the "tracee"), 
and examine and change the tracee's memory  and  registers. 
It is primarily used to implement breakpoint debugging and system call tracing.

man手冊是這麼描述的:ptrace可以讓tracer觀察並控制tracee的執行,並可以獲取並修改tracee的記憶體和暫存器。可以用來實現偵錯程式或system call跟蹤。實際上gdb和strace都是依賴ptrace來實現的。

引數

  • request:決定ptrace的行為,一會用到哪個介紹哪個。
  • pid:tracee的pid,被監控者。
  • addr,data:根據request不同有不同含義。

3.2、wait介紹

wait也是strace工作時也很重要,先看看man手冊。

pid_t wait(int *wstatus);

man wait

wait is used to wait for state changes in a child of the calling process, 
A state change is considered to be: 
the child  terminated;
the  child  was stopped by a signal; 
or the child was resumed by a signal.  

If a child has already changed state, then these calls return immediately.  
Otherwise, they block until either a child changes state。

man手冊是這麼描述的:wait用來等待子程式狀態改變,包括退出、stopped、resumed。
如果子程式狀態已經改變了,wait會立刻返回。否則會卡住等待狀態改變。


狀態通過wstatus返回,wait也提供了一系列配套巨集來判斷狀態。

strace使用wait有2個場景:

  • 建立trace關係後,等待tracee變成stop狀態。
  • 開始跟蹤,等到tracee呼叫system call。

4、strace實現

4.1、建立trace關係

strace工作的第一步就是建立trace關係,按照不同啟動模式採取不同的方式建立。無論是哪種模式,都需要與tracee建立trace關係。才能監控system call的呼叫。

strace的啟動模式:

  • attach模式:strace -p pid,trace已經啟動的程式。
  • strace啟動模式:strace cmd,trace新啟動程式。

attach模式建立trace關係
strace呼叫ptrace(request=PTRACE_ATTACH)與tracee建立trace關係。

static bool xx_trace_attach(pid_t pid){
    auto ret = ptrace(PTRACE_ATTACH, pid, NULL, NULL);
    X_CHECK(ret >=0 ,false);
    return true;
}

strace啟動模式建立trace關係
此模式下,strace需要先執行fork。然後父程式作為tracer,子程式作為tracee。

子程式fork以後,還需要執行ptrace(request=PTRACE_TRACEME)來建立trace關係。

static bool xx_trace_me()
{
    auto ret = ptrace(PTRACE_TRACEME, 0L, 0L, 0L);
    X_CHECK(ret >=0 ,false);
    return true;
}

建立好trace關係以後,子程式還需要呼叫execv來執行tracee的邏輯。

void tracee(int argc, char *argv[])
{
    xx_trace_me();
    execv(argv[0], argv);
}

程式碼
無論是attach模式還是strace啟動模式,建立trace關係後,都執行相同的邏輯,程式碼可以複用。

int main(int argc, char *argv[])
{

    const char *spid = xx_get_arg(argc, argv, "-p");
    // atach模式
    if (nullptr != spid)
    {
        pid_t pid = atoi(spid);
        xx_trace_attach(pid); // attch
        tracer(pid);          // 開始跟蹤system call
    }
    // strace啟動模式
    else
    {
        pid_t pid = fork();
        if (0 == pid)
        {
            tracee(argc - 1, argv + 1); // 執行tracee指令
        }
        else if (pid > 0)
        {
            tracer(pid); // 開始跟蹤system call
        }
    }

    return 0;
}

4.2、等待tracee進入stop狀態

建立trace關係後,strace需要呼叫wait,來等待tracer變為stop狀態。

// 等待tracee變為stop狀態
int child_status = 0;
wait(&child_status);
printf("child_status=%s\n", xx_waitstate2str(child_status).c_str());

xx_waitstate2str是封裝好,列印子程式狀態的

static string xx_waitstate2str(int status)
{
    if(WIFEXITED(status))       return "terminated normally\n";
    if(WIFSIGNALED(status))     return "terminated by a signal\n";
    if(WIFSTOPPED(status))      return "stopped by delivery of a signal\n";
    if(WIFCONTINUED(status))    return "resumed\n";

    return "state?\n";
}

螢幕輸出

child_status=stopped by delivery of a signal

4.3、迴圈跟蹤system call

建立好trace關係後,tracee是處於stop狀態的。下一步開始迴圈跟蹤tracee的system call。

strace使用ptrace 跟蹤tracee的system call時會有兩次攔截,一次是呼叫前,一次是呼叫完成後。

4.3.1、呼叫前攔截

呼叫前攔截時,有以下操作:

  1. strace喚醒:strace呼叫ptrace(request=PTRACE_SYSCALL),喚醒tracee繼續執行。
  2. strace等待:strace呼叫wait等待,此時wait會卡住。
  3. tracee呼叫system call前:tracee會進入stop狀態;strace呼叫wait返回,被喚醒。
  4. strace獲取資訊:stracewait返回後,可以呼叫ptrace(request=PTRACE_GETREGS)獲取暫存器的資訊。

呼叫前攔截時,system call還沒被呼叫,通過暫存器資訊,可以獲取:

  • 呼叫的system call的序號。
  • 呼叫前的引數資訊。(不過因為有些system call會通過引數向外傳遞資訊,我們選擇system call之後的攔截來獲取引數。)

4.3.2、呼叫後攔截

  1. strace喚醒:同呼叫前攔截。
  2. strace等待:同呼叫前攔截。
  3. tracee呼叫system call後:同呼叫前攔截。
  4. strace獲取資訊:同呼叫前攔截。

調研後攔截時,system call已呼叫完畢。通過暫存器可以獲取返回值,以及呼叫後的引數。前面1.3章節介紹了system call在不同cpu架構使用哪些暫存器。

4.3.3、攔截程式碼實現

下面我們來看看程式碼實現.

步驟1(喚醒)、2(等待)
我們封裝了一個函式

void wait_syscall(pid_t child)
{
    // 1.喚醒
    int child_status = 0;
    auto ret = ptrace(PTRACE_SYSCALL, child, 0, 0);
    if (ret < 0)
    {
        X_P_INFO;
    }

    // 2. 等待
    wait(&child_status);

    // 3. 是否已退出?
    if (WIFEXITED(child_status))
    {
        printf("exited in syscalls with status=%d\n", child_status);
        exit(0);
    }
}

迴圈跟蹤主題
迴圈主題主要就是兩次攔截、獲取資訊、列印資訊。

while (1)
{
    // 呼叫前攔截
    syscall_info info;
    wait_syscall(child);
    {
        // 獲取暫存器資訊
        struct user_regs_struct reg;
        bool get_reg = xx_trace_get_reg(child, reg);
        assert(get_reg);
        // 獲取:system call 序號
        info.set_before_call(reg);
    }
    // 呼叫後攔截
    wait_syscall(child);
    {
        // 獲取暫存器資訊
        struct user_regs_struct reg;
        bool get_reg = xx_trace_get_reg(child, reg);
        assert(get_reg);
        // 獲取:引數、返回值
        info.set_after_call(reg);
    }
    // 列印資訊
    info.print();
}

儲存system call資訊

struct syscall_info
{
    uint64_t syscall_no = 0;
    uint64_t syscall_ret = 0;// 返回值
    uint64_t para[6] = {0};// 引數
    。。。
}

呼叫前攔截、獲取system call序號

struct syscall_info
{
    void set_before_call(const user_regs_struct ®)
    {
        syscall_no = reg.orig_rax;
    }

}

呼叫後攔截、獲取引數、返回值

struct syscall_info
{
    void set_after_call(const user_regs_struct ®)
    {
        syscall_ret = reg.rax;
        para[0] = reg.rdi;
        para[1] = reg.rsi;
        para[2] = reg.rdx;
        para[3] = reg.r10;
        para[4] = reg.r8;
        para[5] = reg.r9;
    }
}

不足200行程式碼,實現了strace基礎功能。造個輪子能更好的學習,大家學會了麼?

最後,東北碼農,全網同名,求關注、點贊、轉發,謝謝~

相關文章