大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全家桶1年46,售后保障稳定
因为最近分析的一个壳用到了SEH相关的代码,所以要学习一下
内容转述自《软件加密技术内幕》
一些问题和回答
异常是谁提出的
Intel 提出了中断和异常的概念,中断跟外部硬件设备有关,而异常跟内部事件有关
异常分为故障,陷阱,终止,终止异常为不可恢复异常
为什么要有异常处理机制?
程序会出现错误,如果到处用if(!fun())这样的形式来侦错的话,代码不好维护。
而异常处理机制的使侦错代码和实际代码分离的作用很好的改善了这种情况,当然还有
其他原因吧,不一一列举
哪里用到了异常处理机制?
C++的语法支持异常处理,Windows也支持异常处理,尽管两个不是一样的东西,但是作用 甚至使用的接口名称都非常相似
异常处理工作流程
1 判断异常是何种类型,是否应该发给程序,如果应该发送则将结构_EXETPTION_DEBUG_INFO中的变量dwFirstChance置1并发给调试器,让调试器来处理(也可能会不处理)
_EXETPTION_DEBUG_INFO结构如下
typedef struct _EXCEPTION_DEBUG_INFO
{
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO;
2 没有调试器或者调试器不处理这个异常的话系统会找到程序线程中的异常处理程序并交由其处理
3 线程中可以有多个异常处理,如果一个无法处理则让其他来处理
4 如果都无法处理则系统将dwFirstChance置0,再通知调试器(如果有的话),如果没有调试器或者调试器继续不处理,而程序又曾经调用过API SetUnhandledFilter来设置异常处理的过程,系统将会调用这个过程来处理异常(这是进程级别的异常处理过程了)
5 现在如果异常还没被线程/进程相关的异常处理程序解决的话系统就会显示一个框框告诉你程序崩溃了,让你关闭或者调试这个程序
6 在程序终结之前,系统再次调用异常线程中的所有线程(这是释放资源最后的机会)
一些关于SEH的结构
只罗列下,具体的后面再叙述
TEB结构
typedef struct _NT_TEB{ //TEB=>Thread Environment Block
000h NT_TIB Tib;
01Ch PVOID EnvironmentPointer;
020h CLIENT_ID Cid;
028h PVOID ActiveRpcInfo;
02Ch PVOID ThreadLocalStoragePointer;
030h PPEB Peb;
034h ULONG LastErrorValue;
038h ULONG CountOfOwnedCriticalSections;
03Ch PVOID CsrClientThread;
040h PVOID Win32ThreadInfo;
044h ULONG Win32ClientInfo[0x1F];
0C0h PVOID WOW32Reserved;
0C4h ULONG CurrentLocale;
0C8h ULONG FpSoftwareStatusRegister;
0CCh PVOID SystemReserved1[0x36];
1A4h PVOID Spare1;
1A8h LONG ExceptionCode;
1ACh ULONG SpareBytes1[0x28];
1D4h PVOID SystemReserved2[0xA];
1FCh GDI_TEB_BATCH GdiTebBatch;
6DCh ULONG gdiRgn;
6E0h ULONG gdiPen;
6E4h ULONG gdiBrush;
6E8h CLIENT_ID RealClientId;
6F0h PVOID GdiCachedProcessHandle;
6F4h ULONG GdiClientPID;
6F8h ULONG GdiClientTID;
6FCh PVOID GdiThreadLocaleInfo;
700h PVOID UserReserved[5];
714h PVOID glDispatchTable[0x118];
B74h ULONG glReserved1[0x1A];
BDCh PVOID glReserved2;
BE0h PVOID glSectionInfo;
BE4h PVOID glSection;
BE8h PVOID glTable;
BECh PVOID glCurrentRC;
BF0h PVOID glContext;
BF4h NTSTATUS LastStatusValue;
BF8h UNICODE_STRING StaticUnicodeString;
C00h WCHAR StaticUnicodeBuffer[0x105];
E0Ch PVOID DeallocationStack;
E10h PVOID TlsSlots[0x40];
F10h LIST_ENTRY TlsLinks;
F18h PVOID Vdm;
F1Ch PVOID ReservedForNtRpc;
F20h PVOID DbgSsReserved[0x2];
F28h ULONG HardErrorDisabled;
F2Ch PVOID Instrumentation[0x10];
F6Ch PVOID WinSockData;
F70h ULONG GdiBatchCount;
F74h ULONG Spare2;
F78h ULONG Spare3;
F7Ch ULONG Spare4;
F80h PVOID ReservedForOle;
F84h ULONG WaitingOnLoaderLock;
F88h PVOID StackCommit;
F8Ch PVOID StackCommitMax;
F90h PVOID StackReserve;
???h PVOID MessageQueue;
}NT_TEB, *PNT_TEB;
TEB永远由[FS:0]指向,其第一项是指向SEH链表的指针
_EXCEPTION_REGISTRATION_RECORD结构
EXCEPTION_REGISTRATION_RECORD
Prev dd
Handler dd
EXCEPTION_REGISTRATION_RECORD ENDS
链中的前一个EXCEPTION_REGISTRATION_RECORD
当前的Handler
_EXCEPTION_POINTERS
pEXCEPTION_RECORD ExceptionRecord dd
pCONTEXT ContextRecord dd
_EXCEPTION_POINTERS ENDS
这是给进异常程回调函数的参数
EXCEPTION_RECORD
DWORD ExceptionCode
DWORD ExceptionFlags
struct EXCEPTION_RECORD
DVOID ExceptionAddress
DWORD NumberParameters
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]
}EXCEPTION_RECORD ends
异常代码(内存读写/除0等)
异常标志(可恢复/不可恢复/要终止程序了,请释放资源)
指向嵌套的异常结构(因为在异常里面又发生了异常)
发生异常的地址
附加消息(读冲突/写冲突)
Context结构(寄存器们)
typedef struct _CONTEXT
{
DWORD ContextFlags;
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
} CONTEXT;
一些关于SEH的API
只罗列,不多解释
UINT SetErrMode(UINT nMode);
//nMode=0, 显示错误对话框
//nMode=NOGPFAULTERRORBOX, 不显示对话框
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter(
_In_ LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter
);
//设置进程级别的异常处理过程
void WINAPI RaiseException(
_In_ DWORD dwExceptionCode,
_In_ DWORD dwExceptionFlags,
_In_ DWORD nNumberOfArguments,
_In_ const ULONG_PTR *lpArguments
);
//引发一个异常,可以自己定义异常代码
BOOL WINAPI GetThreadContext(
_In_ HANDLE hThread,
_Inout_ LPCONTEXT lpContext
);
//获取线程环境
BOOL WINAPI SetThreadContext(
_In_ HANDLE hThread,
_In_ const CONTEXT *lpContext
);
//设置线程环境
void WINAPI RtlUnwind(
_In_opt_ PVOID TargetFrame,
_In_opt_ PVOID TargetIp,
_In_opt_ PEXCEPTION_RECORD ExceptionRecord,
_In_ PVOID ReturnValue
);
//进程(顶层)异常处理
//进程(顶层)异常处理用SetUnhandledExceptionFilter来设置,只能有一个,如果设置了几次,以最后一个为准
//回调函数的格式
Handler proc lpExceptionPointer
....
ret
Handler endp
//lpExceptionPointer里存放着异常的信息(就是EXECPTION_POINTERS指针),指向了EXCEPTION_RECORD和CONTEXT
//回调函数的返回值可以有三种
//1 EXECPTION_EXECUTE_HANDLER, 表示程序已经处理过,可以退出了,但是不要显示错误对话框
//2 EXECPTION_CONTINUE_SEARCH, 表示程序无法处理,让系统交给其他代码处理,现在只有系统自己可以处理了,那就给你弹个错误对话框(弹不弹取决于SetErrMode)
//3 EXECPTION_CONTINUE_EXECUTION, 表示程序已经处理好了,回到刚才的异常代码继续执行吧
顶层SEH
下面展示一个小程序,这个程序会因为访问地址违规而出现错误,程序可以忽略这个错误继续运行 安静的退出程序 和弹出一个丑陋的错误框再退出程序
;******************************************
;coded by Hume,2K+
;******************************************
;例子1.演示Final型异常处理及参数获取
;******************************************
include hhd.h ;编译所必需的头文件
.DATA
szTit db "SEH例子-Final,written by Hume",0
mes000 db "We are in the Exception handler,Kill Prog",0dh,0ah
db "in silence(Y)、noisily(N) or Continue(cancel)?",0
messuc db "Hello,We manage to return after exception!",0
impossible db "It's impossible...",0
.DATA?
OldFilter dd ?
;;-----------------------------------------
.CODE
;Final 型异常处理回调函数
myFinalHandler proc uses esi edi ebx lpExceptionPointers
invoke MessageBox,0,addr mes000,addr szTit,MB_ICONINFORMATION or MB_YESNOCANCEL
.if eax==IDYES
mov eax,EXCEPTION_EXECUTE_HANDLER ;处理完毕,不会显示对话框
.elseif eax==IDNO
orCannotHanle:
mov eax,EXCEPTION_CONTINUE_SEARCH ;继续查找,显示对话框
.elseif eax==IDCANCEL ;处理完毕,修改CONTEXT上下文
mov ebx,[lpExceptionPointers] ;继续执行程序
ASSUME ebx:ptr EXCEPTION_POINTERS
ASSUME esi:ptr EXCEPTION_RECORD
ASSUME edi:ptr CONTEXT
mov esi,[ebx].pExceptionRecord
mov edi,[ebx].ContextRecord
test [esi].ExceptionFlags,3
jnz orCannotHanle
cmp [esi].ExceptionCode,STATUS_ACCESS_VIOLATION
jne orCannotHanle ;是内存读写异常吗
mov [edi].regEip,offset suc_ret ;改变返回地址
mov eax,EXCEPTION_CONTINUE_EXECUTION
.endif
ret
myFinalHandler endp
;程序入口点
_StArT:
invoke SetErrorMode,0
invoke SetUnhandledExceptionFilter,offset myFinalHandler
mov [OldFilter],eax
xor eax,eax
mov [eax],eax ;向地址0处写数据!产生异常
nop ;永不可能执行下面的语句
invoke MessageBox,0,addr impossible,addr szTit,MB_ICONINFORMATION
nop
suc_ret: ;返回后执行到这里
invoke MessageBox,0,addr messuc,addr szTit,MB_ICONINFORMATION
invoke ExitProcess,0
END _StArT
在OD中加载:
00401065 s> 6A 00 push 0
00401067 E8 4E000000 call <jmp.&KERNEL32.SetErrorMode>
0040106C 68 00104000 push s1.00401000
00401071 E8 4A000000 call <jmp.&KERNEL32.SetUnhandledExceptionFil>
00401076 A3 C0304000 mov dword ptr ds:[4030C0],eax
0040107B 33C0 xor eax,eax
0040107D 8900 mov dword ptr ds:[eax],eax
在最后一句,OD会提示有异常。你可以在OD菜单中的查看-SEH链中找对最前的一个SEH handler地址在反汇编中查看
你也可以在其第一句代码处下断点,然后SHIFT+F9运行程序,程序会断在SEH handler第一句处
线程SEH
注册线程SEH的方法(汇编),进OD调试下就知道为什么了
assume fs:nothing
push SehHandler
push [fs:0]mov [fs:0],esp
线程SEH使用例子1
;******************************************
;coded by Hume,2K+
;******************************************
;例子2.演示Per_Thread型异常处理
;******************************************
include c:\hd\hhd.h
.DATA
szTit db "SEH例子-Per_Thread,Hume,2k+",0
mesSUC db "WE SUCEED IN FIX DIV0 ERROR.",0
.DATA?
hInstance dd ?
;;-----------------------------------------
.CODE
SehHandler proc C uses ebx esi edi pExcept,pFrame,pContext,pDispatch
Assume esi:ptr EXCEPTION_RECORD
Assume edi:ptr CONTEXT
mov esi,pExcept
mov edi,pContext
test [esi].ExceptionFlags,3
jne _continue_search
cmp [esi].ExceptionCode,STATUS_INTEGER_DIVIDE_BY_ZERO ;是除0错?
jne _continue_search
mov [edi].regEcx,10 ;将被除数改为非0值继续返回执行
;这次可以得到正确结果是10
mov eax,ExceptionContinueExecution ;修复完毕,继续执行
ret
_continue_search:
mov eax,ExceptionContinueSearch ;其他异常,无法处理,继续遍历seh回调函数列表
ret
SehHandler endp
_StArT:
assume fs:nothing
push offset SehHandler
push fs:[0]
mov fs:[0],esp ;建立EXCEPTION_REGISTRATION_RECORD结构并将
;TIB偏移0改为该结构地址
xor ecx,ecx ;ECX=0
mov eax,100 ;EAX=100
xor edx,edx ;EDX=0
div ecx ;产生除0错!
invoke MessageBox,0,addr mesSUC,addr szTit,0
pop fs:[0] ;恢复原异常回调函数
add esp,4 ;平衡堆栈
invoke ExitProcess,0
END _StArT
线程SEH使用例子2
;******************************************
;coded by Hume,2K+
;******************************************
;例子3.演示Per_Thread型异常处理的嵌套处理
;******************************************
include c:\hd\hhd.h
;~~~~~~~~~~~~~~~~MACROs
;注册回调函数
InstSEHframe MACRO CallbackFucAddr
push offset CallbackFucAddr
push fs:[0]
mov fs:[0],esp
ENDM
;卸载回调函数
UnInstSEHframe MACRO
pop fs:[0]
add esp,4
ENDM
;用宏简化重复代码,对应于handler中判断部分
SEHhandlerProcessOrNot MACRO ExceptType,Exit2SearchAddr
Assume esi:ptr EXCEPTION_RECORD
Assume edi:ptr CONTEXT
mov esi,[pExcept]
mov edi,pContext
test [esi].ExceptionFlags,7
jnz Exit2SearchAddr
cmp [esi].ExceptionCode,ExceptType
jnz Exit2SearchAddr
;;below should follow the real processing codes
ENDM
;~~~~~~~~~~~~~~~~~~~~~~~
.DATA
szTit db "SEH例子-Per_Thread嵌套,Hume,2k+",0
FixDivSuc db "Fix Div0 Error Suc!",0
FixWriSuc db "Fix Write Acess Error Suc!",0
FixInt3Suc db "Fix Int3 BreakPoint Suc!",0
DATABUF dd 0
;;-----------------------------------------
.CODE
;除0错异常处理函数
Div_handler0 proc C uses ebx esi edi pExcept,pFrame,pContext,pDispatch
PUSHAD
SEHhandlerProcessOrNot STATUS_INTEGER_DIVIDE_BY_ZERO,@ContiSearch ;是否是整数除0错
mov [edi].regEcx,10 ;修正被除数
POPAD
mov eax,ExceptionContinueExecution ;返回继续执行
ret
@ContiSearch:
POPAD
mov eax,ExceptionContinueSearch
ret
Div_handler0 endp
;读写冲突内存异常处理函数
Wri_handler1 proc C uses ebx esi edi pExcept,pFrame,pContext,pDispatch
PUSHAD
SEHhandlerProcessOrNot STATUS_ACCESS_VIOLATION,@ContiSearch ;是否是读写内存冲突
mov [edi].regEip,offset safePlace1 ;改变返回后指令的执行地址
;mov [edi].regEdx,offset DATABUF ;将写地址转换为有效值
POPAD
mov eax,ExceptionContinueExecution
ret
@ContiSearch:
POPAD
mov eax,ExceptionContinueSearch
ret
Wri_handler1 endp
;断点中断异常处理函数
Int3_handler2 proc C uses ebx esi edi pExcept,pFrame,pContext,pDispatch
PUSHAD
SEHhandlerProcessOrNot STATUS_BREAKPOINT,@ContiSearch ;是否是断点
INC [edi].regEip ;调整返回后指令的执行地址,越过断点继续执行
;注意在9X下INT3异常发生后指令地址为
POPAD
mov eax,ExceptionContinueExecution
ret
@ContiSearch:
POPAD
mov eax,ExceptionContinueSearch
ret
Int3_handler2 endp
;mesAddr应含有指向欲显示消息的地址
MsgBox proc mesAddr
invoke MessageBox,0,mesAddr,offset szTit,MB_ICONINFORMATION
ret
MsgBox endp
;-----------------------------------------
_StArT:
Assume fs:nothing
invoke SetErrorMode,0
InstSEHframe Div_handler0
InstSEHframe Wri_handler1
InstSEHframe Int3_handler2
mov eax,100
cdq ;eax=100 edx=0
xor ecx,ecx ;ecx=0
div ecx ;除0异常!
invoke MsgBox,offset FixDivSuc ;如果处理除0错成功
xor edx,edx
mov [edx],eax ;向地址0处写入,发生写异常!
safePlace1:
invoke MsgBox,offset FixWriSuc ;如果处理写保护内存成功
int 3
nop
invoke MsgBox,offset FixInt3Suc ;如果处理断点int 3成功
invoke MessageBox,0,CTEXT("Test Illegal INSTR without Handler or Not(Y/N)?"),offset szTit,MB_YESNO
cmp eax,IDYES
jnz no_test
db 0Fh,17h ;为非法指令测试
invoke MsgBox,CTEXT("here,will Exit")
no_test:
UnInstSEHframe ;卸载所有的回调函数
UnInstSEHframe
UnInstSEHframe
invoke ExitProcess,0
END _StArT
——————————————-
堆栈展开
EXCEPTION_RECORD的ExeptionFlags如果等于2,ExceptionCode置为STATUS_UNWIND则表明正在展开堆栈.
展开堆栈是为了让程序在结束之前能够释放资源
堆栈展开例子
;******************************************
;coded by Hume,2K+
;******************************************
;例子4.演示异常处理的堆栈展开
;******************************************
include c:\hd\hhd.h
;~~~~~~~~~~~~~~~~MACROs
;注册回调函数
InstSEHframe MACRO CallbackFucAddr
push offset CallbackFucAddr
push fs:[0]
mov fs:[0],esp
ENDM
;卸载回调函数
UnInstSEHframe MACRO
pop fs:[0]
add esp,4
ENDM
;用宏简化重复代码,对应于handler中判断部分
SEHhandlerProcessOrNot MACRO ExceptType,Exit2SearchAddr
Assume esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT
mov esi,[pExcept]
mov edi,pContext
test [esi].ExceptionFlags,1
jnz Exit2SearchAddr
test [esi].ExceptionFlags,2
jnz @_Unwind
cmp [esi].ExceptionCode,ExceptType
jnz Exit2SearchAddr
;;below should follow the real processing codes
ENDM
;~~~~~~~~~~~~~~~~~~~~~~~
.DATA
szTit db "SEH例子-嵌套及展开,Hume,2k+",0
FixDivSuc db "Fix Div0 Error Suc!",0
FixWriSuc db "Fix Write Acess Error Suc!",0
FixInt3Suc db "Fix Int3 BreakPoint Suc!",0
DATABUF dd 0
.DATA?
OLDHANDLER dd ?
;;-----------------------------------------
.CODE
;除0错异常处理函数
;-----------------------------------------
;mesAddr应含有指向欲显示消息的地址
MsgBox proc mesAddr
invoke MessageBox,0,mesAddr,offset szTit,MB_ICONINFORMATION
ret
MsgBox endp
;-----------------------------------------
Div_handler0 proc C pExcept,pFrame,pContext,pDispatch
PUSHAD
SEHhandlerProcessOrNot STATUS_INTEGER_DIVIDE_BY_ZERO,@ContiSearch ;是否是整数除0错
mov [edi].regEcx,10 ;修正被除数
POPAD
mov eax,ExceptionContinueExecution ;返回继续执行
ret
@ContiSearch:
POPAD
mov eax,ExceptionContinueSearch
ret
@_Unwind:
invoke MsgBox,CTEXT("Div Handler unwinds")
jmp @ContiSearch
Div_handler0 endp
;读写冲突内存异常处理函数
Wri_handler1 proc C pExcept,pFrame,pContext,pDispatch
PUSHAD
SEHhandlerProcessOrNot STATUS_ACCESS_VIOLATION,@ContiSearch ;是否是读写内存冲突
mov [edi].regEip,offset safePlace1 ;改变返回后指令的执行地址
;mov [edi].regEdx,offset DATABUF ;将写地址转换为有效值
POPAD
mov eax,ExceptionContinueExecution
ret
@ContiSearch:
POPAD
mov eax,ExceptionContinueSearch
ret
@_Unwind:
invoke MsgBox,CTEXT("Write Acess Handler unwinds")
jmp @ContiSearch
Wri_handler1 endp
;断点中断异常处理函数
Int3_handler2 proc C pExcept,pFrame,pContext,pDispatch
PUSHAD
SEHhandlerProcessOrNot STATUS_BREAKPOINT,@ContiSearch ;是否是断点
INC [edi].regEip ;调整返回后指令的执行地址,越过断点继续执行
;注意在9X下INT3异常发生后指令地址为
POPAD
mov eax,ExceptionContinueExecution
ret
@ContiSearch:
POPAD
mov eax,ExceptionContinueSearch
ret
@_Unwind:
invoke MsgBox,CTEXT("Int3 Handler unwinds")
jmp @ContiSearch
Int3_handler2 endp
SELF_UNWIND proc C pExcept,pFrame,pContext,pDispatch
PUSHAD
SEHhandlerProcessOrNot STATUS_ILLEGAL_INSTRUCTION,@ContiSearch ;是否是无效指令测试
invoke MessageBox,0,CTEXT("Unwind by SELF(Y) or Let SYSTEM do(N)?"),addr szTit,MB_YESNO
cmp eax,IDYES
jnz @ContiSearch
mov [edi].regEip,offset unwind_return
invoke RtlUnwind,[pFrame],0,[pExcept],0
POPAD
mov eax,ExceptionContinueExecution
ret
@ContiSearch:
POPAD
mov eax,ExceptionContinueSearch
ret
@_Unwind:
invoke MsgBox,CTEXT("The LAST Handler unwinds")
jmp @ContiSearch
SELF_UNWIND endp
;-----------------------------------------
_StArT:
Assume fs:nothing
invoke SetErrorMode,0
mov [OLDHANDLER],eax
InstSEHframe SELF_UNWIND
InstSEHframe Div_handler0
InstSEHframe Wri_handler1
InstSEHframe Int3_handler2
mov eax,100
cdq ;eax=100 edx=0
xor ecx,ecx ;ecx=0
div ecx ;除0异常!
invoke MsgBox,offset FixDivSuc ;如果处理除0错成功
xor edx,edx
mov [edx],eax ;向地址0处写入,发生写异常!
safePlace1:
invoke MsgBox,offset FixWriSuc ;如果处理写保护内存成功
nop
invoke MsgBox,offset FixInt3Suc ;如果处理断点int 3成功
invoke MessageBox,0,CTEXT("Test Illegal INSTR with Handler&Unwind ability or Not(Y/N)?"),offset szTit,MB_YESNO
cmp eax,IDYES
jnz no_test
db 0Fh,0bh ;为非法指令测试
nop
nop
unwind_return:
invoke MsgBox,CTEXT("SELF UNWIND SUC NOW EXIT!")
;xor eax,eax ;uncomment this to test
;mov [eax],eax ;
no_test:
UnInstSEHframe ;卸载所有的回调函数
UnInstSEHframe
UnInstSEHframe
UnInstSEHframe
invoke ExitProcess,0
END _StArT
感觉SEH的作用非常大,对于正面
能处理异常
单步调试自己(虽然没多大意义,因为有Softice,OD了)
还有反跟踪
对于负面
能进入Ring0(在9x下)
栈溢出时能被利用,也可以用来绕过安全机制
剩下的一小部分是VC++对SEH的封装:
这一部分迟点再更新
未完待续。
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/231328.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...