VEH,VCH,UEF Windows向量化异常处理机制详解
先声明一下,本文重点是讲VCH的。
可能在SEH的介绍上有一些误区,请各位大牛体谅,也感谢各位指出我的错误。
最近在搞的SR,因为涉及到Windows下的异常处理机制。
所以研究了下,看了很多的文章,有了不少收获,但是也遇到了一些问题。
所以把我的一些经验和心得写成本文,发出来学习、交流一下。
本文重点讲的是向量化异常处理程序。
首先,先简单解释下这几个名词:
VEH: 向量化异常处理程序(进程相关)
VCH: 同上,也是向量化异常处理程序,不过它总是在最后被调用(进程相关)
SEH: 结构化异常处理程序,这个不用解释了吧。就是fs:[0]那个(线程相关)
UEF: 即TopLevalEH,基于SEH的,是进程相关,但是没测试过
其次再解释什么是EH(异常处理程序):
EH全称就是ExceptionHandler,中文意为异常处理器。
EH(异常处理程序)是做什么的呢,就是当程序发生一些错误、异常时,系统会保存好线程的CONTEXT(线程上下文)。
再交给EH来处理异常,有时候不仅仅是错误、异常。一些调试用的中断,异常处理程序也可以处理。比如int 1、int 3。
因为SEH的的头部被保存在TEB(fs:[0]),所以它是线程相关的。
UEF、VEH、VCH异常处理函数定义(UEF和VEH、VCH的函数类型名不一样,但是结构是一样的):
LONG NTAPI ExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo);
SEH异常处理函数定义:
EXCEPTION_DISPOSITION __cdecl _except_handler ( _In_ struct _EXCEPTION_RECORD *_ExceptionRecord, //异常记录结构指针_In_ void * _EstablisherFrame, //指向EXCEPTION_REGISTRATION结构,即SEH链_Inout_ struct _CONTEXT *_ContextRecord, //Context结构指针 (线程上下文)_Inout_ void * _DispatcherContext //无意义 (调度器上下文?)
);
UEF、VEH、VCH的异常处理函数调用约定是stdcall的,windows下的系统api、回调,基本都是stdcall的。
SEH的异常处理函数调用约定cdecl的。
EH(异常处理程序)的结构,UEF、VEH、VCH都一样的,但是VEH、VCH和UEF异常处理程序不同的一点就是返回值,见下文详解。
先讲UEF、VEH、VCH函数的参数。
参数只有一个,是指向结构_EXCEPTION_POINTERS的指针。具体结构如下:
typedef struct _EXCEPTION_POINTERS {PEXCEPTION_RECORD ExceptionRecord; //异常记录(EXCEPTION_RECORD)的指针PCONTEXT ContextRecord; //线程上下文的指针
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
typedef struct _EXCEPTION_RECORD {DWORD ExceptionCode; //异常代码,说明是什么异常,比如单步、除零、断点等等DWORD ExceptionFlags; //异常标志struct _EXCEPTION_RECORD *ExceptionRecord; //指向下一个异常记录(EXCEPTION_RECORD)的指针PVOID ExceptionAddress; //发生异常的地址DWORD NumberParameters; //异常信息的个数(即数组ExceptionInformation的个数)ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常信息数组} EXCEPTION_RECORD;typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;
以下是发生异常线程的cpu各个寄存器的值(线程上下文),就不解释了。
/* 浮点寄存器 */
typedef struct _FLOATING_SAVE_AREA {DWORD ControlWord;DWORD StatusWord;DWORD TagWord;DWORD ErrorOffset;DWORD ErrorSelector;DWORD DataOffset;DWORD DataSelector;BYTE RegisterArea[SIZE_OF_80387_REGISTERS];DWORD Spare0;
} FLOATING_SAVE_AREA;typedef FLOATING_SAVE_AREA *PFLOATING_SAVE_AREA;
typedef struct _CONTEXT {//// The flags values within this flag control the contents of// a CONTEXT record.//// If the context record is used as an input parameter, then// for each portion of the context record controlled by a flag// whose value is set, it is assumed that that portion of the// context record contains valid context. If the context record// is being used to modify a threads context, then only that// portion of the threads context will be modified.//// If the context record is used as an IN OUT parameter to capture// the context of a thread, then only those portions of the thread's// context corresponding to set flags will be returned.//// The context record is never used as an OUT only parameter.//DWORD ContextFlags;//// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT// included in CONTEXT_FULL.//DWORD Dr0;DWORD Dr1;DWORD Dr2;DWORD Dr3;DWORD Dr6;DWORD Dr7;//// This section is specified/returned if the// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.//FLOATING_SAVE_AREA FloatSave;//// This section is specified/returned if the// ContextFlags word contians the flag CONTEXT_SEGMENTS.//DWORD SegGs;DWORD SegFs;DWORD SegEs;DWORD SegDs;//// This section is specified/returned if the// ContextFlags word contians the flag CONTEXT_INTEGER.//DWORD Edi;DWORD Esi;DWORD Ebx;DWORD Edx;DWORD Ecx;DWORD Eax;//// This section is specified/returned if the// ContextFlags word contians the flag CONTEXT_CONTROL.//DWORD Ebp;DWORD Eip;DWORD SegCs; // MUST BE SANITIZEDDWORD EFlags; // MUST BE SANITIZEDDWORD Esp;DWORD SegSs;//// This section is specified/returned if the ContextFlags word// contains the flag CONTEXT_EXTENDED_REGISTERS.// The format and contexts are processor specific//BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];} CONTEXT;typedef CONTEXT *PCONTEXT;
因为SEH的文章很多,所以本文就不说了。
SEH的使用方法:
使用汇编
push handler // 异常处理函数的地址
push fs:[0] // 前一个异常处理函数函数的地址
mov fs:[0], esp // 装入新的SEH链结构
或者C++中的__try、__expect块。
UEF的注册方法:
使用SetUnhandledExceptionFilter函数。
VEH注册和移除的方法:
使用AddVectoredExceptionHandler函数添加。
使用RemoveVectoredExceptionHandler函数移除。
VCH注册和移除的方法:
使用AddVectoredContinueHandler函数。
使用RemoveVectoredContinueHandler函数移除。
结构化异常处理程序(SEH)的返回值:
typedef enum _EXCEPTION_DISPOSITION {ExceptionContinueExecution, //0ExceptionContinueSearch, //1ExceptionNestedException, //2ExceptionCollidedUnwind //3
} EXCEPTION_DISPOSITION;
结构化异常处理程序(__expect和UEF)可以返回3种值:
EXCEPTION_EXECUTE_HANDLER :(1)
这里要说明一下,很多帖子这里的解释都是 “该异常被处理。从异常处下一条指令继续执行”,个人感觉不太准确
查阅了MSDN之后发现,该值的解释为:
“ Return from UnhandledExceptionFilter and execute the associated exception handler. This usually results in process termination.”
中文翻译为:“返回从UnhandledExceptionFilter并执行相关的异常处理程序。这通常会导致进程终止。”
就是说如果EH(异常处理程序)返回EXCEPTION_EXECUTE_HANDLER,那么通常会导致进程终止。
EXCEPTION_CONTINUE_SEARCH:(0)
这个就是继续搜索执行下一个EH(异常处理程序)。
EXCEPTION_CONTINUE_EXECUTION:(1)
这个是继续执行,如果EH(异常处理程序)返回该值,那么系统(异常调度器)会恢复传递给EH的CONTEXT(线程上下文),并继续执行。
向量化异常处理程序(VEH、VCH)只能返回2种值:
EXCEPTION_CONTINUE_SEARCH:(0)
这个就是继续搜索下一个EH(异常处理程序)。
EXCEPTION_CONTINUE_EXECUTION:(1)
这个是继续执行,如果EH(异常处理程序)返回该值,那么系统(异常调度器)会恢复传递给EH的CONTEXT(线程上下文),并继续执行。
再来说下C++中的__expect和SEH的关系:
__except是对_except_handler函数的封装,__expect里的表达式(可以是函数)返回值是和UEF一样的。
_except_handler函数里会执行__except()里面的代码。根据表达式返回的值。再转换成相应EXCEPTION_DISPOSITION的值。
说的明白一点,就是__except()里的值是3态的。_except_handler返回值是4态的。
再来说下这些个EH(异常处理程序)的调用顺序:
先来个链接
看雪:【原创】白话windows之四 异常处理机制
按照上面帖子的说法。这些EH(异常处理程序)的顺序如下所示:
1. 第一次交给调试器(进程必须被调试)
2. 执行VEH
3. 执行SEH
4. UEF (TopLevelEH 进程被调试时不会被执行)
-->这里应该还有个VCH
5. 最后一次交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6. 调用异常端口通知csrss.exe
其实在4和5之间还有个VCH,不过这个VCH在一般xp下是没有的。在MSDN上,这个API最低的系统版本如下:
客户端:Windows Vista, Windows XP Professional x64 Edition
服务端:Windows Server 2008, Windows Server 2003 with SP1
不过个人认为这个VCH有点很特立独行,如果现在还不能理解这句话,没关系,往下看。
起初我也以为它跟别的异常处理程序没什么区别,仅仅是执行先后的问题。
但是很多人都发现,在支持VCH的系统中,经常VCH不会被触发,而导致进程终止了。
于是写了个程序,用来测试UEF、VEH、VCH。(SEH不是本文重点,UEF也是基于SEH的)。
先上代码:
注意:代码请在支持VCH的系统中运行,另外在MSVC中,需要配置禁用SafeSEH。
禁用SafeSEH方法:项目属性->链接器->高级->映像具有安全异常处理程序 设置为 否 (/SAFESEH:NO)
运行程序时,请不要使用MSVC运行,编译后不要用调试器启动,否则UEF会失效。
#include #include #include #include #define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 1 //是否启用UEF
#define UEF_HANDLE 0 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 0 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
LONG NTAPI F_ExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
printf("VEH!");
switch (ExceptionInfo->ExceptionRecord->ExceptionCode)
{
#if VEH_INT3
case EXCEPTION_BREAKPOINT:
printf("√F_ECODE:%08X Int3!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->Eip++;
return EXCEPTION_CONTINUE_EXECUTION;
#else
case EXCEPTION_SINGLE_STEP:
printf("√F_ECODE:%08X SingleStep!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->EFlags &= 0xFFFFFEFF;
return EXCEPTION_CONTINUE_EXECUTION;
#endif
default:
printf("×F_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
return EXCEPTION_CONTINUE_SEARCH;
}
}
LONG NTAPI L_ExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
printf("VCH!");
switch (ExceptionInfo->ExceptionRecord->ExceptionCode)
{
#if VEH_INT3
case EXCEPTION_SINGLE_STEP:
printf("√L_ECODE:%08X SingleStep!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->EFlags &= 0xFFFFFEFF;
return EXCEPTION_CONTINUE_EXECUTION;
#else
case EXCEPTION_BREAKPOINT:
printf("√L_ECODE:%08X Int3!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->Eip++;
return EXCEPTION_CONTINUE_EXECUTION;
#endif
default:
printf("×L_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
return EXCEPTION_CONTINUE_SEARCH;
}
}
#if ENABLE_UEF
LONG NTAPI MyUEF(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
printf("UEF!");
switch (ExceptionInfo->ExceptionRecord->ExceptionCode)
{
#if VEH_INT3
case EXCEPTION_SINGLE_STEP:
#if UEF_HANDLE
printf("√U_ECODE:%08X SingleStep!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->EFlags &= 0xFFFFFEFF;
return EXCEPTION_CONTINUE_EXECUTION;
#else
printf("×U_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
#if UEF_C_SEARCH
return EXCEPTION_CONTINUE_SEARCH;
#else
return EXCEPTION_EXECUTE_HANDLER;
#endif
#endif
#else
case EXCEPTION_BREAKPOINT:
#if UEF_HANDLE
printf("√U_ECODE:%08X Int3!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->Eip++;
return EXCEPTION_CONTINUE_EXECUTION;
#else
printf("×U_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
#if UEF_C_SEARCH
return EXCEPTION_CONTINUE_SEARCH;
#else
return EXCEPTION_EXECUTE_HANDLER;
#endif
#endif
#endif
default:
printf("×U_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
#if UEF_C_SEARCH
return EXCEPTION_CONTINUE_SEARCH;
#else
return EXCEPTION_EXECUTE_HANDLER;
#endif
}
}
#endif
int _tmain(int argc, _TCHAR* argv[])
{
AddVectoredExceptionHandler(0, F_ExceptionHandler);
AddVectoredContinueHandler(0, L_ExceptionHandler);
#if DISABLE_SEH
__asm xor eax,eax
__asm mov dword ptr fs : [0], eax
#endif
#if ENABLE_UEF
/* 设置uef */
SetUnhandledExceptionFilter(MyUEF);
#endif
printf("准备抛出单步异常\n");
/* 激活TF标志位 */
__asm pushfd
__asm or dword ptr [esp],0x100
__asm popfd
__asm nop //此处单步异常
printf("准备抛出断点异常\n");
/* 触发int3断点 */
__asm int 3 //此处断点异常
__asm nop
return 0;
}
printf("准备抛出单步异常\n");/* 激活TF标志位 */__asm pushfd__asm or dword ptr [esp],0x100__asm popfd__asm nop //此处单步异常printf("准备抛出断点异常\n");/* 触发int3断点 */__asm int 3 //此处断点异常__asm nop
第一次是单步异常、第二次是断点异常。并且可以根据需要配置UEF。
为了方便测试,我定义了几个宏,用来配置和测试程序。
首先配置成:
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 1 //是否启用UEF
#define UEF_HANDLE 0 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 0 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
编译后,运行程序(不要使用MSVC运行)。
因为程序会一闪而退,所以我用cmd启动的程序。
先简单介绍下,VEH!和VCH!后面的那个√和×表示的意义。
√表示返回EXCEPTION_CONTINUE_EXECUTION。
×表示返回EXCEPTION_CONTINUE_SEARCH或者EXCEPTION_EXECUTE_HANDLER。
很明显,当引发单步异常时,VEH最先收到并处理了异常,如下代码:
printf("√F_ECODE:%08X SingleStep!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);ExceptionInfo->ContextRecord->EFlags &= 0xFFFFFEFF; //去掉TF标志位return EXCEPTION_CONTINUE_EXECUTION; //继续执行
收到异常后,先打印异常信息,然后取消TF标志位,再继续执行代码。
这里跟预料的一样,完全没有UEF什么事了,因为异常已经被处理了。
但是,即使VEH处理了异常,VCH还是会被触发。
然后是执行了int 3,引发断点异常。
这里没有让VEH处理该异常。于是异常会被继续传递下去。
到达UEF,因为配置的UEF_C_SEARCH为0。所以UEF返回了EXCEPTION_EXECUTE_HANDLER。
到这里,进程自杀了VCH也没有被触发。或许你想问为什么,别着急慢慢看。
继续测试,配置成下面这样:
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 1 //是否启用UEF
#define UEF_HANDLE 0 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 1 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
编译后,运行程序(不要使用MSVC运行)。
结果如下图:
这回因为UEF_C_SEARCH设置成1了,所以UEF会返回EXCEPTION_CONTINUE_SEARCH。
但是结果似乎没什么区别,除了多了个进程停止的对话框。
再继续测试,配置成下面这样:
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 1 //是否启用UEF
#define UEF_HANDLE 1 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 1 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
编译后,运行程序(不要使用MSVC运行)。
结果如下图:
这回因为UEF_HANDLE设置成了1,所以UEF_C_SEARCH就无效了。并且UEF会处理断点异常,如下面所示:
printf("√U_ECODE:%08X Int3!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);ExceptionInfo->ContextRecord->Eip++; //跳过int 3return EXCEPTION_CONTINUE_EXECUTION;
这次VCH终于现身了。但是UEF已经把异常处理完了。
可能你已经有所了解了,但是别着急,还有最后一个测试。
配置成如下所示:
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 0 //是否启用UEF
#define UEF_HANDLE 1 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 1 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
这里直接禁用了UEF,那么UEF_HANDLE和UEF_C_SEARCH就都没用了。
编译后,运行程序(不要使用MSVC运行)。
结果如下图:
结论:
当异常被处理,并且返回EXCEPTION_CONTINUE_EXECUTION时,会触发VCH。
说的通俗一点:
VCH就好像是老板,而VEH和SEH、UEF等算是打工的,它们是"异常处理器",而VCH是"继续处理器"。
当异常这个烂摊子让"打工的"收拾完之后,会通知"老板",老板来做决定。
如果没人处理异常,烂摊子没人收拾,老板自然不会去"收拾烂摊子"了。
以上内容为本人分析的结果,如有错误,欢迎指出。
VEH,VCH,UEF Windows向量化异常处理机制详解相关推荐
- java异常处理机制详解
java异常处理机制详解 参考文章: (1)java异常处理机制详解 (2)https://www.cnblogs.com/vaejava/articles/6668809.html 备忘一下.
- SpringMVC异常处理机制详解[附带源码分析]
SpringMVC异常处理机制详解[附带源码分析] 参考文章: (1)SpringMVC异常处理机制详解[附带源码分析] (2)https://www.cnblogs.com/fangjian0423 ...
- Java------IO流与异常处理机制 详解
IO流与异常处理机制 File类 File类的每一个实例可以表示硬盘(文件系统)中的一个文件或目录(实际上表示的是一个抽象路径) 使用File可以做到: 1:访问其表示的文件或目录的属性信息,例如:名 ...
- Windows窗口刷新机制详解
1.Windows的窗口刷新管理 窗口句柄(HWND)都是由操作系统内核管理的,系统内部有一个z-order序列,记录着当前窗口从屏幕底部(假象的从屏幕到眼睛的方向),到屏幕最高层的一个窗口句柄的排序 ...
- C++异常处理机制详解
异常处理是一种允许两个独立开发的程序组件在程序执行期间遇到程序不正常的情况(异常exception)时相互通信的机制.本文总结了19个C++异常处理中的常见问题,基本涵盖了一般C++程序开发所需 ...
- windows下c语言钩子,Windows的钩子机制详解
一.概述: 了解windows程序设计的人都知道,Windows系统程序的运行是建立在消息传递机制的基础之上的,几乎所有的程序活动都由消息来驱动.钩子机制可以看作是一个消息的中转站,控制系统发出消息的 ...
- c语言windows驱动编程入门,Windows驱动开发技术详解 PDF扫描版[175MB]
Windows驱动开发技术详解由浅入深.循序渐进地介绍了windows驱动程序的开发方法与调试技巧.本书共分23章,内容涵盖了windows操作系统的基本原理.nt驱动程序与wdm驱动程序的构造.驱动 ...
- python语言程序的特点_Python语言概述及其运行机制详解
即日起,我们将打开一个新的编程世界的大门--Python语言.Python是一种跨平台的计算机程序设计语言.是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新 ...
- Python语言概述及其运行机制详解
即日起,我们将打开一个新的编程世界的大门--Python语言.Python是一种跨平台的计算机程序设计语言.是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新 ...
- SpringBoot异常处理ErrorController详解
文章目录 一.背景 二.SpringBoot的默认异常处理BasicErrorController 三.自定义错误异常 写在前面: 我是「境里婆娑」.我还是从前那个少年,没有一丝丝改变,时间只不过是考 ...
最新文章
- linux上查看网络限制,如何在Linux上限制网络带宽
- MyBatis复习笔记5:MyBatis代码生成器
- 科学家从脑电图中解读大脑的运动意图
- 《中国人工智能学会通讯》——2.35 敏捷和灵巧精细动作技能(Agile and Dexterous Fine Motor Skills)...
- 2014025675 《嵌入式系统程序设计》第七周学习总结
- SQL Server触发器创建、删除、修改、查看
- 如何将IE11降级到IE10
- cad填充密度怎么调整_CAD填充实例教程,CAD2018怎么修改填充图案的比例方法
- 海康威视存储服务器的作用,海康存储服务器CVR存储方式配置说明
- 三日月くるみ - 魔法みたいな恋したい
- 银行外汇资金业务学习笔记(3)spot rate (即期汇率)和 forward rate(远期汇率)
- 服务器被劫持是什么意思
- PXE高效批量网络装机
- Java 编程技巧之样板代码
- ios添加邮件收件服务器,iOS 系统邮件的基础使用
- 找工作,要做就做最好的自己,大平台去闯闯,一定不要让未来后悔!~附简历...
- Google Assistant SmartHome 入门指南
- 最后一个bate版本typora下载,typora快速上手
- gsoap初始化释放_gSOAP中文文档
- java ftp服务器搭建教程_配置使用IIS的FTP服务器客户端实现 (Java)教程