在一个简单的IRQ处理程序中调用printk会导致内核崩溃

时间:2017-07-10 10:50:50

标签: linux-kernel x86-64 irq isr page-fault

我是内核编程的新手,我找不到足够的信息来了解为什么会这样。基本上我正在尝试用最简单的调用原始处理程序的内容替换内核IDT中的页面错误处理程序。我只是想让这个函数打印一个被调用的通知,并在其中调用printk()总是会导致内核崩溃。否则它运行正常。

#include <asm/desc.h>
#include <linux/mm.h>
#include <asm/traps.h>
#include <linux/types.h>
#include <linux/errno.h>
#include <linux/sched.h>
#include <asm/uaccess.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/desc_defs.h>
#include <linux/moduleparam.h>



#define PAGEFAULT_INDEX 14


// Old and new IDT registers
static struct desc_ptr old_idt_reg, new_idt_reg;

static __attribute__((__used__)) unsigned long old_pagefault_pointer, new_page;



// The function that replaces the original handler
asmlinkage void isr_pagefault(void);
asm("    .text");
asm("    .type isr_pagefault,@function");
asm("isr_pagefault:");
asm("    callq print_something");
asm("    jmp *old_pagefault_pointer");



void print_something(void) {

    // This printk causes the kernel to crash!
    printk(KERN_ALERT "Page fault handler called\n");

    return;

}

void my_idt_load(void *ptr) {

    printk(KERN_ALERT "Loading on a new processor...\n");

    load_idt((struct desc_ptr*)ptr);

    return;

}



int module_begin(void) {

    gate_desc *old_idt_addr, *new_idt_addr;
    unsigned long idt_length;

    store_idt(&old_idt_reg);

    old_idt_addr = (gate_desc*)old_idt_reg.address;
    idt_length   = old_idt_reg.size;

    // Get the pagefault handler pointer from the IDT's pagefault entry
    old_pagefault_pointer = 0
        | ((unsigned long)(old_idt_addr[PAGEFAULT_INDEX].offset_high)   << 32   )
        | ((unsigned long)(old_idt_addr[PAGEFAULT_INDEX].offset_middle) << 16   )
        | ((unsigned long)(old_idt_addr[PAGEFAULT_INDEX].offset_low)            )
    ;
    printk(KERN_ALERT "Saved pointer to old pagefault handler: %p\n", (void*)old_pagefault_pointer);

    // Allocate a new page for the new IDT
    new_page = __get_free_page(GFP_KERNEL);
    if (!new_page)
        return -1;

    // Copy the original IDT to the new page
    memcpy((void*)new_page, old_idt_addr, idt_length);

    // Set up the new IDT
    new_idt_reg.address = new_idt_addr = new_page;
    new_idt_reg.size = idt_length;
    pack_gate(
        &new_idt_addr[PAGEFAULT_INDEX],
        GATE_INTERRUPT,
        (unsigned long)isr_pagefault, // The interrupt written in assembly at the start of the code
        0, 0, __KERNEL_CS
    );

    // Load the new table
    load_idt(&new_idt_reg);
    smp_call_function(my_idt_load, (void*)&new_idt_reg, 1); // Call load_idt on the rest of the cores

    printk(KERN_ALERT "New IDT loaded\n\n");

    return 0;

}

void module_end(void) {

    printk(
        KERN_ALERT
        "Exit handler called now. Reverting changes and exiting...\n\n"
    );

    load_idt(&old_idt_reg);
    smp_call_function(my_idt_load, (void*)&old_idt_reg, 1);

    if (new_page)
        free_page(new_page);

}



module_init(module_begin);
module_exit(module_end);

非常感谢能告诉我这里我做错了什么的人。

3 个答案:

答案 0 :(得分:3)

对不起,请您死者复活,

挂钩IDT条目时,我遇到了类似的问题;一种可能是堆栈空间不足。在64位模式下,当调用陷阱或故障处理程序时,CPU会根据相应中断描述符的“中断堆栈表”(IST)字段(第32位至第34位)和处理器来确定新的堆栈指​​针核心的任务状态段(TSS)。从第3A卷的《英特尔软件开发人员手册》第6.14.5节:

在IA-32e模式下,可以使用新的中断堆栈表(IST)机制来替代上述经过修改的传统堆栈切换机制。启用此机制后,将无条件切换堆栈。可以使用IDT条目中的字段在单个中断向量的基础上启用它。这意味着某些中断向量可以使用修改后的遗留机制,而其他中断向量可以使用IST机制。

IST机制仅在IA-32e模式下可用。它是64位模式TSS的一部分。 IST机制的动机是为特定的中断(例如NMI,双重故障和机器检查)提供一种始终在已知良好堆栈上执行的方法。在传统模式下,中断可以通过位于IDT中的任务门访问中断服务程序,从而使用任务切换机制来建立已知良好的堆栈。但是,IA-32e模式不支持传统的任务切换机制。

IST机制在TSS中最多提供七个IST指针。指针由中断描述符表(IDT)中的中断门描述符引用;参见图6-8。门描述符包含一个3位IST索引字段,该字段为TSS的IST部分提供了偏移量。处理器使用IST机制,将IST指针指向的值加载到RSP中。

...如果IST索引为零,则使用上述经过修改的旧式堆栈切换机制。

同一章的6.14.2节中描述了“修改后的传统堆栈切换”机制,最重要的是,仅将RSP0的{​​{1}}条目作为新堆栈指针加载。这是描述TSS的图:

enter image description here

因此,总而言之,如果中断描述符的IST字段为0,则TSS的{​​{1}}条目将被加载为新的堆栈指​​针,并且如果IST字段为non -零,则它指示的RSP0的条目将作为新的堆栈指​​针加载。在TSS Linux中,页面错误的IST字段为0,因此,每当发生页面错误时,TSS就会切换到x64的{​​{1}}条目。不幸的是,这里分配的堆栈空间很小。在与内核调试器一起玩耍时发现,Linux仅为此堆栈分配512个字节,而我怀疑rsp可能需要更大的堆栈空间。

对此的一种可能的解决方案是,在页面错误挂钩的开头,手动将堆栈指针切换到RSP0的{​​{1}}条目,该条目应包含当前的内核堆栈,因此TSS有足够的空间。这是一个非常笨拙且不雅致的解决方案,但以我的经验,它可以解决问题。 (要查找printk的地址,您应该使用RSP1获取任务寄存器(TSS),然后从GDT的相应条目中获取基地址,该地址称为“ printk描述符。有关详细信息,请参见第3A卷的7.2.3节。)


免责声明:但是,今天有一个重要的警告与您提出该问题时不相关;针对Meltdown引入的新内核页表隔离缓解将在这种挂钩中引起另一个致命的问题。特别是,TSS的用户模式值将无法访问新的中断描述符表,因此,一旦您加载了新的IDT,用户土地上的任何故障实际上都会导致三重故障(首先是原始错误,然后是页面错误,因为用户模式页表中不存在IDT地址,然后是三次错误,因为同样无法访问IDT的双重错误条目)。没有手动更改所有用户模式页表,这将使您的IDT挂钩方法变得不可能。

唯一的解决方案是手动覆盖您将在用户模式页表中显示的内存区域。例如,IDT中引用的原始IRQ处理程序将指向用户模式页表中始终存在的一小段代码,其作用是将str更改为内核模式变体。 Linux通过清除tr的第11位和第12位来完成此操作,因此您可以使用一个小的汇编存根(stub)覆盖该代码区域,该存根会清除这些位,然后跳转到您的钩子。作为概念证明,请参见here

答案 1 :(得分:0)

据我所知,printk()比ftrace需要更多的资源和复杂性(控制台/文件系统/存储)。 如果崩溃只发生在你使用了printk()的情况下,为什么不使用ftrace而不是printk()?

许多Linux内核专家都喜欢ftrace。

答案 2 :(得分:-1)

为什么要尝试替换页面错误处理程序?如果你想跟踪页面错误,那么可以采用kprobes。

至于手头的问题,这里可能没有什么神秘之处,即使你没有展示崩溃的样子。很可能在用户空间中出现页面错误,但这意味着gs包含用户空间垃圾而不是内核每个CPU数据。据推测,它可以在以后访问,并且不会很好地结束。所有内核入口点都包含swapgs。

一般情况下,安装自己的处理程序意味着你必须获得足够的关卡。你无缘无故地让我很难看到。

我们来看看:

Out-file C:\Users\xx\Desktop\12.csv -Append -InputObject  “Computer Name,KB Patch,Hotfix”

就在那里

crash> dis page_fault
0xffffffffbb804da0 <page_fault>:        data32 xchg %ax,%ax
0xffffffffbb804da3 <page_fault+3>:      data32 xchg %ax,%ax
0xffffffffbb804da6 <page_fault+6>:      data32 xchg %ax,%ax
0xffffffffbb804da9 <page_fault+9>:      add    $0xffffffffffffff88,%rsp
0xffffffffbb804dad <page_fault+13>:     callq  0xffffffffbb804f30 <error_entry>
0xffffffffbb804db2 <page_fault+18>:     mov    %rsp,%rdi
0xffffffffbb804db5 <page_fault+21>:     mov    0x78(%rsp),%rsi
0xffffffffbb804dba <page_fault+26>:     movq   $0xffffffffffffffff,0x78(%rsp)
0xffffffffbb804dc3 <page_fault+35>:     callq  0xffffffffbb062840 <do_page_fault>
0xffffffffbb804dc8 <page_fault+40>:     jmpq   0xffffffffbb804ff0 <error_exit>
0xffffffffbb804dcd <page_fault+45>:     nopl   (%rax)


crash> dis error_entry
0xffffffffbb804f30 <error_entry>:       cld    
0xffffffffbb804f31 <error_entry+1>:     mov    %r11,0x38(%rsp)
0xffffffffbb804f36 <error_entry+6>:     mov    %r10,0x40(%rsp)
0xffffffffbb804f3b <error_entry+11>:    mov    %r9,0x48(%rsp)
0xffffffffbb804f40 <error_entry+16>:    mov    %r8,0x50(%rsp)
0xffffffffbb804f45 <error_entry+21>:    mov    %rax,0x58(%rsp)
0xffffffffbb804f4a <error_entry+26>:    mov    %rcx,0x60(%rsp)
0xffffffffbb804f4f <error_entry+31>:    mov    %rdx,0x68(%rsp)
0xffffffffbb804f54 <error_entry+36>:    mov    %rsi,0x70(%rsp)
0xffffffffbb804f59 <error_entry+41>:    mov    %rdi,0x78(%rsp)
0xffffffffbb804f5e <error_entry+46>:    mov    %r15,0x8(%rsp)
0xffffffffbb804f63 <error_entry+51>:    mov    %r14,0x10(%rsp)
0xffffffffbb804f68 <error_entry+56>:    mov    %r13,0x18(%rsp)
0xffffffffbb804f6d <error_entry+61>:    mov    %r12,0x20(%rsp)
0xffffffffbb804f72 <error_entry+66>:    mov    %rbp,0x28(%rsp)
0xffffffffbb804f77 <error_entry+71>:    mov    %rbx,0x30(%rsp)
0xffffffffbb804f7c <error_entry+76>:    xor    %ebx,%ebx
0xffffffffbb804f7e <error_entry+78>:    testb  $0x3,0x90(%rsp)
0xffffffffbb804f86 <error_entry+86>:    je     0xffffffffbb804f9a <error_entry+106>
0xffffffffbb804f88 <error_entry+88>:    swapgs 

那就是说,你为什么要玩这个东西?我强烈建议暂时坚持用户空间中的某些程序集。