首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
81 阅读
2
nvim番外之将配置的插件管理器更新为lazy
59 阅读
3
2018总结与2019规划
54 阅读
4
PDF标准详解(五)——图形状态
33 阅读
5
为 MariaDB 配置远程访问权限
30 阅读
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
Java
linux
文本编辑器
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
309
篇文章
累计收到
27
条评论
首页
栏目
心灵鸡汤
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
菜谱
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
页面
归档
友情链接
关于
搜索到
309
篇与
的结果
2016-10-23
C语言中处理结构体的原理
汇编中有几种寻址方式,分别是直接寻址:(ds:[idata])、寄存器间接寻址(ds:[bx])、寄存器相对寻址(ds:[bx + idata]、ds:[bx + si])基址变址寻址(ds:[bx + si])、相对基址变址寻址([bx + si + idata])。结构体的存储逻辑图如下:(以下数据表示某公司的名称、CEO、CEO的福布斯排行、收入、代表产品)现在假设公司的CEO在富豪榜上的排名为38,收入增加了70,代表产品变为VAX,通过汇编编程修改上述信息,以下是相应的汇编代码:(假设数据段为seg)mov ax,seg mov ds,ax mov bx,0 mov word ptr ds:[bx + 12],38 add [bx + 14],70 mov si,0 mov byte ptr [bx + 10 + si],'V' inc si mov byte ptr [bx + 10 + si],'A' inc si mov byte ptr [bx + 10 + si],'X'对应的C语言代码可以写成:struct company { char cn[3]; char name[9]; int pm; int salary; char product[3]; }; company dec = {"DEC","Ken Olsen",137,40,"PDP"}; int main() { int i; dec.pm = 38; dec.salary += 70; dec.product[i] = 'V'; ++i; dec.product[i] = 'A'; ++i; dec.product[i] = 'X'; return 0; }对比C语言代码和汇编代码,可以看出,对于结构体变量,系统会先根据定义分配相应大小的空间,并将各个变量名与内存关联起来,结构体对象名与系统分配的空间的首地址相对应(定义的结构体对象的首地址在段中的相对地址存储在bx中),即在使用dec名时实际与汇编代码“mov ax,seg” "mov ds,ax"对应,将数据段段首地址存入ds寄存器中,系统根据对象中的变量名找到对应的偏移地址,偏移地址的大小由对应的数据类型决定,如cn数组前没有变量,cn的偏移地址为0,cn所在的地址为 ds:[bx],cn为长度为3的字符型数组,在上一个偏移地址的基础上加上上一个变量所占空间的大小即为下一个变量的偏移地址,所以name数组的首地址为ds:[bx + 3],这样给出了对象名就相当于给定了该对象在段中的相对地址(上述代码中的bx),给定了对象中的成员变量名就相当于给定了某一内存在对象中的偏移地址(ds:[bx + idata])。根据数组名可以找到数组的首地址,但数组中具体元素的访问则需要给定元素个数,即si的值来定位数组中的具体内存,C语言中的 ++i 相当于汇编中的 (add si ,数组中元素的长度)。根据以上的分析可以看出,构建一个结构体对象时,系统会在代码段中根据结构体的定义开辟相应大小的内存空间,并将该空间在段中的偏移地址与对象名绑定。对象中的变量名与该变量在对象所在内存中的偏移地址相关联,数组中的标号用于定位数组中的元素在数组中的相对位置。(对象名决定bx,变量名决定bx + idata,数组中的元素标号决定bx + idata + si)。
2016年10月23日
6 阅读
0 评论
0 点赞
2016-10-23
汇编debug与masm命令
汇编语言这块是我之前写在网易博客上的,不过那个账号基本已经作废了,所以现在抽个时间把当时的博客搬到CSDN上。汇编命令(编译器masm命令):找到masm所在的文件夹,我的在d:\MASM中,用cmd打开dos界面,输入“d:”切换到D盘,再输入“d:\MASM\masm”打开编译器中的masm程序得到如下结果:再输入路径+含".asm"的文件(若在当前文件夹中则不必输入路径),这个表示生成了一个“.obj”文件,在第二行若不输入任何内容则默认在当前文件夹下生成一个与“.asm”同名的“.obj”文件。下面几个直接输入空格,不生成这几个文件,知道提示所有工作都完成(0 warning error)再按照上述格式找到MASM文件中的link程序,输入所需的“.obj”文件的相对路径 ".exe"行后不输入任何内容表示在该文件夹下生成一个与“.obj”文件同名的“.exe”文件,到这里汇编程序的编译链接工作就完成了。下面是该程序的调试,输入“debug” + 执行程序的路径进入程序,-u命令:查看汇编代码;-t命令:执行下一条语句-g + 的内存:跳转到该内存所对应的语句(再用t命令执行该条命令)-r命令:查看寄存器的内容(后可直接接寄存器的名称,就只查看该寄存器的内容)-d命令:后接内存地址,查看改地址后面8 * 16个字节空间的地址(每行16个字节,共8行)后面是对应的字符‘.’表示没有该数字对应的字符加上地址范围的话就只查看该地址范围内存储的数据
2016年10月23日
5 阅读
0 评论
0 点赞
2016-10-23
Minfilter过滤框架
Minfilter过滤框架优势与传统的Sfilter过滤驱动相比,有这样几个优势Minfilter加载顺序更易控制,Sfilter加载是随意的,也就是说它在IO设备栈上的顺序是根据其创建的顺序决定的,越晚创建的,越排在设备栈的顶部,而Minfilter根据它的一个全局变量——altitude规定了它在设备栈上的顺序具有可卸载能力,一般的hook或者过滤框架在卸载时可能仍然有程序在访问它们的代码,所以如果在有程序访问其代码,而它又被卸载时容易导致蓝屏,这样就不具备可卸载能力。而Minfilter则不会导致蓝屏Minfilter是通过注册回调函数到Minfilter管理器中,由Minfilter管理器来负责调度这些函数,不直接与IO管理器接触,同时我们只需要注册我们感兴趣的回调函数,而不像Sfilter那样,需要提供一个统一的处理函数。所以相对来说更简单兼容性更好,由IO管理器下发的IRP 请求既可以交给Sfilter框架处理,也可以交给Minfilter处理,也可以给下层的设备驱动处理。名字处理处理更加容易,相对与Sfilter中需要另外顶一个一个NAME_CONTROL结构,还需要注意长短名来说,Minfilter更加简单,只需要一个简单的函数就可以获取文件的卷设备名称,文件全名,流名等信息Minfilter的基本框架应用层下发的IO请求首先交由IO管理器处理,IO管理器将请求封装为一个IRP请求包接着网下层分发,当分发到Minfilter管理器时,由Minfilter将IRP封装为一个CALLBACK_DATA结构,并根据不同的请求调用不同的回调函数,由回调函数处理并决定是否分发到下层。Altitude 变量这个变量是Minfilter提供的全局变量,用来规定这个Minfilter管理器在IO栈中的高度,值越大越位于上方,它们在IO管理器中的位置如下图在栈中的位置既不是越低越好也不是越高越好,而是根据其具体需求,比如杀毒操作应该放到加解密操作之前,加解密应该放到真实的读写操作之前,所以它们在栈中从上到下的顺序应该是杀毒、加解密、IO设备对象。所以微软为每个功能的Minfilter中的Altitude都提供了大致的位置值,杀毒的过滤驱动是在320000-329999,而加解密的是140000-149999Minfilter框架详解注册结构FLT_REGISTRATIONMinfilter中最主要的是各个回调函数,向其中注册回调函数实际是在填写一个叫做FLT_REGISTRATION的结构体,在Minfilter中定义了这样一个结构体的变量:const FLT_REGISTRATION fileMonitorRegistration = { sizeof( FLT_REGISTRATION ), // Size FLT_REGISTRATION_VERSION, // Version 0, // Flags ContextRegistration, // ContextRegistration fileMonitorCallbacks, // Operation callbacks fileMonUnload, // FilterUnload fileMonInstanceSetup, // InstanceSetup NULL, // InstanceQueryTeardown fileMonInstanceTeardownStart, // InstanceTeardownStart NULL, // InstanceTeardownComplete NULL, // GenerateFileName NULL, // GenerateDestinationFileName NULL // NormalizeNameComponent };fileMonitorCallbacks是一个函数的指针数组,保存了处理各种事件的回调函数,框架会根据具体的事件来调用这些函数。它的定义如下:const FLT_OPERATION_REGISTRATION fileMonitorCallbacks[] = { { IRP_MJ_CREATE, FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO, HOOK_PreNtCreateFile, HOOK_PostNtCreateFile }, { IRP_MJ_CLEANUP, 0, HOOK_PreNtCleanup, NULL }, { IRP_MJ_WRITE, 0, HOOK_PreNtWriteFile, HOOK_PostNtWriteFile }, { IRP_MJ_SET_INFORMATION, 0, HOOK_PreNtSetInformationFile, HOOK_PostNtSetInformationFile }, { IRP_MJ_OPERATION_END } };利用这个结构体将各种IRP请求与处理它的回调函数绑定起来,每组请求都有两个两个处理它的回调函数,一个Pre表示具体设备处理这个请求之前,Post表示系统处理这个请求之后。拿Sfilter中的sfCreate举例来说,HOOK_PreNtCreateFile处理的是在它调用IoCallDriver之前的操作,而HOOK_PostNtCreateFile表示的是在sfCreate在等到底层设备完成文件创建之后的操作。fileMonUnload 这个函数相当于驱动中的DriverUnload函数,在进行驱动开发时由于很多时候不能进行安全的卸载所以很多驱动不提供DriverUnload函数,防止由于卸载时产生蓝屏,要卸载只能重启机器fileMonInstanceSetup :Minfilter像Sfilter一样,会遍历计算机中的所有卷设备,每当有一个被遍历到,就会绑定一个过滤驱动设备在卷设备上,这个过滤设备就是Minfilter中的Instance,这个函数会在绑定过滤设备对象时触发fileMonInstanceTeardownStart 在卸载这个过滤设备时调用它们的关系如下图所示Minfilter过滤驱动的注册启动与销毁在我们自己写的驱动中可以使用函数FltRegisterFilter来向Minfilter管理器注册这个驱动的相关信息,以便Minfilter管理器将我们的程序放入到IO设备栈的合适位置中,该函数的原型如下:NTSTATUS FltRegisterFilter( IN PDRIVER_OBJECT Driver, IN CONST FLT_REGISTRATION *Registration, OUT PFLT_FILTER *RetFilter ); 第一个参数就是驱动的驱动对象指针,这个可以从DriverEntry中获得。第二个参数是是我们定义的那个装有各种事件回调函数的一个结构体的指针,通过传入这个参数,将这组回调函数注册到Minfilter管理器中第三个参数是一个输出参数,如果注册成功,则会返回这个参数用来唯一标识这个过滤驱动,这个参数一般是保存在NULL_FILTER_DATA结构中的FilterHandle中注册完成后,就是启动这个过滤驱动,进行文件系统的过滤,启动使用函数FltStartFiltering,这个函数需要传入之前注册函数返回的那个过滤驱动的句柄当我们不需要使用这个过滤驱动时使用函数FltUnregisterFilter卸载,它同样需要传入这个过滤驱动句柄。下面是一个具体的例子NTSTATUS DriverEntry ( __in PDRIVER_OBJECT DriverObject, __in PUNICODE_STRING RegistryPath ) { NTSTATUS status; UNREFERENCED_PARAMETER( RegistryPath ); //注册回调函数 status = FltRegisterFilter( DriverObject, &FilterRegistration, &NullFilterData.FilterHandle ); ASSERT( NT_SUCCESS( status ) ); if (NT_SUCCESS( status )) { //如果注册成功则开启minfilter监控 status = FltStartFiltering( NullFilterData.FilterHandle ); if (!NT_SUCCESS( status )) { //如果失败则卸载注册的回调 FltUnregisterFilter( NullFilterData.FilterHandle ); } } DbgPrint("Minifilter started\n"); return status; }对于卸载来说,需要注意的是很多驱动本身是不支持卸载的也就是不提供DriverUnload函数,因为系统中的驱动是被所有进程调用的,如果某个进程正在调用而另外一个进程正在卸载这个驱动,那么很可能会产生蓝屏。回调函数Minfilter过滤驱动中处理各个请求的回调函数一般都有两个:一个在IO设备处理之前,一个在IO设备处理之后。下面将称它们问Pre函数和Post函数Pre函数typedef FLT_PREOP_CALLBACK_STATUS (*PFLT_PRE_OPERATION_CALLBACK) ( IN OUT PFLT_CALLBACK_DATA Data, IN PCFLT_RELATED_OBJECTS FltObjects, OUT PVOID *CompletionContext );这个函数中的Data是对IRP的一个封装,与操作IRP相似,Minfilter向R3层返回结果使用的代码与普通的NT模型中的相同Data->IoStatus.Status = STATUS_SUCCESS; Data->IoStatus.Information = 0;另外过滤函数返回的值时直接返回给Minfilter管理器进行处理,而Sfilter返回的值是直接交给IO管理器,回调函数的返回值一般有这样几个常用的:FLT_PREOP_SUCCESS_WITH_CALLBACK:表示处理请求成功,接着往下发这个请求,下层驱动处理完这个请求后可以触发POST函数的调用FLT_PREOP_SUCCESS_NO_CALLBACK:与上述返回值类似,只是下层驱动处理完这个请求后不会触发POST函数FLT_PREOP_COMPLETE:请求处理完成,返回这个值,Minfilter将不会将请求继续往下发Post函数Post函数的原型如下:typedef FLT_POSTOP_CALLBACK_STATUS (FLTAPI *PFLT_POST_OPERATION_CALLBACK) ( __inout PFLT_CALLBACK_DATA Data, __in PCFLT_RELATED_OBJECTS FltObjects, __in_opt PVOID CompletionContext, __in FLT_POST_OPERATION_FLAGS Flags );在该函数中的返回值一般有如下几个:FLT_POSTOP_FINISHED_PROCESSING:向Minfilter管理器返回成功FLT_POSTOP_MORE_PROCESSING_REQUIRED:需要Minfilter管理器另外开一个线程,用来作为工作者线程,处理需要在低IRQL请求中完成的工作判断DATA的操作类型的宏FLT_IS_IRP_OPERATION:判断这个是否是一个IRP请求FLT_IS_FASTIO_OPERATION:判断这个是否是一个FASTIO请求FLT_IS_FS_FILTER_OPERATION:判断这个是否是一个文件系统过滤的请求Minfilter的安装以inf文件安装inf文件是一个安装信息的配置文件,指明了安装的.sys文件路径,安装到哪个位置,以及写到注册表中的何种位置,下面是一个具体的例子,在这只列举了比较重要的部分:[Version] Signature = "$Windows NT$" Class = "ActivityMonitor" ;This is determined by the work this filter driver does ClassGuid = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2} ;This value is determined by the Class Provider = %Msft% DriverVer = 06/16/2007,1.0.0.0 CatalogFile = nullfilter.cat [DestinationDirs] DefaultDestDir = 12 NullFilter.DriverFiles = 12 ;%windir%\system32\drivers ;; ;; Default install sections ;; [DefaultInstall] OptionDesc = %ServiceDescription% CopyFiles = NullFilter.DriverFiles [DefaultInstall.Services] AddService = %ServiceName%,,NullFilter.Service [DefaultUninstall] DelFiles = NullFilter.DriverFiles [DefaultUninstall.Services] DelService = %ServiceName%,0x200 ;Ensure service is stopped before deleting [NullFilter.Service] DisplayName = %ServiceName% Description = %ServiceDescription% ServiceBinary = %12%\%DriverName%.sys ;%windir%\system32\drivers\ Dependencies = "FltMgr" ServiceType = 2 ;SERVICE_FILE_SYSTEM_DRIVER StartType = 3 ;SERVICE_DEMAND_START ErrorControl = 1 ;SERVICE_ERROR_NORMAL LoadOrderGroup = "FSFilter Activity Monitor" AddReg = NullFilter.AddRegistry [NullFilter.AddRegistry] HKR,"Instances","DefaultInstance",0x00000000,%DefaultInstance% HKR,"Instances\"%Instance1.Name%,"Altitude",0x00000000,%Instance1.Altitude% HKR,"Instances\"%Instance1.Name%,"Flags",0x00010001,%Instance1.Flags% [NullFilter.DriverFiles] %DriverName%.sys [SourceDisksFiles] nullfilter.sys = 1,, [SourceDisksNames] 1 = %DiskId1%,,, [Strings] Msft = "Microsoft Corporation" ServiceDescription = "NullFilter mini-filter driver" ServiceName = "NullFilter" DriverName = "NullFilter" DiskId1 = "NullFilter Device Installation Disk" ;Instances specific information. DefaultInstance = "Null Instance" Instance1.Name = "Null Instance" Instance1.Altitude = "370020" Instance1.Flags = 0x1 ; Suppress automatic attachments下面对这些部分进行说明:以[]括起来的部分是一个节,inf文件就是由不同的节组成version节规定了程序的版本信息2.1 Class是程序的类,这个值是由驱动具体的功能决定的,这个值其实规定了Altitude的值,也就是驱动在IO栈上的位置。微软提供了不同用途的具体的class值,只需要去对应的网站查询,然后根据具体情况填写即可,点击这里查看2.2 ClassGuid:是上述Class所对应的GUID值,这个值在上面的链接中可以查到DestinationDirs 规定目标文件的路径,就是你希望将驱动文件安装到哪个目录下,填入的12表示是在C:\Windows\System32\Drivers目录DefaultInstall.Services节表示了要将这个驱动以服务的形式启动起来,AddService 表示添加一个服务,后面是服务的名称,标志,以及安装详细信息的节名称,这里指定安装详细信息在节NullFilter.Service中DefaultUninstall 表示卸载的节点,DelFiles值表名卸载的信息在节点NullFilter.DriverFiles中NullFilter.Service节点中规定了安装的详细信息:6.1 DisplayName表示的是服务的显示名称6.2 ServiceDescription 服务的描述信息6.3 ServiceBinary服务程序所在的路径6.4 Dependencies该服务的依赖项,就是这个服务要运行必须提前启动他的依赖项服务,Minfilter的驱动是依赖与"FltMgr"服务6.5 ServiceType 表示服务程序的类型,2 是表示文件系统的服务,这些值可以在MSDN中查到,只需要搜索关于Service的函数 即可,比如CreateService6.6 StartType 启动类型,3表示手动启动,这个信息也可以通过查询MSDN,方法与ServiceType值的查询相同6.7 ErrorControl,错误码,当驱动出错时系统怎么处理,1表示一个常规的处理方式6.8 LoadOrderGroup 加载这个驱动的用户组6.9 AddReg加入注册表信息所在的节点NullFilter.AddRegistry 加入注册表信息的节点,其中的每一项都是一个注册表的项String节中的内容可以看作是一组变量值的定义,当某些字符串过长或者需要反复使用,可以为它定义一个变量,为了以后使用时的方便。在使用时按照%变量名%的形式,最终在解析时会将其进行替换这份文件是一个通用的模板,以后在使用inf进行Minfilter驱动程序的安装时,只需要修改其中的名称,另外需要修改的可能就是服务启动的那部分内容了inf文件编写完成后,只需要点击右键-->安装即可,安装完成后在cmd下使用命令net start 驱动名 即可启动命令,卸载则使用net stop 驱动名用编程的方法动态加载安装驱动驱动安装主要的工作是将驱动加载到服务程序并填写相关注册表项,大致需要这样几步:调用OpenSCManager 打开服务控制管理器的句柄调用CreateService函数,为驱动创建一个服务设置注册表的值,其中打开注册表键使用函数RegCreateKeyEx,设置注册表的值用函数RegSetValueEx。BOOL InstallDriver(const char* lpszDriverName,const char* lpszDriverPath,const char* lpszAltitude) { char szTempStr[MAX_PATH]; HKEY hKey; DWORD dwData; char szDriverImagePath[MAX_PATH]; if( NULL==lpszDriverName || NULL==lpszDriverPath ) { return FALSE; } //得到完整的驱动路径 GetFullPathName(lpszDriverPath, MAX_PATH, szDriverImagePath, NULL); SC_HANDLE hServiceMgr=NULL;// SCM管理器的句柄 SC_HANDLE hService=NULL;// NT驱动程序的服务句柄 //打开服务控制管理器 hServiceMgr = OpenSCManager( NULL, NULL, SC_MANAGER_ALL_ACCESS ); if( hServiceMgr == NULL ) { // OpenSCManager失败 CloseServiceHandle(hServiceMgr); return FALSE; } // OpenSCManager成功 //创建驱动所对应的服务 hService = CreateService( hServiceMgr, lpszDriverName, // 驱动程序的在注册表中的名字 lpszDriverName, // 注册表驱动程序的DisplayName 值 SERVICE_ALL_ACCESS, // 加载驱动程序的访问权限 SERVICE_FILE_SYSTEM_DRIVER, // 表示加载的服务是文件系统驱动程序 SERVICE_DEMAND_START, // 注册表驱动程序的Start 值 SERVICE_ERROR_IGNORE, // 注册表驱动程序的ErrorControl 值 szDriverImagePath, // 注册表驱动程序的ImagePath 值 "FSFilter Activity Monitor",// 注册表驱动程序的Group 值 NULL, "FltMgr", // 注册表驱动程序的DependOnService 值 NULL, NULL); if( hService == NULL ) { if( GetLastError() == ERROR_SERVICE_EXISTS ) { //服务创建失败,是由于服务已经创立过 CloseServiceHandle(hService); // 服务句柄 CloseServiceHandle(hServiceMgr); // SCM句柄 return TRUE; } else { CloseServiceHandle(hService); // 服务句柄 CloseServiceHandle(hServiceMgr); // SCM句柄 return FALSE; } } CloseServiceHandle(hService); // 服务句柄 CloseServiceHandle(hServiceMgr); // SCM句柄 //------------------------------------------------------------------------------------------------------- // SYSTEM\\CurrentControlSet\\Services\\DriverName\\Instances子健下的键值项 //------------------------------------------------------------------------------------------------------- strcpy(szTempStr,"SYSTEM\\CurrentControlSet\\Services\\"); strcat(szTempStr,lpszDriverName); strcat(szTempStr,"\\Instances"); if(RegCreateKeyEx(HKEY_LOCAL_MACHINE,szTempStr,0,"",REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&hKey,(LPDWORD)&dwData)!=ERROR_SUCCESS) { return FALSE; } // 注册表驱动程序的DefaultInstance 值 strcpy(szTempStr,lpszDriverName); strcat(szTempStr," Instance"); if(RegSetValueEx(hKey,"DefaultInstance",0,REG_SZ,(CONST BYTE*)szTempStr,(DWORD)strlen(szTempStr))!=ERROR_SUCCESS) { return FALSE; } RegFlushKey(hKey);//刷新注册表 RegCloseKey(hKey); //------------------------------------------------------------------------------------------------------- // SYSTEM\\CurrentControlSet\\Services\\DriverName\\Instances\\DriverName Instance子健下的键值项 //------------------------------------------------------------------------------------------------------- strcpy(szTempStr,"SYSTEM\\CurrentControlSet\\Services\\"); strcat(szTempStr,lpszDriverName); strcat(szTempStr,"\\Instances\\"); strcat(szTempStr,lpszDriverName); strcat(szTempStr," Instance"); if(RegCreateKeyEx(HKEY_LOCAL_MACHINE,szTempStr,0,"",REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&hKey,(LPDWORD)&dwData)!=ERROR_SUCCESS) { return FALSE; } // 注册表驱动程序的Altitude 值 strcpy(szTempStr,lpszAltitude); if(RegSetValueEx(hKey,"Altitude",0,REG_SZ,(CONST BYTE*)szTempStr,(DWORD)strlen(szTempStr))!=ERROR_SUCCESS) { return FALSE; } // 注册表驱动程序的Flags 值 dwData=0x0; if(RegSetValueEx(hKey,"Flags",0,REG_DWORD,(CONST BYTE*)&dwData,sizeof(DWORD))!=ERROR_SUCCESS) { return FALSE; } RegFlushKey(hKey);//刷新注册表 RegCloseKey(hKey); return TRUE; }这里提供一个安装驱动的函数,在这个函数中将某些信息写入了注册表,注册表中变化的信息如下:HKEY_LOCAL_MACHINE中的SYSTEM\CurrentControlSet\Services\Nullfilter\Instances 中键 DefaultInstance = "nullfilter Instance"HKEY_LOCAL_MACHINE中的SYSTEM\CurrentControlSet\Services\Nullfilter\ nullfilter Instances 中键 altitude = “370020”,键Flags = 0启动驱动驱动的启动与普通的服务程序的启动方法一样,这里直接贴上代码:BOOL StartDriver(const char* lpszDriverName) { SC_HANDLE schManager; SC_HANDLE schService; if(NULL==lpszDriverName) { return FALSE; } schManager=OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if(NULL==schManager) { CloseServiceHandle(schManager); return FALSE; } schService=OpenService(schManager,lpszDriverName,SERVICE_ALL_ACCESS); if(NULL==schService) { CloseServiceHandle(schService); CloseServiceHandle(schManager); return FALSE; } if(!StartService(schService,0,NULL)) { CloseServiceHandle(schService); CloseServiceHandle(schManager); if( GetLastError() == ERROR_SERVICE_ALREADY_RUNNING ) { // 服务已经开启 return TRUE; } return FALSE; } CloseServiceHandle(schService); CloseServiceHandle(schManager); return TRUE; }停止驱动的运行BOOL StopDriver(const char* lpszDriverName) { SC_HANDLE schManager; SC_HANDLE schService; SERVICE_STATUS svcStatus; bool bStopped=false; schManager=OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if(NULL==schManager) { return FALSE; } schService=OpenService(schManager,lpszDriverName,SERVICE_ALL_ACCESS); if(NULL==schService) { CloseServiceHandle(schManager); return FALSE; } if(!ControlService(schService,SERVICE_CONTROL_STOP,&svcStatus) && (svcStatus.dwCurrentState!=SERVICE_STOPPED)) { CloseServiceHandle(schService); CloseServiceHandle(schManager); return FALSE; } CloseServiceHandle(schService); CloseServiceHandle(schManager); return TRUE; }删除驱动BOOL DeleteDriver(const char* lpszDriverName) { SC_HANDLE schManager; SC_HANDLE schService; SERVICE_STATUS svcStatus; schManager=OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if(NULL==schManager) { return FALSE; } schService=OpenService(schManager,lpszDriverName,SERVICE_ALL_ACCESS); if(NULL==schService) { CloseServiceHandle(schManager); return FALSE; } ControlService(schService,SERVICE_CONTROL_STOP,&svcStatus); if(!DeleteService(schService)) { CloseServiceHandle(schService); CloseServiceHandle(schManager); return FALSE; } CloseServiceHandle(schService); CloseServiceHandle(schManager); return TRUE; }Minfilter中如何获取各种信息的值Minfilter中将IRP进行了封装,但是在获取各种值时基本上变化不大,而且相对于之前的Sfilter简单了许多,下面假定在函数中有了它的CALL_BACK_DATA结构,有这样一条语句PFLT_CALLBACK_DATA Data获取当前进程的EPROCESS结构/*如果当前线程的ETHREAD结构不为NULL则根据线程ETHREAD来获取进程否则调用函数PsGetCurrentProcess()获取当前进程的EPROCESS结构*/ PEPROCESS processObject = Data->Thread ? IoThreadToProcess(Data->Thread) : PsGetCurrentProcess();向R3层返回数据Data->IoStatus.Status = ntStatus; Data->IoStatus.Information = 0;与使用IRP相似,在DATA中仍然使用IoStatus成员向R3返回,上述两句的意思与使用IRP时完全相同文件路径的获取文件路径的获取与Sfilter相比,简单了许多,只需要调用一个函数FltGetFileNameInformation,这个函数的定义在MSDN中可以查到,所以就不再这里做过多的说明,该函数会返回一个FLT_FILE_NAME_INFORMATION结构体用来保存文件名信息,结构体FLT_FILE_NAME_INFORMATION的定义如下所示typedef struct _FLT_FILE_NAME_INFORMATION { USHORT Size; FLT_FILE_NAME_PARSED_FLAGS NamesParsed; FLT_FILE_NAME_OPTIONS Format; UNICODE_STRING Name; UNICODE_STRING Volume; UNICODE_STRING Share; UNICODE_STRING Extension; UNICODE_STRING Stream; UNICODE_STRING FinalComponent; UNICODE_STRING ParentDir; } FLT_FILE_NAME_INFORMATION, *PFLT_FILE_NAME_INFORMATION;这个结构体记录了文件路径的各种信息,其中各个字符串都是根据Format的值来进行格式化得到的,需要注意的是在上述位置得到的文件名是类似与“\Device\HarddiskVolume1\Documents and Settings\MyUser\My Documents\Test Results.txt:stream1”的而不是我们之前熟悉的"C:\Documents and Settings\MyUser\My Documents\Test Results.txt:stream1",这就需要进行转化。需要注意的是在获取文件名时要在PostCreate函数中获取,因为当调用这个函数时说明根据传进来的文件路径已经正确打开了文件,这个时候的路径名一定是可靠的。 NTSTATUS ntStatus; PFLT_FILE_NAME_INFORMATION pNameInfo = NULL; UNREFERENCED_PARAMETER( Data ); UNREFERENCED_PARAMETER( FltObjects ); UNREFERENCED_PARAMETER( CompletionContext ); UNREFERENCED_PARAMETER( Flags ); //获取文件路径信息 ntStatus = FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED| FLT_FILE_NAME_QUERY_DEFAULT, &pNameInfo); if (NT_SUCCESS(ntStatus)) { //解析获取到的信息 FltParseFileNameInformation(pNameInfo); DbgPrint("FileName:%wZ\n", &pNameInfo->Name); //使用完成后释放这个空间 FltReleaseFileNameInformation(pNameInfo); }函数FltGetFileNameInformation会自动为我们分配一个缓冲区来存储文件的信息,所以最后需要调用Release函数释放。另外通过调试的情况来看,FltGetFileNameInformation函数并不能获取文件路径的所有信息,一些扩展信息像Extension或者Parent这样的信息开始是没有的,只有通过FltParseFileNameInformation在已有信息的基础之上进行解析才会有。至于重命名的文件路径的获取使用下面的语句:PFILE_RENAME_INFORMATION pFileRenameInfomation = (PFILE_RENAME_INFORMATION)Data->Iopb->Parameters.SetFileInformation.InfoBuffer;在MInfilter中进行文件操作尽量使用以Flt开头的函数,不要使用Zw开头的那一组,以Zw开头的函数最终会触发Minfilter的回调函数,最终会造成无限递归Minfilter上下文在编程中,我们经常会遇到上下文这个概念,比如说进程上下文,线程上下文等等,在这里上下文表示某些代码执行的具体环境,系统一般位于进程上下文和中断上下文。当系统处理进程上下文时,系统在代替R3层做某些事,此时系统在调用系统API,执行系统功能,当系统处于进程上下文时是可以被挂起的。而当系统响应中断与具体硬件进行交互时处于中断上下文,此时的数据都位于非分页内存,而且不能睡眠而Minfilter上下文指的并不是代码运行的环境,而是一组数据,这组数据是附加到具体的设备对象上的,由用户自己定义。在使用时先利用函数AllocateContext分配一段内存空间,然后使用一组Set和Get函数来设置和获取设备上下文。具体的函数由不同的上下文来决定,下面是不同的上下文与他们对应的Set函数之间的关系Context TypeSet-Context RoutineFLT_FILE_CONTEXTWindows Vista and later only.) FltSetFileContextFLT_INSTANCE_CONTEXTFltSetInstanceContextFLT_STREAM_CONTEXTFltSetStreamContextFLT_STREAMHANDLE_CONTEXTFltSetStreamHandleContextFLT_TRANSACTION_CONTEXT(Windows Vista and later only.) FltSetTransactionContextFLT_VOLUME_CONTEXTFltSetVolumeContextGet函数的使用与上述相同。最后使用完成后需要使用FltReleaseContext来释放这个上下文。下面是一个使用的具体例子typedef struct _INSTANCE_CONTEXT { … } INSTANCE_CONTEXT, *PINSTANCE_CONTEXT; PINSTANCE_CONTEXT pContext = NULL; //从设备对象上获取上下文 ntStatus = FltGetInstanceContext(FltObjects->Instance, & pContext); if(NT_SUCCESS(Status) == FALSE) { //设备上没有上下文则分配上下文的内存 ntStatus = FltAllocateContext(g_pFilter,FLT_INSTANCE_CONTEXT, sizeof(INSTANCE_CONTEXT), PagedPool,& pContext); if(NT_SUCCESS(Status) == FALSE) { //返回资源不足 return STATUS_INSUFFICIENT_RESOURCES; } RtlZeroMemory(pContext, sizeof(INSTANCE_CONTEXT)); } pContext ->m_DeviceType = VolumeDeviceType; pContext->m_FSType = VolumeFilesystemType; FltSetInstanceContext(FltObjects->Instance, FLT_SET_CONTEXT_REPLACE_IF_EXISTS,pContext,NULL); if (pContext) { FltReleaseContext(pContext); } //获取访问 PINSTANCE_CONTEXT pContext = NULL; Status = FltGetInstanceContext(FltObjects->Instance,&pContext); //下面是使用这个上下文,和一些其他的操作 pContext->xxx = xxx;回调函数运行的中断请求级别(IRQL)pre回调函数可以运行在APC_LEVEL或者PASSIVE_LEVEL 级别,但是一般是运行在PASSIVE_LEVEL级别如果一个Pre函数返回的是FLT_PREOP_SYNCHRONIZE,那么相对的,在同一个线程内部,它对应的POST函数将在IRQL <= APC_LEVEL级别运行。与对应的Pre在一个线程内时,fast IO请求对应的Post函数运行在PASSIVE_LEVEL级别Create回调的Post函数运行在PASSIVE_LEVEL级别当我们不确定当前代码所处的IRQL时可以使用函数KeCurrentIrql获取当前执行环境所在的IRQL。R3 与R0的通信R3在调用Minfilter中的相关函数时需要包含相关的库文件,与Lib文件,具体怎么包含这个我不太清楚,只需要在库项目属性的VC++目录下面这几项包含具体的路径即可包含目录 :“$(VC\_IncludePath);$(WindowsSDK_IncludePath);”库路径: "$(VC\_LibraryPath_x86);$(WindowsSDK\_LibraryPath\_x86);$(NETFXKitsDir)Lib\um\x86"之前R3与R0进行通信是通过R3调用DeviceIoControl函数将数据下发到R0层,然后R3等待一个事件,在R0处理完成R3的请求并准备好往上发的数据后将事件的对象设置为有信号,这样R3再从缓冲区中取数据,完成双方的通信,但是在Minfilter中准备了专门的函数用于双方通信。R3向R0下发数据R3层通过函数FilterSendMessage向R0下发数据,而R0通过fnMessageFromClient接收从R3发下来的数据。在Minfilter中R3与R0是通过各自的端口进行通讯的,这个端口与传统意义上的网络的通信端口不同,是一个抽象的概念,不需要太过于关注。在与R3进行通讯之前需要设置这个端口,端口的设置使用函数FltCreateComunicationPort,在这个函数调用时需要提供这样几个回调函数ConnectNotifyCallback:这个函数在R3层链接到R0的这个通讯端口时调用,在这个函数中可以拿到R3的进程句柄和R3的端口DisconnectNotifyCallback:当R3层与R0层断开时调用这个回调函数MessageNotifyCallback:当R3有数据下发下来调用这个回调,在这个函数中取R3发下来的数据R0向R3上报数据R0向R3上报数据时是一个双向的过程,R0既要上报数据,又要等待R3的返回,就好像之前的弹窗一样,当R0上报数据后就等待40s,在接收到R3的进一步指示时进行下一步,R0向R3上报大致经历这样的几个过程:R0通过函数FltSengMesage将数据发送到R3R3通过函数FilterGetMessage函数接收到R0的数据,并进行相应的处理处理完成后,调用函数FilterReplyMessage将处理结果返回给R0。另外需要注意一点,在进行通讯时需要两套数据结构,这两套分别运用在R3和R0两层,每一层都有两个数据结构,用来表示接收和返回的数据,拿R3来说,它需要一个MESSAGE结构体来接收从R0层发过来的数据,另外需要一个REPLY用于向R0返回数据。R3上报和下发的数据相比于R0需要多加一个FILTER_MESSAGE_HEADER的头,便于Minfilter分辨是哪个客户端下发的数据,Minfilter 根据这个头做相关的处理后将其于的数据发送到R0,所以R0能够正确知道是哪个进程在进行相关请求,而不需要添加额外的结构体下面是一个例子NTSTATUS ScannerpScanFileInUserMode ( __in PFLT_INSTANCE Instance, __in PFILE_OBJECT FileObject, __out PBOOLEAN SafeToOpen ) /*读取文件中的部分数据,然后交由R3处理,并根据R3返回的结果判断R3是否安全*/ { NTSTATUS status = STATUS_SUCCESS; PVOID buffer = NULL; ULONG bytesRead; PSCANNER_NOTIFICATION notification = NULL; FLT_VOLUME_PROPERTIES volumeProps; LARGE_INTEGER offset; ULONG replyLength, length; PFLT_VOLUME volume = NULL; *SafeToOpen = TRUE; if (ScannerData.ClientPort == NULL) { return STATUS_SUCCESS; } try { status = FltGetVolumeFromInstance( Instance, &volume ); if (!NT_SUCCESS( status )) { leave; } status = FltGetVolumeProperties( volume, &volumeProps, sizeof( volumeProps ), &length ); if (NT_ERROR( status )) { leave; } //取1024和扇区的最大值,保证读取的数据至少有一个扇区的大小 length = max( SCANNER_READ_BUFFER_SIZE, volumeProps.SectorSize ); /*分配一块内存用于从文件中读取数据,在minfilter中进行文件操作时申请缓冲区最好使用flt开头的一组函数,因为它不是简单的分配一块内存,还能保证在有程序使用这段内存时不会出现内存已被释放的情况,内部可能也用了引用计数*/ buffer = FltAllocatePoolAlignedWithTag( Instance, NonPagedPool, length, 'nacS' ); if (NULL == buffer) { status = STATUS_INSUFFICIENT_RESOURCES; leave; } //构建发送给R3层的结构体的内存 notification = ExAllocatePoolWithTag( NonPagedPool, sizeof( SCANNER_NOTIFICATION ), 'nacS' ); if(NULL == notification) { status = STATUS_INSUFFICIENT_RESOURCES; leave; } offset.QuadPart = bytesRead = 0; status = FltReadFile( Instance, FileObject, &offset, length, buffer, FLTFL_IO_OPERATION_NON_CACHED | FLTFL_IO_OPERATION_DO_NOT_UPDATE_BYTE_OFFSET, &bytesRead, NULL, NULL ); if (NT_SUCCESS( status ) && (0 != bytesRead)) { notification->BytesToScan = (ULONG) bytesRead; /*将这段信息发送到R3,这个函数是一个阻塞的函数,只有当超时值过了或者R3返回了数据才会返回,在这设置超时值为NULL表示会一直等待,在这返回值也是使用notification做为接受返回值的缓冲,在这不会出现覆盖的情况,因为这个函数在调用后首先是R3接受数据,然后进行处理,处理完成后R3才会主动调用另一个API返回数据,所以这里有一个时间差,当获取到返回值时之前传入的数据已经没有用了,这里将发送的缓冲与返回的缓冲定义为同一个只是为了节省内存*/ RtlCopyMemory( ¬ification->Contents, buffer, min( notification->BytesToScan, SCANNER_READ_BUFFER_SIZE ) ); replyLength = sizeof( SCANNER_REPLY ); //在这我们将 status = FltSendMessage( ScannerData.Filter, &ScannerData.ClientPort, notification, sizeof(SCANNER_NOTIFICATION), notification, &replyLength, NULL ); if (STATUS_SUCCESS == status) { *SafeToOpen = ((PSCANNER_REPLY) notification)->SafeToOpen; } else { DbgPrint( "!!! scanner.sys --- couldn't send message to user-mode to scan file, status 0x%X\n", status ); } } } finally { //最后清理内存 if (NULL != buffer) { FltFreePoolAlignedWithTag( Instance, buffer, 'nacS' ); } if (NULL != notification) { ExFreePoolWithTag( notification, 'nacS' ); } if (NULL != volume) { FltObjectDereference( volume ); } } return status; }这个例子演示了R0层如何从磁盘中读取文件内容,然后发送到R3,并从R3层上接受返回。下面这个例子将演示R3如何读取R0发上来的数据 while (TRUE) { //这是一个阻塞函数,当能从R0接受到数据的时候返回 result = GetQueuedCompletionStatus( Context->Completion, &outSize, &key, &pOvlp, INFINITE ); message = CONTAINING_RECORD( pOvlp, SCANNER_MESSAGE, Ovlp ); if (!result) { hr = HRESULT_FROM_WIN32( GetLastError() ); break; } printf( "Received message, size %d\n", pOvlp->InternalHigh ); notification = &message->Notification; assert(notification->BytesToScan <= SCANNER_READ_BUFFER_SIZE); /*R3层定义的这个SCANNER_MESSAGE结构体是在notification这个结构体的基础之上加了一个头,所以大小会大于这个notification*/ __analysis_assume(notification->BytesToScan <= SCANNER_READ_BUFFER_SIZE); //这个函数是一个自定义函数,用来扫描R0传上来的数据有没有特定的字符 result = ScanBuffer( notification->Contents, notification->BytesToScan ); replyMessage.ReplyHeader.Status = 0; replyMessage.ReplyHeader.MessageId = message->MessageHeader.MessageId; replyMessage.Reply.SafeToOpen = !result; printf( "Replying message, SafeToOpen: %d\n", replyMessage.Reply.SafeToOpen ); //向R0返回数据 hr = FilterReplyMessage( Context->Port, (PFILTER_REPLY_HEADER) &replyMessage, sizeof( replyMessage ) ); if (SUCCEEDED( hr )) { printf( "Replied message\n" ); } else { printf( "Scanner: Error replying message. Error = 0x%X\n", hr ); break; } memset( &message->Ovlp, 0, sizeof( OVERLAPPED ) ); //再次等待从R0下发的数据 hr = FilterGetMessage( Context->Port, &message->MessageHeader, FIELD_OFFSET( SCANNER_MESSAGE, Ovlp ), &message->Ovlp ); if (hr != HRESULT_FROM_WIN32( ERROR_IO_PENDING )) { break; } }
2016年10月23日
4 阅读
0 评论
0 点赞
2016-10-23
我的大学回忆录
我是今年刚从学校毕业的一名软件工程专业的学生,过去的美好时光一直在眼前浮现,我感觉自己的大学生活很美好,生活上有好室友好同学,在以后的规划上又有一位好的人生导师,虽然现在他们在全国各地,有可能这辈子再也见不到他们,但是过去跟他们相处的很愉快,就像一句话说的:不在乎天长地久,只在乎曾今拥有((∩_∩)这句话好像用在这有点不合适,但是我感觉最符合我现在的感觉吧,原谅我读书少)。之前一直想找个机会把在大学的时光记录一下,以便自己以后有个东西好回忆,然后给以后的朋友一个参考吧,但是由于一直在学习或者在忙其他事(其实就是懒)所以拖到现在,不过现在也好,工作了一段时间,我现在又有的新的感悟,现在就把自己的学习经历和现在的感悟一起发出来。同时也把自己当初在学习的时候看的书提一下,算是给后面入行的朋友指一条路。大一——懵懵懂懂在高考那年有点发挥失常,没有考到理想的分数,然后被一所2本学校的软件工程专业录取了,从此开始了4年的大学生活,其实开始的时候我并没有打算学软件工程,当时听家里人的建议主要是报的土木工程,但是好像我这个学校那年新开了一个软件工程专业,然后就这样被录取了。那个时候刚来大学的时候对一切都很好奇,早早的将学校逛了个遍,然后就是新生的军训,入学教育,头一个月就这样平平淡淡的过去了。那个时候我对计算机和相关的技术是没有什么概念的,之前接触计算机是在中学的信息技术课,那个时候的信息技术课对于我们而言就是能上网。所以在学校我对这个专业要学的东西一无所知。我记得那个时候学校开了一门叫做计算机导论的课,将计算机中涉及到的基本上简单介绍了一遍,但是我感觉基本上没什么用,那个时候讲的很多名词基本上都不记得。只知道当初坐我旁边的一个跟我侃各种硬件软件把我听的一愣一愣的。那个时候为学校也有许多社团,但是我一没特长二不知道自己的兴趣在哪,所以当初就报了一个跟专业相关的计算机协会,在这里面我差不多学会了装机以及安装系统,那个时候就社团组织了几次技术讲座,由高年级的学长讲课,然后有几次跟随社团成员一块在社区免费维修电脑,然后在里面被参加了一个装机比赛,苦练了一段时间,拿到了一个预选赛的第二,结果在决赛的时候掉链子了。那个时候还有一门课是C语言,这个是我第一次接触编程语言,那个时候痛苦并快乐着,辛辛苦苦改语法错误(那个时候程序都比较短,没有什么逻辑错误),然后看着它正确的结果,心里的成就感油然而生啊,但是后面接了网线之后一切都变了,为了腾出时间上网,作业没有以前那么认真,而且空余时间也不写程序了,所以说有的学校规定大一不允许带电脑是有道理的,这个时候是打基础的时候,有电脑有网,都没心情学习,基础没打好后面的几年就玩完了。我基本上就是处于这种情况,这个在我大二的时候体现的最为明显。下半年就开了数据结构这门课,很多学生都吐槽数据结构有多难,经过我的真实经历,我发现它是真的难,那个时候指针都没搞明白,而数据结构的算法基本上都是与指针有关,所以那个时候基本上就是想听也听不懂,给的例子代码基本上是一脸懵逼,那个时候只是知道一些基本的概念,大概知道各种数据结构的存储方式,但是算法是写不出来的。然后就这样结束了大一的时光。另外在插一句,网上有段子说亲戚朋友总以为学计算机的就要会修电脑,其实我特别理解这种误解,因为我开始也以为学计算机的基本技能就是要会修电脑,所以那个时候我对一些修电脑的技术特别上心,总是去图书馆借关于维修方面的书,我记得当初看过一本讲怎么挑选硬件,怎么组装那些硬件的书,然后又看了一本关于windows的安装与使用技巧的书,现在想想那个时候真的是什么都不懂,有劲使错了方向。大二——事倍功半我的大二基本上是白费的一年,到大二快结束的时候基本上几门主流语言都陆陆续续学了,那个时候基本的流程是上半年学习C++,下半年学习JAVA,那个时候第一次接触了面向对象,知道了面向对象的语言,那个时候在我的心里以后要找工作基本上只有C++和JAVA,所以需要在这两门语言中选一门学习,当下半年开了JAVA课,学了一段时间的Java后我果断选择了C++,那个时候主要的考虑有3点,第一点是我很不习惯Java那种大括号的格式,每次碰到那种格式,我总是习惯把它放下来,另起一行;第二点是当时我感觉Java很多东西都是用别人的库,感觉没有那种自由,没有太多的发挥空间,而我自己不太想记那些类和类中的方法,那个时候我一直认为C++很酷,要什么都可以自己写,现在想想自己当时真的很幼稚,如果什么都自己写,那现在开发一个应用程序也太慢了;第三点是java强迫你用面向对象,而且那个时候对比C++的hello world 和Java的hello world 我总感觉没有C++的简洁;第四点就是我当时对Visual C++环境太熟悉了,调试、断点、查看临时变量啥的都会,面对开始用命令行编译后来放到我还不会调试的Ecilpse上面我是拒绝的,那段时间主要也是自己懒,不想学新的环境;所以我决定自己以后要往C++上面发展。路选好了,剩下的自然就是学了,那个时候我们还开了一门数据库的课,当时做课程设计的时候需要写一个界面,然后那个时候我从百度上知道了MFC这个东西,那个时候自己比较抠,但是也花钱买了一本孙鑫老师的《VC++深入详解》那个时候就拖了一个对话框然后在网上找了一些代码完成了,而在下半年决定学C++后我下决心要啃完这本书,那个时候书上讲的MFC各个资源的使用我完全蒙了,这个时候我才发现我好多基本的语法都没过关,这个时候我抱着打基础的目的,在许多论坛上得到了很多免费的视频,很多时候我看了一点感觉我好像会了,接着去学一些高级的技术,但是基本上是又卡住了,这个时候再折回头看其他视频的基础部分,那个时候就在这循环,我现在想想那个时候的自己,感觉有点不可思议,为什么不能耐下心来把一套视频完整的看完,而好多看了一半又去找其他的视频,而我的大二就是在这样的一个死循环上面突破不了,那个时候虽说表面上我是在学习,但是我感觉自己并没有学到什么东西,时间都浪费了,而自己也越来越迷茫了,到底该不该走这条路,自己是不是不适合学编程。现在我才体会到网络有时候是个好东西,利用网络确实能给我们学习带来很多东西,但是网络上的学习资料太多,而自己方向不明确,有的时候东西太多会把自己带跑偏了。有的时候社会上都说大学生能力差,大学生在学校怎么堕落,但是我现在感觉到很多时候并不是我们不愿意学,而是缺少高中时候的那种引导,高中的时候一门心思想考大学,考大学需要什么就学什么,而且有老师专门来为你制定学习计划,帮你检查学习成功,这个反馈机制比较及时,这个时候你会感觉到自己距离目标有多少差距,路是不是正确的,会根据这个反馈及时调整,而到大学完全不一样,首先没有了目标,大学即使分了专业,那个专业的就业范围也比较广,而且很多人还从事与所学专业无关的事,没有了目标就不知道往哪个方向努力;另外一点就是即使有了方向,也没有及时的反馈给你,告诉你达成这个目标需要哪些技术,你已经掌握了哪些,还需要哪些,掌握的是不是掌握的很好,需不需要再加强,那个时候我感觉我自己就是不知道学哪些东西,我把关于C++的东西几乎都找到了,每个都想学,但是结果就是什么都没学好,另外就是自己本来基础不好,但是我总感觉知道了那些什么封装,继承,多态我好像就把C++的基础都学会了。没了目标,没有与目标相关的反馈机制,这样再努力也是白搭,这个是我真实的体会。最近我跟一些同学聚会聊天的时候谈论到大学的时候,我跟他们说我的大二是废掉的一年,他们都笑笑说:“你是学霸,经常看到你在那看视频学习,而我们在打游戏,如果你的大二是废掉的,那我们的算什么”,我很认真的跟他们说,那个时候我虽然在看视频,但是真真没有学到多少东西,其实也跟你们打游戏差不多。这个不是谦虚,这个是真实的状态。大三——突飞猛进总结我的整个大学生活,我感觉大三是最有意义的,最累的也是这个大三。在大三我有了志同道合的朋友,有了很好的老师引导,成长当然也是最快的。在大三,学校为了就业率好看,基本上引进了三个培训班,一个是学校老师自己带的C++班,一个是外面的Java,还有一个是安卓的(后来的同学都说那个安卓的是个坑,这是后话)。一般到了大三学生不是报了考研辅导班就是报了这种培训班。当初这个班学费是5500,这个时候我遇到了我的一位人生导师——我们培训班的老师,我们称他老吴,老吴是我们学校关于C/C++最权威的人,我在大二的时候知道有这么一个班,这个班是面向大二大三招生的,那个时候据说只收基础好的学生,我当时怂了一波没有去报名,我有的时候在想,如果当时我在大二的时候报名学下C++然后大三学学操作系统,网络,并结合他们写点程序啥的是不是现在就走向人生巅峰了,但是没有如果。整个大三加上大四的一直到11月份,我都是在老吴的手下学习,那个时候他带着我们从0开始,每周的周一到周五晚上两个小时,周六一天。那个时候基本上为了完成作业天天晚上写代码写到12点以后,而且那个时候我开了这个博客,有的时候也在更新自己的博客,所以基本上学到很晚,但是这种感觉很棒,感觉自己每天都在进步,每天都掌握了新的内容,从基本的语句到函数到指针,再到复杂的函数实现原理,变量的作用域,函数指针,指针函数,数组指针,指针数据,函数传参,不定参数等等,那个时候我把之前自己理解不了的,或者比较模糊的东西基本上都弄清楚了,我感觉自己现在的状态就想某些修真小说里面说的,吐出一口浊气,然后整个眼睛都是雪亮的,没有一丝阴影。在上半年我把整个C的语法部分都学完了,那年的寒假特别长,那个时候有两个月的寒假,所以放假前老吴提示我们回去好好看看汇编,学习汇编对于掌握C语言很有帮助,并且说元旦来了会给汇编的资料,而那个时候我归家心切,找我的另一位人生导师——在这我就称他为林同学,我找林同学要了本关于汇编的书——王爽老师的《汇编语言》,我不知道当初放假前老吴会给什么资料,他会要求我们学到什么程度,但是这个寒假我乖乖把那本书从头到尾看了一遍,把书上的每一个例子都敲了一遍,然后一个个的进行调试,有的时候我经常想如果当初我把老吴的资料拿回去,根据他的要求学(我总是这样,很多时候一遍老师说不需要特别关心的地方我都不会管,不知道这个是不是一个好习惯),会不会有现在的基础(抱歉我总是喜欢假设如果我不这么做会怎样,可能这个就像有的星座的文章上面讲的,双鱼座天生就拿不定主意喜欢胡思乱想吧,有的时候我能脑补好久)。关于这本书对我的影响下面呢还会再说。下半年主要是学习windows编程,从基本的win32窗口程序开始,一步步学习怎么写带主窗口的程序,消息循环,窗口类,窗口,窗口类的回调函数,GDI,基于对话框的程序等等关于界面方面的东西,并带着我们实现了一个仿照MFC的带有消息映射宏的一个纯C的界面库,这个库没有什么特别的牛的功能,就是简单的把窗口的创建,显式以及消息处理函数用宏的方式写了一个映射,这些东西基本上把我之前不了解的MFC的消息映射基本上搞清楚了。接着就是windows上提供的一些与操作系统相关的操作,比如线程,进程的创建,HOOK,DLL,数据库编程,socket编程以及windows上的5大网络模型,通过这些东西我基本上把之前学的操作系统,网络原理,数据库等东西又复习的一遍。之前我一直不理解这些计算机的理论是怎么运用到实践中,通过这些学习,我知道了这些理论是如何被写成接口并给程序调用。暑假的时候我们是没有回家而是接着在培训班里面学习,这个阶段主要是学习C++一些语法,之前在学习C的时候老吴带着我们用C写了一个通用链表,这个链表用统一的代码来实现链表的基本算法,比如遍历,插入,删除,查找,等等,但是对于具体节点的读写操作而是提供了一组统一的函数接口,这些读写函数由具体的数据结构提供者来提供,节点的头四个字节是这个函数数组的首地址,通过强制转化的方式来组成链表,而在需要时通过这头四个字节来调用读写函数,当时他跟我们说的是理解了这些,C++就没有什么问题,当时没太注意,在学习C++的时候我才体会到当时老吴的用心良苦,确实当初写的这些就是模拟C++的虚函数表,而那头四个字节就是C++里边的虚函数指针啊,通过之前的铺垫,我很容易就理解了C++中的多态。另外根据这些特性结合当时学的Windows编程,老吴又带着我们写了一个模仿MFC的库,这次用C++实现,基本上实现了MFC中几大要素:动态类型识别,消息映射等等。至此对于MFC的认识又进了一步。大四——离别前夕大四按照学校的惯例,差不多在每年的11月份就可以离校了,离校前似乎其他同学的心情都挺愉快的,培训班的课基本上都结束了,现在我跟同学差不多就是见一次少一次了,我当时的心情有些沉重,想想一起相处四年的兄弟们就要分离了,有的可能一辈子再也见不到了,总有一丝伤感,心里总是不愿意分离,这段时间总是胡思乱想,不知道以后找工作是否顺利,那些我前面出去的同学不知道怎么样了,总之那段时间我基本上放弃了学习,有时间就找还在学校的同学打打游戏,聊聊天,一起吃个饭啥的,我是本着能多说一句话就多说一句,能多在一起待会就多待会。晚上躺在床上我总是回忆起当初在一起的点点滴滴,思考未来会怎么样,总之那段时间我内心是复杂的,既担心以后该怎么混,也不想就这样离开,那段时间我把回家的时间往后一推再推,但不管怎么往后推,离别的时刻总会来临。说说我当初看过一些书下面我把我之前看过的书从头到尾梳理一遍,毕竟读书也是大学生活的一部分。其实我挺喜欢读书的,平时没有别的爱好,也没啥特长,从小学开始我基本上是靠着书打发时间,从小学的漫画,到中学时候的小说,再到大学时候在图书馆借各种各样的书。《黑客与画家》我非常喜欢这本书,它告诉了我程序员的伟大之处,既然以后是信息化时代,那么做程序员的前景应该还不错。那本书里面将程序员和画家进行类比,里面说好的程序是艺术品而好的程序员是艺术家,我感觉说的挺有道理,所以现在我自认为是一个手工艺人(虽说自己做出来的就是粗制滥造的东西),有一次我的一位朋友跟我说:“你的手又细又长,跟女孩子的手一样”,我跟他开玩笑说,我们程序员是靠手吃饭的是手艺人,手不好看能行吗。后来我发现我的同学或者同事基本上都跟我差不多,都有一双不错的手,不知道是不是真的证明我们程序员是靠手吃饭的(手动滑稽)《疯狂的程序员》这本书主要记录了一个程序员从大学到工作的日子,算是作者的回忆录,之前我百度过这本书的作者,好像说是因为写DNF的外挂被抓了,也不知道是不是真的。我非常喜欢这本书,当时我看的是电子版,后来毕业之后我在淘宝上找到了纸质版,并把它买了下来。书中的主人公绝影是一个对技术比较狂热的程序员,经常为了写程序熬夜,里面没有多好深奥的技术,只有一些简单朴实的话语,同时包含了作者对现实世界的理解,我觉得他里面写的关于资本家对员工的压迫和当前相亲时男方看中女生的相貌,女生着重关注男方的资产这块的分析很有意思,在一定程度上代表了作者对这个世界的思考。但是真正让我着迷的还是里面经常出现的,为了一个技术难题,绝影和BOOS Liu两个通宵加班的场景,每次看到这部分我总是热血沸腾,恨不得给还躺在床上的自己一巴掌(滑稽脸),或许是这本书奠定了我现在的想法——要做就做那些难度大的,没人愿意做的;所以现在我基本上是一条路走到黑,坚持自己的底层之路。另外这本书的副标题很有意思叫程序员版的奋斗,里面确实讲述了一个程序员的奋斗历史。《汇编语言》这本书和下面我要说的一本书对我影响最大,是我为数不多的认真看了两遍以上的技术书籍,这本书最大的价值是前面讲的内存、寄存器、CPU的相关知识,以及后面的寻址方式,函数调用等等,而至于中断宏汇编什么的就不那么重要了,前面的都掌握了之后,可以看看后面的几个深入讨论的部分,那部分是关于C与汇编的,我觉得那些是这本书的重点,当初在看这本书的时候是下足了功夫,我把书上的每个例子都敲进电脑,编译运行,调试。给我影响最深的是3个程序,一个是关于读写一个保存了学生数据的结构体数组的,这个程序用一个寄存器保存数组的首地址,每个数组成员的地址用另外一个寄存器,另外还需要计算每个成员在结构体中的偏移地址,这个例子解决了,我基本上对C中的数组和结构体的寻址有了很深的理解。另外一个是关于函数调用的,当时我写的这个程序最大的问题是少出栈了一个寄存器,结果导致在调用ret时候返回到了错误的地址,结果程序崩溃了,通过这个例子,我终于理解到了程序中引入ebp寄存器的作用,不管你是不是出栈错了,只要运行mov esp, ebp后再直接返回总能返回正确的地址,总之汇编是学习C语言迈步过去的一道坎《VC++反汇编与逆向技术解密》这本是钱松林老师的一本书,在看雪论坛上还有专门的板块讨论它,我当时跟朋友聊天的时候戏称为小黄书,这本小黄书通过汇编的方式详细介绍了VC++语言各个特性具体的实现细节,通过这本书可以很好的理解C/C++,这本书配合之前说的汇编可以很好的理解C/C++。如果不想从事逆向分析,那主要看从第二章到最后的结构化异常处理就好(我是只看了这些)至于后面的例子,不从事这方面可以不用管它其他好书后面要说的书有不少,而且很多我只是简单看了一遍,理解并不深厚,不向上面的那些,有自己的理解,或者夹杂了一些自己有趣的回忆,所以就简单的说说。《windows程序设计》与《win32汇编语言程序设计》我觉得这两本书很详细的讲解了windows应用层的开发,但是需要互相补充,Win32汇编这本书更偏向于底层,比如后面讲的SEH,和PE文件结构,另外这本书前面讲的32位的保护模式,函数的调用约定都很经典,可以看看,然后结合汇编码分析一下,可以更好的理解windows上的开发。我在读Win32汇编这本书的时候就觉得微软的宏汇编真的做的很出色,在使用时就好像是在用C语言,特别是在函数调用这块,既不需要我们考虑参数的压栈和出栈,也不需要进行栈平衡,另外如果你的汇编基础比较好可以考虑将这本书里面的汇编代码改写为C代码。《windows核心编程》这本书是进行windows高级开发必读的经典,这个没什么好说的《windows网络编程》我之前一直以为这本书是老外写得,最后发现原来是国人写的,搞的我之前以为自己买的是盗版,这本书详细介绍了网络方面的编程,其实不光是网络,比如串口通信,管道通信。《现代操作系统》这本书可以结合之前的那本核心编程一起看,主要看看操作系统提供的功能在windows上怎么样实现的,以达到理论结合实际的效果。对于理解这两本书都有好处《VC++深入详解》之前提到过的书,我之前看不懂这本书,并不是书的问题,而是我自己基础的问题,MFC库文件微软都给了,结合之前说的《windows程序设计》关于界面的知识,这本书就不再是问题。《TCP/IP详解》这个主要讲的网络原理的,一般是有两本,卷一和卷二,这个结合之前的windows网络编程一起,主要也是达到理论结合实践的目的。还有一些内核的书比如《寒江独钓 Windows 安全编程》,这个是我目前在研究的书,我自己觉得还不错,可以看看。至于Linux方面的,我没有怎么接触,只能推荐一些大家都觉得好的比如《鸟哥的Linux私房菜》和《Unix核心编程》。写在最后的话其实总结自己的大学的生活,我还是觉得大三过的是最充实的。我得出一个结论人真的需要一个目标,而有了目标之后,有一个引路人也是十分重要的,而我自己的引路人一个是上面说的老吴,一个是林同学。我觉得老吴可能并没有交给我许多知识,而且也没有讲完当初传单上面的知识,很多由于时间关系都略过了,但是我仍然觉得值,主要是因为他教给了我学习的方法,给我指出了一个C++程序员应该走的路。老话说的好:“师傅领进门,修行在个人”,确实是这样啊!!!
2016年10月23日
4 阅读
0 评论
0 点赞
2016-10-07
驱动开发入门——NTModel
上一篇博文中主要说明了驱动开发中基本的数据类型,认识这些数据类型算是驱动开发中的入门吧,这次主要说明驱动开发中最基本的模型——NTModel。介绍这个模型首先要了解R3层是如何通过应用层API进入到内核,内核又是如何将信息返回给R3,另外会介绍R3是如何直接向R0层下命令。API调用的基本流程一般在某些平台上进行程序开发,都需要使用系统提供的统一接口,linux平台直接提供系统调用,而windows上提供API,这两个并不是同一个概念(之前我一直分不清楚),虽然它们都是系统提供的实现某种功能的接口,但是它们有着本质的区别,系统调用在调用时会陷入到内核态,而API则不是这样,例如对于CreateFile这个我们不能说它是一个系统调用,在这个函数中并没有立即陷入到内核态,而是先进行参数检查,然后通过其他的一系列操作之后调用系统调用而进入到内核,所以它并不是系统调用。windows在应用层提供了3个重要的动态库,分别是kernel.dll uer32.dll gdi.dll (现在基本上将所有的API都封装到kernell.dll中)当用户程序调用一个API函数时,在这个API内部会调用用封装到ntdll.dll中以Zw或者Nt开头的同名函数,这些函数主要负责从用户态切换到内核态,这些函数叫做Native API,Native API进入到内核的方式是产生一个中断(XP及以前的版本)和调用sysenter(XP以上的版本),Native API在进入到内核中时会带上一个服务号,系统根据这个服务号在SSDT表中查找到相关的服务函数,最后调用这些服务函数完成相关功能,这个过程可以用下面的图来说明:下面以CreateFile为例说明具体的调用过程:应用层调用CreateFile函数这个函数实际上被封装到了kernel32.dll中,在这个函数中调用NtCreateFile,这就是调用ntdll.dll中的native api ,ntdll.dll中一般又两组函数——以Nt开头,以Zw开头的,这两组函数本身没有什么太大的区别。native api中通过中断 int 2eh(windows 2000 及以下),或者通过sysenter指令(windows xp及以上)进入内核,这种方式称为软中断,在产生中断时会带上一个服务号,根据服务号在ssdt表中以服务号进行查找(类似与8086中的中断机制)根据SSDT表中记录的服务函数地址,调用相关的服务函数。然后进入到执行组件中,对于CreateFile的操作,这个时候会调用IO管理器,IO管理器负责发起IO操作请求,并管理这些请求,主要时生成一个IRP结构,系统中有不同的管理器,主要有这样几个——虚拟内存管理器,IO管理器,对象管理器,进程管理器,线程管理器,配置管理器。管理器生成一个IRP请求,并调用内核中的驱动,来相应这个操作,对于CreateFile来说会调用NtCreateFile函数。最后调用内核实现部分,也就是硬件抽象层。最后由硬件抽象层操作硬件,完成打开或者创建文件的操作。NTModel详解R3与R0互相通信在驱动程序中,入口是DriverEntry,函数会带入一个DriverObject指针。这个对象中有许多回调函数,它会根据R3层下发的操作调用对应的回调函数,比如应用层调用CreatFile时在驱动层会调用DispatchCreate。这样我们只要写好DispatchCreate就可以处理由R3层下发的CreateFile命令。上述的一些函数只适用于一般的操作,对于一些特殊的,比如R3层要R0层产生一个输出语句等等,这个特殊的操作是通过DeviceIoControl向R0下发一个控制命令,在R0层根据这个控制码来识别具体是哪种控制,需要R0做哪种操作,函数原型如下:BOOL DeviceIoControl( HANDLE hDevice, //驱动的设备对象句柄 DWORD dwIoControlCode, //控制码 LPVOID lpInBuffer, //发往R0层的数据 DWORD nInBufferSize, //数据大小 LPVOID lpOutBuffer, //提供一个缓冲区,接受R0返回的数据 DWORD nOutBufferSize, //缓冲区的大小 LPDWORD lpBytesReturned, //R0实际返回数据的大小 LPOVERLAPPED lpOverlapped //完成例程 );IRP的简介R3与R0的通信是通过IRP进行数据的交换,IRP的定义如下:typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP { CSHORT Type; USHORT Size; PMDL MdlAddress; ULONG Flags; union { struct _IRP *MasterIrp; PVOID SystemBuffer; } AssociatedIrp; ... IO_STATUS_BLOCK IoStatus; CHAR StackCount; CHAR CurrentLocation; ... PVOID UserBuffer; ... struct { union { struct _IO_STACK_LOCATION *CurrentStackLocation; ... }; } IRP;IRP主要分为两部分,一部分是头,另一部分是IRP栈,在上一篇分析驱动中的数据结构时,说过驱动设备时分层的,上层驱动设备完成后,需要发到下层驱动设备,所有驱动设备公用IRP的栈顶,但是每个驱动都各自有自己的IRP栈,它们的关系如下如所示:_IO_STACK_LOCATION 的结构如下:typedef struct _IO_STACK_LOCATION { UCHAR MajorFunction; UCHAR MinorFunction; UCHAR Flags; UCHAR Control; union { struct { PIO_SECURITY_CONTEXT SecurityContext; ULONG Options; USHORT POINTER_ALIGNMENT FileAttributes; USHORT ShareAccess; ULONG POINTER_ALIGNMENT EaLength; } Create; ... struct { ULONG Length; ULONG POINTER_ALIGNMENT Key; LARGE_INTEGER ByteOffset; } Read; struct { ULONG Length; ULONG POINTER_ALIGNMENT Key; LARGE_INTEGER ByteOffset; } Write; ... struct { ULONG OutputBufferLength; ULONG POINTER_ALIGNMENT InputBufferLength; ULONG POINTER_ALIGNMENT IoControlCode; PVOID Type3InputBuffer; } DeviceIoControl; ... struct { PVOID Argument1; PVOID Argument2; PVOID Argument3; PVOID Argument4; } Others; } Parameters; PDEVICE_OBJECT DeviceObject; PFILE_OBJECT FileObject; PIO_COMPLETION_ROUTINE CompletionRoutine; PVOID Context; } IO_STACK_LOCATION, *PIO_STACK_LOCATION;这个结构中有一个共用体,当处理不同的R3层请求时系统会填充对应的共用体。源代码分析//设备名 #define DEVICE_NAME L"\\device\\NtDevice" #define LINK_NAME L"\\??\\NtDevice" //DriverEntry NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegisterPath) { PDEVICE_OBJECT pDeviceObject = NULL; UNICODE_STRING uDeviceName = { 0 }; UNICODE_STRING uLinkName = { 0 }; NTSTATUS status = 0; UNREFERENCED_PARAMETER(pRegisterPath); DbgPrint("Start Driver......\n"); pDriverObject->DriverUnload = UnloadDriver; //创建设备对象 RtlInitUnicodeString(&uDeviceName, DEVICE_NAME); status = IoCreateDevice(pDriverObject, 0, &uDeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject); if (!NT_SUCCESS(status)) { DbgPrint("create device error!\n"); return status; } pDeviceObject->Flags |= DO_BUFFERED_IO; //创建符号连接 RtlInitUnicodeString(&uLinkName, LINK_NAME); status = IoCreateSymbolicLink(&uLinkName, &uDeviceName); if (!NT_SUCCESS(status)) { DbgPrint("create link name error!\n"); return status; } //注册分发函数 for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++) { pDriverObject->MajorFunction[i] = IoDispatchCommon; } pDriverObject->MajorFunction[IRP_MJ_CREATE] = IoDispatchCreate; pDriverObject->MajorFunction[IRP_MJ_READ] = IoDispatchRead; pDriverObject->MajorFunction[IRP_MJ_WRITE] = IoDispatchWrite; pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoDispatchControl; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = IoDispatchClose; pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = IoDispatchClean; return STATUS_SUCCESS; }这个函数是驱动的入口函数,类似与main函数或者WinMain函数。在该函数中首先创建一个控制设备对象,并为它创建一个符号链接,因为R3层不能直接通过设备的名称来访问设备,必须通过其符号链接。需要注意,设备名称必须以“\\device”开头,而符号链接需要以“\\??”开头,否则创建设备和符号链接会失败。然后为这个驱动程序注册分发函数,分发函数保存在DriverObject结构中MajorFunction中,这个时一个数组,元素个数为IRP_MJ_MAXIMUM_FUNCTION,系统为每个位置定义一个宏,我们根据这个宏,在数据中填入对应 的函数指针,系统会根据R3层的操作来调用具体的函数。另外DriverObject中的DriverUnload 保存的是卸载驱动时系统回调用的函数,在这个函数中主要完成资源的释放工作//UnloadDriver VOID UnloadDriver(PDRIVER_OBJECT pDriverObject) { UNICODE_STRING uLinkName = { 0 }; UNREFERENCED_PARAMETER(pDriverObject); RtlInitUnicodeString(&uLinkName, LINK_NAME); IoDeleteSymbolicLink(&uLinkName); IoDeleteDevice(pDriverObject->DeviceObject); DbgPrint("Unload Driver......\n"); }在这个函数中主要释放了之前创建的符号链接和控制设备对象。NTSTATUS IoDispatchCommon(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { UNREFERENCED_PARAMETER(DeviceObject); //向R3返回成功 pIrp->IoStatus.Status = STATUS_SUCCESS; //向R3返回的数据长度为0,不向R3返回数据 pIrp->IoStatus.Information = 0; //默认直接返回 IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这个函数是我们注册的默认处理函数,它每步的操作在注释中也写了,需要注意的是在pIrp->IoStatus.Status = STATUS_SUCCESS;语句是向R3返回执行的状态,而最后返回成功是给驱动程序看的。//IoDispatchRead NTSTATUS IoDispatchRead(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { //处理R3层的读命令,将数据返回给R3层 WCHAR wHello[] = L"hello world"; ULONG uReadLength = 0; WCHAR *pBuffer = pIrp->AssociatedIrp.SystemBuffer; ULONG uBufferLen = 0; ULONG uMin = 0; PIO_STACK_LOCATION pCurrStack = IoGetCurrentIrpStackLocation(pIrp); UNREFERENCED_PARAMETER(DeviceObject); uBufferLen = pCurrStack->Parameters.Read.Length; uReadLength = sizeof(wHello); uMin = (uReadLength < uBufferLen) ? uReadLength : uBufferLen; RtlCopyMemory(pBuffer, wHello, uMin); pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = uMin; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这个函数主要用来处理应用层的ReadFile请求,这个函数主要是将一段字符串拷贝到通信用的缓冲区中,模拟读的操作。,需要注意的是这个地址要根据不同的设备类型来不同的对待,对于DO_BUFFERED_IO类型的设备,是保存在pIrp->AssociatedIrp.SystemBuffer中,对于DO_DIRECT_IO类型的设备,这个缓冲区的地址是MdlAddress。而对于ReadFile这个API来说,应用层在调用这个函数时会给一个缓冲区的大小,为了获取这个大小,首先得到当前的IRP栈,这个操作用函数IoGetCurrentIrpStackLocation可以得到,然后在当前栈的Parameters共用体中,调用Read部分的Length。当得到这个缓冲区大小后,取缓冲区大小和对应字符串的大小的最小值,为什么要这样做?我们不妨考虑如果用缓冲区的长度的话,当这个长度比字符串的长度长,那么在拷贝时就会将字符串后面的一些无用内存给拷贝进去了,一来效率不高,二来这样应用层得到了内核层中内存的部分数据,存在安全隐患,如果我们采用字符串的长度,可能会出现用户提供的缓冲区不够的情况,这样会造成越界。所以采用它们的最小值是最合理的。完成之后返回,这个时候要注意返回的长度这一项需要填上真实拷贝的大小,不然R3是得不到数据的,或者得到的数据不完整。NTSTATUS IoDispatchWrite(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { //接受R3的写命令 UNICODE_STRING uWriteString = { 0 }; WCHAR *pBuffer = NULL; ULONG uWriteLength = 0; PIO_STACK_LOCATION pStackIrp = IoGetCurrentIrpStackLocation(pIrp); UNREFERENCED_PARAMETER(DeviceObject); uWriteLength = pStackIrp->Parameters.Write.Length; uWriteString.MaximumLength = uWriteLength; uWriteString.Length = uWriteLength - 1 * sizeof(WCHAR); uWriteString.Buffer = ExAllocatePoolWithTag(PagedPool, uWriteLength, 'TSET'); pBuffer = pIrp->AssociatedIrp.SystemBuffer; if (NULL == uWriteString.Buffer) { DbgPrint("Allocate Memory Error!\n"); pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_INSUFFICIENT_RESOURCES; } RtlCopyMemory(uWriteString.Buffer, pBuffer, uWriteLength); DbgPrint("Write Date: %wZ\n", &uWriteString); pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这个函数用来处理WriteFile的请求,首先通过IRP中传进来的缓冲区的地址得到这个数据,然后将数据打印出来,通过这种方式来模拟向R3文件中写入数据。//定义的控制码 #define IOCTL_BASE 0x800 #define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, (IOCTL_BASE + i), METHOD_BUFFERED, FILE_ANY_ACCESS) #define CTL_PRINT MYIOCTRL_CODE(1) #define CTL_HELLO MYIOCTRL_CODE(2) #define CTL_BYE MYIOCTRL_CODE(3) //IoDispatchControl函数 NTSTATUS IoDispatchControl(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { UNREFERENCED_PARAMETER(DeviceObject); ULONG uBufferLength = 0; WCHAR *pBuffer = NULL; ULONG uIOCtrlCode = 0; PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(pIrp); uBufferLength = pStack->Parameters.DeviceIoControl.InputBufferLength; uIOCtrlCode = pStack->Parameters.DeviceIoControl.IoControlCode; pBuffer = pIrp->AssociatedIrp.SystemBuffer; switch (uIOCtrlCode) { case CTL_BYE: DbgPrint("Good Bye"); break; case CTL_HELLO: DbgPrint("Hello World\n"); break; case CTL_PRINT: DbgPrint("%S", pBuffer); break; default: DbgPrint("unknow command\n"); } pIrp->IoStatus.Information = 0; pIrp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这个宏CTL_CODE是微软官方定义的,主要用来将控制码和对应类型的设备进行绑定,主要传入4个参数,第一个是设备的类型,第二个是具体的控制码,需要注意的是:为了与微软官方的控制码区分,自定义的控制码需要在0x800以上。第三个参数是对应控制码传递参数的方式,主要的几种方式与设备对象和R3层传递数据的方式类似,我们在这传入的是METHOD_BUFFERED,表示的是通过内存拷贝的方式将R3的数据传入到R0。最后一个是操作权限,我们给它所有的权限。在函数中我们根据R3传入的控制码来进行不同的操作。R3部分的代码R3部分主要完成的是驱动程序的加载、卸载、以及向驱动程序发送命令。驱动的加载和卸载是通过注册并启动服务和关闭并删除服务的方式进行的,至于怎么操作一个服务,请看本人另外一篇关于服务操作的博客。需要注意的是在创建服务时需要填入服务程序所在的路径,这个时候需要填生成的.sys文件的路径,不要写之前定义的设备名或者符号链接名。在这主要贴出控制部分的代码://打开设备,获取它的设备句柄 HANDLE hDevice = CreateFileA("\\\\.\\NtDevice", GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (NULL == hDevice) { printf("打开设备失败\n"); return; } //读 CHAR szBuf[255] = ""; ULONG uLength = 0; ReadFile(hDevice, szBuf, 255, &uLength, NULL); printf("Read Date:%s\n", szBuf); //写 WCHAR wHello[] = L"Hello world"; WriteFile(hDevice, wHello, (wcslen(wHello) + 1) * sizeof(WCHAR), &uLength, NULL); printf("写操作完成"); //向其发送控制命令 WCHAR wCtlString[] = L"C:\\test.txt"; DeviceIoControl(hDevice, CTL_PRINT, wCtlString, (wcslen(wCtlString) + 1) * sizeof(WCHAR), NULL, 0, NULL, NULL); DeviceIoControl(hDevice, CTL_HELLO, NULL, 0, NULL, 0, NULL, NULL); DeviceIoControl(hDevice, CTL_BYE, NULL, 0, NULL, 0, NULL, NULL); printf("控制操作完成"); CloseHandle(hDevice);要控制一个设备对象,必须先得到设备对象的句柄,获得这个句柄,我们是通过函数CreateFile来得到的,这个时候填入的文件名应该是之前注册的符号链接的名字,在R3中这个名字以“\\\\ .”开头并加上我们为它提供的符号链接名。在调用CreateFile时会触发之前定义的DispatchCreate函数。然后我们通过调用ReadFile和WriteFile分别触发读写操作,最后调用DeviceIoControl函数,发送控制命令,在R3层中也要定义一份与R0中一模一样的控制码。这样就基本实现了R3与R0通信。有的时候在加载驱动的时候,系统会报错,返回码为2,表示系统找不到驱动对应的文件,这个时候可能是文件的路径的问题,这个时候可以在系统的注册表HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\services\下,找到我们的驱动(还有可能是在ControlSet002)对应的路径,然后将R3程序拷贝到这个路径下,基本就可以解决这个问题
2016年10月07日
4 阅读
0 评论
0 点赞
2016-09-24
windows 驱动开发入门——驱动中的数据结构
最近在学习驱动编程方面的内容,在这将自己的一些心得分享出来,供大家参考,与大家共同进步,本人学习驱动主要是通过两本书——《独钓寒江 windows安全编程》 和 《windows驱动开发技术详解》。驱动开发过程中,主要使用的C语言,虽说C中定义了许多数据类型,但是一般来说在编码上还是习惯与使用WDK的规范,虽说这个不是必须的,比如有这样一句unsigned long ul = 0;这个数据的大小根据不同的机器不同的编译器环境略有不同,这样代码就产生了不可控的行为,但是WDK上专门定义了相关的宏,环境不同,只需要修改一下宏定义,这样就避免了这个问题。在这列举一些常用的数据类型,以免以后在编写代码或者查看例子代码时犯迷糊:普通数据类型#define ULONG unsigned long #define UCHAR unsigned char #define UINT unsigned int #define VOID void #define PULONG unsigned * #define PUCHAR unsigned char* #define PUINT unsigned int* #define PVOID void* 字符串类型在驱动的编程中,为字符串操作专门定义了一个数据类型UNICODE_STRING ANSI_STRING,他们的定义大致相同,只是一个是表示UNICODE字符串,一个表示ANSI字符串,下面主要来说明一下UNICODE_STRINGtypedef struct _UNICODE_STRING { USHORT Length; // 字符串的中字符所占的内存大小 USHORT MaximumLength;//用来存储字符串缓冲的大小 PWCHAR Buffer;//缓冲的地址 } UNICODE_STRING;这个结构体在使用是需要注意的是上述两个大小单位是字节数而不是字符个数,另外在操作UNICODE_STRING 的时候只是简单的操作Buffer指向的内存,并不会特意的为其分配另外的空间,字符串处理函数主要有这样几个:RtlInitUnicodeString(&uStr1, str1); RtlCopyUnicodeString(&uStr1, &uStr2); RtlAppendUnicodeToString(&uStr1, str1); RtlAppendUnicodeStringToString(&uStr1, &uStr2); RtlCompareUnicodeString(&uStr1, &uStr2, TRUE/FALSE); RtlAnsiStringToUnicodeString(&uStr1, &aStr1, TRUE/FALSE); RtlFreeUnicodeString(&uStr1);这些函数从字面上就可以知道它们是干什么用的,需要注意的是,除了Init,这些函数只是简单的操作Buffer已指向的内存,并不会改变指针的指向。所以在使用时要特别注意不要试图改变静态常量区的内容,也要特别注意指向的内存是在栈中还是在堆中。下面是一个简单的例子: UNICODE_STRING uStr1 = { 0 }; UNICODE_STRING uStr2 = { 0 }; UNICODE_STRING uStr3 = { 0 }; ANSI_STRING aStr = { 0 }; RtlInitUnicodeString(&uStr1, L"Hello"); RtlInitUnicodeString(&uStr2, L"Goodbye"); //打印字符串结构用%Z表示%wZ表示是宽字符 DbgPrint("uStr1 = %wZ\n", &uStr1); DbgPrint("uStr2 = %wZ\n", &uStr2); RtlInitAnsiString(&aStr, "Hello World"); DbgPrint("aStr = %Z\n", &aStr); /*这个操作是由于uStr3中的Buffer指向NULL,所以会失败*/ RtlCopyUnicodeString(&uStr3, &uStr1); DbgPrint("uStr3 = %wZ\n", &uStr3); //失败 /*下面两个失败是由于Str1 Str2 指向的是字符串常量区,不可修改*/ RtlAppendUnicodeToString(&uStr1, &uStr2); DbgPrint("uStr1 = %wZ\n", &uStr1); //失败 RtlAppendUnicodeStringToString(&uStr1, L"World"); DbgPrint("uStr1 = %wZ\n", &uStr1); //失败LARGE_INTEGER这个结构就像它的名字一样,用来表示一个比较大的整数,它的定义如下:typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER, *PLARGE_INTEGER;这是一个公用体,可以认为它是由两部分组成高32位的HighPart和低32位的LowPart,它分了高位优先和低位优先两种情况,也可以认为它是一个64位的整形。在使用时根据需求来决定NTSTATUS绝大多数驱动函数都返回这个值,用来表示当前处理的状态,一般STATUS_SUCCESS表示成功,其余的都表示失败。微软根据不同情况定义了它的状态值,一般常用的有下面几个值含义STATUS_SUCCESS函数执行成功STATUS_UNSUCCESSFUL函数执行不成功STATUS_NOT_IMPLEMENTED函数违背实现STATUS_INVALID_INFO_CLASS输入参数是无效的类别STATUS_ACCESS_VIOLATION不允许访问STATUS_IN_PAGE_ERROR发生页面故障STATUS_INVALID_HANDLE输入的是无效的句柄STATUS_INVALID_PARAMETER输入的是无效的参数STATUS_NO_SUCH_DEVICE指定的设备不存在STATUS_NO_SUCH_FILE指定的文件不存在STATUS_INVALID_DEVICE_REQUEST无效的设备请求STATUS_END_OF_FILE文件已到结尾STATUS_INVALID_SYSTEM_SERVICE无效的系统调用STATUS_ACCESS_DENIED访问被拒绝STATUS_BUFFER_TOO_SMALL输入的缓冲区过小STATUS_OBJECT_TYPE_MISMATCH输入的对象类型不匹配STATUS_OBJECT_NAME_INVALIE输入的对象名无效STATUS_OBJECT_NAME_NOT_FOUND输入的对象没有找到STATUS_PORT_DISCONNNECTED需要连接的端口没有被连接STATUS_OBJECT_PATH_INVALID输入的对象路径无效另外在使用WinDbg进行调试的时候,一般都会得到函数调用的错误码,根据错误码可以找到对应的错误信息,微软提供了一种解决方案:LPVOID lpMessageBuffer; HMODULE Hand = LoadLibrary(_T("NTDLL.DLL")); DWORD dwErrCode = 0; //获取错误码 FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_FROM_HMODULE, Hand, dwErrCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMessageBuffer, 0, NULL ); // Now display the string. // Free the buffer allocated by the system. LocalFree( lpMessageBuffer ); FreeLibrary(Hand);驱动对象驱动程序的入口函数是DriverEntry,函数会传入一个驱动对象的指针——PDRIVER_OBJECT,每个驱动都有一个唯一的驱动对象,就好像每个Win32应用程序有一个唯一的实例句柄。它的定义如下:typedef struct _DRIVER_OBJECT { CSHORT Type; CSHORT Size; PDEVICE_OBJECT DeviceObject; ULONG Flags; PVOID DriverStart; ULONG DriverSize; PVOID DriverSection; PDRIVER_EXTENSION DriverExtension; UNICODE_STRING DriverName; PUNICODE_STRING HardwareDatabase; PFAST_IO_DISPATCH FastIoDispatch; PDRIVER_INITIALIZE DriverInit; PDRIVER_STARTIO DriverStartIo; PDRIVER_UNLOAD DriverUnload; PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]; } DRIVER_OBJECT;下面主要对几个重要的部分做介绍:DeviceObject:保存的是驱动中设备对象的指针,另外每个设备对象又有一个指向下一个设备对象的指针,这样同一个驱动程序中的不同设备对象就构成了一个链表DriverName:这个里面存储的是驱动程序的名称,该字符串一般为“\Driver\驱动名称”HardwareDatabase:这里记录的是设备的硬件数据库键名,这个数据库一般是注册表,字符串一般为“REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM”DriverStartIo:记录StartIo这个例程回调函数的地址DriverUnload:当驱动卸载时会调用这个指针所指向的函数MajorFunction,这是一个回调函数的指针数组,处理IRP包的不同请求,就好像应用层里面的消息处理函数,根据不同的请求,调用不同的函数。设备对象在windows平台将每个设备抽象为一个设备对象,驱动层一般通过设备对象来操作具体的设备,每个驱动可以有多个设备对象。typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT { ... struct _DRIVER_OBJECT *DriverObject; struct _DEVICE_OBJECT *NextDevice; struct _DEVICE_OBJECT *AttachedDevice; struct _IRP *CurrentIrp; ULONG Flags; PVOID DeviceExtension; DEVICE_TYPE DeviceType; CCHAR StackSize; ... } DEVICE_OBJECT;设备对象本身定义是十分复杂的,在这我们只列举出部分,以后写程序会经常使用的部分,下面是对这些部分的说明:DriverObject: 指向所属驱动的驱动对象的指针NextDevice:指向下一个设备驱动的指针AttachedDevice:指向它被附加的驱动的指针,设备对象之上还可以在附加上其他的设备对象,这样每当有消息传来时总会由附加在它之上的设备对象处理,然后才会交由它自身处理,这个指针就是指向附加在它之上的设备对象的指针CurrentIrp:指向当前IRP域的指针Flags:表名该设备的一些标志信息,主要有下面几个值:标志描述DO_BUFFERED_IO读写使用缓冲方式,内核层在使用用户缓冲区时会将用户分区中的数据拷贝到内核分区中DO_EXCLUSIVE一次只允许一个线程使用这个设备对象DO_DIRECT_IO读写直接方式,应用层将某块内存锁定在内存,然后将内存映射到内核空间中,这种方式是最快的方式DO_DEVICE_INITIALIZING设备正在初始化DO_POWER_PAGABLE设备必须在PASSIVE_LEVEL上处理IRP_MJ_PNP请求DO_POWER_INRUSH设备上电期间需要大电流DeviceExtension:指向一块扩展的内存,系统允许用户在创建设备对象时自定义一块区域用来保存结构体中没有但是用户自己感兴趣的内容。在驱动程序中需要尽量避免使用全局变量,所以可以通过使用这块扩展内存来传输全局变量DeviceType:驱动的类型,主要有下面几个值设备类型描述FILE_DEVICE_BEEP该设备是一个蜂鸣器FILE_DEVICE_CD_ROM该设备时一个CD光驱FILE_DEVICE_CD_ROM_FILE_SYSTEMCD光驱文件系统设备FILE_DEVICE_CONTROLLER控制器设备FILE_DEVICE_DATALINK数据链设备FILE_DEVICE_DFSDFS设备对象FILE_DEVICE_DISK磁盘设备对象FILE_DEVICE_DISK_FILE_SYSTEM磁盘文件系统设备对象FILE_DEVICE_FILE_SYSTEM文件系统设备对象FILE_DEVICE_INPORT_PORT输入端口设备对象FILE_DEVICE_KEYBOARD键盘设备对象FILE_DEVICE_MAILSLOT邮件曹设备对象FILE_DEVICE_MIDI_INMIDI输入设备对象FILE_DEVICE_MIDI_OUTMIDI输出设备对象FILE_DEVICE_MOUSE鼠标设备对象FILE_DEVICE_MULTI_UNC_PROVIDER多UNC设备对象FILE_DEVICE_NAMED_PIPE命名管道设备对象FILE_DEVICE_NETWORK网络设备对象FILE_DEVICE_NETWORK_BROWSER网络浏览器设备对象FILE_DEVICE_NETWORK_FILE_SYSTEM网络文件系统设备对象FILE_DEVICE_NULL空设备对象FILE_DEVICE_PARALLEL_PORT并口设备对象FILE_DEVICE_PHYSICAL_NETCARD物理网卡设备对象FILE_DEVICE_PRINTER打印机设备对象FILE_DEVICE_SCANNER扫描仪设备对象FILE_DEVICE_SERIAL_MOUSE_PORT串口鼠标设备对象LE_DEVICE_SERIAL_PORT串口设备对象FILE_DEVICE_SCREEN屏幕设备对象FILE_DEVICE_SOUND声音设备对象FILE_DEVICE_STREAMS流设备对象LE_DEVICE_TAPE磁带设备对象FILE_DEVICE_TAPE_FILE_SYSTEM磁带文件系统设备对象FILE_DEVICE_TRANSPORT传输设备对象FILE_DEVICE_UNKNOWN未知设备对象FILE_DEVICE_VIDEO视频设备对象FILE_DEVICE_VIRTUAL_DISK虚拟磁盘设备对象FILE_DEVICE_WAVE_IN声音输入设备对象FILE_DEVICE_WAVE_OUT声音输出设备对象在创建设备对象时如果不知道这个设备对象是何种类型,可以直接给FILE_DEVICE_UNKNOWN;StackSize:之前说到过,设备对象存在附加的情况,附加时每个设备对象会存储它上层的设备对象的指针,这样就形成了类似堆栈的结构,而这个值就表示从该设备对象到栈底还有多少个设备对象为了便于理解我们做了这样一个示意图:
2016年09月24日
7 阅读
0 评论
0 点赞
2016-09-14
windows 线程
在windows中进程只是一个容器,用于装载系统资源,它并不执行代码,它是系统资源分配的最小单元,而在进程中执行代码的是线程,线程是轻量级的进程,是代码执行的最小单位。从系统的内核角度看,进程是一个内核对象,内核用这个对象来存储一些关于线程的信息,比如当前线程环境等等,从编程的角度看,线程就是一堆寄存器状态以及线程栈的一个结构体对象,本质上可以理解为一个函数调用,一般线程有一个代码的起始地址,系统需要执行线程,只需要将寄存器EIP指向这个代码的地址,那么CPU接下来就会自动的去执行这个线程,线程切换时也是修改EIP的值,那么CPU就回去执行另外的代码了。什么时候不用多线程?一般我们都讨论什么时候需要使用多线程,很多人认为只要计算任务能拆分成多个不想关的部分,就可以使用多线程来提升效率,但是需要知道的是,线程是很耗费资源的,同时CPU在线程之间切换也会消耗大量的时间,所以在使用上并不是多线程就是好的,需要慎重使用,在下列情况下不应该使用多线程:当一个计算任务是严重串行化的,也就是一个计算步骤严重依赖上一个计算步骤的时候。但是如果是针对不同的初始参数可以得到不同的结果那么可以考虑用多线程的方式,将每个传入参数当作一个线程,一次计算出多组数据。当多个任务有严格的先后逻辑关系的时候,这种情况下利用多线程需要额外考虑线程之间执行先后顺序的问题,实际上可能它的效率与普通的单线程程序差不多,它还需要额外考虑并发控制,这将得不偿失当一个服务器需要处理多个客户端连接的时候,优先考虑的是使用线程池,而不是简单的使用多线程,为每个客户端连接创建一个线程,这样会严重浪费资源,而且线程过的,CPU在进程调度时需要花大量的时间来执行轮询算法,会降低效率。主线程进程的入口函数就是主线程的入口函数,一般主线程推出进程退出(这是由于VC++中在主线程结束时会隐含的调用ExitProcess)线程入口地址在windows中需要为线程指定一个执行代码的开始地址,这个在VC++中体现为需要为每个进程制定一个函数指针,并制定一个void* 型的指针类型的参数。当CPU执行这个线程时会将寄存器环境改为这个线程指定的环境,然后执行代码(具体就是前面所说的更改EIP的值)创建线程在windows中创建线程所使用的API是CreateThread,这个函数的原型如下:HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack, LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpvThreadParam, DWORD fdwCreate, LPDWORD lpIDThread );第一个参数是线程对象的安全描述符第二个参数是线程栈的大小,每个线程都有一个栈环境用来存储局部变量,以及调用函数,这个值可以给0,这个时候系统会根据线程中调用函数情况动态的增长,但是如果需要很大的线程栈,比如要进行深层的递归,并且每个递归都有大量的局部变量和函数时,运行一段时间后,可能会出现没有足够大的地址空间再来增长线程栈,这个时候就会出现栈溢出的异常,为了解决这个问题,我们可以填入一个较大的值,以通知系统预留足够大的地址空间。第三个参数是线程的入口函数地址,这个是一个函数指针,函数的原型是DWORD ThreadProc( LPVOID lpParameter); 函数中就是线程将要执行的代码。第四个参数是函数中将要传入的参数,为了方便传入多个参数,一般将要使用的过个参数定义为一个结构体,将这个结构体指针传入,然后再函数中将指针转化为需要的结构体指针,这样就可以使用多个参数。第五个参数是创建标志,默认一般传入0,但表示线程一被创建马上执行,如果传入CREATE_SUSPENDED,则表示线程先创建不执行,需要使用函数ResumeThread唤醒线程,另外在XP以上的系统中可以使用STACK_SIZE_PARAM_IS_A_RESERVATION结合上面的第二个参数,表示当前并不需要这么内存,只是先保留当前的虚拟地址空间,在需要时有程序员手动提交物理页面。如果没有指定那么回默认提交物理页面。第六个参数会返回一个线程的ID。下面是一个创建线程的例子:typedef struct tag_THREAD_PARAM { int nThreadNo; int nValue; }THREAD_PARAM, *LPTHREAN_PARAM; int _tmain(int argc, TCHAR *argv[]) { LPTHREAN_PARAM lpValues = (LPTHREAN_PARAM)HeapAlloc(GetProcessHeap(), 0, 10 * sizeof(THREAD_PARAM)); for (int i =0; i < 10; i++) { lpValues[i].nThreadNo = i; lpValues[i].nValue = i + 100; } HANDLE hHandle[10] = {NULL}; for(int i = 0; i < 10; i++) { hHandle[i] = CreateThread(NULL, 0, ThreadProc, &lpValues[i], 0, NULL); if (NULL == hHandle[i]) { return 0; } } WaitForMultipleObjects(10, hHandle, TRUE, INFINITE); return 0; } DWORD WINAPI ThreadProc(LPVOID lpParam) { LPTHREAN_PARAM pThreadValues = (LPTHREAN_PARAM)lpParam; printf("%d, values = %d\n", pThreadValues->nThreadNo, pThreadValues->nValue); return 0; }上述代码中我们将两个整型数据定义为了一个结构体,并在创建线程中,将这个结构体地址作为参数传入,这样在线程中就可以使用这样的两个参数了。线程退出当满足下列条件之一时线程就会退出调用ExitThread线程函数返回调用ExitProcess用线程句柄调用TerminateThread用进程句柄调用TerminateProcess当线程终止时,线程对象的状态会变为有信号,线程状态码会由STILL_ACTIVE改为线程的退出码,可以用GetExitThreadCode得到推出码,然后根据推出码是否为STILL_ACTIVE判断线程是否在运行线程栈溢出的恢复使用C++时由于下标溢出而造成的栈溢出将无法恢复。当栈溢出时会抛出EXCEPTION_STACK_OVERFLOW的结构化异常,之后使用_resetstkflow函数可以恢复栈环境,这个函数只能在_except的语句块中使用。调用 SetThreadStackGuarantee函数可以保证当栈溢出时有足够的栈空间能使用结构话异常处理,SEH在底层仍然是使用栈进行异常的抛出和处理的,所以如果不保证预留一定的栈空间,可能在最后使用不了SEH。下面是一个使用的例子:void ArrayErr(); void Stackflow(); DWORD _exception_stack_overflow(DWORD dwErrorCcode); int g_nCnt = 0; int _tmain(int argc, TCHAR *argv[]) { for (int i = 0; i < 10; i++) { __try { /* ArrayErr();*/ Stackflow(); } __except(_exception_stack_overflow(GetExceptionCode())) { int nResult = _resetstkoflw(); if (!nResult) { printf("处理失败\n"); break; }else { printf("处理成功\n"); } } } return 0; } void ArrayErr() { int array[] = {1, 2}; array[10000] = 10; } void Stackflow() { g_nCnt++; int array[1024] = {0}; Stackflow(); } DWORD _exception_stack_overflow(DWORD dwErrorCcode) { if (EXCEPTION_STACK_OVERFLOW == dwErrorCcode) { return EXCEPTION_EXECUTE_HANDLER; }else { return EXCEPTION_CONTINUE_SEARCH; } }在上述的例子中定义两个会引起异常的函数,一个是下标越界,一个是深度的递归,两种情况都会引起栈溢出,但是下标越界是不可恢复的,所以这个异常不能被处理,在异常处理中我们使用函数_resetstkoflw来恢复栈,使得程序可以继续运行下去。线程本地存储当线程需要访问一个共同的全局变量,并且某个线程对这个变量的修改不会影响到其他的进程的时候可以给予每个线程一份拷贝,每个线程访问变量在它自己中的拷贝,而不用去争抢这一个全局变量,有时候我们可能会想出用数组的方式来存储这每份拷贝,每个线程访问数组中的固定元素,但是当线程是动态创建和销毁也就是线程数量动态变化时,维护这个数组将会非常困难,这个时候可以使用线程本地存储技术(TLS),它的基本思想是在访问全局变量时给每个线程一个实例,各个线程访问这个实例而不用去争抢一个全局变量。就好像系统为我们维护了一个动态数组,让每个线程拥有这个数组中的固定元素。使用TLS有两种方法关键字法和API法。关键字法: 使用关键字__declspec(thread)修饰一个变量,这样就可以为每个访问它的线程创建一份拷贝。动态API法,TlsAlloc为每个全局变量分配一个TLS索引, TlsSetValue为某个索引设置一个值,TlsGetValue获取某个索引的值TlsFree释放这个索引两中方式各有优缺点,第一种方式使用简单,但是仅仅局限于VC++,第二中方式使用相对复杂但是可以跨语言只要能使用windowsAPI就可以使用这种方式下面分别展示了使用这两种方式的例子:DWORD _declspec(thread) g_dwCurrThreadID = 0; DWORD WINAPI ThreadProc(LPVOID lpParam); int main() { g_dwCurrThreadID = GetCurrentThreadId(); HANDLE hThread[10] = {NULL}; for (int i = 0; i < 10; i++) { hThread[i] = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); } printf("Main Thread id = %x, g_dwCurrThreadID = %x, the address g_dwCurrThreadID = %08x\n", GetCurrentThreadId(), g_dwCurrThreadID, &g_dwCurrThreadID); WaitForMultipleObjects(10, hThread, TRUE, INFINITE); for (int i = 0; i < 10; i++) { CloseHandle(hThread[i]); } return 0; } DWORD WINAPI ThreadProc(LPVOID lpParam) { g_dwCurrThreadID = GetCurrentThreadId(); Sleep(100); printf("the thread id = %x, g_dwCurrThreadID = %x, the address g_dwCurrThreadID = %08x\n", GetCurrentThreadId(), g_dwCurrThreadID, &g_dwCurrThreadID); return 0; }首先定义了一个TLS的变量,然后在线程中引用,在输出结果中发现,没个线程中的值和它的地址值都不一样,所以说使用的只是一份拷贝而已DWORD g_dwTLSIndex = 0; DWORD WINAPI ThreadProc(LPVOID lpParam); int main() { HANDLE hThread[10] = {NULL}; g_dwTLSIndex = TlsAlloc(); TlsSetValue(g_dwTLSIndex, (LPVOID)GetCurrentThreadId()); for (int i = 0; i < 10; i++) { hThread[i] = CreateThread(NULL, 0, ThreadProc, NULL, CREATE_SUSPENDED, NULL); ResumeThread(hThread[i]); } printf("Main Thread id = %x, g_dwCurrThreadID = %x\n", GetCurrentThreadId(), TlsGetValue(g_dwTLSIndex)); WaitForMultipleObjects(10, hThread, TRUE, INFINITE); for (int i = 0; i < 10; i++) { CloseHandle(hThread[i]); } TlsFree(g_dwTLSIndex); return 0; } DWORD WINAPI ThreadProc(LPVOID lpParam) { TlsSetValue(g_dwTLSIndex, (LPVOID)GetCurrentThreadId()); printf("the thread id = %x, g_dwCurrThreadID = %x\n", GetCurrentThreadId(), TlsGetValue(g_dwTLSIndex)); return 0; } 上述使用中,在主线程中申请和释放一个TLS变量,在每个进程中仍然是使用这个变量,输出的结果也是每个变量都不同。线程的挂起和恢复用函数SuspendThread和ResumeThread控制线程的暂停和恢复,一个暂停的线程无法用ResumeThread来唤醒自身,除非有其他线程调用ResumeThread来唤醒。暂停的线程总是立即被暂停,而不管它执行到了哪个指令。需要注意的是,线程这个内核对象中有一个暂停计数器,每当调用一个SuspendThread这个计数器会加一,而每当调用一次ResumeThread计数器会减一,只有当计数器为0时线程才会立即启动,所以可能会出现这样的情况,调用了ResumeThread之后线程并没有立即启动,这个是正常现象。另外可以使用Sleep函数使线程休眠一段时间后再启动,这个填入的时间只是一个参考值,并不是填入多少,就让线程暂停多久,比如说我们填入10ms,这个时候当线程真正陷入到休眠状态时CPU可能执行其他线程去了,如果没有特殊情况,CPU会一直执行到这个线程的时间片结束。然后运行调度程序,调度下一个线程,所以说线程休眠的时间理论上最少也有20ms,通常会比我们设置的时间长。在创建线程的时候可以使用CREATE_SUSPEND标志来明确表示以暂停的方式创建一个线程,如果不用这个方式,那么新创建线程的行为将难以掌握,有可能在CreateThread函数返回之后,线程就开始执行了,也有可能在返回前执行,所以推荐使用这个标志,创建完成后,进行想干的初始化操作,并在必要的时候调用ResumeThread启动它。线程的寄存器状态线程环境也就是线程在运行中,一大堆相关寄存器的值,这些值Windows维护在CONTEXT这个结构体中,在获取时可以通过设置结构体中成员的ContextFlag的值来表示我们需要获取哪些寄存器的值,一般填入CONTEXT_ALL或者CONTEXT_FULL获取所有寄存器的值,用SetThreadContext设置线程寄存器的值,用GetThreadContext获取线程的寄存器环境。需要注意的时,SetThreadContext只能用来修改通用寄存器的值,而像DS, CS这样的段寄存器是收到系统保护的,这些函数是处于RING3层,并不能进入到内核态。线程调度的优先级windows是抢占式多任务的,各个线程是抢占式的获取CPU,一般遵循先到先执行的顺序,windows中的带调度线程是存储在线程队列中的,但是这个队列并不是真正意义上的队列,这个队列是允许插队的,比如当用户点击了某个窗口,那么系统为了响应用户操作,系统会激活窗口并调整队列的顺序,将于窗口相关的线程排到较前的位置,这个插队时通过提高线程的优先级的方式实现的。我们在程序中可以使用SetThreadPriority调整线程的优先级。但是在程序中不要依赖这个值来判断线程的执行顺序,这个值对于系统来说只是一个参考值,当我们的线程进入到队列中时,系统会动态的调整它的优先级,如果某个进程由于优先级的问题长时间没有运行,系统可能会提高它的优先级,而有的线程由于优先级过高,已经执行了多次,系统可能会降低它的优先级。一来可以快速响应用户程序,二来可以防止某些恶意程序抢占式的使用CPU资源。线程的亲缘性线程的亲缘性与进程的相似,但是线程的亲缘性只能在进程亲缘性的子集之上,比如进程的亲缘性被设置在0 2 3 8这几个CPU上,线程就只能在这几个CPU上运行即使设置亲缘性为7,系统也不会将线程加载到这个7号CPU上运行。调用函数SetThreadAffinityMask可以设置亲缘性,使所有线程都在不同的CPU上运行,已达到真正的并发执行。下面是一个使用亲缘性的例子://设置CPU的位掩码 #define SETCPUMASK(i) (1<<(i)) DWORD WINAPI ThreadProc(LPVOID lpParam); #define FOREVER for(;;); int _tmain() { SYSTEM_INFO si = {0}; GetSystemInfo(&si); DWORD dwCPUCnt = si.dwNumberOfProcessors; for (int i = 0; i < dwCPUCnt; i++) { HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, CREATE_SUSPENDED, NULL); SetThreadAffinityMask(hThread, SETCPUMASK(i)); ResumeThread(hThread); printf("启动线程%x成功", GetCurrentThreadId()); system("PAUSE"); } //线程创建成功,并绑定到各个CPU上,理论上CPU的利用率能达到100%,在任务管理器上查看性能可以看到CPU的利用率 return 0; } DWORD WINAPI ThreadProc(LPVOID lpParam) { //死循环 FOREVER }上述程序首先判断了CPU核数,然后有几个CPU就创建几个线程,通过亲缘性设置为每个CPU绑定一个线程,在线程中我们什么都不做,只是一个死循环,主要是为了耗干CPU资源,这样通过查看资源管理器中的CPU使用情况发现CPU的使用率达到了100%,但是系统却没有一丝卡顿,这也说明Windows还是比较智能的,并没有让用户进程占据所有资源。线程可警告状态与异步函数在程序中可以通过一些方法使线程暂停,如使用SleepEx,Wait族的函数(是以Wait开始并且以Ex结尾的函数)可以使线程进入一种可警告状态,这种状态本质上是暂停当前线程,保存当前线程环境,并用这个环境加载并执行新的函数,当这个函数执行完成后,再恢复线程环境,继续执行线程接下来的代码。这些函数被称为异步函数。也就是说这些函数是借用当前的线程环境来作为自生的环境执行里面的代码。这样就类似于创建了一个轻量级的线程,它与线程的区别就是没有自身的环境状态,而是使用其他线程的环境状态,并且也不用进入到线程队列,供调度程序调度。这些异步函数有自己的队列称为异步函数队列,当线程调用这些暂停或者等待的函数时,进入休眠状态,系统会保存当前线程环境,并从异步函数队列中加载异步函数,利用当前的线程环境继续运行,等到休眠时间到达后系统恢复之前保存的环境,继续执行线程的代码。在使用时需要注意的是这些异步函数不要进行复杂的算法或者进程长时间的I/O操作,否则当线程休眠时间达到,而异步函数却未执行完,这样会造成一定的错误。默认线程是不具有异步函数队列,可以利用函数QueueUserAPC将一个异步函数加入到队列中,然后利用上述所说的函数让线程进入假休眠状态,这样就会自动调用异步函数,下面是这样的一个例子:VOID CALLBACK APCProc(ULONG_PTR dwParam) { printf("%d Call APC Function!\n", dwParam); } int _tmain(int argc, TCHAR *argv[]) { for (int i = 0 ; i < 100; i++) { QueueUserAPC(APCProc, GetCurrentThread(), i); } //如果参数改为FALSE,或者注释掉这个函数,那么将不会调用这个APC函数 SleepEx(10000, true); return 0; }上述代码中,我们在主线程中插入100个异步函数,虽然它们执行的都是同样的操作,然后让主线程休眠,SleepEx函数的第二个参数表示是否调用异步函数,如果填入FALSE,或者调用Sleep函数则不会调用异步函数。线程的消息队列线程默认是不具有消息队列的,同时也没有创建消息队列的函数,一般只需要调用与消息相关的函数,系统就会默认创建消息队列。调用PostThreadMessage可以向指定的线程发送消息到消息队列,调用PostThread可以向当前线程发送消息到消息队列,在编写带有消息循环的线程时可能由于线程有耗时的初始化操作,发送到线程的消息可能还没有被接受就丢掉了,但是如果在进行初始化时调用消息函数,让其创建一个消息队列,可以解决这问题,下面是一段例子代码:DWORD WINAPI ThreadProc(LPVOID lpParam) { MSG msg = {0}; /*利用消息相关的函数让线程创建消息队列,如果不创建,可能所有消息都不能收到 这个时候子线程会陷入死循环,主线程会一直等待子进程*/ PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE); //模拟进行耗时的初始化操作 for (int i = 0; i < 10000000; i++); while (GetMessage(&msg, NULL, 0, 0)) { printf("receive msg %d\n", msg.message); } printf("子线程退出\n"); return 0; } int _tmain(int argc, TCHAR *argv[]) { DWORD dwThreadID = 0; HANDLE hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, &dwThreadID); //让主线程休眠一段时间,一遍子线程能创建消息队列 Sleep(10); //发送消息 PostThreadMessage(dwThreadID, 0x0001, 0, 0); PostThreadMessage(dwThreadID, 0x0002, 0, 0); PostThreadMessage(dwThreadID, 0x0003, 0, 0); PostThreadMessage(dwThreadID, WM_QUIT, 0, 0); //休眠一段时间让子进程能够执行完成 WaitForSingleObject(hThread, INFINITE); return 0; }线程执行时间在一些性能分析工具中可能需要使用得到具体执行某一算法的函数的执行时间,一般调用GetTickCount计算调用前时间然后在算法函数调用完成后再次调用GetTickCount再次得到时间,这两个时间详相减则得到具体算法的时间,一般这种算法没有问题,但是需要考虑的时,如果在多任务环境下,该线程的时间片到达,CPU切换到其他线程,该线程没有运行但是时间却在增加,所以这种方式得到的结果并不准确,替代的方案是使用GetThreadTimes。这个函数可以精确到100ns,并且得出系统执行该线程的具体时间,下面是函数的原型:BOOL GetThreadTimes ( HANDLE hThread, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime );这个函数后面几个输出参数分别是线程的创建时间,线程的推出时间,执行内核的时间以及执行用户代码的时间。下面演示了他的具体用法:DWORD WINAPI ThreadProc(LPVOID lpParam) { DWORD dwStart = GetTickCount(); HANDLE hWait = (HANDLE)lpParam; //假设这是一个复杂的算法 while (TRUE) { if (WAIT_OBJECT_0 == WaitForSingleObject(hWait, 0)) { break; } } //模拟CPU调度其他线程 Sleep(3000); DWORD dwEnd = GetTickCount(); printf("子线程即将结束,当前执行算法所用的时间为:%d\n", dwEnd - dwStart); return 0; } int _tmain(int argc, TCHAR *argv[]) { HANDLE hWait = CreateEvent(NULL, TRUE, FALSE, NULL); DWORD dwCPUNum = 0; SYSTEM_INFO si = {0}; GetSystemInfo(&si); dwCPUNum = si.dwNumberOfProcessors; HANDLE* phThread = (HANDLE*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwCPUNum * sizeof(HANDLE)); DWORD dwExitCode = 0; for (int i = 0; i < dwCPUNum; i++) { phThread[i] = CreateThread(NULL, 0, ThreadProc, hWait, CREATE_SUSPENDED, NULL); SetThreadAffinityMask(phThread[i], 1 << i); ResumeThread(phThread[i]); } _tsystem(_T("PAUSE")); //通知所有线程停止 SetEvent(hWait); FILETIME tmCreate = {0}; FILETIME tmExit = {0}; FILETIME tmKernel = {0}; FILETIME tmUser = {0}; SYSTEMTIME sysTm = {0}; ULARGE_INTEGER bigTmp1 = {0}; ULARGE_INTEGER bigTmp2 = {0}; for (int i = 0; i < dwCPUNum; i++) { GetExitCodeThread(phThread[i], &dwExitCode); printf("线程[H:0x%08X]退出,退出码:%u,以下为时间统计信息:\n", phThread[i], dwExitCode); GetThreadTimes(phThread[i], &tmCreate, &tmExit, &tmKernel, &tmUser); //得到创建时间 FileTimeToLocalFileTime(&tmCreate, &tmCreate); FileTimeToSystemTime(&tmCreate, &sysTm); printf("\t创建时间:%2u:%02u:%02u.%04u\n", sysTm.wHour, sysTm.wMinute, sysTm.wSecond, sysTm.wMilliseconds); //得到退出时间 FileTimeToLocalFileTime(&tmExit, &tmExit); FileTimeToSystemTime(&tmExit, &sysTm); printf("\t退出时间:%2u:%02u:%02u.%04u\n", sysTm.wHour, sysTm.wMinute, sysTm.wSecond, sysTm.wMilliseconds); //得到执行内核代码的时间 FileTimeToLocalFileTime(&tmKernel, &tmKernel); FileTimeToSystemTime(&tmKernel, &sysTm); printf("\t退出时间:%2u:%02u:%02u.%04u\n", sysTm.wHour, sysTm.wMinute, sysTm.wSecond, sysTm.wMilliseconds); //得到执行用户代码的时间 FileTimeToLocalFileTime(&tmUser, &tmUser); FileTimeToSystemTime(&tmUser, &sysTm); printf("\t退出时间:%2u:%02u:%02u.%04u\n", sysTm.wHour, sysTm.wMinute, sysTm.wSecond, sysTm.wMilliseconds); //线程开始时间 bigTmp1.HighPart = tmCreate.dwHighDateTime; bigTmp1.LowPart = tmCreate.dwLowDateTime; bigTmp2.HighPart = tmExit.dwHighDateTime; bigTmp2.LowPart = tmExit.dwLowDateTime; //函数GetThreadTimes返回的时间单位是100ns,是微妙的10000倍 printf("\t间隔时间(线程存活周期):%I64dms\n", (bigTmp2.QuadPart - bigTmp1.QuadPart) / 10000); //内核执行时间 bigTmp1.HighPart = tmKernel.dwHighDateTime; bigTmp1.LowPart = tmKernel.dwLowDateTime; printf("\t内核模式(RING0)耗时:%I64dms!\n", bigTmp1.QuadPart / 10000); //执行用户程序的时间 bigTmp2.HighPart = tmUser.dwHighDateTime; bigTmp2.LowPart = tmUser.dwLowDateTime; printf("\t内核模式(RING0)耗时:%I64dms!\n", bigTmp2.QuadPart / 10000); //实际占用总时间(用户代码时间 + 内核代码执行时间) printf("\t内核模式(RING0)耗时:%I64dms!\n", (bigTmp1.QuadPart + bigTmp2.QuadPart)/ 10000); CloseHandle(phThread[i]); system("PAUSE"); } //释放资源 CloseHandle(hWait); HeapFree(GetProcessHeap(), 0, phThread); return 0; }以类成员函数的方式封装线程类一般在如果要将线程函数封装到C++类中时一般采用的是静态成员的方式,因为C++中默认总会多传入一个参数this,而CreateThread需要传入的函数指针并不包含this,所以为了解决这个问题,一般传入一个静态函数的指针,但是静态函数不能定义为虚函数,也就是不能在派生类中重写它,所以在这介绍一种新的封装方式,将其封装为成员函数,并且允许被派生类重写。它的基本思想:利用函数指针的强制转化让类成员函数指针强制转化为CreateThread需要的类型,这样在真正调用函数我们给定的函数地址时就不会传入this指针,但是为了使用类成员函数又需要这个指针,所以我们将this 指针的值变为参数,通过CreateThread的进行传递,这样就模拟了C++类成员函数的调用,下面是实现的部分代码://申明了这样一个线程的入口地址函数 DWORD WINAPI ThreadProc(LPVOID lpParam); //创建线程的代码 DWORD CMyThread::CreateThread(LPSECURITY_ATTRIBUTES lpsa, DWORD cbStack, DWORD fdwCreate) { typedef DWORD (WINAPI *LPTHREAD_START_ADDRESS)(LPVOID); typedef DWORD (WINAPI CMyThread::*PCLASS_PROC)(LPVOID); PCLASS_PROC pThreadProc = &CMyThread::ThreadProc; m_hThread = ::CreateThread(lpsa, cbStack, *(LPTHREAD_START_ADDRESS*)(&pThreadProc), this, fdwCreate, &m_dwThreadID); return (NULL != m_hThread) ? -1 : 0; }完整的代码 请戳这里下载
2016年09月14日
5 阅读
0 评论
0 点赞
2016-09-06
windows 多任务与进程
多任务,进程与线程的简单说明多任务的本质就是并行计算,它能够利用至少2处理器相互协调,同时计算同一个任务的不同部分,从而提高求解速度,或者求解单机无法求解的大规模问题。以前的分布式计算正是利用这点,将大规模问题分解为几个互不不相关的问题,将这些计算问题交给局域网中的其他机器计算完成,然后再汇总到某台机器上,显示结果,这样就充分利用局域网中的计算机资源。相对的,处理完一步接着再处理另外一步,将这样的传统计算模式称为串行计算。在提高处理器的相关性能主要有两种方式,一种是提高单个处理器处理数据的速度,这个主要表现在CPU主频的调高上,而当前硬件总有一个上限,以后再很难突破,所以现在的CPU主要采用的是调高CPU的核数,这样CPU的每个处理器都处理一定的数据,总体上也能带来性能的提升。在某些单核CPU上Windows虽然也提供了多任务,但是这个多任务是分时多任务,也就是每个任务只在CPU中执行一个固定的时间片,然后再切换到另一个任务,由于每个任务的时间片很短,所以给人的感觉是在同一时间运行了多个任务。单核CPU由于需要来回的在对应的任务之间切换,需要事先保存当前任务的运行环境,然后通过轮循算法找到下一个运行的任务,再将CPU中寄存器环境改成新任务的环境,新任务运行到达一定时间,又需要重复上述的步骤,所以在单核CPU上使用多任务并不能带来性能的提升,反而会由在任务之间来回切换,浪费宝贵的资源,多任务真正使用场合是多核的CPU上。windows上多任务的载体是进程和线程,在windows中进程是不执行代码的,它只是一个载体,负责从操作系统内核中分配资源,比如每个进程都有4GB的独立的虚拟地址空间,有各自的内核对象句柄等等。线程是资源分配的最小单元,真正在使用这些资源的是线程。每个程序都至少有一个主线程。线程是可以被执行的最小的调度单位。进程的亲缘性进程或者线程只在某些CPU上被执行,而不是由系统随机分配到任何可用的CPU上,这个就是进程的亲缘性。例如某个CPU有8个处理器,可以通过进程的亲缘性设置让该进程的线程只在某两个处理器上运行,这样就不会像之前那样在8个CPU中的任意几个上运行。在windows上一般更倾向于优先考虑将线程安排在之前执行它的那个处理器上运行,因为之前的处理器的高速缓存中可能存有这个线程之前的执行环境,这样就提高的高速缓存的命中率,减少了从内存中取数据的次数能从一定程度上提高性能。需要注意的是,在拥有三级高速缓存的CPU上,这么做意义就不是很大了,因为三级缓存一般作为共享缓存,由所有处理器共享,如果之前在2号处理器上执行某个线程,在三级缓存上留下了它的运行时的数据,那么由于三级缓存是由所有处理器所共享的,这个时候即使将这个线程又分配到5号处理器上,也能访问这个公共存储区,所以再设置线程在固定的处理器上运行就显得有些鸡肋,这也是拥有三级缓存的CPU比二级缓存的CPU占优势的地方。但是如果CPU只具有二级缓存,通过设置亲缘性可以发挥高速缓存的优势,提高效率。亲缘性可以通过函数SetProcessAffinityMask设置。该函数的原型如下:BOOL WINAPI SetProcessAffinityMask( __in HANDLE hProcess, __in DWORD_PTR dwProcessAffinityMask );第一个参数是设置的进程的句柄,第二个参数是DWORD类型的指针,是一个32位的整数,每一位代表一个处理器的编号,当希望设置进程中的线程在此处理器上运行的话,将该位设置为1,否则为0。为了设置亲缘性,首先应该了解机器上CPU的情况,根据实际情况来妥善安排,获取CPU的详细信息可以通过函数GetLogicalProcessInformation来完成。该函数的原型如下:BOOL WINAPI GetLogicalProcessorInformation( __out PSYSTEM_LOGICAL_PROCESSOR_INFORMATION Buffer, __in_out PDWORD ReturnLength );第一个参数是一个结构体的指针,第二个参数是需要缓冲区的大小,也就是说这个函数我们可以采用两次调用的方式来合理安排缓冲区的大小。结构体SYSTEM_LOGICAL_PROCESSOR_INFORMATION的定义如下: typedef struct _SYSTEM_LOGICAL_PROCESSOR_INFORMATION { ULONG_PTR ProcessorMask; LOGICAL_PROCESSOR_RELATIONSHIP Relationship; union { struct { BYTE Flags; } ProcessorCore; struct { DWORD NodeNumber; }NumaNode; CACHE_DESCRIPTOR Cache; ULONGLONG Reserved[2]; }; } SYSTEM_LOGICAL_PROCESSOR_INFORMATION, *PSYSTEM_LOGICAL_PROCESSOR_INFORMATION;ProcessorMask是一个位掩码,每个位代表一个逻辑处理器,第二个参数是一个标志,表示该使用第三个共用体中的哪一个结构体,这三个分别表示核心处理器,NUMA节点,以及高速缓存的信息。下面是使用的一个例子代码:#include <windows.h> #include <tchar.h> #include <stdio.h> #include <locale.h> DWORD CountBits(ULONG_PTR uMask); typedef BOOL (WINAPI *CPUINFO)(PSYSTEM_LOGICAL_PROCESSOR_INFORMATION Buffer, PDWORD ReturnLength); int _tmain(int argc, TCHAR *argv[]) { _tsetlocale(LC_ALL, _T("chs")); CPUINFO pGetCpuInfo = (CPUINFO)GetProcAddress(GetModuleHandle(_T("kernel32")), "GetLogicalProcessorInformation"); if (NULL == pGetCpuInfo) { _tprintf(_T("系统不支持该函数,程序结束\n")); return 0; } PSYSTEM_LOGICAL_PROCESSOR_INFORMATION plpt = NULL; DWORD dwLength = 0; if (!pGetCpuInfo(plpt, &dwLength)) { if (ERROR_INSUFFICIENT_BUFFER == GetLastError()) { plpt = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); }else { _tprintf(_T("函数调用失败\n")); return 0; } } if (!pGetCpuInfo(plpt, &dwLength)) { _tprintf(_T("函数调用失败\n")); return 0; } DWORD dwSize = dwLength / sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION); PSYSTEM_LOGICAL_PROCESSOR_INFORMATION pOffsite = plpt; DWORD dwCacheCnt = 0; DWORD dwCoreCnt = 0; DWORD dwPackageCnt = 0; DWORD dwNodeCnt = 0; for (int i = 0; i < dwSize; i++) { switch (pOffsite->Relationship) { case RelationCache: dwCacheCnt++; break; case RelationNumaNode: dwNodeCnt++; break; case RelationProcessorCore: { if (pOffsite->ProcessorCore.Flags) { dwCoreCnt++; }else { dwCoreCnt += CountBits(pOffsite->ProcessorMask); } } break; case RelationProcessorPackage: dwPackageCnt++; break; default: break; } pOffsite++; } _tprintf(_T("处理器个数:%d\n"), dwPackageCnt); _tprintf(_T("处理器核数:%d\n"), dwCoreCnt); _tprintf(_T("NUMA个数:%d\n"), dwNodeCnt); _tprintf(_T("高速缓存的个数:%d\n"), dwCacheCnt); return 0; } DWORD CountBits(ULONG_PTR uMask) { DWORD dwTest = 1; DWORD LSHIFT = sizeof(ULONG_PTR) * 8 -1; dwTest = dwTest << LSHIFT; DWORD dwCnt = 0; for (int i = 0; i <= LSHIFT; i++) { dwCnt += ((uMask & dwTest) ? 1 : 0); dwTest /= 2; } return dwCnt; }这个程序首先采用两次调用的方式分配一个合适的缓冲区,用来接收函数的返回,然后分别统计CPU数目,物理处理器数目以及逻辑处理器数量。我们根据MSDN上面得到的信息,首先判断缓冲区中有多少个结构体,也就是由多少条处理器的信息,然后根据第二个成员的值,来判断当前存储的是哪种信息。并将对应的值加1,当计算逻辑处理器的数目时需要考虑超线程的问题,所谓超线程就是intel提供的一个新的技术,可以将一个处理器虚拟成多个处理器来使用,已达到多核处理器的效果,如果它支持超线程,那么久不能简单的根据是否为核心处理器而加1,这个时候需要采用计算位掩码的方式来统计逻辑存储器,根据MSDN上说的当flag为1时表示支持超线程。windows下的进程windows中进程是已装入内存中,准备或者已经在执行的程序,磁盘上的exe文件虽说可以执行,但是它只是一个文件,并不是进程,一旦它被系统加载到内存中,系统为它分配了资源,那么它就是一个进程。进程由两个部分组成,一个是系统内核用来管理进程的内核对象,一个是它所占的地址空间。windows下的进程主要分为3大类:控制台,窗口应用,服务程序。写过控制台与窗口程序的人都知道,控制台的主函数是main,而窗口应用的主函数是WinMain,那么是否可以根据这个来判断程序属于那种呢,很遗憾,windows并不是根据这个来区分的。在VS编译器上可以通过设置将Win32 控制台程序的主函数指定为WinMain,或者将窗口程序的主函数指定为main,设置方法:属性-->连接器-->系统-->子系统,将这项设置为/SUBSYSTEM:CONSOLE,那么它的主函数为main,设置为/SUBSYSTEM:WINDOWS那么它的主函数为WinMain,甚至我们可以自己设置主函数。我们知道在C/C++语言中main程序是从main函数开始的,但是这个函数只是语法上的开始,并不是真正意义上的入口,在VC++中,系统会首先调用mainCRTStartup,在这个函数中调用main或者WinMain, 这个函数主要负责对C/C++运行环境的初始化,比如堆环境或者C/C++库函数环境的初始化。如果需要自定义自己的入口,那么这些环境将得不到初始化,也就意味着我们不能使用C/C++库函数。只能使用VC++提供的API,这么做也有一定的好处,毕竟这些库函数都是在很早之前产生的,到现在来看有很多问题,有许多有严重的安全隐患,使用API可以避免这些问题。下面是一个简单的例子:#include <Windows.h> #include <tchar.h> #include <strsafe.h> #define PRINTF(...) \ {\ TCHAR szBuf[4096] = _T("");\ StringCchPrintf(szBuf, sizeof(szBuf), __VA_ARGS__);\ size_t nStrLen = 0;\ StringCchLength(szBuf, STRSAFE_MAX_CCH, &nStrLen);\ WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), szBuf, nStrLen, NULL, NULL);\ } int LHMain() { PRINTF(_T("this is a %s"), _T("test hello world\n")); return 0; }这个例子中,入口函数是LHMain我们完全使用API的方式来编写程序,如果想要做到自定义入口,那么需要进行这样的设置:属性-->高级-->入口点,在入口点中输出我们希望作为入口点的函数名称即可。入口参数各个参数的含义现在这部分主要说明main函数以及WinMain函数。int main()这是main函数的一种原型,这种原型不带入任何参数int main(int argc, char *argv[])这种原型主要接受命令行输入的参数,参数以空格隔开,第一个参数表示输入命令行参数的总数,第二个是一个字符串指针数组,每个字符串指针成员代表的是具体输入的命令以及参数,这些信息包括我们输入的程序的名称,比如我们输入test.exe -s -a -t 来启动程序,那么argc = 4 ,argv[0] = "test.exe" argv[1] = "-s" argv[2] = "-a" argv[3] = "-t"int main(int argc, char argv[], char envs[])这个原型中第一个和第二个参数的函数与上述的带有两个参数的main函数相同,多了一个表示环境变量的指针数组,它会将环境变量以”变量名=值“的形式来组织字符串。int WINAPI WinMain(HANDLE hInstance, HANDLE hPrevInstance, LPSTR lpszCmdLine, int nCmdShow);函数中的WINAPI表示函数的调用约定为__stdcall的调用方式,函数的调用方式主要与参数压栈方式以及环境回收的方式有关。在这就不再说明这部分的内容,有兴趣的可以看本人另外一篇博客专门讲解了这部分的内容,点击这里hInstance:进程的实例句柄,该值是exe文件映射到虚拟地址控件的基址hPrevInstance:上一个进程的实例句柄,为了防止进程越界访问,这个是16位下的产物,在32位下,这个没有作用。lpszCmdLine:启动程序时输入的命令行参数nCmdShow:表示程序的显示方式。进程的环境变量与工作路径进程的环境变量可以通过main函数的第三个参数传入,也可以在程序中利用函数GetEnvrionmentStrings和GetEnvrionVariable获取,下面是获取进程环境变量的简答例子: setlocale(CP_ACP, "chs"); TCHAR **ppArgs = NULL; int nArgCnt = 0; ppArgs = CommandLineToArgvW(GetCommandLine(), &nArgCnt); for (int i = 0; i < nArgCnt; i++) { _tprintf(_T("%d %s\n"), i, ppArgs[i]); } HeapFree(GetProcessHeap(), 0, ppArgs);函数的第一个参数是开启这个进程需要的完整的命令行字符串,这个字符串使用函数GetCommandLine来获取,第二个参数是一个接受环境变量的字符串指针数组。函数返回数组中元素个数。一般情况下不推荐使用环境变量的方式来保存程序所需的数据,一般采用文件或者注册表的方式,但是最好的办法是采用xml文件的方式来村粗。至于进程的工作目录可以通过函数GetCurrentDirectory方法获取。进程创建在windows下进程创建采用API函数CreateProcess,该函数的原型如下:BOOL CreateProcess( LPCWSTR pszImageName, LPCWSTR pszCmdLine, LPSECURITY_ATTRIBUTES psaProcess, LPSECURITY_ATTRIBUTES psaThread, BOOL fInheritHandles, DWORD fdwCreate, LPVOID pvEnvironment, LPWSTR pszCurDir, LPSTARTUPINFOW psiStartInfo, LPPROCESS_INFORMATION pProcInfo ); 该函数参数较多,下面对这几项做重点说明,其余的请自行查看MSDN。pszImageName:表示进程对应的exe文件所在的完整路径或者相对路径pszCmdLine:启动进程传入的命令行参数,这是一个字符串类型,需要注意的是,这个命令行参数可以带程序所在的完整路径,这样就可以将第一个参数设置为NULL。参数中的安全描述符表示的是创建的子进程的安全描述符,与当前进程的安全描述符无关。fInheritHandles表示,可否被继承,这个继承关系是指父进程中的信息能否被子进程所继承psiStartInfo规定了新进程的相关启动信息,主要有这样几个重要的值:对于窗口程序: LPTSTR lpTitle; DWORD dwX; DWORD dwY; DWORD dwXSize; DWORD dwYSize;这些值规定了窗口的标题和所在的屏幕位置与长高的相关信息,对于控制台程序,主要关注: HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError;标准输入、输出、以及标准错误 下面是一个创建控制台与创建窗口的简单例子: STARTUPINFO si = {0}; si.dwXSize = 400; si.dwYSize = 300; si.dwX = 10; si.dwY = 10; PROCESS_INFORMATION pi = {0}; SECURITY_ATTRIBUTES sa = {sizeof(SECURITY_ATTRIBUTES)}; //创建一个窗口应用的进程,其中szExePath表示进程所在exe文件的路径,而szAppDirectory表示exe所在的目录 CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, 0, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动控制台窗口,与父进程公用输入输出环境 ZeroMemory(szExePath, sizeof(TCHAR) * (MAX_PATH + 1)); StringCchPrintf(szExePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("SubConsole.exe")); ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, 0, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动控制台,在新的控制台上做输入输出 ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); CreateProcess(szExePath, szExePath, &sa, &sa, FALSE, CREATE_NEW_CONSOLE, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread);上述程序中,对于窗口程序,在创建时没有给出特别的创建标志,窗口本身就是一个个独立的,并且我们通过指定si的部分成员指定了窗口的显示位置,而对于控制台,如果在创建时不特别指定创建的标志,那么它将与父进程共享一个输入输出控制台。为了区分子进程和父进程的输入输出,一般通过标志CREATE_NEW_CONSOLE为新进程新建一个另外的控制台。进程输入输出重定向输入输出重定向的实现可以通过函数CreateProcess在参数psiStartInfo中的HANDLE hStdInput; HANDLE hStdOutput; HANDLE hStdError中指定,但是需要注意的是,在父进程中如果采用了Create之类的函数创建了输入输出对象的句柄时一定要指定他们可以被子进程所继承。下面是一个重定向的例子: //启动控制台,做输入输出重定向到文件中 TCHAR szFilePath[MAX_PATH + 1] = _T(""); //指定文件对象可以被子进程所继承,以便子进程可以使用这个内核对象句柄 sa.bInheritHandle = TRUE; ZeroMemory(&si, sizeof(STARTUPINFO)); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); StringCchPrintf(szFilePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("input.txt")); HANDLE hInputFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, &sa, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); ZeroMemory(szFilePath, sizeof(szFilePath)); StringCchPrintf(szFilePath, MAX_PATH, _T("%s%s"), szAppDirectory, _T("output.txt")); HANDLE hOutputFile = CreateFile(szFilePath, GENERIC_READ | GENERIC_WRITE, 0, &sa, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); si.hStdInput = hInputFile; si.hStdOutput = hOutputFile; si.dwFlags = STARTF_USESTDHANDLES; CreateProcess(szExePath, szExePath,NULL, NULL, TRUE, DETACHED_PROCESS, NULL, szAppDirectory, &si, &pi); WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(hOutputFile); CloseHandle(hInputFile); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); //启动ping命令,并将输出重定向到管道中 StringCchPrintf(szExePath, MAX_PATH, _T("ping 127.0.0.1")); DWORD dwLen = 0; BYTE byte[1024] = {0}; HANDLE hReadP = NULL; HANDLE hWriteP = NULL; sa.bInheritHandle = TRUE; CreatePipe(&hReadP, &hWriteP, &sa, 1024); si.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; si.hStdOutput = hWriteP; CreateProcess(NULL, szExePath, NULL, NULL, TRUE, DETACHED_PROCESS, NULL, szAppDirectory, &si, &pi); //关闭管道的写端口,不然读端口会被阻塞 CloseHandle(hWriteP); dwLen = 1000; DWORD dwRead = 0; while (ReadFile(hReadP, byte, dwLen, &dwRead, NULL)) { if ( 0 == dwRead ) { break; } //写入管道的数据为ANSI字符 printf("%s\n", (char*)byte); ZeroMemory(byte, sizeof(byte)); }进程的退出进程在遇到如下情况中的任意一种时会退出:进程中任意一个线程调用函数ExitProcess进程的主线程结束进程中最后一个线程结束调用TerminateProcess在这针对第2、3中情况作特别的说明:这两种情况看似矛盾不是吗,当主线程结束时进程就已经结束了,这个时候还会等到最后一个线程吗。其实真实的情况是主线程结束,进程结束这个限制是VC++上的,之前在自定义入口的时候说过,main函数只是语法上的,并不是实际的入口,在调用main之前会调用mainCRTStartup,这个函数会负责调用main函数,当main函数调用结束后,这个函数会隐式的调用ExitProcess结束进程,所以只有当我们自定了程序入口才会看到3所示的现象,下面的例子说明了这点:DWORD WINAPI ThreadProc(LPVOID lpParam); #define PRINT(s) \ {\ size_t sttLen = 0;\ StringCchLengthA(s, 1024, &sttLen);\ WriteConsoleA(GetStdHandle(STD_OUTPUT_HANDLE), s, sttLen, NULL, NULL);\ } int LHMain(int argc, char *argv[]) { CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); return 0; } DWORD WINAPI ThreadProc(LPVOID lpParam) { PRINT("main thread is ending,this is child thread\r\n"); return 0; }这种程序,如果我们采用系统中规定的main函数的话,是看不到线程中输出的信息的,因为主线程先结束的话,整个进程就结束了,线程还来不及输出,就被终止了。但是我们采用自定义入口的方式,屏蔽了这个特性,所以它会等到所有线程执行完成后才会结束,这个时候就会看到这句话输出了。进程在终止时会发生以下事件:关闭进程打开的对象句柄,但是对象本身不一定会关闭,这是因为每个对象都有一个计数器,每当有一个线程在使用这个对象时计数器会加1,而释放它的句柄时会减一,只有当计数器为0时才会销毁这个对象。对象是可以跨进程访问的,而且所有相同的对象在内存中只有一份,所以需要一个计数器以便在没有被任何进程访问的时候系统删除它。进程对象的状态设为有信号,以便所有等待该进程对象信号的函数(像函数WaitForSingleObject)能够正常返回。进程的终止状态从STILL_ACTIVE变成进程的退出码,可以通过这个特性判断某个进程是否在运行,具体的方式是通过函数GetExitProcess获取进程的终止码,如果函数返回STILL_ACTIVE,则说明进程仍在运行。
2016年09月06日
4 阅读
0 评论
0 点赞
2016-08-30
windows 安全模型简介
操作系统中有些资源是不能由用户代码直接访问的,比如线程进程,文件等等,这些资源必须由系统级代码由RING3层进入到RING0层操作,并且返回一些标识供用户程序使用,一般调用某个函数陷入到内核,这样的函数叫做系统调用,而有些不直接陷入到内核,一般叫做系统API,linux中使用系统调用,而windows中封装了一系列的API。windows对象与句柄windows对象操作系统为了安全,提供了一种保护机制,这种机制会禁止用户操作某些资源,避免用户过于关注细节,或者由于操作不当而造成系统崩溃。windows中将所有这些资源封装成了一个个对象。对象就是操作系统为了维护这些资源而定义的一系列的数据结构。对象这个词我们很容易联想到面向对象编程语言中的对象,对象其实就是对一些数据以及操作这些数据的一个封装,而windows是采用面向对象思想开发的,所以我们可以将windows中的对象想象成面向对象中的对象,而windows针对每种对象都提供了操作函数,这些函数一般都会以句柄作为第一个参数,这就有点像类函数中传入的this指针。而这操作对象的函数就是类函数。windows中总共有三种对象:GUI对象、GDI对象、内核对象。windows中的句柄windows中对象的操作是由系统提供的一系列的API函数来完成,这些函数有一个共同特点,就是以HANDLE 句柄作为第一个参数,windows中采用句柄来唯一标识每个内核对象。在windows中句柄的定义如下:typedef void * HANDLE从上面的定义可以看出,这个句柄应该是指向这个对象的结构体的指针。由于每次在程序启动时内存都是随机分配的,所以句柄不要使用硬编码的方式,同时在复制内核对像的时候,并不是简单的复制它的句柄,对象的复制有专门的函数,DuplicateHandle,该函数原型如下:BOOL DuplicateHandle( HANDLE hSourceProcessHandle, //源对象所在进程句柄 HANDLE hSourceHandle, //源对象句柄 HANDLE hTargetProcessHandle, //目标对象所在进程句柄 LPHANDLE lpTargetHandle, //返回目标对象的句柄 DWORD dwDesiredAccess, //以何种权限复制 BOOL bInheritHandle, //复制的对象句柄是否可继承 DWORD dwOptions //标记一些特殊的动作 );下面是一个例子代码:HANDLE hMutex = CreateMutex(NULL, FALSE, NULL); HANDLE hMutexDup, hThread; DWORD dwThreadId; DuplicateHandle(GetCurrentProcess(),hMutex,GetCurrentProcess(),&hMutexDup, 0,FALSE,DUPLICATE_SAME_ACCESS);上述代码在本进程中复制了一个互斥对象对象的句柄。windows 安全对象模型windows中的内核对象由进程和线程进行操作,而对象就好像一个被锁上的房间,进程想要访问对象,并对对象进程某种操作,就必须获取这个对象的钥匙,而线程就好像拥有钥匙的人,只有钥匙是对的,才可以访问。这把锁能够由不同的钥匙打开,这些钥匙信息存储在ACE中,而只有当ACE中的信息与访问字串这个钥匙匹配才可以打开。我们一般把这个钥匙称为访问字串(Token)访问字串访问字串主要包括:用户标识、组标识、优先权信息、以及其他访问信息。用户标识:用于唯一标识每个用户,就好像为每个用户都分配了一个唯一的用户ID组标识:用户所属组的唯一标识ID优先权:一般系统对每个用户以及它所属组分配了一些权限,而有的时候这些权限并不够,这个时候需要通过这个优先权信息额外新增一些权限当用户登录windows系统时,系统就为这个用户分配了一个带有该用户信息的访问字串,该用户创建的每个安全对象都有这个访问字串的拷贝,当用户打开的进程试图访问某个安全对象时系统就在对象的ACL中查找该用户是否有某项权限。有这个权限才能对对象进行这项操作。子进程的访问字串一般继承与父进程,但是子进程也可以自行创建访问字串来覆盖原来的访问字串。操作访问字串所使用的API主要有以下几个:OpenProcessToken //打开进程的访问令牌OpenThreadToken //打开线程的访问令牌AdjustTokenGroups //改变用户组的访问令牌AdjustTokenPrivileges //改变令牌的访问特权GetTokenInformation //获取访问令牌的信息SetTokenInformation //设置访问令牌信息下面主要介绍一下函数GetTokenInformation 的用法:BOOL WINAPI GetTokenInformation( __in HANDLE TokenHandle, //访问字串的句柄 __in TOKEN_INFORMATION_CLASS TokenInformationClass, //需要返回访问字串哪方面的信息 __out_opt LPVOID TokenInformation, //用于接收返回信息的缓冲 __in DWORD TokenInformationLength, //缓冲的长度 __out PDWORD ReturnLength //实际所需的缓冲的长度 ); 这个函数可以返回访问令牌多方面的信息,具体返回哪个方面的信息,需要通过第二个参数来指定,这个参数是一个枚举类型,表示需要获取的信息,每个方面的信息都定义了对应的结构体,这些信息可以由MSDN中查到。另外这个函数支持两次调用,第一次传入NULL指针和0长度,这样通过最后一个参数可以得到具体所需要的缓冲的大小。SID访问字串中用户与用户组采用安全标识符的方式唯一标识(Security Indentifer SID),系统中的SID是唯一的。它主要用来标识下面的这些内容:安全描述符中的所有者和用户组被ACE认可的访问者访问字串的用户和组SID的长度是可变的,在使用时不应该使用SID这个数据类型,因为这个时候还不知道需要的长度是多少,应该由系统来创建并返回它的指针,所以在使用时需要使用SID的指针。下面是一些操作SID的APIAllocateAndInitializeSid //初始化一个SIDFreeSid //释放一个SIDCopySid //拷贝一个SIDEqualSid //判断两个SID是否相等GetLengthSid //获取SID的长度IsValidSid //是否是有效的SIDConvertSidToStringSid //将SID转化为字符串的方式下面是一个利用这些API获取系统用户组的SID和当前登录用户的SID的例子:BOOL GetLoginSid(HANDLE hToken, PSID *ppsid); //SID本身大小是不可知的,所以在传入参数时应该传入2级指针,由函数自己决定大小 void FreeSid(PSID *ppsid); int _tmain(int argc, TCHAR* argv[]) { setlocale(LC_ALL, "chs"); HANDLE hProcess = GetCurrentProcess(); HANDLE hToken = NULL; OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hToken); PSID pSid = NULL; GetLoginSid(hToken, &pSid); LPTSTR pStringSid = NULL; ConvertSidToStringSid(pSid, &pStringSid); _tprintf(_T("当前登录的用户sid = %s\n"), pStringSid); FreeSid(&pSid); return 0; } BOOL GetLoginSid(HANDLE hToken, PSID *ppsid) { TOKEN_GROUPS *ptg = NULL; BOOL bSuccess = FALSE; DWORD dwLength = 0; if(!GetTokenInformation(hToken, TokenGroups, ptg, 0, &dwLength)) { if (ERROR_INSUFFICIENT_BUFFER != GetLastError()) { goto __CLEAN_UP; } ptg = (TOKEN_GROUPS*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); if (!GetTokenInformation(hToken, TokenGroups, ptg, dwLength, &dwLength)) { goto __CLEAN_UP; } } _tprintf(_T("共找到%d个组SID\n"), ptg->GroupCount); LPTSTR pStringSid = NULL; for (int i = 0; i < ptg->GroupCount; i++) { ConvertSidToStringSid(ptg->Groups[i].Sid, &pStringSid); _tprintf(_T("\t id = %d sid = %s"), i, pStringSid); if ((ptg->Groups[i].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID) { _tprintf(_T("此用户为当前登录的用户")); *ppsid = (PSID)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwLength); CopySid(dwLength, *ppsid, ptg->Groups[i].Sid); } _tprintf(_T("\n")); } __CLEAN_UP: if (ptg != NULL) { HeapFree(GetProcessHeap(), 0, ptg); } return bSuccess; } void FreeSid(PSID *ppsid) { HeapFree(GetProcessHeap(), 0, *ppsid); *ppsid = NULL; }这个例子是MSDN中的一个例子,上述代码中首先获取进程的访问令牌,然后通过函数GetTokenInformation 获取访问令牌的信息。通过传入TokenGroups这个值获取当前用户所在用户组的访问字串。并将信息保存到结构体TOKEN_GROUPS中最后通过(ptg->Groups[i].Attributes & SE_GROUP_LOGON_ID) == SE_GROUP_LOGON_ID这样一个表达式来判断当前的SID是否是登录用户的。优先权优先权是由字符串标识的局部唯一的标识符(LUID)优先权是由系统管理员分配给对应的用户,一般不能通过编程的方式提升用户的优先权,但是有时候即使用户具有某个优先权,但是它启动的程序并不具有相关的优先权。这个时候可以通过编程的方式提升用户进程特权。系统有3个值代表一个优先权:字符串名,整个系统上都有意义,称为全局名,这个字符串并不是一个可读的字符串,显示出来的信息不一定能看得懂。显示给用户的可读名称;如:改变系统时间(可以在组策略中查看)每个计算机都不同的局部值;下面有几个常用的优先权:#define SE_DEBUG_NAME TEXT("SeDebugPrivilege") //调试进程 #define SE_LOAD_DRIVER_NAME TEXT("SeLoadDriverPrivilege") //装载驱动 #define SE_LOCK_MEMORY_NAME TEXT("SeLockMemoryPrivilege") //锁定内存页面 #define SE_SHUTDOWN_NAME TEXT("SeShutdownPrivilege") //关机下面是几个优先权函数LookupPrivilegeValue //查询优先权的值LookupPrivilegeDisplayName //查询优先权的输出名LookupPrivilegeName //查询优先权的名称PrivilegeCheck //优先权信息检查下面是一个获取用户特权信息的代码:int _tmain(int argc, TCHAR *argv[]) { setlocale(LC_ALL, "chs"); HANDLE hToken = NULL; HANDLE hProcess = GetCurrentProcess(); OpenProcessToken(hProcess, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken); TOKEN_PRIVILEGES* ptg = NULL; DWORD dwTgSize = 0; TCHAR szName[255] = _T(""); TCHAR szDisplay[255] = _T(""); DWORD languageID = GetUserDefaultLangID(); if (!GetTokenInformation(hToken, TokenPrivileges, ptg, 0, &dwTgSize)) { if (ERROR_INSUFFICIENT_BUFFER != GetLastError()) { _tprintf(_T("获取令牌失败\n")); return 0; } ptg = (TOKEN_PRIVILEGES*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwTgSize); if (!GetTokenInformation(hToken, TokenPrivileges, ptg, dwTgSize, &dwTgSize)) { _tprintf(_T("读取令牌信息失败\n")); return 0; } } _tprintf(_T("用户的特权信息:\n")); for(int i = 0; i < ptg->PrivilegeCount; i++) { DWORD dwName = sizeof(szName) / sizeof(TCHAR); DWORD dwDisplay = sizeof(szDisplay) / sizeof(TCHAR); LookupPrivilegeName(NULL, &ptg->Privileges[i].Luid, szName, &dwName); LookupPrivilegeDisplayName(NULL, szName, szDisplay, &dwDisplay, &languageID); _tprintf(_T("\t 特权名%s %s"), szName, szDisplay); if (ptg->Privileges[i].Attributes & ((SE_PRIVILEGE_ENABLED | SE_PRIVILEGE_ENABLED_BY_DEFAULT))) { _tprintf(_T("特权开放\n")); }else { _tprintf(_T("特权关闭\n")); } } HeapFree(GetProcessHeap(), 0, ptg); return 0; }下面是进程提权的代码:int _tmain(int argc, TCHAR *argv[]) { HANDLE hToken = NULL; HANDLE hProcess = GetCurrentProcess(); SetPrivileges(hToken, SE_TCB_NAME , TRUE); return 0; } BOOL SetPrivileges(HANDLE hToken, LPTSTR lpPrivilegesName, BOOL bEnablePrivilege) { //获取Token的特权信息 LUID uid = {0}; LookupPrivilegeValue(NULL, lpPrivilegesName, &uid); TOKEN_PRIVILEGES tp = {0}; tp.PrivilegeCount = 1; tp.Privileges[0].Luid = uid; if (bEnablePrivilege) { tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; }else { tp.Privileges[0].Attributes = 0; } AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL); if ( GetLastError() == ERROR_NOT_ALL_ASSIGNED ) { _tprintf(_T("分配指定的特权(%s)失败. \n"),lpPrivilegesName); return FALSE; } return TRUE; }安全描述符安全描述符中通常包含信息:所有者、主组、任选访问控制列表(DACL)、系统访问控制列表(SACL)安全描述符是以SECURITE_DESCRIPTOR结构开始,后面连续跟着安全描述符的其它信息访问控制列表访问控制列表(Access Control List ACL)主要由多个访问访问控制入口(Access Control Entries ACE)组成。ACE用于标识一个用户、组或局部组以及它们中每一个允许的访问权;安全描述符的创建在创建安全访问对象的函数中一般都需要填入一个SECURITY_ATTRIBUTES结构体的指针,我们要么给定一个NULL值使其具有默认的安全属性,或者自己创建一个安全描述符并将他的指针传入。创建一个安全描述符主要有下面几步:用函数 AllocateAndInitializeSid 创建用户的SID。函数的定义如下:BOOL WINAPI AllocateAndInitializeSid( __in PSID_IDENTIFIER_AUTHORITY pIdentifierAuthority, //用于表示该SID标识的颁发机构 __in BYTE nSubAuthorityCount, //SID有多少个子部分 __in DWORD dwSubAuthority0, //第0个子部分 __in DWORD dwSubAuthority1, __in DWORD dwSubAuthority2, __in DWORD dwSubAuthority3, __in DWORD dwSubAuthority4, __in DWORD dwSubAuthority5, __in DWORD dwSubAuthority6, __in DWORD dwSubAuthority7, __out PSID* pSid //返回一个PSID的指针 );SID主要由一个颁发机构以及一个或者多个32位的唯一的RID组成这些RID通过参数dwSubAuthority0到dwSubAuthority7生成。对应的用户SID都有固定的组合。这个我还没找到具体的用户与SID是如何定义的。为该用户SID分配访问控制权限,主要通过填充结构体EXPLICIT_ACCESS成员来实现,这个结构体的定义如下:typedef struct _EXPLICIT_ACCESS { DWORD grfAccessPermissions; //制定用户权限 ACCESS_MODE grfAccessMode; //用于表示允许、拒绝、审查特定用户的权限 DWORD grfInheritance; //当前的权限是否可以继承 TRUSTEE Trustee; //这是一个访问托管 } EXPLICIT_ACCESS, *PEXPLICIT_ACCESS;3.用API SetEntriesInAcl将上述结构体放入到ACL中分配并初始化SECURITY_DESCRIPTOR结构体,初始化该结构体所用到的API 是InitializeSecurityDescriptor将SECURITY_DESCRIPTOR结构加入到安全描述符中,安全描述符的结构体是:SECURITY_ATTRIBUTES下面是一个具体的例子(这个例子来自MSDN): SID_IDENTIFIER_AUTHORITY SIDAuthorityNT = SECURITY_WORLD_SID_AUTHORITY; SID_IDENTIFIER_AUTHORITY SIDAuthorityWord = SECURITY_NT_AUTHORITY; PSID pEveryOneSid = NULL; PSID pAdminSid = NULL; EXPLICIT_ACCESS ea[2] = {0}; PACL pAcl = NULL; //创建everyone用户的SID AllocateAndInitializeSid(&SIDAuthorityWord, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &pEveryOneSid); PSECURITY_DESCRIPTOR pSD = NULL; //定义everyone 的访问控制信息 ea[0].grfAccessMode = SET_ACCESS; //用于表示允许、拒绝、审查特定用户的权限 ea[0].grfAccessPermissions = KEY_READ; //制定用户权限 ea[0].grfInheritance = FALSE; ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[0].Trustee.ptstrName = (LPTSTR)pEveryOneSid; //创建administor用户组的SID AllocateAndInitializeSid(&SIDAuthorityNT, 2, SECURITY_BUILTIN_DOMAIN_RID,DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pAdminSid); ea[1].grfAccessMode = SET_ACCESS; ea[1].grfAccessPermissions = KEY_ALL_ACCESS; ea[1].grfInheritance = FALSE; ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; ea[1].Trustee.TrusteeType = TRUSTEE_IS_GROUP; ea[1].Trustee.ptstrName = (LPTSTR)pAdminSid; //将以上两个SID加入到ACL中 SetEntriesInAcl(2, ea, NULL, &pAcl); pSD = (PSECURITY_DESCRIPTOR) HeapAlloc(GetProcessHeap(), 0, SECURITY_DESCRIPTOR_MIN_LENGTH); //初始化一个SECURITY_DESCRIPTOR结构 InitializeSecurityDescriptor(&pSD, SECURITY_DESCRIPTOR_REVISION); //将SECURITY_DESCRIPTOR结构加入到安全描述符中 SECURITY_ATTRIBUTES sa = {0}; sa.bInheritHandle = FALSE; sa.lpSecurityDescriptor = pSD; sa.nLength = sizeof(SECURITY_ATTRIBUTES); HKEY hkSub = NULL; DWORD dwDisposition = 0; RegCreateKeyEx(HKEY_CURRENT_USER, _T("mykey"), 0, _T(""), 0, KEY_READ | KEY_WRITE, &sa, &hkSub, &dwDisposition); if (pEveryOneSid) { FreeSid(pEveryOneSid); } if (pAdminSid) { FreeSid(pAdminSid); } if (pAcl) { LocalFree(pAcl); } HeapFree(GetProcessHeap(), 0, pSD); if (hkSub) { RegCloseKey(hkSub); }
2016年08月30日
4 阅读
0 评论
0 点赞
2016-08-16
windows 异常处理
为了程序的健壮性,windows 中提供了异常处理机制,称为结构化异常,异常一般分为硬件异常和软件异常,硬件异常一般是指在执行机器指令时发生的异常,比如试图向一个拥有只读保护的页面写入内容,或者是硬件的除0错误等等,而软件异常则是由程序员,调用RaiseException显示的抛出的异常。对于一场处理windows封装了一整套的API,平台上提供的异常处理机制被叫做结构化异常处理(SEH)。不同于C++的异常处理,SEH拥有更为强大的功能,并且采用C风给的代码编写方式。异常处理机制的流程简介一般当程序发生异常时,用户代码停止执行,并将CPU的控制权转交给操作系统,操作系统接到控制权后,将当前线程的环境保存到结构体CONTEXT中,然后查找针对此异常的处理函数。系统利用结构EXCEPTION_RECORD保存了异常描述信息,它与CONTEXT一同构成了结构体EXCEPTION_POINTERS,一般在异常处理中经常使用这个结构体。异常信息EXCEPTION_RECORD的定义如下typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; //异常码 DWORD ExceptionFlags; //标志异常是否继续,标志异常处理完成后是否接着之前有问题的代码 struct _EXCEPTION_RECORD* ExceptionRecord; //指向下一个异常节点的指针,这是一个链表结构 PVOID ExceptionAddress; //异常发生的地址 DWORD NumberParameters; //异常附加信息 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常的字符串 } EXCEPTION_RECORD, *PEXCEPTION_RECORD;当系统在用户程序中查找异常处理代码时主要通过查找当前的这个链表。下面详细说明异常发生时操作系统是如何处理的:如果程序是被调试运行的(比如我们在VS编译器中调试运行程序),当异常发生时,系统首先将异常信息交给调试程序,如果调试程序处理了那么程序继续运行,否则系统便在发生异常的线程栈中查找可能的处理代码。若找到则处理异常,并继续运行程序如果在线程栈中没有找到,则再次通知调试程序,如果这个时候仍然不能处理这个异常,那么操作系统会对异常进程默认处理,比如强制终止程序。SEH的基本框架结构化异常处理一般有下面3个部分组成:保护代码体过滤表达式异常处理块其中保护代码体:是指有可能发生异常的代码,一般在SEH中是用__try{}包含的那部分过滤表达式:是在__except表达式的括号中的部分,一般可以是函数或者表达式,过滤表达式一般只能返回3个值:EXCEPTION_CONTINUE_SEARCH表示继续向下寻找异常处理的程序,也就是说本__exception不能处理这个异常;EXCEPTION_CONTINUE_EXECUTION表示异常已被处理,继续执行当初发生异常的代码;EXCEPTION_EXECUTE_HANDLER:表示异常已被处理,直接跳转到__exception(){}代码块中执行,这个时候就有点像C++中的异常处理了。一般一个__try块可以跟随多个__except块异常处理块:是指__except大括号中的代码块另外可以在过滤表达式中调用GetExceptionCode和GetExceptionInformagtion函数取得正在处理的异常信息,这两个函数不能再过滤表达式中使用,但是可以作为过滤表达式中的函数参数。下面是一个异常处理的简单的例子:#define PAGELIMIT 1024 DWORD dwPageCnt = 0; LPVOID lpPrePage = NULL; DWORD dwPageSize = 0; INT FilterFunction(DWORD dwExceptCode) { if(EXCEPTION_ACCESS_VIOLATION != dwExceptCode) { return EXCEPTION_EXECUTE_HANDLER; } if(dwPageCnt >= PAGELIMIT) { return EXCEPTION_EXECUTE_HANDLER; } if(NULL == VirtualAlloc(lpPrePage, dwPageSize, MEM_COMMIT, PAGE_READWRITE)) { return EXCEPTION_EXECUTE_HANDLER; } lpPrePage = (char*)lpPrePage + dwPageSize; dwPageCnt++; return EXCEPTION_CONTINUE_EXECUTION; } int _tmain(int argc, TCHAR *argv[]) { SYSTEM_INFO si = {0}; GetSystemInfo(&si); dwPageSize = si.dwPageSize; char* lpBuffer = (char*)VirtualAlloc(NULL, dwPageSize * PAGELIMIT, MEM_RESERVE, PAGE_READWRITE); lpPrePage = lpBuffer; for(int i = 0; i < PAGELIMIT * dwPageSize; i++) { __try { lpBuffer[i] = 'a'; } __except(FilterFunction(GetExceptionCode())) { ExitProcess(0); } } VirtualFree(lpBuffer, dwPageSize * PAGELIMIT, MEM_FREE); return 0; }这段代码我们通过结构化异常处理实现了内存的按需分配,首先程序保留了4M的地址空间,但是并没有映射到具体的物理内存,接着向这4M的空间中写入内容,这个时候会造成非法的内存访问异常,系统会执行过滤表达式中调用的函数,在函数中校验异常的异常码,如果不等于EXCEPTION_ACCESS_VIOLATION,也就是说这个异常并不是读写非法内存造成的,那么直接返回EXCEPTION_EXECUTE_HANDLER,这个时候会执行__exception块中的代码,也就是结束程序,如果是由于访问非法内存造成的,并且读写的范围没有超过4M那么就提交一个物理页面供程序使用,并且返回EXCEPTION_CONTINUE_EXECUTION,让程序接着从刚才的位置执行也就是说再次执行写入操作,这样保证了程序需要多少就提交多少,节约了物理内存。终止处理块终止处理块是结构化异常处理特有的模块,它保证了当__try块执行完成后总会执行终止处理块中的代码。一般位于__finally块中。只有当线程在__try中结束,也就是在__try块中调用ExitProcess或者ExitThread。由于系统为了保证__try块结束后总会调用__finally所以某些跳转语句如:goto return break等等就会添加额外的机器码以便能够跳入到__try块中,所以为了效率可以用__leave语句代替这些跳转语句。另外需要注意的一点是一个__try只能跟一个__finally块但是可以跟多个__except块。同时__try块后面要么跟__except要么跟__finally这两个二选一,不能同时跟他们两个。抛出异常在SEH中抛出异常需要使用函数:RaiseException,它的原型如下:void WINAPI RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, const ULONG_PTR* lpArguments);第一个是异常代码,第二个参数是异常标志,第三个是异常参数个数,第四个是参数列表,这个函数主要是为了填充EXCEPTION_RECORD结构体并将这个节点添加到链表中,当发生异常时系统会查找这个链表,下面是一个简单的例子:DWORD FilterException() { wprintf(_T("1\n")); return EXCEPTION_EXECUTE_HANDLER; } int _tmain(int argc, TCHAR *argv[]) { __try { __try { RaiseException(1, 0, 0, NULL); } __finally { wprintf(_T("2\n")); } } __except(FilterException()) { wprintf(_T("3\n")); } _tsystem(_T("PAUSE")); return 0; }上面的程序使用RaiseException抛出一个异常,按照异常处理的流程,程序首先会试着执行FilterException,以便处理这个异常,所以首先会输出1,然后根据返回值EXCEPTION_EXECUTE_HANDLER决定下一步会执行异常处理块__except中的内容,这个时候也就表示最里面的__try块执行完了,在前面说过,不管遇到什么情况,执行完__try块,都会接着执行它对应的__finally块,所以这个时候会首先执行__finally块,最后执行外层的__except块,最终程序输出结果为1 2 3win32下的向量化异常处理为什么向量化异常要强调是win32下的呢,因为64位windows不支持这个特性理解这个特性还是回到之前说的操作系统处理异常的顺序上面,首先会交给调试程序,然后再由用户程序处理,根据过滤表达式返回的值决定这个异常是否被处理,而这个向量化异常处理,就是将异常处理的代码添加到这个之前,它的代码会先于过滤表达式之前执行。我们知道异常是由内层向外层一层一层的查找,如果在内层已经处理完成,那么外层是永远没有机会处理的,这种情况在我们使用第三方库开发应用程序,而这个库又不提供源码,并且当发生异常时这个库只是简单的将线程终止,而我们想处理这个异常,但是由于内部处理了,外层的try根本捕获不到,这个时候就可以使用向量化异常处理了。这样我们可以编写异常处理代码先行处理并返回继续执行,这样库中就没有机会处理这个异常了。使用这个机制通过AddVectoredExceptionHandler函数可以添加向量化异常处理过滤函数,而调用RemoveVectoredExceptionHandler可以移除一个已添加的向量化异常处理过滤函数。下面是一个简单的例子:int g_nVal = 0; void Func(int nVal) { __try { nVal /= g_nVal; } __except(EXCEPTION_EXECUTE_HANDLER) { printf("正在执行Func中的__try __except块\n"); ExitProcess(0); } } LONG CALLBACK VH1(PEXCEPTION_POINTERS pExceptionInfo) { printf("正在执行VH1()函数\n"); return EXCEPTION_CONTINUE_SEARCH; } LONG CALLBACK VH2(PEXCEPTION_POINTERS pExceptionInfo) { printf("正在执行VH2()函数\n"); if (EXCEPTION_INT_DIVIDE_BY_ZERO == pExceptionInfo->ExceptionRecord->ExceptionCode) { g_nVal = 25; return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } LONG CALLBACK VH3(PEXCEPTION_POINTERS pExceptionInfo) { printf("正在执行VH3()函数\n"); return EXCEPTION_CONTINUE_SEARCH; } LONG SEH1(EXCEPTION_POINTERS *pEP) { //除零错误 if (EXCEPTION_INT_DIVIDE_BY_ZERO == pEP->ExceptionRecord->ExceptionCode) { g_nVal = 34; return EXCEPTION_EXECUTE_HANDLER; } return EXCEPTION_CONTINUE_SEARCH; } int _tmain(int argc, TCHAR *argv[]) { LPVOID lp1 = AddVectoredExceptionHandler(0, VH1); LPVOID lp2 = AddVectoredExceptionHandler(0, VH2); LPVOID lp3 = AddVectoredExceptionHandler(1, VH3); __try { Func(g_nVal); printf("Func()函数执行完成后g_nVal = %d\n", g_nVal); } __except(SEH1(GetExceptionInformation())) { printf("正在执行main()中的__try __except块"); } RemoveVectoredExceptionHandler(lp1); RemoveVectoredExceptionHandler(lp2); RemoveVectoredExceptionHandler(lp3); return 0; }上述的程序模拟了调用第三方库的情况,比如我们调用了第三方库Func进行某项操作,我们在外层进行了异常处理,但是由于在Func函数中有异常捕获的代码,所以不管外层如何处理,总不能捕获到异常,外层的异常处理代码总是不能执行,这个时候我们注册了3个向量处理函数,由于VH1返回的是EXCEPTION_CONTINUE_SEARCH,这个时候会在继续执行后面注册的向量函数——VH2,VH2返回EXCEPTION_CONTINUE_SEARCH,会继续执行VH3,VH3还是返回EXCEPTION_CONTINUE_SEARCH,那么它会继续执行库函数内层的异常处理,内层的过滤表达式返回EXCEPTION_EXECUTE_HANDLER,这个时候会继续执行异常处理块中的内容,结束程序,如果我们将3个向量函数中的任何一个的返回值改为EXCEPTION_CONTINUE_EXECUTION,那么库中的异常处理块中的内容将不会被执行。函数AddVectoredExceptionHandler中填入的处理函数也就是上述代码中的VH1 VH2 VH3只能返回EXCEPTION_CONTINUE_EXECUTION和EXCEPTION_CONTINUE_SEARCH,对于其他的值操作系统不认。将SEH转化为C++异常C++异常处理并不能处理所有类型的异常而将SEH和C++异常混用,可以达到使用C++异常处理处理所有异常的目的要混用二者需要在项目属性->C/C++->代码生成->启动C++异常的选项中打开SEH开关。在混用时可以在SEH的过滤表达式的函数中使用C++异常,当然最好的方式是将SEH转化为C++异常。通过调用_set_se_translator这个函数指定一个规定格式的回调函数指针就可以利用标准C++风格的关键字处理SEH了。下面是它们的定义:_set_se_translator(_se_translator_function seTransFunction); typedef void (*_se_translator_function)(unsigned int, struct _EXCEPTION_POINTERS* );使用时,需要自定义实现_se_translator_function函数,在这个函数中通常可以通过throw一个C++异常的方式将捕获的SEH以标准C++EH的方式抛出下面是一个使用的例子:class SE_Exception { public: SE_Exception(){}; SE_Exception(DWORD dwErrCode) : dwExceptionCode(dwErrCode){}; ~SE_Exception(){}; private: DWORD dwExceptionCode; }; void STF(unsigned int ui, PEXCEPTION_POINTERS pEp) { printf("执行STF函数\n"); throw SE_Exception(); } void Func(int i) { int x = 0; int y = 5; x = y / i; } int _tmain(int argc, TCHAR *argv[]) { try { _set_se_translator(STF); Func(0); } catch(SE_Exception &e) { printf("main 函数中捕获到异常 \n"); } return 0; }程序首先调用_set_se_translator函数定义了一个回掉函数,当异常发生时,系统调用回掉函数,在函数中抛出一个自定义的异常类,在主函数中使用C++的异常处理捕获到了这个异常并成功输出了一条信息。
2016年08月16日
5 阅读
0 评论
0 点赞
1
...
26
27
28
...
31