| |
 |
|
 |
 |
|
 |
论“Linux操作系统下糟糕的异常处理方式”
|
|
|
看来作者并不清楚linux内核的异常处理是做什么的。
为什么是精确指定哪些异常的指令?
像get_user(c, buf)这个函数,哪里会出错?
只可能是访问用户层空间时,访问的地址无效,其它地方出错那么问题就严重是,是BUG。
但是,内核访问应用层的空间时,并不能保证应用层给我们的地址是正确的,也就是说:我们访问到异常的地址,不应该出现错误,而应该是get_user(c, buf)返回错误。
那么问题就很明显了,为什么不对一段范围的代码进行出错处理,而是指定一条指令。
再说说对一段代码进行处理的错理,实现很简单,但是意义呢?
当然,以上的是个人的想法,可能不周,但是,linux这么实现,我想总有它的道理。
[周宝舟 zhoubaozhou.cublog.cn]
原方如下:
linux下发生异常,芯片会自动产生一个异常中断。在这异常中断处理程序中会判断异常来自用户程序或者内核,如果是发生在用户程序,那么会产生一个异常信号,再根据异常信号的回调函数通知用户程序发生异常。如果发生在内核里面,那么就会搜索内核模块的异常结构表,找到相应的处理调用地址,修改异常中断的返回地址为异常处理的地址,中断返回的时候程序就跳到异常处理程序处理执行了。但具体这两种处理方法都很糟糕,下面简要分析一下。
linux系统把所有进程数据结构都放于内核,这就增加了一些不必要的切换时间。 linux可以通过系统调用,安装信号的回调函数,这回调函数指针存放在内核的进程数据结构里面。这点windows处理得比较好,windows把进程数据结构分成了两部分,一部分敏感数据放于内核的进程数据结构里面,加以保护,另一部分不敏感数据就放于用户空间,这样当访问那些不加保护的数据时,就不用切换到内核,节约了时间。像windows下异常处理,也是一种回调函数,但因为结构放于用户空间,安装的时候就很方便,也节约切换时间。
上面那一点只是效率问题,但linux内核的异常处理那才是糟糕。先介绍一下linux内核的异常处理结构吧,看明白了你自然就知道糟糕到什么程度了。要了解这,显然应该是先从异常中断入手。下面主要是x86芯片的一些处理,但别的芯片下的也应该差不多。
文件:entry.S:
ENTRY(general_protection)
pushl $ SYMBOL_NAME(do_general_protection)
jmp error_code |
这是异常中断入口,显然会执行do_general_protection。文件traps.c:
asmlinkage void do_general_protection(struct pt_regs * regs, long error_code)
{
if (regs->eflags & VM_MASK)
goto gp_in_vm86;
/*
虚拟8086下发生的异常否
*/
if (!(regs->xcs & 3))
goto gp_in_kernel;
/*
内核发生的异常否
*/
current->tss.error_code = error_code;
current->tss.trap_no = 13;
force_sig(SIGSEGV, current);
/*
用户程序发生的异常,产生异常信号,
根据异常信号的句柄回调处理函数
*/
return;
gp_in_vm86:
lock_kernel();
handle_vm86_fault((struct kernel_vm86_regs *) regs, error_code);
/*
虚拟8086的处理
*/
unlock_kernel();
return;
gp_in_kernel:
{
unsigned long fixup;
fixup = search_exception_table(regs->eip);
/*
根据异常时的eip搜索异常结构链
找到处理程序地址
*/
if (fixup) {
regs->eip = fixup;
/*
找到异常处理地址,修改中断返回地址,中断返回时跳到异常处理程序处
*/
return;
}
die("general protection fault", regs, error_code);
/*
没找到异常处理程序地址,显示内核异常信息后死机
*/
}
} |
搜索异常处理程序代码文件extable.c:
extern const struct exception_table_entry __start___ex_table[];
extern const struct exception_table_entry __stop___ex_table[];
unsigned long search_exception_table(unsigned long addr)
{
unsigned long ret;
#ifndef CONFIG_MODULES
/* There is only the kernel to search. */
ret = search_one_table(__start___ex_table, __stop___ex_table-1, addr);
if (ret) return ret;
#else
/* The kernel is the last "module" -- no need to treat it special. */
struct module *mp;
for (mp = module_list; mp != NULL; mp = mp->next) {
if (mp->ex_table_start == NULL)
continue;
ret = search_one_table(mp->ex_table_start,
mp->ex_table_end - 1, addr);
if (ret) return ret;
}
#endif
return 0;
}
static inline unsigned long
search_one_table(const struct exception_table_entry *first,
const struct exception_table_entry *last,
unsigned long value)
{
while (first <= last) {
const struct exception_table_entry *mid;
long diff;
mid = (last - first) / 2 + first;
diff = mid->insn - value;
if (diff == 0)
return mid->fixup;
else if (diff < 0)
first = mid+1;
else
last = mid-1;
}
return 0;
} |
看看上面搜索异常处理程序的算法就知道了,有个异常模块表,保存会发生异常时候的eip和异常处理程序指针,发生异常的时候就根据异常时候的eip搜索表里面的eip,发现相等就找到了异常处理指针。这是什么意思呢,就是说你编写的内核程序必须精确的知道哪条指令可能会发生异常,要求真够高的。想想windows下的异常编程是多么轻松?程序员只需要知道哪一段程序可能出现异常,就只需要一个括号一个异常语句保护这段程序就是了。
光看上面算法可能对其这异常的处理还没怎么有感性认识,那么我们再看看其内核的异常形式、编写方式吧。
下面我们再看看exception.txt的一些说明:
查看内核模块表:
> Sections:
> Idx Name Size VMA LMA File off Algn
> 0 .text 00098f40 c0100000 c0100000 00001000 2**4
> CONTENTS, ALLOC, LOAD, READONLY, CODE
> 1 .fixup 000016bc c0198f40 c0198f40 00099f40 2**0
> CONTENTS, ALLOC, LOAD, READONLY, CODE
> 2 .rodata 0000f127 c019a5fc c019a5fc 0009b5fc 2**2
> CONTENTS, ALLOC, LOAD, READONLY, DATA
> 3 __ex_table 000015c0 c01a9724 c01a9724 000aa724 2**2
> CONTENTS, ALLOC, LOAD, READONLY, DATA
> 4 .data 0000ea58 c01abcf0 c01abcf0 000abcf0 2**4
> CONTENTS, ALLOC, LOAD, DATA
> 5 .bss 00018e21 c01ba748 c01ba748 000ba748 2**2
> ALLOC
> 6 .comment 00000ec4 00000000 00000000 000ba748 2**0
> CONTENTS, READONLY
> 7 .note 00001068 00000ec4 00000ec4 000bb60c 2**0
> CONTENTS, READONLY |
看模块__ex_table就是会出现异常的一些程序eip,fixup就是相应的异常处理程序地址。这显然这异常结构是静态的,与windows动态的链表形式有很大的分别。再看看异常程序的编写,下面是get_user(c, buf)的一段代码:
switch ((sizeof(*(buf)))) {
case 1:
__asm__ __volatile__(
"1: mov" "b" " %2,%" "b" "1\n"
/*
这语句可能发生异常
*/
"2:\n"
".section .fixup,\"ax\"\n"
"3: movl %3,%0\n"
/*
异常处理程序
*/
" xor" "b" " %" "b" "1,%" "b" "1\n"
" jmp 2b\n"
".section __ex_table,\"a\"\n"
" .align 4\n"
" .long 1b,3b\n"
/*
1b,3b就是对应1:,3:就是可能会发生异常的eip与异常后的处理程序
*/
".text" : "=r"(__gu_err), "=q" (__gu_val): "m"((*(struct __large_struct *)
( __gu_addr )) ), "i"(- 14 ), "0"( __gu_err )) ;
break; |
看看上面代码,是不是要严格的知道哪条指令可能会产生异常?你也清楚了linux整个异常处理方式了吧。编写是不是也太麻烦?弄不好你不知道哪条语句会发生异常,而执行到那发生了异常,那么系统就会出现可怕的异常提示后死机了。
这点完全可以用一个链表的方式处理,哪段程序可能发生异常,就安装异常处理程序指针到链表里面,执行完这段代码就从链表里面删除这段异常处理代码指针,如果这段代码发生异常,系统的异常中断处理程序只需要调用这链表的异常处理程序指针就是了,不要匹配发生异常时的eip。linux的处理主要是因为处理成了一个静态的链表,而不是动态的,这就使得需要确定发生异常时的异常处理指针,所以就增加了一个检测eip完全相等的匹配条件,而这就造成了程序编写上的苦难。这处理方式不用我说你就会知道是多么的糟糕了,弄不好就会留下一些会让系统崩溃的地方呢。
|
|
|
发表于: 2008-07-12,修改于: 2008-07-12 11:55,已浏览96次,有评论0条
推荐
投诉
|
|
 |
|
 |
|  |
|
 |
|