通过系统调用,内核断点方法定位用户进程被内核踩内存的问题

Posted xingmuxin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了通过系统调用,内核断点方法定位用户进程被内核踩内存的问题相关的知识,希望对你有一定的参考价值。

请看我的上一篇博客,https://www.cnblogs.com/xingmuxin/p/11287935.html 介绍了具体的踩内存的问题。下面我来介绍下如何通过一些手段和方法,定位内核踩内存的问题。

1、系统调用拦截

    系统调用拦截的目的其实就是把系统真正要执行的系统调用替换为我们自己写的内核函数,这里有一篇博客,对此作了介绍,https://blog.csdn.net/zhangyifei216/article/details/49872861

系统调用拦截的两个问题,一个是找到sys_call_table地址,一个是修改内存页的属性,让其变为可写。

1)找sys_call_table地址

    我们可以通过两种方法来查找sys_call_table地址:

一、使用kallsyms_lookup_name

    使用kallsyms_lookup_name函数,来读取对应的sys_call_table的地址,但是kallsyms_lookup_name这个函数能否可以被我们使用,能否在我们写的内核模块中导出。这要看内核代码中是否有加入EXPORT_SYMBOL。

    EXPORT_SYMBOL标签内定义的函数或者符号对全部内核代码公开,不用修改内核代码就可以在您的内核模块中直接调用,即使用EXPORT_SYMBOL可以将一个函数以符号的方式导出给其他模块使用。

使用方法
   第一、在模块函数定义之后使用EXPORT_SYMBOL(函数名)
   第二、在掉用该函数的模块中使用extern对之声明
   第三、首先加载定义该函数的模块,再加载调用该函数的模块

    通过查看内核代码,我们看到,我们自己的内核中,是有对kallsyms_lookup_name这个函数导出符号的。

EXPORT_SYMBOL_GPL(kallsyms_lookup_name);

    使用的是EXPORT_SYMBOL_GPL,_GPL版本的宏定义只能使符号对GPL许可的模块可用,如果你的module的协议不是GPL, 那么EXPORT_SYMBOL_GPL导出的那些符号,你就不能用。

综上,我们要查找到sys_call_table的地址,可以这样来写代码。

MODULE_LICENSE("GPL"); /* 这个必须要加,否则不可以使用EXPORT_SYMBOL_GPL导出的符号,加载模块时,会提示unknown symbol */
extern unsigned long kallsyms_lookup_name(const char *name); /* 将kallsyms_lookup_name符号导出 */
static
int a_init(void) unsigned long p = 0UL; p = kallsyms_lookup_name("sys_call_table"); if (!p) printk("can not find sys_call_table"); return -1; return 0;

二、读取读取/proc/kallsyms,再把值作为参数传入内核模块

/proc/kallsyms是一个特殊的文件,它并不是存储在磁盘上的文件。这个文件只有被读取的时候,才会由内核产生内容。因为这些内容是内核动态生成的,所以可以保证其中读到的地址是正确的。可以在加载模块时用脚本获取符号的地址。命令:

#cat /proc/kallsyms | grep "\\<sys_call_table\\>" | awk print $1

2)、在修改系统调用地址时,需要修改内存内的属性

为了可以对sys_call_table所在内存地址处,进行读写,需要重新设置该地址对应的页表项的属性。物理地址本来是没有什么读写属性的,内核只通过修改物理地址对应的页表项的一些属性位来设置该物理地址的读写属性而已。下面是具体修改的过程。

static void set_addr_rw(unsigned long addr)

    unsigned int level;
    pte_t *pte;

    pte = lookup_address(addr, &level); /* 查找虚拟地址所在的页表地址 */
    pte->pte |= _PAGE_RW; /* 设置页表读写属性 */


static void set_addr_ro(unsigned long addr)

    unsigned int level;
    pte_t *pte;

    pte = lookup_address(addr, &level);
    pte->pte = pte->pte &~_PAGE_RW; /* 设置只读属性 */

下面我们来看下,如何设置系统调用,是系统调用到我们自己写的内核函数。在模块载入的时候,先保存原有的系统调用的地址,然后再修改该地址所在页表的属性,为可写的,然后再赋值为新的我们自己的系统调用地址。

static int a_init(void)

    unsigned long p = 0UL;

    call_table_p = kallsyms_lookup_name("sys_call_table");
    if (!p) 
        printk("can not find sys_call_table");
        return -1;
    

    /* afs_syscall 183 is not use */
    syscall_p =(unsigned long*)(p + 8*183); /* 在sys_call_table中有很多系统调用是没有实现的,我们可以占用 */
    save_syscall_val = *syscall_p;
    set_syscall_ptr((unsigned long)my_syscall);

    return 0;
set_syscall_ptr这个函数的具体实现为:

static unsigned long *syscall_p = 0UL;
static unsigned long save_syscall_val = 0UL;

static void set_syscall_ptr(unsigned long val)

    unsigned long flags;
    if (save_syscall_val == 0UL || call_table_p == 0UL) 
        return; 
    

    preempt_disable();     /* 关闭内核抢占*/
    local_irq_save(flags);  /*  对 local_irq_save的调用将把当前中断状态保存到flags中,然后禁用当前处理器上的中断发送 */

    set_addr_rw((unsigned long)call_table_p);  /* 设置sys_cal_table地址所在的内存页面为可读写*/
    mb();
    *syscall_p = val; 
    mb();
    set_addr_ro((unsigned long)syscall_p);   /* 设置sys_cal_table地址所在的内存页面为写保护*/
local_irq_restore(flags); 
preempt_enable();
printk(
"new syscall 183 is %lx\\n", val);

2、hardware breakpoint

    内核由于共享内存地址空间,如果没有合适的工具,很多踩内存的问题即使复现,也无法快速定位;在新的内核版本中引入了一个新工具hardware breakpoint,其能够监视对指定的地址的特定类型(读/写)的数据访问,有利于该类问题的定位;https://blog.csdn.net/phenix_lord/article/details/41415559
下面,我们再来看一下我们替换的这个my_syscall的具体实现。

static unsigned long g_pid;
static unsigned long g_addr;

struct perf_event * __percpu *sample_hbp;

static void sample_hbp_handler(struct perf_event *bp, struct perf_sample_data *data, struct pt_regs *regs)

    printk("%lu, %016lx value is changed\\n", g_pid, g_addr);
    dump_stack();

asmlinkage long my_syscall(unsigned long addr)  /* 通过堆栈而不是通过寄存器传递参数 */

    int ret = 0;
    struct perf_event_attr attr;

    g_pid = (unsigned long)task_tgid_vnr(current);
    g_addr = addr;

    hw_breakpoint_init(&attr);
    attr.bp_addr = addr;      /* 待监视的地址 */
    attr.bp_len = HW_BREAKPOINT_LEN_4;
    attr.bp_type = HW_BREAKPOINT_W; /* 待监视的访问类型 */

    if (sample_hbp)
        unregister_wide_hw_breakpoint(sample_hbp);  /* 内核对断点数量有限制,为4,所以要及时释放 */
    sample_hbp = register_wide_hw_breakpoint(&attr, sample_hbp_handler, NULL);
    if (IS_ERR((void __force *)sample_hbp)) 
        ret = PTR_ERR((void __force *)sample_hbp);
        sample_hbp = NULL;
        printk("Breakpoint registration failed\\n");
        return ret;
    
    printk("%lu, %016lx write installed\\n", g_pid, g_addr);
    return 0; 

最后,在模块卸载时要做如下操作:

static void a_exit(void)

    set_syscall_ptr(save_syscall_val);

    if (sample_hbp)
        unregister_wide_hw_breakpoint(sample_hbp);


module_init(a_init);
module_exit(a_exit);

 

 


以上是关于通过系统调用,内核断点方法定位用户进程被内核踩内存的问题的主要内容,如果未能解决你的问题,请参考以下文章

鸿蒙内核之内存调测:动态内存池信息统计

Linux进程内存如何管理?

strace命令使用分析

内存管理:用户模式和内核模式

linux进程为啥有用户栈和内核栈,

Linux进程地址空间与虚拟内存