浅谈Windows API编程 (这个经典)

原文地址:http://blog.sina.com.cn/s/blog_46d85b2a01010qpt.html

WinSDK是编程中的传统难点,个人写的WinAPI程序也不少了,其实之所以难就难在每个调用的API都包含着Windows这个操作系统的潜规则或者是windows内部的运行机制……

WinSDK是编程中的传统难点,曾经听有一个技术不是很好的朋友乱说什么给你API谁都会用,其实并非那么简单,个人写的WinAPI程序也不少了,其实之所以难就难在每个调用的API都包含着Windows这个操作系统的潜规则或者是windows内部的运行机制。

首先来谈谈句柄,初学习WinSDK的朋友刚看到这个词头大了吧?其实我也是了,我们来看看programming windows里面是怎么说的,一个句柄仅仅是用来识别某些事情的数字。它唯一的标识这当前的一个实例。这样说确实不容易懂。那么我们这么看,比如你打开 windows自带的计算器。你多打开几次是不是桌面上出现了很多个计算器呢?你使用其中一个计算器的时候当你按下等于按钮的时候运算结果是否会出现在其他的计算机结果栏里?不会,那windows怎么知道让结果出现在哪里呢?这就是句柄的作用了,句柄唯一的标识着一个程序,你打开的每一个窗口(计算器) 都有一个不同的句柄你你每一步操作都是指定了在某个句柄下的,所以,他不会出错。而且你打开的每一个计算机都共享着同样的代码和内存。通过句柄系统会把所需的资源充分的调用到当前的某个程序自己的数据区。

不仅是窗口,各种菜单,GDI对象都有自己的句柄,获取句柄的手段也是多重多样,不过当然是通过调用API函数实现了,如:

MFC中的hHandle = GetSafeHandle();

API编程中的hBrush = GetStorkObject(BLACK_BRUSH);

很多操作都需要将句柄添加到参数列表中,当你没有直接定义句柄变量的时候可能要记忆很多API的返回类型来间接获取。如:

      hPen = SelectObject(hdc,GetStockObject(&logicpen));
// SelectObject()这个函数在设置本设备描述表下的GDI对象时会返回设置前的GDI对象句柄
MoveToEx(hdc, pt1.x, pt1.y, &apt);
LineTo(hdc, pt2.x,pt2.y);
SelectObject(hdc,hPen);

完成选择自定义的GDI对象的操作。句柄的种类很多,掌握一种的使用方法所有的不学自通,WinAPI编程永远伴随的元素中句柄是其中之一。非常重要。由于是浅谈,所以就说到这里了.

接下来是windows下的消息映射机制了,呵呵,窗口过程,刚学的朋友难理解吧?WinSDK编程基于C,但是和C的理念有着完全的不同,这中间的不同,在我看来最多的也就是来自于这个消息映射,后面什么吹的很炫的Hook技术,木马技术,键盘截获,都是来自于特殊消息的捕捉,映射自定义的特殊消息来实现的(当然和我接下来谈的稍微有点不同)。

首先我们应该先明白消息和事件的区别,Windows是消息驱动的操作系统,这里的消息的产生来自于某个实例化的对象上用户的操作,来自控件,菜单,或者是系统本身产生的,而事件是靠消息触发的,但这也不是绝对的。可以用一个简单的例子去解释,我这里越写越觉得自己难表达清楚,就比如这么一个例子:“某男杀人这条消息导致被枪毙这个事件”不过最重要的区别是在消息产生后并不会被直接处理,而是先插入windows系统的消息队列,然后系统判断此消息产生于哪个程序,送入此程序的消息循环,由LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam)处理。而事件是操作系统处理消息的过程中反馈的结果。

用户操作-> 产生消息->发送系统->系统判断来源->发给相应的窗口过程或者其他Callback函数->消息处理->等待下一条消息的产生

以上为消息循环整个过程。

      LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam);
int WINAPI WinMain(…)
{
MSG msg;
RegisterClass(…); // 注册窗口类
CreateWindow(…); // 创建窗口
ShowWindow(…); // 显示窗口
UpdateWindow(…);
While(GetMessage(&msg,…)){ // 消息循环
TranslateMessage(…);
DispatchMessage(…);
}
LRSULT CALLBACK winprc(hwnd , uint,wParam,lParam);
//窗口过程函数,用于映射switch语句中各个需要被处理的消息
{
While((UINT)message)
{
Switch(message)
Case…
Case…
………
Default……….
}
}

以上是最基本的WinAPi编程的代码结构。其实这里面最重要的结构莫过于while(GetMessage(&msg))和 Winproc这个函数,这也是传统的C面向过程编程的区别,win编程总等着特定事件触发对应的消息映射函数来完成代码功能,并不是一条代码从头走到尾。关于特殊消息的映射,这里不谈,这里仅是个入门指引。

最后谈一点就是重绘问题。其实在我看来这个东西更多是属于GDI编程里面的东西,说起来其实难度不大,但是处理起来确实是个难点。先拿刚才的代码来说吧。先添加一条关于WM_LBUTTONDOWN的消息映射:

      Static int apt[2];
case WM_LBUTTONDOWN:
hdc = GetDC(hwnd);
apt[1].x = LOWORD (lParam);
apt[1].y = HIWORD (lParam);
hPen = CreatePen(BLACK_PEN,3,RGB(125,125,125));
SelectObject(hdc,hPen);
MoveToEx(hdc,apt[0].x,apt[0].y,NULL);
LineTo(hdc,apt[1].x,apt[1].y);
apt[0].x = apt[1].x;
apt[0].y = apt[1].y;
DeleteObject(hPen);
ReleaseDC(hwnd,hdc);
return 0;

这段代码实现一个简单的画线功能,当你在你的客户区胡点一通鼠标后试着拖动一下窗口大小,或者将其最小化或者被其他窗口覆盖一下你都会发现你原来画的线没了,可是其他窗口为什么被覆盖了以后再弹出窗口还会有原来的东西呢?那就是重绘,要重新绘制整个客户区(准确的说是失效的矩形),以上说的操作都会导致你的客户区失效,这时会产生重绘消息WM_PAINT,我们要想保存这些线那么我们就必须保存这些你用鼠标左键点过的点。当然这是重绘技术中最简单的,当你的客户区上是一个复杂的画面的话,就不仅仅需要保存点,还有各种形状的图形,颜色等等……这里给大家一段我自己写的代码来实现以上的 WM_LBUTTONDOWN消息映射来产生的点。通过单链表来动态添加点来实现重绘。

      case WM_PAINT:
hdc = BeginPaint(hwnd,&ps);
TextOut(hdc,cxClient/6,cyClient/6,TEXT("图形重绘"),strlen("图形重绘"));
ReDrawLines(&MyList,hdc);
EndPaint(hwnd,&ps);
return 0;
case WM_LBUTTONDOWN:
hdc = GetDC(hwnd);
apt[1].x = LOWORD (lParam);
apt[1].y = HIWORD (lParam);
hPen = CreatePen(BLACK_PEN,2,RGB(125,0,0));
SelectObject(hdc,hPen);
MoveToEx(hdc,apt[0].x,apt[0].y,NULL);
LineTo(hdc,apt[1].x,apt[1].y);
MyList.pCurrent->x = apt[0].x;
MyList.pCurrent->y = apt[0].y;
MyList.pCurrent->pNext->x = apt[1].x;
MyList.pCurrent->pNext->y = apt[1].y;
MyList.m_iCounter = MyList.m_iCounter+2;
MyList.pCurrent = MyList.pCurrent->pNext->pNext;
apt[0].x = apt[1].x;
apt[0].y = apt[1].y;
DeleteObject(hPen);
ReleaseDC(hwnd,hdc);
return 0;

其中的重绘函数代码如下:

      void ReDrawLines(LinkList* pLinkList,HDC hdc)
{
pMyPoint p = pLinkList->pHead;
int iSaver =pLinkList->m_iCounter;
while(iSaver!=0)
{
MoveToEx(hdc,p->x,p->y,NULL);
LineTo(hdc,p->pNext->x,p->pNext->y);
p=p->pNext->pNext;
iSaver=iSaver-2;
}
}

添加了以上的代码你会发现再次拖动窗口大小等等你原来画的线就都能重现出来了。呵呵是不是觉得一个看似简单的东西其实里面需要很多代码实现呢?也许,这就是windows.

好了,WinSDK入门的东西就谈这么多,希望能给初学者一定的帮助,这么多的字,都是我一个一个打出来没有任何借鉴和摘抄的。相信做为一个过来人能更多的理解大家学习中的困难。

Win32环境下动态链接库(DLL)编程原理

比较大应用程序都由很多模块组成,这些模块分别完成相对独立的功能,它们彼此协作来完成整个软件系统的工作。其中可能存在一些模块的功能较为通用,在构造其它软件系统时仍会被使用。在构造软件系统时,如果将所有模块的源代码都静态编译到整个应用程序EXE文件中,会产生一些问题:一个缺点是增加了应用程序的大小,它会占用更多的磁盘空间,程序运行时也会消耗较大的内存空间,造成系统资源的浪费;另一个缺点是,在编写大的EXE程序时,在每次修改重建时都必须调整编译所有源代码,增加了编译过程的复杂性,也不利于阶段性的单元测试。

Windows系统平台上提供了一种完全不同的较有效的编程和运行环境,你可以将独立的程序模块创建为较小的DLL(Dynamic Linkable Library)文件,并可对它们单独编译和测试。在运行时,只有当EXE程序确实要调用这些DLL模块的情况下,系统才会将它们装载到内存空间中。这种方式不仅减少了EXE文件的大小和对内存空间的需求,而且使这些DLL模块可以同时被多个应用程序使用。Microsoft Windows自己就将一些主要的系统功能以DLL模块的形式实现。例如IE中的一些基本功能就是由DLL文件实现的,它可以被其它应用程序调用和集成。

一般来说,DLL是一种磁盘文件(通常带有DLL扩展名),它由全局数据、服务函数和资源组成,在运行时被系统加载到进程的虚拟空间中,成为调用进程的一部分。如果与其它DLL之间没有冲突,该文件通常映射到进程虚拟空间的同一地址上。DLL模块中包含各种导出函数,用于向外界提供服务。Windows 在加载DLL模块时将进程函数调用与DLL文件的导出函数相匹配。

在Win32环境中,每个进程都复制了自己的读/写全局变量。如果想要与其它进程共享内存,必须使用内存映射文件或者声明一个共享数据段。DLL模块需要的堆栈内存都是从运行进程的堆栈中分配出来的。

DLL现在越来越容易编写。Win32已经大大简化了其编程模式,并有许多来自AppWizard和MFC类库的支持。

一、导出和导入函数的匹配

DLL文件中包含一个导出函数表。这些导出函数由它们的符号名和称为标识号的整数与外界联系起来。函数表中还包含了DLL中函数的地址。当应用程序加载 DLL模块时时,它并不知道调用函数的实际地址,但它知道函数的符号名和标识号。动态链接过程在加载的DLL模块时动态建立一个函数调用与函数地址的对应表。如果重新编译和重建DLL文件,并不需要修改应用程序,除非你改变了导出函数的符号名和参数序列。

简单的DLL文件只为应用程序提供导出函数,比较复杂的DLL文件除了提供导出函数以外,还调用其它DLL文件中的函数。这样,一个特殊的DLL可以既有导入函数,又有导入函数。这并不是一个问题,因为动态链接过程可以处理交叉相关的情况。

在DLL代码中,必须像下面这样明确声明导出函数:

__declspec(dllexport) int MyFunction(int n);

但也可以在模块定义(DEF)文件中列出导出函数,不过这样做常常引起更多的麻烦。在应用程序方面,要求像下面这样明确声明相应的输入函数:

__declspec(dllimport) int MyFuncition(int n);

仅有导入和导出声明并不能使应用程序内部的函数调用链接到相应的DLL文件上。应用程序的项目必须为链接程序指定所需的输入库(LIB文件)。而且应用程序事实上必须至少包含一个对DLL函数的调用。

二、与DLL模块建立链接

应用程序导入函数与DLL文件中的导出函数进行链接有两种方式:隐式链接和显式链接。所谓的隐式链接是指在应用程序中不需指明DLL文件的实际存储路径,程序员不需关心DLL文件的实际装载。而显式链接与此相反。

采用隐式链接方式,程序员在建立一个DLL文件时,链接程序会自动生成一个与之对应的LIB导入文件。该文件包含了每一个DLL导出函数的符号名和可选的标识号,但是并不含有实际的代码。LIB文件作为DLL的替代文件被编译到应用程序项目中。当程序员通过静态链接方式编译生成应用程序时,应用程序中的调用函数与LIB文件中导出符号相匹配,这些符号或标识号进入到生成的EXE文件中。LIB文件中也包含了对应的DLL文件名(但不是完全的路径名),链接程序将其存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows根据这些信息发现并加载DLL,然后通过符号名或标识号实现对DLL函数的动态链接。

显式链接方式对于集成化的开发语言(例如VB)比较适合。有了显式链接,程序员就不必再使用导入文件,而是直接调用Win32 的LoadLibary函数,并指定DLL的路径作为参数。LoadLibary返回HINSTANCE参数,应用程序在调用 GetProcAddress函数时使用这一参数。GetProcAddress函数将符号名或标识号转换为DLL内部的地址。假设有一个导出如下函数的 DLL文件:

extern "C" __declspec(dllexport) double SquareRoot(double d);

下面是应用程序对该导出函数的显式链接的例子:

typedef double(SQRTPROC)(double);
HINSTANCE hInstance;
SQRTPROC* pFunction;
VERIFY(hInstance=::LoadLibrary("c:\\winnt\\system32\\mydll.dll"));
VERIFY(pFunction=(SQRTPROC*)::GetProcAddress(hInstance,"SquareRoot"));
double d=(*pFunction)(81.0);//调用该DLL函数

在隐式链接方式中,所有被应用程序调用的DLL文件都会在应用程序EXE文件加载时被加载在到内存中;但如果采用显式链接方式,程序员可以决定DLL文件何时加载或不加载。显式链接在运行时决定加载哪个DLL文件。例如,可以将一个带有字符串资源的DLL模块以英语加载,而另一个以西班牙语加载。应用程序在用户选择了合适的语种后再加载与之对应的DLL文件。

三、使用符号名链接与标识号链接

在Win16环境中,符号名链接效率较低,所有那时标识号链接是主要的链接方式。在Win32环境中,符号名链接的效率得到了改善。Microsoft 现在推荐使用符号名链接。但在MFC库中的DLL版本仍然采用的是标识号链接。一个典型的MFC程序可能会链接到数百个MFC DLL函数上。采用标识号链接的应用程序的EXE文件体相对较小,因为它不必包含导入函数的长字符串符号名。



四、编写DllMain函数

DllMain函数是DLL模块的默认入口点。当Windows加载DLL模块时调用这一函数。系统首先调用全局对象的构造函数,然后调用全局函数 DLLMain。DLLMain函数不仅在将DLL链接加载到进程时被调用,在DLL模块与进程分离时(以及其它时候)也被调用。下面是一个框架 DLLMain函数的例子。

HINSTANCE g_hInstance;
extern "C" int APIENTRY DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved)
{
if(dwReason==DLL_PROCESS_ATTACH)
{
TRACE0("EX22A.DLL Initializing!\n");
//在这里进行初始化
}
else if(dwReason=DLL_PROCESS_DETACH)
{
TRACE0("EX22A.DLL Terminating!\n");
//在这里进行清除工作
}
return 1;//成功
}

如果程序员没有为DLL模块编写一个DLLMain函数,系统会从其它运行库中引入一个不做任何操作的缺省DLLMain函数版本。在单个线程启动和终止时,DLLMain函数也被调用。正如由dwReason参数所表明的那样。

五、模块句柄

进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识。进程自己还有一个HINSTANCE句柄。所有这些模块句柄都只有在特定的进程内部有效,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。在Win32中,HINSTANCE和HMODULE的值是相同的,这个两种类型可以替换使用。进程模块句柄几乎总是等于0x400000,而DLL模块的加载地址的缺省句柄是0x10000000。如果程序同时使用了几个DLL模块,每一个都会有不同的HINSTANCE值。这是因为在创建DLL文件时指定了不同的基地址,或者是因为加载程序对DLL代码进行了重定位。
模块句柄对于加载资源特别重要。Win32 的FindResource函数中带有一个HINSTANCE参数。EXE和DLL都有其自己的资源。如果应用程序需要来自于DLL的资源,就将此参数指定为DLL的模块句柄。如果需要EXE文件中包含的资源,就指定EXE的模块句柄。

但是在使用这些句柄之前存在一个问题,你怎样得到它们呢?如果需要得到EXE模块句柄,调用带有Null参数的Win32函数GetModuleHandle;如果需要DLL模块句柄,就调用以DLL文件名为参数的Win32函数GetModuleHandle

六、应用程序怎样找到DLL文件

如果应用程序使用LoadLibrary显式链接,那么在这个函数的参数中可以指定DLL文件的完整路径。如果不指定路径,或是进行隐式链接,Windows将遵循下面的搜索顺序来定位DLL:

1. 包含EXE文件的目录,
2. 进程的当前工作目录,
3. Windows系统目录,
4. Windows目录,
5. 列在Path环境变量中的一系列目录。

这里有一个很容易发生错误的陷阱。如果你使用VC++进行项目开发,并且为DLL模块专门创建了一个项目,然后将生成的DLL文件拷贝到系统目录下,从应用程序中调用DLL模块。到目前为止,一切正常。接下来对DLL模块做了一些修改后重新生成了新的DLL文件,但你忘记将新的DLL文件拷贝到系统目录下。下一次当你运行应用程序时,它仍加载了老版本的DLL文件,这可要当心!

七、调试DLL程序

Microsoft 的VC++是开发和测试DLL的有效工具,只需从DLL项目中运行调试程序即可。当你第一次这样操作时,调试程序会向你询问EXE文件的路径。此后每次在调试程序中运行DLL时,调试程序会自动加载该EXE文件。然后该EXE文件用上面的搜索序列发现DLL文件,这意味着你必须设置Path环境变量让其包含DLL文件的磁盘路径,或者也可以将DLL文件拷贝到搜索序列中的目录路径下。

HOOK API是一个永恒的话题,如果没有HOOK,许多技术将很难实现,也许根本不能实现。这里所说的API,是广义上的API,它包括DOS下的中断, WINDOWS里的API、中断服务、IFS和NDIS过滤等。比如大家熟悉的即时翻译软件,就是靠HOOK TextOut()或ExtTextOut()这两个函数实现的,在操作系统用这两个函数输出文本之前,就把相应的英文替换成中文而达到即时翻译;IFS 和NDIS过滤也是如此,在读写磁盘和收发数据之前,系统会调用第三方提供的回调函数来判断操作是否可以放行,它与普通HOOK不同,它是操作系统允许的,由操作系统提供接口来安装回调函数。

甚至如果没有HOOK,就没有病毒,因为不管是DOS下的病毒或WINDOWS里的病毒,都是靠HOOK系统服务来实现自己的功能的:DOS下的病毒靠HOOK INT 21来感染文件(文件型病毒),靠HOOK INT 13来感染引导扇区(引导型病毒);WINDOWS下的病毒靠HOOK系统API(包括RING0层的和RING3层的),或者安装IFS(CIH病毒所用的方法)来感染文件。因此可以说“没有HOOK,就没有今天多姿多彩的软件世界”。

由于涉及到专利和知识产权,或者是商业机密,微软一直不提倡大家HOOK它的系统API,提供IFS和NDIS等其他过滤接口,也是为了适应杀毒软件和防火墙的需要才开放的。所以在大多数时候,HOOK API要靠自己的力量来完成。

HOOK API有一个原则,这个原则就是:被HOOK的API的原有功能不能受到任何影响。就象医生救人,如果把病人身体里的病毒杀死了,病人也死了,那么这个 “救人”就没有任何意义了。如果你HOOK API之后,你的目的达到了,但API的原有功能失效了,这样不是HOOK,而是REPLACE,操作系统的正常功能就会受到影响,甚至会崩溃。

HOOK API的技术,说起来也不复杂,就是改变程序流程的技术。在CPU的指令里,有几条指令可以改变程序的流程:JMP,CALL,INT,RET, RETF,IRET等指令。理论上只要改变API入口和出口的任何机器码,都可以HOOK,但是实际实现起来要复杂很多,因为要处理好以下问题:

1,CPU指令长度问题,在32位系统里,一条JMP/CALL指令的长度是5个字节,因此你只有替换API里超过5个字节长度的机器码(或者替换几条指令长度加起来是5字节的指令),否则会影响被更改的小于5个字节的机器码后面的数条指令,甚至程序流程会被打乱,产生不可预料的后果;
2,参数问题,为了访问原API的参数,你要通过EBP或ESP来引用参数,因此你要非常清楚你的HOOK代码里此时的EBP/ESP的值是多少;
3,时机的问题,有些HOOK必须在API的开头,有些必须在API的尾部,比如HOOK CreateFilaA(),如果你在API尾部HOOK API,那么此时你就不能写文件,甚至不能访问文件;HOOK RECV(),如果你在API头HOOK,此时还没有收到数据,你就去查看RECV()的接收缓冲区,里面当然没有你想要的数据,必须等RECV()正常执行后,在RECV()的尾部HOOK,此时去查看RECV()的缓冲区,里面才有想要的数据;
4,上下文的问题,有些HOOK代码不能执行某些操作,否则会破坏原API的上下文,原API就失效了;
5,同步问题,在HOOK代码里尽量不使用全局变量,而使用局部变量,这样也是模块化程序的需要;
6,最后要注意的是,被替换的CPU指令的原有功能一定要在HOOK代码的某个地方模拟实现。

下面以ws2_32.dll里的send()为例子来说明如何HOOK这个函数:

Exported fn(): send - Ord:0013h
地址         机器码                 汇编代码
:71A21AF4 55                   push ebp //将被HOOK的机器码(第1种方法)
:71A21AF5 8BEC                 mov ebp, esp //将被HOOK的机器码(第2种方法)
:71A21AF7 83EC10                 sub esp, 00000010
:71A21AFA 56                   push esi
:71A21AFB 57                   push edi
:71A21AFC 33FF                 xor edi, edi
:71A21AFE 813D1C20A371931CA271       cmp dword ptr [71A3201C], 71A21C93 //将被HOOK的机器码(第4种方法)
:71A21B08 0F84853D0000             je 71A25893
:71A21B0E 8D45F8                 lea eax, dword ptr [ebp-08]
:71A21B11 50                   push eax
:71A21B12 E869F7FFFF             call 71A21280
:71A21B17 3BC7                 cmp eax, edi
:71A21B19 8945FC                 mov dword ptr [ebp-04], eax
:71A21B1C 0F85C4940000             jne 71A2AFE6
:71A21B22 FF7508                 push [ebp+08]
:71A21B25 E826F7FFFF    

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭