【发布时间】:2020-10-19 20:32:31
【问题描述】:
注意:我在 FreeBSD 上运行,但我也将 Linux 作为标签包含在内,因为这个问题有点笼统,而且我对特定于 Linux 的解决方案很感兴趣。
编辑:为了确认问题不是 FreeBSD 特有的,我将模块移植到 Linux,并且确实得到了完全相同的行为。该模块的 Linux 版本的代码如下所示;它本质上是一样的,唯一的主要区别是 IDT 在 Linux 中显然被赋予了只读保护,因此我必须禁用 cr0 中的写保护位才能使代码正常工作。
我正在学习一些有关 x86-64 架构上的内核开发的知识,目前一直在阅读英特尔开发人员手册中有关中断处理的内容。作为实践,我正在尝试编写一个小内核模块来挂钩 IDT 中的条目,但一直遇到问题。我的一般问题是:如何确保钩子的代码(或新 IDT 表的数据,如果您使用 lidt 更改整个 idtr 而不仅仅是覆盖 IDT 的各个条目)总是存在于 RAM 中?我遇到的问题是,我将更改 IDT 条目,触发相应的中断,然后出现双重错误,因为我的钩子的代码没有映射到 RAM。一般有没有办法避免这个问题?
对于我的具体情况,以下是我编写的 FreeBSD LKM 的代码,它只是覆盖 IDT 条目中列出的地址以处理零除数错误,并将其替换为asm_hook 的地址,该地址位于无条件地将jmps 呈现回原始中断处理程序。 (未来我当然会添加更多功能。)
#include <sys/types.h>
#include <sys/param.h>
#include <sys/proc.h>
#include <sys/module.h>
#include <sys/sysent.h>
#include <sys/kernel.h>
#include <sys/syscall.h>
#include <sys/sysproto.h>
#include <sys/systm.h>
//idt entry
struct idte_t {
unsigned short offset_0_15;
unsigned short segment_selector;
unsigned char ist; //interrupt stack table
unsigned char type:4;
unsigned char zero_12:1;
unsigned char dpl:2; //descriptor privilege level
unsigned char p:1; //present flag
unsigned short offset_16_31;
unsigned int offset_32_63;
unsigned int rsv; }
__attribute__((packed))
*zd_idte;
#define ZD_INT 0x00
unsigned long idte_offset; //contains absolute address of original interrupt handler
//idt register
struct idtr_t {
unsigned short lim_val;
struct idte_t *addr; }
__attribute__((packed))
idtr;
__asm__(
".text;"
".global asm_hook;"
"asm_hook:;"
"jmp *(idte_offset);");
extern void asm_hook(void);
static int
init() {
__asm__ __volatile__ (
"cli;"
"sidt %0;"
"sti;"
:: "m"(idtr));
uprintf("[*] idtr dump\n"
"[**] address:\t%p\n"
"[**] lim val:\t0x%x\n"
"[*] end dump\n\n",
idtr.addr, idtr.lim_val);
zd_idte=(idtr.addr)+ZD_INT;
idte_offset=(long)(zd_idte->offset_0_15)|((long)(zd_idte->offset_16_31)<<16)|((long)(zd_idte->offset_32_63)<<32);
uprintf("[*] old idt entry %d:\n"
"[**] addr:\t%p\n"
"[**] segment:\t0x%x\n"
"[**] ist:\t%d\n"
"[**] type:\t%d\n"
"[**] dpl:\t%d\n"
"[**] p:\t\t%d\n"
"[*] end dump\n\n",
ZD_INT, (void *)idte_offset, zd_idte->segment_selector,
zd_idte->ist, zd_idte->type, zd_idte->dpl, zd_idte->p);
if(!zd_idte->p) {
uprintf("[*] fatal: handler segment not present\n");
return ENOSYS; }
__asm__ __volatile__("cli");
zd_idte->offset_0_15=((unsigned long)(&asm_hook))&0xffff;
zd_idte->offset_16_31=((unsigned long)(&asm_hook)>>16)&0xffff;
zd_idte->offset_32_63=((unsigned long)(&asm_hook)>>32)&0xffffffff;
__asm__ __volatile__("sti");
uprintf("[*] new idt entry %d:\n"
"[**] addr:\t%p\n"
"[**] segment:\t0x%x\n"
"[**] ist:\t%d\n"
"[**] type:\t%d\n"
"[**] dpl:\t%d\n"
"[**] p:\t\t%d\n"
"[*] end dump\n\n",
ZD_INT, (void *)(\
(long)zd_idte->offset_0_15|((long)zd_idte->offset_16_31<<16)|((long)zd_idte->offset_32_63<<32)),
zd_idte->segment_selector, zd_idte->ist, zd_idte->type, zd_idte->dpl, zd_idte->p);
return 0; }
static void
fini() {
__asm__ __volatile__("cli");
zd_idte->offset_0_15=idte_offset&0xffff;
zd_idte->offset_16_31=(idte_offset>>16)&0xffff;
zd_idte->offset_32_63=(idte_offset>>32)&0xffffffff;
__asm__ __volatile__("sti"); }
static int
load(struct module *module, int cmd, void *arg) {
int error=0;
switch(cmd) {
case MOD_LOAD:
error=init();
break;
case MOD_UNLOAD:
fini();
break;
default:
error=EOPNOTSUPP;
break; }
return error; }
static moduledata_t idt_hook_mod = {
"idt_hook",
load,
NULL };
DECLARE_MODULE(idt_hook, idt_hook_mod, SI_SUB_DRIVERS, SI_ORDER_MIDDLE);
(我还编写了另一个 LKM,它使用 malloc(9) 创建一个全新的 IDT 表并使用 lidt 将该表加载到 idtr,但在我看来,这似乎是一种劣等方法,因为它只会改变 IDT在它运行的特定 CPU 内核上,因此在多处理器系统中无法可靠地工作。除非我缺少某些东西,否则这是一个准确的评估吗?)
无论如何,编译代码并加载内核模块不会导致任何问题:
# kldload ./idt_hook.ko
[*] idtr dump
[**] address: 0xffffffff81fb2c40
[**] lim val: 0xfff
[*] end dump
[*] old idt entry 0:
[**] addr: 0xffffffff81080f90
[**] segment: 0x20
[**] ist: 0
[**] type: 14
[**] dpl: 0
[**] p: 1
[*] end dump
[*] new idt entry 0:
[**] addr: 0xffffffff8281d000
[**] segment: 0x20
[**] ist: 0
[**] type: 14
[**] dpl: 0
[**] p: 1
[*] end dump
但是,当我使用以下代码测试钩子时,内核会挂起:
#include <stdio.h>
int main() {
int x=1, y=0;
printf("x/y=%d\n", x/y);
return 0; }
为了了解发生了什么,我启动了 VirtualBox 内置调试器,并在 IDT 的双重错误异常处理程序(条目 8)上设置了一个断点。调试显示我的 LKM 正确更改了 IDT,但运行上面的零除数代码会触发双重故障。当我尝试访问0xffffffff8281d000(我的asm_hook代码的地址)的内存时,我意识到了这个原因,这在VirtualBox调试器中触发了VERR_PAGE_TABLE_NOT_PRESENT错误。所以,除非我误解了什么,显然问题确实是我的asm_hook 在某个时候从内存中删除了。关于如何解决这个问题的任何想法?例如,有没有办法告诉 FreeBSD 内核永远不应该从 RAM 中取消映射特定页面?
编辑:下面 cmets 中的 Nate Eldredge 帮助我找到了我的代码中的一些错误(现已更正),但不幸的是问题仍然存在。提供更多调试细节:首先我加载内核模块,然后在 VirtualBox 调试器中的asm_hook 代码 (0xffffffff8281d000) 的列出地址上设置断点。我已经通过反汇编该地址的内存确认它确实包含asm_hook 的代码。 (不过,正如 Nate 所指出的,它恰好位于页面边界上有点奇怪——有人知道为什么会这样吗?)
无论如何,当我触发零除数中断时,不幸的是断点永远不会被命中,而且,一旦我进入双重错误中断处理程序,当我尝试访问0xffffffff8281d000@987654340 的内存时@error 仍然会出现。
确实,FreeBSD 设计的一个不同寻常的 (?) 功能是从 RAM 中换出/取消映射其内核的部分,所以也许更好的问题是“是什么导致了这个页面错误?”
编辑:这是移植到 Linux 的模块版本:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/io.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Hooks the zero divisor IDT entry");
MODULE_VERSION("0.01");
struct idte_t {
unsigned short offset_0_15;
unsigned short segment_selector;
unsigned char ist; //interrupt stack table
unsigned char type:4;
unsigned char zero_12:1;
unsigned char dpl:2; //descriptor privilege level
unsigned char p:1; //present flag
unsigned short offset_16_31;
unsigned int offset_32_63;
unsigned int rsv; }
__attribute__((packed))
*zd_idte;
#define ZD_INT 0x00
unsigned long idte_offset; //contains absolute address of original interrupt handler
struct idtr_t {
unsigned short lim_val;
struct idte_t *addr; }
__attribute__((packed))
idtr;
__asm__(
".text;"
".global asm_hook;"
"asm_hook:;"
"jmp *(idte_offset);");
extern void asm_hook(void);
static int __init
idt_init(void) {
__asm__ __volatile__ (
"cli;"
"sidt %0;"
"sti;"
:: "m"(idtr));
printk("[*] idtr dump\n"
"[**] address:\t%px\n"
"[**] lim val:\t0x%x\n"
"[*] end dump\n\n",
idtr.addr, idtr.lim_val);
zd_idte=(idtr.addr)+ZD_INT;
idte_offset=(long)(zd_idte->offset_0_15)|((long)(zd_idte->offset_16_31)<<16)|((long)(zd_idte->offset_32_63)<<32);
printk("[*] old idt entry %d:\n"
"[**] addr:\t%px\n"
"[**] segment:\t0x%x\n"
"[**] ist:\t%d\n"
"[**] type:\t%d\n"
"[**] dpl:\t%d\n"
"[**] p:\t\t%d\n"
"[*] end dump\n\n",
ZD_INT, (void *)idte_offset, zd_idte->segment_selector,
zd_idte->ist, zd_idte->type, zd_idte->dpl, zd_idte->p);
if(!zd_idte->p) {
printk("[*] fatal: handler segment not present\n");
return ENOSYS; }
unsigned long cr0;
__asm__ __volatile__("mov %%cr0, %0" : "=r"(cr0));
cr0 &= ~(long)0x10000;
__asm__ __volatile__("mov %0, %%cr0" :: "r"(cr0));
__asm__ __volatile__("cli");
zd_idte->offset_0_15=((unsigned long)(&asm_hook))&0xffff;
zd_idte->offset_16_31=((unsigned long)(&asm_hook)>>16)&0xffff;
zd_idte->offset_32_63=((unsigned long)(&asm_hook)>>32)&0xffffffff;
__asm__ __volatile__("sti");
cr0 |= 0x10000;
__asm__ __volatile__("mov %0, %%cr0" :: "r"(cr0));
printk("[*] new idt entry %d:\n"
"[**] addr:\t%px\n"
"[**] segment:\t0x%x\n"
"[**] ist:\t%d\n"
"[**] type:\t%d\n"
"[**] dpl:\t%d\n"
"[**] p:\t\t%d\n"
"[*] end dump\n\n",
ZD_INT, (void *)(\
(long)zd_idte->offset_0_15|((long)zd_idte->offset_16_31<<16)|((long)zd_idte->offset_32_63<<32)),
zd_idte->segment_selector, zd_idte->ist, zd_idte->type, zd_idte->dpl, zd_idte->p);
return 0; }
static void __exit
idt_fini(void) {
unsigned long cr0;
__asm__ __volatile__("mov %%cr0, %0" : "=r"(cr0));
cr0 &= ~(long)0x10000;
__asm__ __volatile__("mov %0, %%cr0" :: "r"(cr0));
__asm__ __volatile__("cli");
zd_idte->offset_0_15=idte_offset&0xffff;
zd_idte->offset_16_31=(idte_offset>>16)&0xffff;
zd_idte->offset_32_63=(idte_offset>>32)&0xffffffff;
__asm__ __volatile__("sti");
cr0 |= 0x10000;
__asm__ __volatile__("mov %0, %%cr0" :: "r"(cr0)); }
module_init(idt_init);
module_exit(idt_fini);
【问题讨论】:
-
嗯,出于这个原因,我认为内核内存(包括模块)始终驻留并且从不换出是一般原则。除非 FreeBSD 以某种方式改变了这一点,否则我会怀疑问题出在其他地方。
-
@NateEldredge 我一直认为情况也是如此!所以也许我误解了一些东西。不幸的是,VirtualBox 调试器没有很好的文档,所以我实际上不确定我是否正确解释了
VERR_PAGE_TABLE_NOT_PRESENT错误。您还有其他想法吗? -
我有点怀疑您的
asm_hook似乎正好落在了页面边界上。我会尝试检查该地址是否真的正确。 -
@NateEldredge 哦,这是一个非常好的观点,谢谢 - 会尽快看看。
-
我不确定,但你需要在
asm_hook的asm 定义之前有一个.text吗?
标签: c linux assembly x86-64 freebsd