歡迎您光臨本站 註冊首頁

linux下糟糕的異常處理方式

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  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完全相等的匹配條件,而這就造成了程序編寫上的苦難。這處理方式不用我說你就會知道是多麼的糟糕了,弄不好就會留下一些會讓系統崩潰的地方呢。



附exception.txt:

Kernel level exception handling in Linux 2.1.8
Commentary by Joerg Pommnitz

When a process runs in kernel mode, it often has to access user
mode memory whose address has been passed by an untrusted program.
To protect itself the kernel has to verify this address.

In older versions of Linux this was done with the
int verify_area(int type, const void * addr, unsigned long size)
function.

This function verified that the memory area starting at address
addr and of size size was accessible for the operation specified
in type (read or write). To do this, verify_read had to look up the
virtual memory area (vma) that contained the address addr. In the
normal case (correctly working program), this test was successful.
It only failed for a few buggy programs. In some kernel profiling
tests, this normally unneeded verification used up a considerable
amount of time.

To overcome this situation, Linus decided to let the virtual memory
hardware present in every Linux-capable CPU handle this test.

How does this work?

Whenever the kernel tries to access an address that is currently not
accessible, the CPU generates a page fault exception and calls the
page fault handler

void do_page_fault(struct pt_regs *regs, unsigned long error_code)

in arch/i386/mm/fault.c. The parameters on the stack are set up by
the low level assembly glue in arch/i386/kernel/entry.S. The parameter
regs is a pointer to the saved registers on the stack, error_code
contains a reason code for the exception.

do_page_fault first obtains the unaccessible address from the CPU
control register CR2. If the address is within the virtual address
space of the process, the fault probably occurred, because the page
was not swapped in, write protected or something similar. However,
we are interested in the other case: the address is not valid, there
is no vma that contains this address. In this case, the kernel jumps
to the bad_area label.

There it uses the address of the instruction that caused the exception
(i.e. regs->eip) to find an address where the execution can continue
(fixup). If this search is successful, the fault handler modifies the
return address (again regs->eip) and returns. The execution will
continue at the address in fixup.

Where does fixup point to?

Since we jump to the contents of fixup, fixup obviously points
to executable code. This code is hidden inside the user access macros.
I have picked the get_user macro defined in include/asm/uaccess.h as an
example. The definition is somewhat hard to follow, so let's peek at
the code generated by the preprocessor and the compiler. I selected
the get_user call in drivers/char/console.c for a detailed examination.

The original code in console.c line 1405:
get_user(c, buf);

The preprocessor output (edited to become somewhat readable):

(
{
long __gu_err = - 14 , __gu_val = 0;
const __typeof__(*( ( buf ) )) *__gu_addr = ((buf));
if (((((0 + current_set[0])->tss.segment) == 0x18 ) ||
(((sizeof(*(buf))) <= 0xC0000000UL) &&
((unsigned long)(__gu_addr ) <= 0xC0000000UL - (sizeof(*(buf)))))))
do {
__gu_err = 0;
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"
".text" : "=r"(__gu_err), "=q" (__gu_val): "m"((*(struct __large_struct *)
( __gu_addr )) ), "i"(- 14 ), "0"( __gu_err )) ;
break;
case 2:
__asm__ __volatile__(
"1: mov" "w" " %2,%" "w" "1\n"
"2:\n"
".section .fixup,\"ax\"\n"
"3: movl %3,%0\n"
" xor" "w" " %" "w" "1,%" "w" "1\n"
" jmp 2b\n"
".section __ex_table,\"a\"\n"
" .align 4\n"
" .long 1b,3b\n"
".text" : "=r"(__gu_err), "=r" (__gu_val) : "m"((*(struct __large_struct *)
( __gu_addr )) ), "i"(- 14 ), "0"( __gu_err ));
break;
case 4:
__asm__ __volatile__(
"1: mov" "l" " %2,%" "" "1\n"
"2:\n"
".section .fixup,\"ax\"\n"
"3: movl %3,%0\n"
" xor" "l" " %" "" "1,%" "" "1\n"
" jmp 2b\n"
".section __ex_table,\"a\"\n"
" .align 4\n" " .long 1b,3b\n"
".text" : "=r"(__gu_err), "=r" (__gu_val) : "m"((*(struct __large_struct *)
( __gu_addr )) ), "i"(- 14 ), "0"(__gu_err));
break;
default:
(__gu_val) = __get_user_bad();
}
} while (0) ;
((c)) = (__typeof__(*((buf))))__gu_val;
__gu_err;
}
);

WOW! Black GCC/assembly magic. This is impossible to follow, so let's
see what code gcc generates:

> xorl %edx,%edx
> movl current_set,%eax
> cmpl $24,788(%eax)
> je .L1424
> cmpl $-1073741825,64(%esp)
> ja .L1423
> .L1424:
> movl %edx,%eax
> movl 64(%esp),%ebx
> #APP
> 1: movb (%ebx),%dl /* this is the actual user access */
> 2:
> .section .fixup,"ax"
> 3: movl $-14,%eax
> xorb %dl,%dl
> jmp 2b
> .section __ex_table,"a"
> .align 4
> .long 1b,3b
> .text
> #NO_APP
> .L1423:
> movzbl %dl,%esi

The optimizer does a good job and gives us something we can actually
understand. Can we? The actual user access is quite obvious. Thanks
to the unified address space we can just access the address in user
memory. But what does the .section stuff do?????

To understand this we have to look at the final kernel:

> objdump --section-headers vmlinux
>
> vmlinux: file format elf32-i386
>
> 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

There are obviously 2 non standard ELF sections in the generated object
file. But first we want to find out what happened to our code in the
final kernel executable:

> objdump --disassemble --section=.text vmlinux
>
> c017e785 xorl %edx,%edx
> c017e787 movl 0xc01c7bec,%eax
> c017e78c cmpl $0x18,0x314(%eax)
> c017e793 je c017e79f
> c017e795 cmpl $0xbfffffff,0x40(%esp,1)
> c017e79d ja c017e7a7
> c017e79f movl %edx,%eax
> c017e7a1 movl 0x40(%esp,1),%ebx
> c017e7a5 movb (%ebx),%dl
> c017e7a7 movzbl %dl,%esi

The whole user memory access is reduced to 10 x86 machine instructions.
The instructions bracketed in the .section directives are not longer
in the normal execution path. They are located in a different section
of the executable file:

> objdump --disassemble --section=.fixup vmlinux
>
> c0199ff5 <.fixup+10b5> movl $0xfffffff2,%eax
> c0199ffa <.fixup+10ba> xorb %dl,%dl
> c0199ffc <.fixup+10bc> jmp c017e7a7

And finally:
> objdump --full-contents --section=__ex_table vmlinux
>
> c01aa7c4 93c017c0 e09f19c0 97c017c0 99c017c0 ................
> c01aa7d4 f6c217c0 e99f19c0 a5e717c0 f59f19c0 ................
> c01aa7e4 080a18c0 01a019c0 0a0a18c0 04a019c0 ................

or in human readable byte order:

> c01aa7c4 c017c093 c0199fe0 c017c097 c017c099 ................
> c01aa7d4 c017c2f6 c0199fe9 c017e7a5 c0199ff5 ................
^^^^^^^^^^^^^^^^^
this is the interesting part!
> c01aa7e4 c0180a08 c019a001 c0180a0a c019a004 ................

What happened? The assembly directives

.section .fixup,"ax"
.section __ex_table,"a"

told the assembler to move the following code to the specified
sections in the ELF object file. So the instructions
3: movl $-14,%eax
xorb %dl,%dl
jmp 2b
ended up in the .fixup section of the object file and the addresses
.long 1b,3b
ended up in the __ex_table section of the object file. 1b and 3b
are local labels. The local label 1b (1b stands for next label 1
backward) is the address of the instruction that might fault, i.e.
in our case the address of the label 1 is c017e7a5:
the original assembly code: > 1: movb (%ebx),%dl
and linked in vmlinux : > c017e7a5 movb (%ebx),%dl

The local label 3 (backwards again) is the address of the code to handle
the fault, in our case the actual value is c0199ff5:
the original assembly code: > 3: movl $-14,%eax
and linked in vmlinux : > c0199ff5 <.fixup+10b5> movl $0xfffffff2,%eax

The assembly code
> .section __ex_table,"a"
> .align 4
> .long 1b,3b

becomes the value pair
> c01aa7d4 c017c2f6 c0199fe9 c017e7a5 c0199ff5 ................
^this is ^this is
1b 3b
c017e7a5,c0199ff5 in the exception table of the kernel.

So, what actually happens if a fault from kernel mode with no suitable
vma occurs?

1.) access to invalid address:
> c017e7a5 movb (%ebx),%dl
2.) MMU generates exception
3.) CPU calls do_page_fault
4.) do page fault calls search_exception_table (regs->eip == c017e7a5);
5.) search_exception_table looks up the address c017e7a5 in the
exception table (i.e. the contents of the ELF section __ex_table)
and returns the address of the associated fault handle code c0199ff5.
6.) do_page_fault modifies its own return address to point to the fault
handle code and returns.
7.) execution continues in the fault handling code.
8.) 8a) EAX becomes -EFAULT (== -14)
8b) DL becomes zero (the value we "read" from user space)
8c) execution continues at local label 2 (address of the
instruction immediately after the faulting user access).

The steps 8a to 8c in a certain way emulate the faulting instruction.

That's it, mostly. If you look at our example, you might ask why
we set EAX to -EFAULT in the exception handler code. Well, the
get_user macro actually returns a value: 0, if the user access was
successful, -EFAULT on failure. Our original code did not test this
return value, however the inline assembly code in get_user tries to
return -EFAULT. GCC selected EAX to return this value.


[火星人 ] linux下糟糕的異常處理方式已經有703次圍觀

http://coctec.com/docs/program/show-post-72167.html