Linux內核跟蹤之syscall tracer

------------------------------------------
本文系本站原創,歡迎轉載!
轉載請註明出處:http://ericxiao.cublog.cn/
------------------------------------------

一: 前言

Syscall tracer是用來跟蹤系統調用的,它會檢測所有系統調用的入口和出口,再將相關的信息保存到ring buffer.以下是syscall tracer的輸出的一個例子:
# echo syscall > current_tracer
# cat trace | tail
         <...>-13607 [000] 29097.902910: sys_close(fd: 3)
           <...>-13607 [000] 29097.902912: sys_close -> 0x0
           <...>-13607 [000] 29097.902962: sys_fstat64(fd: 1, statbuf: bfaac95c)
           <...>-13607 [000] 29097.902963: sys_fstat64 -> 0x0
           <...>-13607 [000] 29097.902965: sys_open(filename: bfaad8f4, flags: 8000, mode: 0)
從上面的信息號可以看出,有一個sys_close的系統調用,關閉的文件描述符是3(sys_close(fd: 3)
), 這個系統調用返回的是0(sys_close -> 0x0).
下面就從linux kernel源代碼的角度來分析syscall 的相關操作. 本文分析的源代碼版本基於v2.6.31-rc1,代碼基本上位於kernel/trace/trace_syscalls.c中.
 

二: syscall的初始化

Syscall的初始化入口爲:
device_initcall(register_ftrace_syscalls);
它的初始化函數爲register_ftrace_syscalls(),代碼如下:
__init int register_ftrace_syscalls(void)
{
    int ret;
 
    ret = register_ftrace_event(&syscall_enter_event);
    if (!ret) {
        printk(KERN_WARNING "event %d failed to register\n",
               syscall_enter_event.type);
        WARN_ON_ONCE(1);
    }
 
    ret = register_ftrace_event(&syscall_exit_event);
    if (!ret) {
        printk(KERN_WARNING "event %d failed to register\n",
               syscall_exit_event.type);
        WARN_ON_ONCE(1);
    }
 
    return register_tracer(&syscall_tracer);
}
從剛開始的例子可以看出,syscall entry和syscall exit的顯示方式是不相同的,這也是這個初始化函數中註冊兩個trace_event的原因.
Trace_event的相關操作在之前分析trace框架的時候已經分析過了,這裏不再贅述.具體這兩個trace_event是如何顯示信息的,在之後聯合syscall數據的保存再做分析.
 
此外,我們在初始化函數中還註冊了syscall tracer,它就是今天分析的重點.
 

三: syscall tracer

Syscall tracer定義如下:
static struct tracer syscall_tracer __read_mostly = {
    .name            = "syscall",
    .init       = init_syscall_tracer,
    .reset      = reset_syscall_tracer,
    .flags      = &syscalls_flags,
};
 
結合trace框架的分析,在register_tracer()的時候,會進行self test,但syscall 中並沒有selftest接口,說明syscall tracer在註冊的時候不會有self test操作. 這是因爲syscall是依賴於用戶空間的系統調用,在系統初始化的時候不可能發生用戶空間系統調用事件,因此,syscall在系統初始化時間是沒有實際操作的.
 
如果我們在用戶空間當syscall設置成當前的tracer:
# echo syscall > current_tracer
就會觸發tracing_set_tracer(),結合之前的分析,在”安裝”tracer的時候會調用:
tracer->init().並且會創建option文件.
在移除tracer的時候會調用tracer->reset().
從上面的結構中可以看出,syscall沒有自己的set_flag()操作,也即採用默認操作,在默認操作下,不管在任何情況下,設置或者清除任何標誌都是允許的(直接返回0).
 
Syscall的相關flags定義如下:
static struct tracer_opt syscalls_opts[] = {
    { TRACER_OPT(syscall_arg_type, TRACE_SYSCALLS_OPT_TYPES) },
    { }
};
 
static struct tracer_flags syscalls_flags = {
    .val = 0, /* By default: no parameters types */
    .opts = syscalls_opts
};
 
TRACER_OPT定義如下:
#define TRACER_OPT(s, b)    .name = #s, .bit = b
 
由此可見它的flags默認爲0,只有一個標誌,名稱爲”syscall_arg_type”,它的標誌爲:
enum {
    TRACE_SYSCALLS_OPT_TYPES = 0x1,
};
即佔用第一位.
 
在用戶空間驗證一下:
# echo syscall > current_tracer
# ls options/syscall_arg_type
options/syscall_arg_type
# cat options/syscall_arg_type
0
說明已經創建了一個名爲”syscall_arg_type”的文件,且初始值爲0.
 
Syscall的reset()接口爲reset_syscall_tracer(),代碼如下:
static void reset_syscall_tracer(struct trace_array *tr)
{
    stop_ftrace_syscalls();
    tracing_reset_online_cpus(tr);
}
先是調用stop_ftrace_syscalls()來停止syscall的跟蹤,因爲這時syscall tracer已經被別的tracer替換了. 然後再是調用traing_reset_online_cpus()來清空ring buffer.以免在別的tracer沒有init接口污染ring buffer(在tracing_set_tracer()中,只有tracer->init有定義的時候纔會清空ring buffer).
 
Stop_ftrace_syscalls()是用來停止syscall的跟蹤操作,它的代碼如下:
void stop_ftrace_syscalls(void)
{
    unsigned long flags;
    struct task_struct *g, *t;
 
    mutex_lock(&syscall_trace_lock);
 
    /* There are perhaps still some users */
    if (--refcount)
        goto unlock;
 
    read_lock_irqsave(&tasklist_lock, flags);
 
    do_each_thread(g, t) {
        clear_tsk_thread_flag(t, TIF_SYSCALL_FTRACE);
    } while_each_thread(g, t);
 
    read_unlock_irqrestore(&tasklist_lock, flags);
 
unlock:
    mutex_unlock(&syscall_trace_lock);
}
syscall_trace_lock鎖用來保護設置進程flag,以及操作計數,以保證其串行化.
在這裏設置refcount是爲了避免多次重複的操作,比如說,syscall已經是stop狀態了,但又有一個stop操作過來了,這時就沒必須再次stop syscall.
然後持有進程的保護讀寫自旋鎖,清除掉所有進程的TIF_SYSCALL_FTRACE標誌.
 
Syscall的init接口爲init_syscall_tracer(),代碼如下:
static int init_syscall_tracer(struct trace_array *tr)
{
    start_ftrace_syscalls();
 
    return 0;
}
Start_ftrace_syscalls代碼如下:
void start_ftrace_syscalls(void)
{
    unsigned long flags;
    struct task_struct *g, *t;
 
    mutex_lock(&syscall_trace_lock);
 
    /* Don't enable the flag on the tasks twice */
    if (++refcount != 1)
        goto unlock;
 
    arch_init_ftrace_syscalls();
    read_lock_irqsave(&tasklist_lock, flags);
 
    do_each_thread(g, t) {
        set_tsk_thread_flag(t, TIF_SYSCALL_FTRACE);
    } while_each_thread(g, t);
 
    read_unlock_irqrestore(&tasklist_lock, flags);
 
unlock:
    mutex_unlock(&syscall_trace_lock);
}
代碼很簡單,start_ftrace_syscalls()和stop_ftrace_syscall()做的是相反的事情,即爲每個進程設置TIF_SYSCALL_FTRACE標誌.
注意到,這裏還有一個新的操作,即arch_init_ftrace_syscalls(),這個函數用來初始化平臺的的syscalls,在x86平臺,該函數如下:
void arch_init_ftrace_syscalls(void)
{
    int i;
    struct syscall_metadata *meta;
    unsigned long **psys_syscall_table = &sys_call_table;
    static atomic_t refs;
 
    if (atomic_inc_return(&refs) != 1)
        goto end;
 
    syscalls_metadata = kzalloc(sizeof(*syscalls_metadata) *
                    FTRACE_SYSCALL_MAX, GFP_KERNEL);
    if (!syscalls_metadata) {
        WARN_ON(1);
        return;
    }
 
    for (i = 0; i < FTRACE_SYSCALL_MAX; i++) {
        meta = find_syscall_meta(psys_syscall_table[i]);
        syscalls_metadata[i] = meta;
    }
    return;
 
    /* Paranoid: avoid overflow */
end:
    atomic_dec(&refs);
}
首先,refs是局部靜態變量,用來防止過多的初始化,從上面的代碼可以看出,進入函數的時候,該計數+1,如果失敗,纔會減計數.
那是否在有些情況下,該函數會初始化失敗? 所以需要多次調用,直到它成功爲止?
先來看struct syscall_metadata的定義,它保存的是系統調用的元數據,如下:
struct syscall_metadata {
    const char  *name;
    int     nb_args;
    const char  **types;
    const char  **args;
};
這些保存的元數包括: 系統調用的名字(name),參數個數(nb_args),系統調用的參數類型(types),以及系統調用的參數名字(args).
 
從上面的代碼可以看到,syscall tracer所能支持的最大系統調用數是FTRACE_SYSCALL_MAX.
首先爲syscalls_metadata分配空間,然後調用find_syscall_meta()找到該系統調用對應的元數據.
find_syscall_meta()接受的參數是系統調用表中對應的處理函數,代碼如下:
static struct syscall_metadata *find_syscall_meta(unsigned long *syscall)
{
    struct syscall_metadata *start;
    struct syscall_metadata *stop;
    char str[KSYM_SYMBOL_LEN];
 
 
    start = (struct syscall_metadata *)__start_syscalls_metadata;
    stop = (struct syscall_metadata *)__stop_syscalls_metadata;
    kallsyms_lookup((unsigned long) syscall, NULL, NULL, NULL, str);
 
    for ( ; start < stop; start++) {
        if (start->name && !strcmp(start->name, str))
            return start;
    }
    return NULL;
}
從此可見,所有系統調用的元數據都會保存在從__start_syscalls_metadata到__stop_syscalls_metadata的區域.這個區域到底是怎麼形成的呢?
 
從vmlinux.lds.h中可以看到,有它的相關信息:
#define TRACE_SYSCALLS() VMLINUX_SYMBOL(__start_syscalls_metadata) = .; \
             *(__syscalls_metadata)             \
             VMLINUX_SYMBOL(__stop_syscalls_metadata) = .;
那就是說,他們表示的是__syscalls_metadata鏈接段的部份,所以只需要找到鏈接到這段的數據即可.
 
我們還是從系統調用的定義開始,有兩種情況,(下面的分析都是假設已經配置了syscall tracer的編譯宏:CONFIG_FTRACE_SYSCALLS):
1: 系統調用不帶參數
    這種情況下,是以SYSCALL_DEFINE0()定義的,這類系統調用有getpid()之類,它的定義如下:
#define SYSCALL_DEFINE0(sname)                  \
    static const struct syscall_metadata __used     \
      __attribute__((__aligned__(4)))           \
      __attribute__((section("__syscalls_metadata")))   \
      __syscall_meta_##sname = {            \
        .name       = "sys_"#sname,         \
        .nb_args    = 0,                \
    };                          \
    asmlinkage long sys_##sname(void)
從上面可以看出,這類系統調用的syscall_metadata數據中只有系統調用的名稱和參數個數(0).例如,如果是getpid系統調用,上面的數據爲:
__syscall_meta_get_pid = {
    .name = “sys_getpid”,
    .nb_args = 0,
}
 
2: 如果系統調用帶有參數
    這種情況下,通常是由SYSCALL_DEFINE1, SYSCALL_DEFINE2,……所定義,但歸根到底,它們都是由SYSCALL_DEFINEx擴展來的,如下示:
#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
……
……
 
來看一下SYSCALL_DEFINEx的定義:
#define SYSCALL_DEFINEx(x, sname, ...)              \
    static const char *types_##sname[] = {          \
        __SC_STR_TDECL##x(__VA_ARGS__)          \
    };                          \
    static const char *args_##sname[] = {           \
        __SC_STR_ADECL##x(__VA_ARGS__)          \
    };                          \
    SYSCALL_METADATA(sname, x);             \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)
上面的type_###sname就是類型數組,args###sname是參數名稱數組,這些都是在struct syscall_metadata的相關部份.
SYSCALL_METADATA()定義如下:
#define SYSCALL_METADATA(sname, nb)             \
    static const struct syscall_metadata __used     \
      __attribute__((__aligned__(4)))           \
      __attribute__((section("__syscalls_metadata")))   \
      __syscall_meta_##sname = {            \
        .name       = "sys"#sname,          \
        .nb_args    = nb,               \
        .types      = types_##sname,        \
        .args       = args_##sname,         \
    }
這個賦值了它的調用名稱,參數個數,它的參數類型和參數名稱分別指向了types_###sname, args###sname.
這兩個數組中的數據是怎麼樣形成的呢? 問題就回到了__SC_STR_TDECL##x(__VA_ARGS__)和__SC_STR_ADECL##x(__VA_ARGS__)是怎麼樣實現的.
對於__SC_STR_TDECL##x(__VA_ARGS__),如下示:
#define __SC_STR_TDECL1(t, a)       #t
#define __SC_STR_TDECL2(t, a, ...)  #t, __SC_STR_TDECL1(__VA_ARGS__)
#define __SC_STR_TDECL3(t, a, ...)  #t, __SC_STR_TDECL2(__VA_ARGS__)
#define __SC_STR_TDECL4(t, a, ...)  #t, __SC_STR_TDECL3(__VA_ARGS__)
#define __SC_STR_TDECL5(t, a, ...)  #t, __SC_STR_TDECL4(__VA_ARGS__)
#define __SC_STR_TDECL6(t, a, ...)  #t, __SC_STR_TDECL5(__VA_ARGS__)
該宏定義是一個遞歸定義,也就是說,它是取參數列表的第一個參數,然後跳過一個參數,再取......
我們以sendto系統調用爲例進行分析:
它的定義爲:
SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
        unsigned, flags, struct sockaddr __user *, addr,
        int, addr_len)
因爲__SC_STR_TDECL##x()是先取第一個參數,然後隔一個參數再取一個參數,因此,上面的例子就成了:
__SC_STR_TDECL6 = int, void __user*, size_t, unsigned, struct sockaddr, int
 
__SC_STR_ADECL##x的定義如下:
#define __SC_STR_ADECL1(t, a)       #a
#define __SC_STR_ADECL2(t, a, ...)  #a, __SC_STR_ADECL1(__VA_ARGS__)
#define __SC_STR_ADECL3(t, a, ...)  #a, __SC_STR_ADECL2(__VA_ARGS__)
#define __SC_STR_ADECL4(t, a, ...)  #a, __SC_STR_ADECL3(__VA_ARGS__)
#define __SC_STR_ADECL5(t, a, ...)  #a, __SC_STR_ADECL4(__VA_ARGS__)
#define __SC_STR_ADECL6(t, a, ...)  #a, __SC_STR_ADECL5(__VA_ARGS__)
它跟__SC_STR_TDECL##x相反,它是先取第二個參數,然後隔一參數再取.對於sendto來說,就是這樣子的:
__SC_STR_ ADECL6 = fd, buff, size_t, len, flags, addr, addr_len
 
到這裏,終於水落石出了,我們對struct syscall_metadata的數據組織應該很清楚了.
 
Syscall的相關操作接口,到這就分析完了,下面我們來分析一下,syscall到底是怎樣去跟蹤的.
 

四: syscall的tracer原理

接下來看一下syscall的相關執行流,在arch/x86/kernel/entry_32.S中:
ENTRY(system_call)
    RING0_INT_FRAME         # can't unwind into user space anyway
    /*將系統調用號入棧*/
    pushl %eax          # save orig_eax
    CFI_ADJUST_CFA_OFFSET 4
    /*保存寄存器環境*/
    SAVE_ALL
    /*取得當前進程的thread_info並將其存放到ebp中*/
    GET_THREAD_INFO(%ebp)
                    # system call tracing in operation / emulation
    /*檢查thread_info標誌中是否包含_TIF_WORK_SYSCALL_ENTRY
*中的標誌,如有包含,此跳轉到syscall_trace_entry
*/
    testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
    jnz syscall_trace_entry
    /*如果系統調用號比最大的允許調用號還要大,非法情況,跳轉到syscall_badsys*/
    cmpl $(nr_syscalls), %eax
    jae syscall_badsys
syscall_call:
    /*調用對應的系統調用函數*/
    call *sys_call_table(,%eax,4)
    /*將返回值存放到eax*/
    movl %eax,PT_EAX(%esp)      # store the return value
syscall_exit:
    LOCKDEP_SYS_EXIT
    DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    TRACE_IRQS_OFF
    /*將thread_info的標誌位存放到ecx*/
    movl TI_flags(%ebp), %ecx
    /*判斷標誌位中是否含有_TIF_ALLWORK_MASK中的標誌,如果有,
*跳轉到syscall_exit_work
*/
    testl $_TIF_ALLWORK_MASK, %ecx  # current->work
    jne syscall_exit_work
    ......
    .......
_TIF_WORK_SYSCALL_ENTRY 的定義如下:
#define _TIF_WORK_SYSCALL_ENTRY \
    (_TIF_SYSCALL_TRACE | _TIF_SYSCALL_EMU | _TIF_SYSCALL_FTRACE |  \
     _TIF_SYSCALL_AUDIT | _TIF_SECCOMP | _TIF_SINGLESTEP)
應該會注意到,裏面的標誌中就有我們在前面分析中涉及到的
TIF_SYSCALL_FTRACE(_TIF_SYSCALL_FTRACE  (1 << TIF_SYSCALL_FTRACE))
 
_TIF_ALLWORK_MASK定義如下:
#define _TIF_ALLWORK_MASK ((0x0000FFFF & ~_TIF_SECCOMP) | _TIF_SYSCALL_FTRACE)
如果也有_TIF_SYSCALL_FTRACE標誌.
 
那也就是說,syscall tracer如果被啓動,在進入到syscall的時候,會跳轉至syscall_trace_entry().在退出syscall的時候會跳轉到syscall_exit_work().
 
先來看syscall_trace_entry,如下:
syscall_trace_entry:
    /*默認將返回值置爲-ENOSYS */
    movl $-ENOSYS,PT_EAX(%esp)
    /*將esp copy到eax.這是因爲syscall_trace_entry是前三個參數用寄存器傳遞的
     *它的第一個參數放置在eax中,也就是當前的esp
     */
    movl %esp, %eax
    /*調用sycall_trace_enter*/
    call syscall_trace_enter
    /* What it returned is what we'll actually use.  */
    /* syscall_trace_enter()會返回實際所用的系統調用號,出錯返回負值*/
    cmpl $(nr_syscalls), %eax
    jnae syscall_call
    jmp syscall_exit
END(syscall_trace_entry)
也就是說,在進行實際的系統調用前,流程會先轉入到syscall_trace_enter()進行判斷.
 
syscall_exit_work定義如下:
syscall_exit_work:
    /*如果不包含_TIF_WORK_SYSCALL_EXIT 中的標誌,會跳轉到work_pending*/
    testl $_TIF_WORK_SYSCALL_EXIT, %ecx
    jz work_pending
    TRACE_IRQS_ON
    ENABLE_INTERRUPTS(CLBR_ANY) # could let syscall_trace_leave() call
                    # schedule() instead
    /*將第一個參數放入到eax中,再調用syscall_trace_leave()*/
    movl %esp, %eax
    call syscall_trace_leave
    jmp resume_userspace
END(syscall_exit_work)
 
又看到了一個標誌集: _TIF_WORK_SYSCALL_EXIT, 定義如下:
#define _TIF_WORK_SYSCALL_EXIT  \
    (_TIF_SYSCALL_TRACE | _TIF_SYSCALL_AUDIT | _TIF_SINGLESTEP |    \
     _TIF_SYSCALL_FTRACE)
注意到了,裏面也會包含_TIF_SYSCALL_FTRACE, 也就是說,在退出系統調用前,如果syscall tracer被打開,會先轉入到syscall_trace_leave()中.
 
下面分兩個部份來分析,一個部份是syscall entry操作,一個是syscall exit操作.
 

4.1: syscall entry分析:

從上面的分析可得到,在啓用syscall tracer的時候,進行實際的系統調用之前,會先調用syscall_trace_enter(), 代碼片段如下:
asmregparm long syscall_trace_enter(struct pt_regs *regs)
{
    ......
    ......
    if (unlikely(test_thread_flag(TIF_SYSCALL_FTRACE)))
        ftrace_syscall_enter(regs);
    ......
    ......
}
也就是說,流程會轉入到ftrace_syscall_enter(),該函數代碼如下:
void ftrace_syscall_enter(struct pt_regs *regs)
{
    struct syscall_trace_enter *entry;
    struct syscall_metadata *sys_data;
    struct ring_buffer_event *event;
    int size;
    int syscall_nr;
 
    syscall_nr = syscall_get_nr(current, regs);
 
    sys_data = syscall_nr_to_meta(syscall_nr);
    if (!sys_data)
        return;
 
    size = sizeof(*entry) + sizeof(unsigned long) * sys_data->nb_args;
 
    event = trace_current_buffer_lock_reserve(TRACE_SYSCALL_ENTER, size,
                            0, 0);
    if (!event)
        return;
 
    entry = ring_buffer_event_data(event);
    entry->nr = syscall_nr;
    syscall_get_arguments(current, regs, 0, sys_data->nb_args, entry->args);
 
    trace_current_buffer_unlock_commit(event, 0, 0);
    trace_wake_up();
}
該函數就是把系統調用的相關信息保存下來罷了.
首先,調有syscall_get_nr()取得系統調用號,其實它就是取regs->orig_ax.因爲在用戶空間進行系統調用的時候,系統調用號是保存在eax寄存器中的.
然後調用syscall_nr_to_meta()取得從該系統調用號對應的syscall_metadata, 綜合我們在上面的分析,其實它就是在syscalls_metadata[]數組中的對應項.
 
我們先來看一下syscall tracer entry的數據組織,它的數據是存放在struct syscall_trace_enter中的,該結構中下示:
struct syscall_trace_enter {
    struct trace_entry  ent;
    int         nr;
    unsigned long       args[];
};
Nr就是系統調用號, argsargs就是參數值數組.
綜合上面的分析,可得知,nr系統調用對應的參數個數是sys_data->nb_args,因此它需要分配的長度是:
Sizeof(struct syscall_trace_enter) + sys_data->nb_args*sizeof(unsigned long)
然後就是一個具體取參數的過程,它是調用syscall_get_arguments()來完成的,在x86 32位平臺上,代碼如下:
static inline void syscall_get_arguments(struct task_struct *task,
                     struct pt_regs *regs,
                     unsigned int i, unsigned int n,
                     unsigned long *args)
{
    BUG_ON(i + n > 6);
    memcpy(args, &regs->bx + i, n * sizeof(args[0]));
}
參數的含義爲:
Task: 當前進程
Regs: 寄存器列表
i,n: 從第i個系統調用參數開始,連續取n項
 
上面的函數很好理解,因爲系統調用時,參數是放在ebx, ecx,edx ……等寄存器中,在SAVE_ALL的時候把這些寄存器安排在了一次,也就是在regs->bx開始的部份.
然後再提交數據,並調用trace_wake_up()來喚醒pipe_read操作.
疑問,在trace_current_buffer_unlock_commit()也會有一次喚醒,這裏的trace_wake_up()是否可以去掉?
 
此外,從上面的代碼中可以看出:
1: syscall tracer entry沒有去跟蹤CPU flags和preempt_count等信息.
2: syscall tracer entry寫入的消息type爲TRACE_SYSCALL_ENTER
 

4.2: syscall exit分析

在上面的分析中,提到過,在系統調用退出之前會調用syscall_trace_leave(),該函數代碼段如下:
asmregparm void syscall_trace_leave(struct pt_regs *regs)
{
    ......
    ......
    if (unlikely(test_thread_flag(TIF_SYSCALL_FTRACE)))
        ftrace_syscall_exit(regs);
    ......
    ......
}
 
由此可見,流程會轉入到ftrace_syscall_exit(),代碼如下:
void ftrace_syscall_exit(struct pt_regs *regs)
{
    struct syscall_trace_exit *entry;
    struct syscall_metadata *sys_data;
    struct ring_buffer_event *event;
    int syscall_nr;
 
    syscall_nr = syscall_get_nr(current, regs);
 
    sys_data = syscall_nr_to_meta(syscall_nr);
    if (!sys_data)
        return;
 
    event = trace_current_buffer_lock_reserve(TRACE_SYSCALL_EXIT,
                sizeof(*entry), 0, 0);
    if (!event)
        return;
 
    entry = ring_buffer_event_data(event);
    entry->nr = syscall_nr;
    entry->ret = syscall_get_return_value(current, regs);
 
    trace_current_buffer_unlock_commit(event, 0, 0);
    trace_wake_up();
}
這個過程跟syscall tracer entry大部份都一樣,不同的是,這裏的數據組織是不一樣的,這種情況下,數織組織是放在struct syscall_trace_exit中的:
struct syscall_trace_exit {
    struct trace_entry  ent;
    int         nr;
    unsigned long       ret;
};
Nr是系統調用號,ret是系統調用的返回值.
系統調用的返回值很好取,它就是存放在reg->ax中.
另外,它的數據type爲TRACE_SYSCALL_EXIT.
此外,其它操作都跟ftrace_syscall_enter()中是一樣的,這裏就不做重複分析.
 

五: syscall tracer的數據顯示

在實始化的時候,我們看到它註冊了兩種trace_event,現在是到分析它們的時候了.他們的定義如下:
static struct trace_event syscall_enter_event = {
    .type      = TRACE_SYSCALL_ENTER,
    .trace      = print_syscall_enter,
};
 
static struct trace_event syscall_exit_event = {
    .type      = TRACE_SYSCALL_EXIT,
    .trace      = print_syscall_exit,
};
 
一個是用來輸出syscall entry信息的,另一個是用來輸出syscall exit 信息的.
先來看syscall entry信息的輸出.
 

5.1: sycall entry信息的輸出

它的輸了操作是在print_syscall_enter()中完成的,代碼如下:
enum print_line_t
print_syscall_enter(struct trace_iterator *iter, int flags)
{
    struct trace_seq *s = &iter->seq;
    struct trace_entry *ent = iter->ent;
    struct syscall_trace_enter *trace;
    struct syscall_metadata *entry;
    int i, ret, syscall;
 
    /*將ent轉換成 struct trace_entry*/
    trace_assign_type(trace, ent);
 
    /*取得系統調用號*/
    syscall = trace->nr;
 
    /*取得該系統調用號對應的syscall_metadata*/
    entry = syscall_nr_to_meta(syscall);
    if (!entry)
        goto end;
 
    /*顯示”系統調用名稱(“*/
    ret = trace_seq_printf(s, "%s(", entry->name);
    if (!ret)
        return TRACE_TYPE_PARTIAL_LINE;
   
    /*循環輸出每個參數的信息*/
    for (i = 0; i < entry->nb_args; i++) {
        /* parameter types */
        /*如果設置了TRACE_SYSCALLS_OPT_TYPES 標誌,就需要輸出系統
*調用參數的類型,這些信息都是保存在syscall_metadata 中的
*/
        if (syscalls_flags.val & TRACE_SYSCALLS_OPT_TYPES) {
            ret = trace_seq_printf(s, "%s ", entry->types[i]);
            if (!ret)
                return TRACE_TYPE_PARTIAL_LINE;
        }
        /* parameter values */
        /*輸出參數的名稱和參數的值,如果是最後一個參數,附加”)”,否則
*附加”,”*/
        ret = trace_seq_printf(s, "%s: %lx%s ", entry->args[i],
                       trace->args[i],
                       i == entry->nb_args - 1 ? ")" : ",");
        if (!ret)
            return TRACE_TYPE_PARTIAL_LINE;
    }
 
    /*末尾輸出”/n”*/
end:
    trace_seq_printf(s, "\n");
    return TRACE_TYPE_HANDLED;
}
這個函數比較簡單,對照代碼中的註釋應該很容易看懂,這裏就不加詳細分析了.
 

5.2: syscall exit信息的輸出

對應的接口爲print_syscall_exit().代碼如下:
enum print_line_t
print_syscall_exit(struct trace_iterator *iter, int flags)
{
    struct trace_seq *s = &iter->seq;
    struct trace_entry *ent = iter->ent;
    struct syscall_trace_exit *trace;
    int syscall;
    struct syscall_metadata *entry;
    int ret;
 
    trace_assign_type(trace, ent);
 
    syscall = trace->nr;
 
    entry = syscall_nr_to_meta(syscall);
    if (!entry) {
        trace_seq_printf(s, "\n");
        return TRACE_TYPE_HANDLED;
    }
 
    ret = trace_seq_printf(s, "%s -> 0x%lx\n", entry->name,
                trace->ret);
    if (!ret)
        return TRACE_TYPE_PARTIAL_LINE;
 
    return TRACE_TYPE_HANDLED;
}
這個函數也很簡單,它就是輸出”系統調用名稱 -> 返回值”.
 

六: 小結

總的來說,syscall tracer代碼比較清晰, 是一個極容易理解的tracer, 以它爲起點分析tracer, 對於理順前面的框架分析是很有幫助的.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章