为什么 orig_ax 没有正确的系统调用号

Why is orig_ax not having proper system call number

我写了一个基本的 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

我是不是搞错了

发生了什么:

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

你应该怎么做:

如果你想获取系统调用号,你应该在 do_syscall_64 上放置一个探测器,这是调用系统调用时执行的第一个 C 函数。或者您可以探测 entry_SYSCALL_64,它是 syscall/int 0x80 汇编指令的中断处理程序。


详情:

系统调用机制是使用CPU陷阱门实现的。当你在 C 语言中调用 fork() 时,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相关的函数。当_do_fork被调用时,regs信息已经丢失,所以你无法得到系统调用相关的任何信息。