Linux内核的异常修复原理

参考

Linker Script in Linux (3.1.1 Exception Table)
Linux异常表
linux Oops和Panic关系
5. Kernel level exception handling

环境

ARM64
Linux-5.8

场景

    用户通过系统调用给内核传递了一个参数,这个参数有一个该用户地址空间的地址,然后内核在访问时会发生什么情况呢?如果这个用户空间地址处于当前进程的有效vma中,那么正常的缺页异常可以处理。
    但是如果这个参数是一个非法的用户地址,内核访问的话,会不会由于访问了非法地址而导致崩溃呢?

测试程序

驱动

static ssize_t fixup_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
        int err;

        err = put_user(10, buf);
        printk("%s: %d\n", __func__, err);
        return count;
}

用户程序

int main(int argc, const char *argv[])
{
        int fd;
        char *buf;

        fd = open("/dev/fixup", O_RDWR);

        buf = 0x4000000;

        printf("read buf\n");
        read(fd, buf, 5);

        return 0;
}

内核log

[11131.867589] fixup_read: -14

    可以看到put_user返回了错误码:-EFAULT,并没有导致内核崩溃,测试程序也没有异常退出。在这背后发生了什么呢?其实内核在访问这个非法用户地址的时候,确实发生了缺页异常,不过在缺页异常中使用这里说的exception_table进行了修复,返回了错误码。要完成这个功能,需要依赖put_user和缺页异常的支持。

原理

put_user

put_user的实现在arch/arm64/include/asm/uaccess.h中:

#define _ASM_EXTABLE(from, to)						\
	"	.pushsection	__ex_table, \"a\"\n"			\
	"	.align		3\n"					\
	"	.long		(" #from " - .), (" #to " - .)\n"	\
	"	.popsection\n"

#define __put_mem_asm(store, reg, x, addr, err)				\
	asm volatile(							\
	"1:	" store "	" reg "1, [%2]\n"			\
	"2:\n"								\
	"	.section .fixup,\"ax\"\n"				\
	"	.align	2\n"						\
	"3:	mov	%w0, %3\n"					\
	"	b	2b\n"						\
	"	.previous\n"						\
	_ASM_EXTABLE(1b, 3b)						\
	: "+r" (err)							\
	: "r" (x), "r" (addr), "i" (-EFAULT))

#define __raw_put_mem(str, x, ptr, err)					\
do {									\
	__typeof__(*(ptr)) __pu_val = (x);				\
	switch (sizeof(*(ptr))) {					\
	case 1:								\
		__put_mem_asm(str "b", "%w", __pu_val, (ptr), (err));	\
		break;							\
	case 2:								\
		__put_mem_asm(str "h", "%w", __pu_val, (ptr), (err));	\
		break;							\
	case 4:								\
		__put_mem_asm(str, "%w", __pu_val, (ptr), (err));	\
		break;							\
	case 8:								\
		__put_mem_asm(str, "%x", __pu_val, (ptr), (err));	\
		break;							\
	default:							\
		BUILD_BUG();						\
	}								\
} while (0)

#define __raw_put_user(x, ptr, err)					\
do {									\
	__chk_user_ptr(ptr);						\
	uaccess_ttbr0_enable();						\
	__raw_put_mem("sttr", x, ptr, err);				\
	uaccess_ttbr0_disable();					\
} while (0)

#define __put_user_error(x, ptr, err)					\
do {									\
	__typeof__(*(ptr)) __user *__p = (ptr);				\
	might_fault();							\
	if (access_ok(__p, sizeof(*__p))) {				\
		__p = uaccess_mask_ptr(__p);				\
		__raw_put_user((x), __p, (err));			\
	} else	{							\
		(err) = -EFAULT;					\
	}								\
} while (0)

#define __put_user(x, ptr)						\
({									\
	int __pu_err = 0;						\
	__put_user_error((x), (ptr), __pu_err);				\
	__pu_err;							\
})

#define put_user	__put_user

重点关注下面的内容:

#define _ASM_EXTABLE(from, to)						\
	"	.pushsection	__ex_table, \"a\"\n"			\
	"	.align		3\n"					\
	"	.long		(" #from " - .), (" #to " - .)\n"	\
	"	.popsection\n"

#define __put_mem_asm(store, reg, x, addr, err)				\
	asm volatile(							\
	"1:	" store "	" reg "1, [%2]\n"			\
	"2:\n"								\
	"	.section .fixup,\"ax\"\n"				\
	"	.align	2\n"						\
	"3:	mov	%w0, %3\n"					\
	"	b	2b\n"						\
	"	.previous\n"						\
	_ASM_EXTABLE(1b, 3b)						\
	: "+r" (err)							\
	: "r" (x), "r" (addr), "i" (-EFAULT))

    上面用到了两个section,.fixup__ex_table,其中前者是修复程序的入口,后者用于存放触发异常的指令所在的地址跟对应的修复程序的入口地址之间的映射关系。比如当1b处的代码写addr触发了异常后,陷入内核缺页异常,然后在内核缺页异常里搜索1b对应的__ex_table,这个表里记录了异常指令地址跟对应的修复程序的入口地址的映射关系,找到后在异常返回时会跳转到修复程序入口,也就是跳转到上面.fixup3b处,然后将错误码-EFAULT存入err中,最后跳转到异常指令1b的下一行2b继续执行。
    在每一个__ex_table中存放了两个偏移量,分别是异常指令的地址1b和修改指令的地址3b跟当前地址的偏移,这样遍历__ex_table数组的时候可以很容易获得1b3b的地址。

    在内核的链接脚本中有专门存放__ex_table.fixup的段:
__ex_table:

/*
 * Exception table
 */
#define EXCEPTION_TABLE(align)						\
	. = ALIGN(align);						\
	__ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) {		\
		__start___ex_table = .;					\
		KEEP(*(__ex_table))					\
		__stop___ex_table = .;					\
	}

.fixup:

	.text : ALIGN(SEGMENT_ALIGN) {	/* Real text segment		*/
		_stext = .;		/* Text and read-only data	*/
			IRQENTRY_TEXT
			SOFTIRQENTRY_TEXT
			ENTRY_TEXT
			TEXT_TEXT
			SCHED_TEXT
			CPUIDLE_TEXT
			LOCK_TEXT
			KPROBES_TEXT
			HYPERVISOR_TEXT
			IDMAP_TEXT
			HIBERNATE_TEXT
			TRAMP_TEXT
			*(.fixup)
			*(.gnu.warning)
		. = ALIGN(16);
		*(.got)			/* Global offset table		*/
	}

内核缺页

do_translation_fault
    ----> do_page_fault
      ---->__do_kernel_fault
        ----> fixup_exception

int fixup_exception(struct pt_regs *regs)
{
	const struct exception_table_entry *fixup;

	fixup = search_exception_tables(instruction_pointer(regs));
	if (!fixup)
		return 0;

	if (in_bpf_jit(regs))
		return arm64_bpf_fixup_exception(fixup, regs);

	regs->pc = (unsigned long)&fixup->fixup + fixup->fixup;
	return 1;
}

  instruction_pointer(regs)返回异常指令的地址,也就是上面的1b,然后调用search_exception_tables搜索,搜索顺序是先从内核的__start___ex_table ~ __stop___ex_table搜索,如果没有找到,那么会依次从内核module和bpf里搜索。

/* Given an address, look for it in the exception tables. */
const struct exception_table_entry *search_exception_tables(unsigned long addr)
{
	const struct exception_table_entry *e;

	e = search_kernel_exception_table(addr); // 静态编译到内核中的
	if (!e)
		e = search_module_extables(addr);    // 从内核module里搜索
	if (!e)
		e = search_bpf_extables(addr);       // 从bpf里搜索
	return e;
}

search_kernel_exception_table的实现如下:

/* Given an address, look for it in the kernel exception table */
const
struct exception_table_entry *search_kernel_exception_table(unsigned long addr)
{
	return search_extable(__start___ex_table,
			      __stop___ex_table - __start___ex_table, addr);
}

search_extable的实现如下:

struct exception_table_entry
{
	int insn, fixup;
};

static inline unsigned long ex_to_insn(const struct exception_table_entry *x)
{
	return (unsigned long)&x->insn + x->insn;
}

static int cmp_ex_search(const void *key, const void *elt)
{
	const struct exception_table_entry *_elt = elt;
	unsigned long _key = *(unsigned long *)key;

	/* avoid overflow */
	if (_key > ex_to_insn(_elt))
		return 1;
	if (_key < ex_to_insn(_elt))
		return -1;
	return 0;
}

/*
 * Search one exception table for an entry corresponding to the
 * given instruction address, and return the address of the entry,
 * or NULL if none is found.
 * We use a binary search, and thus we assume that the table is
 * already sorted.
 */
const struct exception_table_entry *
search_extable(const struct exception_table_entry *base,
	       const size_t num,
	       unsigned long value)
{
	return bsearch(&value, base, num,
		       sizeof(struct exception_table_entry), cmp_ex_search);
}

    对于遍历到的每一个__ex_table都会调用cmp_ex_search,第一个参数key中存放的是&value,第二个参数存放的是当前__ex_table的地址。_key中存放的是异常指令1b的地址,ex_to_insn(_elt)返回记录的异常指令1b的地址(__ex_table的地址+偏移),如果二者相等,表示当前__ex_table就是我们想要的。
    最后回到 fixup_exception中,search_exception_tables返回找到的__ex_table,然后计算修复指令的地址(unsigned long)&fixup->fixup + fixup->fixup(地址+偏移),将结果赋值给regs->pc,这样当异常返回后就会跳转到修复指令的地址开始执行,也就是上面提到的3b位置。

    上面分析了put_user,对于get_usercopy_to_user以及copy_from_user都采用了类似的方法来处理用户传递了非法的地址的情况,防止内核崩溃。

完。

posted @ 2021-08-22 19:29  摩斯电码  阅读(1231)  评论(0编辑  收藏  举报