Windows异常学习笔记(四)—— 编译器扩展SEH
Windows異常學習筆記(四)—— 編譯器擴展SEH
- 要點回顧
- 編譯器支持的SEH
- 過濾表達式
- 實驗一:理解_try_except
- 實驗二:_try_except 嵌套
- 拓展SEH結構體
- scopetable
- 實驗三:理解scopetable
- trylevel
- 實驗四:理解trylevel
- 總結
- __try__finally
- 實驗五:理解__try__finally
- 局部展開
- 實驗六:理解局部展開
- 全局展開
- 實驗七:理解全局展開
要點回顧
當我們通過編程實現windows的異常處理方式使用SEH結構化異常時,我們必須做如下幾件事情
注意:這種方式相對而言不太方便,因此現在的編譯器都對SEH結構化異常提供了語法支持,并在其之上做了一定的拓展
編譯器支持的SEH
語法:
_try //掛入鏈表 {//代碼 } _except(過濾表達式) //異常過濾 {//異常處理程序 //異常處理 }拆解:
_try //相當于 __asm {mov eax, FS:[0]mov temp, eaxlea ecx, myExceptionmov FS:[0], ecx } _except(過濾表達式) //相當于 if( ExceptionRecord->ExceptionCode == 0xC0000094 ) //除0異常過濾表達式
三個值:
1. EXCEPTION_EXECUTE_HANDLER(1) 執行except代碼 2. EXCEPTION_CONTINUE_SEARCH(0) 不處理異常,尋找下一個異常處理函數 3. EXCEPTION_CONTINUE_EXECUTION(-1) 返回出錯位置重新執行三種寫法:
1)常量
2)表達式
try {//代碼 } _except(GetExceptionCode()==0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH) {//異常處理程序 }3)調用函數
int ExceptFilter(LPEXCEPTION_POINTERS pExceptionInfo) {printf("%x", pExceptionInfo->ExceptionRecord->ExceptionCode);printf("%p", pExceptionInfo->ContextRecord->Eip);return EXCEPTION_CONTINUE_EXECUTION; }try {//代碼 } _except(ExceptFilter(GetExceptionInformation())) {//異常處理程序 }實驗一:理解_try_except
1)編譯并運行以下代碼
#include <stdio.h> #include <windows.h>void TestException() {_try{}_except(1){} }int main() {TestException();getchar();return 0; }2)查看TestException函數的反匯編
4: void TestException() 5: { 00401020 push ebp 00401021 mov ebp,esp 00401023 push 0FFh 00401025 push offset string "_filbuf.c"+0FFFFFFD4h (00422020) 0040102A push offset __except_handler3 (0040127c) 0040102F mov eax,fs:[00000000] 00401035 push eax 00401036 mov dword ptr fs:[0],esp 0040103D add esp,0FFFFFFB8h 00401040 push ebx 00401041 push esi 00401042 push edi 00401043 mov dword ptr [ebp-18h],esp 00401046 lea edi,[ebp-58h] 00401049 mov ecx,10h 0040104E mov eax,0CCCCCCCCh 00401053 rep stos dword ptr [edi] 6: _try 00401055 mov dword ptr [ebp-4],0 7: { 8: 9: } 0040105C mov dword ptr [ebp-4],0FFFFFFFFh 00401063 jmp $L53962+0Ah (00401075) 10: _except(1) 00401065 mov eax,1 $L53963: 0040106A ret $L53962: 0040106B mov esp,dword ptr [ebp-18h] 11: { 12: 13: } 0040106E mov dword ptr [ebp-4],0FFFFFFFFh 14: } 00401075 mov ecx,dword ptr [ebp-10h] 00401078 mov dword ptr fs:[0],ecx 0040107F pop edi 00401080 pop esi 00401081 pop ebx 00401082 mov esp,ebp 00401084 pop ebp 00401085 ret3)總結
在編譯器支持的情況下,當我們寫入_try_except程序塊時,編譯器就會自動在當前堆棧鏈表中掛入一個結構體,并且在掛入時指定的處理函數時固定的,當我們手動掛入時,處理函數是我們手動編寫的
實驗二:_try_except 嵌套
描述:當我們手動掛入鏈表時,若我們需要兩個處理函數時,就需要掛入兩個處理函數,需要多少個就要掛多少個,而當我們使用編譯器提供的這種格式,無論我們嵌套了多少個異常處理,編譯器只會生成一個異常處理函數
1)編譯并運行以下代碼
#include <stdio.h> #include <windows.h>void TestException() {_try{_try{}_except(1){}}_except(1){}_try{}_except(1){} }int main() {TestException();getchar();return 0; }2)查看TestException函數的反匯編
4: void TestException() 5: { 00401020 push ebp 00401021 mov ebp,esp 00401023 push 0FFh 00401025 push offset string "_filbuf.c"+0FFFFFFD4h (00422020) 0040102A push offset __except_handler3 (0040127c) 0040102F mov eax,fs:[00000000] 00401035 push eax 00401036 mov dword ptr fs:[0],esp //只有此處修改了fs,只掛入了一個異常處理函數 0040103D add esp,0FFFFFFB8h 00401040 push ebx 00401041 push esi 00401042 push edi 00401043 mov dword ptr [ebp-18h],esp 00401046 lea edi,[ebp-58h] 00401049 mov ecx,10h 0040104E mov eax,0CCCCCCCCh 00401053 rep stos dword ptr [edi] 6: _try 00401055 mov dword ptr [ebp-4],0 7: { 8: _try 0040105C mov dword ptr [ebp-4],1 9: { 10: 11: } 00401063 mov dword ptr [ebp-4],0 0040106A jmp $L53968+0Ah (0040107c) 12: _except(1) 0040106C mov eax,1 $L53969: 00401071 ret $L53968: 00401072 mov esp,dword ptr [ebp-18h] 13: { 14: 15: } 00401075 mov dword ptr [ebp-4],0 16: } 0040107C mov dword ptr [ebp-4],0FFFFFFFFh 00401083 jmp $L53964+0Ah (00401095) 17: _except(1) 00401085 mov eax,1 $L53965: 0040108A ret $L53964: 0040108B mov esp,dword ptr [ebp-18h] 18: { 19: 20: } 0040108E mov dword ptr [ebp-4],0FFFFFFFFh 21: 22: _try 00401095 mov dword ptr [ebp-4],2 23: { 24: 25: } 0040109C mov dword ptr [ebp-4],0FFFFFFFFh 004010A3 jmp $L53972+0Ah (004010b5) 26: _except(1) 004010A5 mov eax,1 $L53973: 004010AA ret $L53972: 004010AB mov esp,dword ptr [ebp-18h] 27: { 28: 29: } 004010AE mov dword ptr [ebp-4],0FFFFFFFFh 30: } 004010B5 mov ecx,dword ptr [ebp-10h] 004010B8 mov dword ptr fs:[0],ecx 004010BF pop edi 004010C0 pop esi 004010C1 pop ebx 004010C2 mov esp,ebp 004010C4 pop ebp 004010C5 ret3)總結:當我們使用編譯器提供的這種格式,在一個函數中,無論我們嵌套了多少個異常處理,編譯器只會往FS:[0]中掛入一次,并且指向的異常處理函數也是確定的(遞歸除外,遞歸相當于一次次調用了新的函數)
思考:編譯器只掛入一次異常處理函數,它如何實現對所有異常進行處理?
答案: 拓展_EXCEPTION_REGISTRATION_RECORD結構體
拓展SEH結構體
原先的SEH結構體
typedef struct _EXCEPTION_REGISTRATION_RECORD{struct _EXCEPTION_REGISTRATION_RECORD *Next;PEXCEPTION_ROUTINE Handler; }EXCEPTION_REGISTRATION_RECORD;拓展后如下(原先的兩個成員必須存在)
struct _EXCEPTION_REGISTRATION{struct _EXCEPTION_REGISTRATION *prev;void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);struct scopetable_entry *scopetable;int trylevel;int _ebp; }新的堆棧結構如下:
以實驗一的匯編代碼為例,該結構體在函數初始化時被壓入堆棧
注意:一般情況下,當我們將代碼分別編譯為debug與release版本時,其匯編代碼變化較大(release進行了優化),而當函數中存在_try_except這樣的語法時,兩者變化較小(保證異常處理語句的堆棧結構)
scopetable
描述:結構體指針,指向了一堆結構體數組
struct scopetable_entry{DWORD previousTryLevel //上一個try{}結構編號PDWRD lpfnFilter //過濾函數的起始地址PDWRD lpfnHandler //異常處理程序的地址 }/* scopetable[0].previousTryLevel = -1; scopetable[0].lpfnFilter = 過濾函數1; scopetable[0].lpfnHandler = 異常處理函數1;scopetable[1].previousTryLevel = -1; scopetable[1].lpfnFilter = 過濾函數2; scopetable[1].lpfnHandler = 異常處理函數2;scopetable[2].previousTryLevel = 1; scopetable[2].lpfnFilter = 過濾函數3; scopetable[2].lpfnHandler = 異常處理函數3; */實驗三:理解scopetable
1)編譯并運行以下代碼
#include <stdio.h> #include <windows.h>int ExceptFilter() {return EXCEPTION_CONTINUE_EXECUTION; }void TestException() {_try{}_except(EXCEPTION_EXECUTE_HANDLER){printf("異常處理函數X\n");}_try{_try{}_except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH){printf("異常處理函數Y\n");}}_except(ExceptFilter()){printf("異常處理函數Z\n");} }int main() {TestException();getchar();return 0; }2)觀察TestException函數的反匯編
9: void TestException() 10: { 00401050 push ebp 00401051 mov ebp,esp 00401053 push 0FFh 00401055 push offset string "_filbuf.c"+0FFFFFFD4h (00422020) //scopetable 0040105A push offset __except_handler3 (0040127c) 0040105F mov eax,fs:[00000000] 00401065 push eax 00401066 mov dword ptr fs:[0],esp 0040106D add esp,0FFFFFFB4h 00401070 push ebx 00401071 push esi 00401072 push edi 00401073 mov dword ptr [ebp-18h],esp 00401076 lea edi,[ebp-5Ch] 00401079 mov ecx,11h 0040107E mov eax,0CCCCCCCCh 00401083 rep stos dword ptr [edi] 11: _try 00401085 mov dword ptr [ebp-4],0 12: { 13: 14: } 0040108C mov dword ptr [ebp-4],0FFFFFFFFh 00401093 jmp $L53975+17h (004010b2) 15: _except(EXCEPTION_EXECUTE_HANDLER) 00401095 mov eax,1 $L53976: 0040109A ret $L53975: 0040109B mov esp,dword ptr [ebp-18h] 16: { 17: printf("異常處理函數X\n"); 0040109E push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdX\n" (00422fc8) 004010A3 call printf (0040de70) 004010A8 add esp,4 18: } 004010AB mov dword ptr [ebp-4],0FFFFFFFFh 19: 20: _try 004010B2 mov dword ptr [ebp-4],1 21: { 22: _try 004010B9 mov dword ptr [ebp-4],2 23: { 24: 25: } 004010C0 mov dword ptr [ebp-4],1 004010C7 jmp $L53983+17h (004010fa) 26: _except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH) 004010C9 mov eax,dword ptr [ebp-14h] 004010CC mov ecx,dword ptr [eax] 004010CE mov edx,dword ptr [ecx] 004010D0 mov dword ptr [ebp-1Ch],edx 004010D3 mov eax,dword ptr [ebp-1Ch] 004010D6 xor ecx,ecx 004010D8 cmp eax,0C0000094h 004010DD sete cl 004010E0 mov eax,ecx $L53984: 004010E2 ret $L53983: 004010E3 mov esp,dword ptr [ebp-18h] 27: { 28: printf("異常處理函數Y\n"); 004010E6 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdY\n" (00422fb8) 004010EB call printf (0040de70) 004010F0 add esp,4 29: } 004010F3 mov dword ptr [ebp-4],1 30: } 004010FA mov dword ptr [ebp-4],0FFFFFFFFh 00401101 jmp $L53979+17h (00401120) 31: _except(ExceptFilter()) 00401103 call @ILT+10(ExceptFilter) (0040100f) $L53980: 00401108 ret $L53979: 00401109 mov esp,dword ptr [ebp-18h] 32: { 33: printf("異常處理函數Z\n"); 0040110C push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdZ\n" (00422fa8) 00401111 call printf (0040de70) 00401116 add esp,4 34: } 00401119 mov dword ptr [ebp-4],0FFFFFFFFh 35: } 00401120 mov ecx,dword ptr [ebp-10h] 00401123 mov dword ptr fs:[0],ecx 0040112A pop edi 0040112B pop esi 0040112C pop ebx 0040112D add esp,5Ch 00401130 cmp ebp,esp 00401132 call __chkesp (00401690) 00401137 mov esp,ebp 00401139 pop ebp 0040113A ret3)查看scopetable內存
//scopetable_entry X 00422020 FF FF FF FF .... //不存在上級異常處理 00422024 95 10 40 00 ..@. 00422028 9B 10 40 00 ..@. //scopetable_entry Z 0042202C FF FF FF FF .... //不存在上級異常處理 00422030 03 11 40 00 ..@. 00422034 09 11 40 00 .@. //scopetable_entry Y 00422038 01 00 00 00 .... //存在一個上級異常處理 0042203C C9 10 40 00 ..@. 00422040 E3 10 40 00思考:如何判斷當前需要調用第幾個scopetable_entry?
答案:根據trylevel的值進行判斷
trylevel
描述:標識當前代碼處于第幾個try中,從而調用對應的scopetable_entry結構體中的函數,初始值為-1(0xff)
實驗四:理解trylevel
1)編譯并運行以下代碼
#include <stdio.h> #include <windows.h>int ExceptFilter() {return EXCEPTION_CONTINUE_EXECUTION; }void TestException() {_try{_try{}_except(EXCEPTION_EXECUTE_HANDLER){printf("異常處理函數\n");}}_except(EXCEPTION_EXECUTE_HANDLER){printf("異常處理函數X\n");}_try{_try{}_except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH){printf("異常處理函數Y\n");}}_except(ExceptFilter()){printf("異常處理函數Z\n");} }int main() {TestException();getchar();return 0; }2)查看TestException函數的反匯編
9: void TestException() 10: { 00401050 push ebp 00401051 mov ebp,esp 00401053 push 0FFh //trylevel,初始值為-1,位于[ebp-4] 00401055 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfd\n"+10h (00423018) 0040105A push offset __except_handler3 (0040127c) 0040105F mov eax,fs:[00000000] 00401065 push eax 00401066 mov dword ptr fs:[0],esp 0040106D add esp,0FFFFFFB4h 00401070 push ebx 00401071 push esi 00401072 push edi 00401073 mov dword ptr [ebp-18h],esp 00401076 lea edi,[ebp-5Ch] 00401079 mov ecx,11h 0040107E mov eax,0CCCCCCCCh 00401083 rep stos dword ptr [edi] 11: _try 00401085 mov dword ptr [ebp-4],0 //修改trylevel為0 12: { 13: _try 0040108C mov dword ptr [ebp-4],1 //修改trylevel為1 14: { 15: 16: } 00401093 mov dword ptr [ebp-4],0 //恢復到0 0040109A jmp $L53982+17h (004010b9) 17: _except(EXCEPTION_EXECUTE_HANDLER) 0040109C mov eax,1 $L53983: 004010A1 ret $L53982: 004010A2 mov esp,dword ptr [ebp-18h] 18: { 19: printf("異常處理函數\n"); 004010A5 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfd\n" (00423008) 004010AA call printf (0040de70) 004010AF add esp,4 20: } 004010B2 mov dword ptr [ebp-4],0 //恢復到0 21: } 004010B9 mov dword ptr [ebp-4],0FFFFFFFFh 004010C0 jmp $L53978+17h (004010df) 22: _except(EXCEPTION_EXECUTE_HANDLER) 004010C2 mov eax,1 $L53979: 004010C7 ret $L53978: 004010C8 mov esp,dword ptr [ebp-18h] 23: { 24: printf("異常處理函數X\n"); 004010CB push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdX\n" (00422fc8) 004010D0 call printf (0040de70) 004010D5 add esp,4 25: } 004010D8 mov dword ptr [ebp-4],0FFFFFFFFh //恢復到-1 26: 27: _try 004010DF mov dword ptr [ebp-4],2 //修改trylevel為2 28: { 29: _try 004010E6 mov dword ptr [ebp-4],3 //修改trylevel為3 30: { 31: 32: } 004010ED mov dword ptr [ebp-4],2 //恢復到2 004010F4 jmp $L53990+17h (00401127) 33: _except(GetExceptionCode() == 0xC0000094?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH) 004010F6 mov eax,dword ptr [ebp-14h] 004010F9 mov ecx,dword ptr [eax] 004010FB mov edx,dword ptr [ecx] 004010FD mov dword ptr [ebp-1Ch],edx 00401100 mov eax,dword ptr [ebp-1Ch] 00401103 xor ecx,ecx 00401105 cmp eax,0C0000094h 0040110A sete cl 0040110D mov eax,ecx $L53991: 0040110F ret $L53990: 00401110 mov esp,dword ptr [ebp-18h] 34: { 35: printf("異常處理函數Y\n"); 00401113 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdY\n" (00422fb8) 00401118 call printf (0040de70) 0040111D add esp,4 36: } 00401120 mov dword ptr [ebp-4],2 //恢復到2 37: } 00401127 mov dword ptr [ebp-4],0FFFFFFFFh //恢復到-1 0040112E jmp $L53986+17h (0040114d) 38: _except(ExceptFilter()) 00401130 call @ILT+10(ExceptFilter) (0040100f) $L53987: 00401135 ret $L53986: 00401136 mov esp,dword ptr [ebp-18h] 39: { 40: printf("異常處理函數Z\n"); 00401139 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfdZ\n" (00422fa8) 0040113E call printf (0040de70) 00401143 add esp,4 41: } 00401146 mov dword ptr [ebp-4],0FFFFFFFFh //恢復到-1 42: } 0040114D mov ecx,dword ptr [ebp-10h] 00401150 mov dword ptr fs:[0],ecx 00401157 pop edi 00401158 pop esi 00401159 pop ebx 0040115A add esp,5Ch 0040115D cmp ebp,esp 0040115F call __chkesp (00401690) 00401164 mov esp,ebp 00401166 pop ebp 00401167 ret總結
異常處理整體流程:
CPU檢測到異常 ↓ 查中斷表執行處理函數 ↓ CommonDispatchException //存儲異常相關信息 ↓ KiDispatchException //異常分發處理函數,判斷0環異常還是3環異常//若為3環,修正EIP,指向KiUserExceptionDispatcher ↓ KiUserExceptionDispatcher //通過RtlDispatchException查找異常處理函數位置,VEH/SEH ↓ RtlDispatchException //先查找VEH,若找不到,查找SEH(從FS:[0]開始遍歷) ↓ VEH/SEH_except_handler3執行過程:
1)根據trylevel選擇scopetable數組
2)調用scopetable數組中對應的lpfnFilter函數
4)如果lpfnFilter函數返回0,向上遍歷,直到previousTryLevel = -1
__try__finally
描述:即使try中的代碼不會產生異常,finally中的代碼一定會執行
語法格式:
實驗五:理解__try__finally
1)編譯并運行以下代碼
#include <stdio.h> #include <windows.h>void TestException() {for(int i=0; i<10; i++){__try{//continue;//break;//return;printf("其他代碼\n");}__finally{printf("一定會執行的代碼\n");}} }int main() {TestException();getchar();return 0; }2)取消continue注釋,查看執行結果
3)取消break注釋,查看執行結果
4)取消return注釋,查看執行結果
局部展開
展開時機:當__try __finally中__try代碼提前流程代碼塊時會產生比如:Continue、Break、Return等
實驗六:理解局部展開
1)編譯并運行以下代碼
#include <stdio.h> #include <windows.h>void TestException() {for(int i=0; i<10; i++){__try{return;printf("其他代碼\n");}__finally{printf("一定會執行的代碼\n");}} }int main() {TestException();getchar();return 0; }2)查看TestException函數的匯編代碼
9: void TestException() 10: { 00401050 push ebp 00401051 mov ebp,esp 00401053 push 0FFh 00401055 push offset string "\xd2\xec\xb3\xa3\xb4\xa6\xc0\xed\xba\xaf\xca\xfd\n"+10h (00423018) 0040105A push offset __except_handler3 (0040127c) 0040105F mov eax,fs:[00000000] 00401065 push eax 00401066 mov dword ptr fs:[0],esp 0040106D add esp,0FFFFFFB4h 00401070 push ebx 00401071 push esi 00401072 push edi 00401073 lea edi,[ebp-5Ch] 00401076 mov ecx,11h 0040107B mov eax,0CCCCCCCCh 00401080 rep stos dword ptr [edi] 11: for(int i=0; i<10; i++) 00401082 mov dword ptr [ebp-1Ch],0 00401089 jmp TestException+44h (00401094) 0040108B mov eax,dword ptr [ebp-1Ch] 0040108E add eax,1 00401091 mov dword ptr [ebp-1Ch],eax 00401094 cmp dword ptr [ebp-1Ch],0Ah 00401098 jge $L53977+2 (004010c1) 12: { 13: __try 0040109A mov dword ptr [ebp-4],0 004010A1 push 0FFh 14: { 004010A3 lea ecx,[ebp-10h] 004010A6 push ecx 004010A7 call __local_unwind2 (004011c6) //局部展開,就是這個函數找到了finally中的代碼//需要中途從__try中出來時由編譯器生成 004010AC add esp,8 15: return; 004010AF jmp $L53977+2 (004010c1) //跳轉至退出函數前的準備工作 16: printf("其他代碼\n"); 17: } 18: __finally 19: { 20: printf("一定會執行的代碼\n"); 004010B1 push offset string "\xd2\xbb\xb6\xa8\xbb\xe1\xd6\xb4\xd0\xd0\xb5\xc4\xb4\xfa\xc2\xeb\n" (0042 004010B6 call printf (0040de70) 004010BB add esp,4 $L53975: 004010BE ret 21: } 22: } 004010BF jmp TestException+3Bh (0040108b) 23: } 004010C1 mov ecx,dword ptr [ebp-10h] 004010C4 mov dword ptr fs:[0],ecx 004010CB pop edi 004010CC pop esi 004010CD pop ebx 004010CE add esp,5Ch 004010D1 cmp ebp,esp 004010D3 call __chkesp (00401690) 004010D8 mov esp,ebp 004010DA pop ebp 004010DB ret3)觀察scopetable,可以發現異常過濾代碼為0,局部展開函數由此判斷except與finally
4)跟進__local_unwind2
5)執行結果
全局展開
展開時機:每次執行__except代碼之前,會重新從異常發生位置遍歷一次__finally,如果存在則依次調用局部展開函數
實驗七:理解全局展開
1)編譯并運行以下代碼
#include <stdio.h> #include <windows.h>void TestException() {__try{__try{__try{*(int*)0 = 1;}__finally{printf("一定會執行的代碼A\n");}}__finally{printf("一定會執行的代碼B\n");}}__except(1){printf("異常處理函數\n");} }int main() {TestException();getchar();return 0; }2)執行結果
總結
以上是生活随笔為你收集整理的Windows异常学习笔记(四)—— 编译器扩展SEH的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows异常学习笔记(二)—— 内
- 下一篇: Windows异常学习笔记(五)—— 未