造輪子-strace(二)實現

這一篇文章會介紹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基礎功能。造個輪子能更好的學習,大家學會了么?

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