1

我编写了一个基本的 kprobe linux 内核模块,它将为 fork 注册一个处理程序,并在处理程序中打印寄存器“orig_ax”的值。

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>

MODULE_LICENSE("GPL");

static struct kprobe kp;
static char *name = "_do_fork";
module_param(name, charp, 0);

static int pre_handler(struct kprobe *p, struct pt_regs *regs)
{
    printk("orig_ax regs:%lu \t ax:%lu\n", regs->orig_ax, regs->ax);
    return 0;
}

static void post_handler(struct kprobe *p, struct pt_regs *regs,
             unsigned long flags)
{
}

static int __init hello_init(void)
{
    /* set the handler functions */
    kp.pre_handler = pre_handler;
    kp.post_handler = post_handler;
    kp.symbol_name = name;
    register_kprobe(&kp);
    return 0;
}

static void __exit hello_exit(void)
{
    unregister_kprobe(&kp);
}

module_init(hello_init);
module_exit(hello_exit);

我得到一个不同的值而不是 57

[ 9251.954392] orig_ax regs:0    ax:18446661681273651032

我犯错了吗

4

1 回答 1

1

发生了什么:

Kprobe 是通过将原始指令替换为int 3会导致 CPU 产生软件中断的指令来实现的。在这种情况下,CPU 上下文必须保存在内核堆栈中,然后您的处理程序将被执行。所以这不是glibc用来触发内核系统调用的指令regs上下文int 3的上下文。syscall您获得的值orig_ax是发生 CPU 中断/异常时的错误代码。它的值为零,因为int 3中断不会产生任何错误,因此内核将零压入堆栈作为占位符,这使整个实现更加通用。

你该怎么办:

如果要获取系统调用号,则应植入一个探针,在do_syscall_64该探针上调用系统调用时执行的第一个 C 函数。或者您可以探测/汇编指令entry_SYSCALL_64的中断处理程序。syscallint 0x80


细节:

系统调用机制是使用 CPU 陷阱门实现的。当你fork()用 C 语言调用时,glibc 将执行syscall汇编指令,系统调用号就rax如你所知存储在其中。CPU 将产生一个软件产生的中断,并开始执行系统调用的中断处理程序,其地址存储在 IDT 中。

以下代码是 x86_64 上系统调用的中断处理程序。

ENTRY(entry_SYSCALL_64)
        UNWIND_HINT_EMPTY                                                                                                                                                                    
        /*   
         * Interrupts are off on entry.
         * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
         * it is too small to ever cause noticeable irq latency.
         */

        swapgs
        /* tss.sp2 is scratch space. */
        movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
        SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
        movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp 

        /* Construct struct pt_regs on stack */
        pushq   $__USER_DS                              /* pt_regs->ss */
        pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2)       /* pt_regs->sp */
        pushq   %r11                                    /* pt_regs->flags */
        pushq   $__USER_CS                              /* pt_regs->cs */
        pushq   %rcx                                    /* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
        pushq   %rax                                    /* pt_regs->orig_ax */

        PUSH_AND_CLEAR_REGS rax=$-ENOSYS

        TRACE_IRQS_OFF

        /* IRQs are off. */
        movq    %rax, %rdi 
        movq    %rsp, %rsi 
        call    do_syscall_64           /* returns with IRQs disabled */

pushq %rax指令将rax(又名系统调用号)保存在内核堆栈上,然后调用do_syscall_64.

__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
        struct thread_info *ti;

        enter_from_user_mode();
        local_irq_enable();                                                                                                                                                                  
        ti = current_thread_info();
        if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
                nr = syscall_trace_enter(regs);

        if (likely(nr < NR_syscalls)) {
                nr = array_index_nospec(nr, NR_syscalls);
                regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
        } else if (likely((nr & __X32_SYSCALL_BIT) &&
                          (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
                nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
                                        X32_NR_syscalls);
                regs->ax = x32_sys_call_table[nr](regs);
#endif
        }   

        syscall_return_slowpath(regs);
}

最重要的语句是regs->ax = sys_call_table[nr](regs);将调用fork-related 函数。_do_fork调用时,信息regs已经丢失,因此您无法获取与系统调用相关的任何信息。

于 2020-01-08T03:37:18.493 回答