3

我是内核编程的新手,我找不到足够的信息来了解为什么会发生这种情况。基本上,我试图用一些简单的方法来替换内核 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);

非常感谢任何可以告诉我我在这里做错了什么的人。

4

2 回答 2

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

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

“修改的遗留堆栈切换”机制在同一章的 6.14.2 节中进行了描述,最重要的是只是将RSP0条目加载TSS为新的堆栈指​​针。这是描述的图TSS

在此处输入图像描述

综上所述,如果中断描述符的 IST 字段为 0,则该RSP0条目TSS将作为新的堆栈指​​针加载,如果 IST 字段不为零,则TSS它所指示的条目将是作为新的堆栈指​​针加载。在x64linux 中,IST 字段为 0 表示页面错误,因此每当发生页面错误时rsp切换到RSP0条目。TSS不幸的是,这里分配的堆栈空间相当小;使用内核调试器发现 linux 只为这个堆栈分配了 512 个字节,我怀疑这printk可能需要更大的堆栈空间。

一种可能的解决方案是,在页面错误挂钩的开头,手动将堆栈指针RSP1切换TSSprintk. 这是一个非常hacky和不优雅的解决方案,但根据我的经验,它可以解决问题。(要找到的地址,TSS你应该使用str获取任务寄存器(tr),然后从GDT的相应条目中获取基地址,称为“TSS描述符”。详见第3A卷第7.2.3节。 )


免责声明:然而,今天有一个重大警告与您提出这个问题时无关;为响应 Meltdown 而引入的新内核页表隔离缓解措施将在这种挂钩中导致不同的致命问题。特别是,您的新中断描述符表将无法从 的用户模式值访问cr3,因此一旦您加载新的 IDT(首先是原始故障,然后是页面错误,因为 IDT 地址不存在于用户模式页表中,然后是三重错误,因为 IDT 的双重错误条目也无法访问)。如果没有手动更改所有用户模式页表,这会使您的 IDT 挂钩方法变得不可能。

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

于 2020-08-23T21:04:09.793 回答
0

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

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

于 2017-08-08T23:44:41.587 回答