C++ 黑客编程揭秘与防范(第3版)

978-7-115-49372-9
作者: 冀云
译者:
编辑: 张涛
分类: C++

图书目录:

详情

本书介绍了操作系统的相关操作,比如进程、线程、注册表等知识。当读者掌握了关于进程、线程、注册表等相关的开发知识后,就可以把一些常规的操作进程、注册表、文件等用代码进行实现,这样,一些日常的操作可与学习的编程知识相结合。除了操作的知识外,本书还介绍了关于网络应用程序的开发,了解Winsock的开发后,读者就会明白在应用层客户端与服务器端通信的原理。

图书摘要

版权信息

书名:C++ 黑客编程揭秘与防范(第3版)

ISBN:978-7-115-49372-9

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    冀 云

责任编辑 张 涛

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


市面上关于黑客入门的书籍较多,比如黑客图解入门类、黑客工具详解类、黑客木马攻防类等,但是,对于很多读者来说,可能并不是掌握简单的工具就够了。很多读者学习黑客知识是为了真正掌握与安全相关的知识。与安全相关的知识涉及面比较广,包括数据安全、存储安全、系统安全、Web安全、网络安全等,本书围绕Windows系统下应用层的开发来介绍一些安全方面的知识。

本书是在第 2 版的基础上新添加了一些内容,同时也删除了一些过时的内容。本书以Win32应用层下的安全开发为中心,介绍Windows系统下的安全开发。

本书介绍了操作系统的相关操作,比如进程、线程、注册表等知识。当读者掌握了关于进程、线程、注册表等的开发知识后,就可以用代码实现一些常规的操作,如对进程、注册表、文件等进行操作,这样,一些日常的操作可与学习的编程知识相结合。除了操作外,本书还介绍了网络应用程序的开发技术,了解Winsock的开发后,读者就会明白在应用层客户端与服务器端通信的原理。当然,除了介绍Win32开发外,本书还介绍了PE结构、调试接口、逆向等知识。本书最后剖析了恶意程序、专杀工具、扫描器等的开发技术,以帮助读者更好地了解安全知识,为开发安全的软件打下基础。


我与冀云兄因参加黑客反病毒论坛会议结识,在认识初期就能感觉到冀云兄是一个非常踏实且又富有思想的人,对安全编程的诸多方面也有自己独到的认识,这令我十分欣赏。认识几个月后,通过一次无意的聊天,我有幸读到冀云兄《C++黑客编程揭秘与防范》第1版,而后承冀云兄高看,才得以诞生此序。

通过阅读《C++黑客编程揭秘与防范》第1版,我有一种相见恨晚的感觉。这本书从最基本的Windows编程到Windows下的各种安全编程技术都有涉及,例如PE文件、DLL注入技术、各种Hook技术、后门编写的技术关键点,乃至像MBR的解析这种很难涉及的点与Rootkit编程这样比较深入的知识点,都有恰到好处的介绍与详解。

因此,就整书而言,将诸如文件/注册表操作、网络通信、PE文件、Rootkit、逆向工程等数个知识点有效组织在一起,是一个非常巨大的工程。对于有过类似写作经验的我来说,这方面的体会尤其深刻。但是不得不说,作为读者,我真的非常幸运,首先拜读了这本书。就我个人而言,这本书至少可以被当成一部“技术字典”来使用。当我在实际的工作中对某种技术生疏后,可以拿起这本书翻一翻,顿时会感觉受益匪浅。

从整书的结构以及知识的组织方式来看,不难发现,这其实是一本相当重视初学者技术的图书。第1章对于工作环境的搭建以及对应IDE的使用都做了必要的介绍,第2章使用一个非常有趣且简单的例子教读者如何打造一个木马的雏形,这些无不体现出了作者对于基础薄弱的读者的细心照顾。

除此之外,当前的政策环境以及社会整体的大环境都对信息安全产业释放了大量的利好信号,无论是国家将信息安全提到国家战略层面,还是发生在美国的著名“棱镜门”事件,抑或是当前的移动互联网大潮,都在预示着信息安全领域人才在未来势必将摆脱“边缘群体”,进而成为“主流群体”中重要的一员。这些改变势必将极大地加剧当前信息安全领域人才的稀缺现状,但是,我相信本书定会为中国的信息安全领域崛起贡献一份力量,进而使得更多的读者从信息安全的“门外汉”成为“圈内人”,以缓解信息安全领域人才稀缺的现状。

——任晓珲[A1Pass],北京蓝森科技有限公司创始人,
15PB计算机高端培训品牌创始人,《黑客免杀攻防》作者


备受关注的黑客到底是什么

什么是黑客?百度百科里黑客的含义如下(摘自百度百科,略有改动):

热衷研究、撰写程序的专才,精通各种计算机语言和系统,且必须具备乐于追根究底、穷究问题的特质。“黑客”一词是由英语Hacker音译出来的,是指专门研究、发现计算机和网络漏洞的计算机爱好者。早期在美国的电脑界是带有褒义的。

看到上面百度百科给出的黑客含义后,很多只会使用工具的所谓的“黑客”就能明白一个道理,即黑客是要会编写程序的。

再看一下百度百科里对只会使用工具的黑客的解释(摘自百度百科):

脚本小子(script kiddie或script boy)“指的是用别人写的程序的人。脚本小子是一个贬义词,用来描述以黑客自居并沾沾自喜的初学者。”

那些自以为是的工具黑客只不过是一个“脚本小子”,是不是心里觉得不是很舒服了?是不是觉得自己应该提高了?如果是的话,那么就请抛开以前当工具黑客的想法,开始学习编写程序吧!

思想准备

新手可能会问:编写自己的黑客工具是不是很难?是不是要懂编程语言?要懂哪种编程语言呢?笔者的回答是肯定的。抛开用工具的想法,其实是让大家抛开浮躁的想法,认真地学一些真正的技术,哪怕只是一些入门的知识。想做黑客就要有创新、研发的精神,如果只是做一个只会用软件的应用级的计算机使用者,那么必定永远达不到黑客级的水平,因为工具人人都会用,而你只是比别人多知道几个工具而已。抛开浮躁,静下心来从头开始学习基础,为将来的成长做好足够的准备。

攻防的广义性

黑客做得最多的就是“入侵”,这里所说的入侵不是一个狭义上的入侵,因为它不单单针对网络、系统的入侵。这里说的是一个广义上的入侵,“入侵”一词是指“在非授权的情况,试图存取信息、处理信息或破坏系统以使系统不可靠、不可用的故意行为。”由此可以看出,入侵并非单指网络或系统。这里说的“入侵”包括两个方面,一个是针对网络(系统)的入侵,另一个是针对软件的入侵。网络的入侵是通常意义上的入侵,而软件的入侵通常就是人们说的软件破解(包括漏洞挖掘等内容)。无论是侵入别人系统,还是破解某款软件,都是在非授权的情况下得到相应的权限,比如系统权限或者软件的使用权限。这些“入侵”都是为了寻找系统的安全漏洞,以便更好地完善系统的安全。

本书内容

本书针对“网络入侵”和“软件入侵”两方面来介绍黑客编程,从攻防两个角度来学习黑客编程的知识,通过一系列知识体系完成“黑客编程”的养成计划。

本书会介绍大量的基础知识,这些基础知识看起来与普通的应用程序编程没有什么差别。其实,所谓 “黑客编程”(也称为“安全编程”),是指“采用常规的编程技术,编写网络安全、黑客攻防类的程序、工具”。因此,普通的编程技术与黑客编程技术并没有本质的差别,只是开发的侧重点不同。普通的编程注重的是客户的需求,而黑客编程注重的则是攻与防。

黑客编程有其两面性,按照攻防角度可以分为“攻击类入侵编程”和“防范类安全编程”。结合上面提到的“网络”和“软件”两方面来说,常见的“网络攻击”程序有扫描器、嗅探器、后门等;常见的“软件攻击”程序有查壳器、动态调试器、静态分析器、补丁等(这些工具是一些调试工具和逆向分析工具,因为软件破解、漏洞挖掘等会用到这些调试工具,所以称其为“软件攻击”工具)。常见的“网络(系统)防范”程序有“杀毒软件”“防火墙”“主动防御系统”等;常见的“软件防范”程序有“壳”“加密狗”“电子令牌”等。

根据前面提到的攻防两方面的内容,本书会涉及扫描器的开发、后门的开发、应用层抓包器的开发等黑客攻防方面的内容。本书还会讲解关于软件方面的知识,主要涉及PE结构、加壳、脱壳、逆向分析等知识。由于技术的两面性,希望读者有一个良好的学习心态,把学到的技术用到安全保护上。

读者能从本书中得到什么

通过本书,读者能学到Windows下基于消息的软件开发、基于Winsock的网络应用程序的开发、软件逆向分析和调试等方面的编程、调试及安全知识。在学习的过程中,读者应该大量阅读和参考其他相关资料,并且一定要亲自动手进行编程。编程绝对不是靠看书能够学会的!

通过本书的指导,再加上自身实践和练习,读者可以具备Windows下基本的应用程序开发、网络程序开发能力,基本的系统底层开发能力。除了提升相关开发能力外,读者还能学到初级的病毒分析、软件保护等相关的安全知识。

如何无障碍阅读本书

阅读本书的读者最好具有C和C++编程的基础知识,有其他编程语言基础知识的读者也可以无障碍阅读。对于无编程知识的读者,在阅读本书的同时,只要学习了本书中涉及的相关基础知识,同样可以阅读本书。

本书涉及范围较多,知识面比较杂,但是本书属于入门级读物,专门为新手准备,只要读者具备一定的基础知识,即可顺利进行阅读。在阅读本书的基础上,读者可以接着学习更深层次的知识,希望本书能帮助读者提高自身的能力。

建议读者深入学习操作系统原理、数据结构、编译原理、计算机体系结构等重要的计算机基础知识。

免责

本书中内容主要用于教学,指导新手如何入门、如何学习编程知识,从编程的过程中了解黑客编程的基础知识。请勿使用自己的知识做出有碍公德之事,在准备通过技术手段进行蓄意破坏时,请想想无数“高手”的下场。读者如若作奸犯科自行承担责任,与作者本人和出版社无任何关系,请读者自觉遵守国家法律。

由于作者水平有限,书中难免会有差错,敬请谅解。

编辑联系邮箱:zhangtao@ptpress.com.cn。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书配套资源包括书中示例的源代码。

要获得以上配套资源,请在异步社区本书页面中单击 ,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


读者是否曾经用别人开发的工具尝试“入侵”,对自己的系统进行安全检查,是否希望开发出自己的“黑器”?本章将介绍Windows操作系统的开发基础,将带领读者进入Windows编程的大门。

Windows是一个庞大而复杂的操作系统,它提供了丰富而强大的功能,不但操作灵活方便,而且有众多的应用软件对其进行支持。Windows因有众多软件的支持,从而长期雄霸于PC系统。之所以有众多软件的支持,是因为Windows提供了良好的应用程序开发平台(接口)、完整的开发文档和各种优秀的开发环境。对于一个程序员来说,除了要掌握基本的开发语言以外,还要掌握具体的开发环境和系统平台的相关知识;在掌握编程语言和开发环境等知识后,还要掌握调试技术以及各种调试分析工具。同样,Windows操作系统提供了良好的调试接口,并且有非常多的调试工具。

本章主要介绍Windows的消息机制,Windows下的开发工具、辅助工具,还有调试工具。本章的目的在于对Windows操作系统的消息机制进行回顾,它是Windows开发的基础,方便后续章节内容的学习。本章对于Windows编程的一些基本概念不会进行过多的介绍。除了对消息机制进行回顾外,本章还要介绍集成在Visual C++(VC6)中的调试工具和其他一些开发辅助工具。

大部分Windows应用程序都是基于消息机制的(命令行下的程序并不基于消息机制),熟悉Windows操作系统的消息机制是掌握Windows操作系统下编程的基础。本节将带领读者认识和熟悉Windows的消息机制。

在真正学习和认识消息之前,先来完成一个简单的任务,看看消息能完成什么样的工作。首先写一个简单的程序,通过编写的程序发送消息来关闭记事本的进程、获取窗口的标题和设置窗口的标题。

程序的具体代码如下:

void CMsgTestDlg::OnClose() 
{
  // 在此处添加处理程序代码
  HWND hWnd = ::FindWindow("Notepad", NULL);
  if ( hWnd == NULL )
  {
    AfxMessageBox("没有找到记事本");
    return ;
  }

  ::SendMessage(hWnd, WM_CLOSE, NULL, NULL);
}

void CMsgTestDlg::OnExec() 
{
  // 在此处添加处理程序代码
  WinExec("notepad.exe", SW_SHOW);
}

void CMsgTestDlg::OnEditWnd() 
{
    // 在此处添加处理程序代码
  HWND hWnd = ::FindWindow(NULL, "无标题 - 记事本");
  if ( hWnd == NULL )
  {
    AfxMessageBox("没有找到记事本");
    return ;
  }

  char *pCaptionText = "消息测试";
  ::SendMessage(hWnd, WM_SETTEXT, (WPARAM)0, (LPARAM)pCaptionText);    
}

void CMsgTestDlg::OnGetWnd() 
{
      // 在此处添加处理程序代码
  HWND hWnd = ::FindWindow("Notepad", NULL);
  if ( hWnd == NULL )
  {
    AfxMessageBox("没有找到记事本");
    return ;
  }

  char pCaptionText[MAXBYTE] = { 0 };
  ::SendMessage(hWnd, WM_GETTEXT, (WPARAM)MAXBYTE, (LPARAM)pCaptionText);

  AfxMessageBox(pCaptionText);
}

编写的代码中有4个函数:第1个函数OnClose()是用来关闭记事本程序的;第2个函数OnExec()是用来打开记事本程序的,主要是测试其他3个函数时可以方便地打开记事本程序;第3个函数OnEditWnd()是用来修改记事本标题的;第4个函数OnGetWnd()是用来获取当前记事本标题的。程序的界面如图1-1所示。

图1-1 消息测试窗口

简单测试一下这个程序。首先单击“打开记事本程序”按钮,出现记事本的窗口(表示记事本程序被打开了);接着单击“修改记事本标题”按钮,可以发现记事本程序的窗口标题改变了;再单击“获取记事本标题”按钮,弹出记事本程序窗口标题的一个对话框;最后单击“关闭记事本程序”按钮,记事本程序被关闭。

上面的代码中要学习的API函数有两个,分别是FindWindow()和SendMessage()。下面看一下它们在MSDN中的定义。

FindWindow()函数的定义如下:

HWND FindWindow(
  LPCTSTR lpClassName,
  LPCTSTR lpWindowName
);

FindWindow()函数的功能是,通过指定的窗口类名(lpClassName)或窗口标题(lpWindowName)查找匹配的窗口并返回最上层的窗口句柄。简单理解就是,通过指定的窗口名(窗口名相对于窗口类来说要直观些,因此往往使用的是窗口名)返回窗口句柄。FindWindow()函数有两个参数,分别是lpClassName和lpWindowName。通过前面的描述,该函数通常使用的是第2个参数lpWindowName,该参数是指定窗口的名称。在例子代码中,为程序指定的窗口名是“无标题—记事本”。“无标题—记事本”是记事本程序打开后的默认窗口标题,当FindWindow()找到该窗口时,会返回它的窗口句柄。例子代码中也使用了lpClassName(窗口类名),在窗口的名称会改变的情况下,只能通过窗口类名来获取窗口的句柄了。

当使用FindWindow()函数获取窗口句柄时,指定窗口名是比较直观和容易的。但是,如果窗口名经常发生变化时,那么就不得不使用窗口类名了。

使用FindWindow()函数返回的窗口句柄是为了给SendMessage()函数来使用的。

SendMessage()函数的定义如下:

LRESULT SendMessage(
  HWND hWnd,
  UINT Msg,
  WPARAM wParam,
  LPARAM lParam
);

该函数的作用是根据指定窗口句柄将消息发送给指定的窗口。该函数有4个参数,第1个参数hWnd是要接收消息的窗口的窗口句柄,第2个参数Msg是要发送消息的消息类型,第3个参数wParam和第4个参数lParam是消息的两个附加参数。第1个参数hWnd在前面已经介绍过了,该参数通过FindWindow()函数获取。

在程序的代码中,SendMessage()函数的第2个参数分别使用的了WM_CLOSE消息、WM_SETTEXT消息和WM_GETTEXT消息。下面来看这3个消息的具体含义。

WM_CLOSE:将WM_CLOSE消息发送后,接收到该消息的窗口或应用程序将要关闭。WM_CLOSE消息没有需要的附加参数,因此wParam和lParam两个参数都为NULL。

WM_SETTEXT:应用程序发送WM_SETTEXT消息对窗口的文本进行设置。该消息需要附加参数,wParam参数未被使用,必须指定为0值,lParam参数是一个指向以NULL为结尾的字符串的指针。

WM_GETTEXT:应用程序发送WM_GETTEXT消息,将对应窗口的文本复制到调用者的缓冲区中。该消息也需要附加参数,wParam参数指定要复制的字符数数量,lParam是接收文本的缓冲区。

例子代码在VC6下进行编译连接,生成可执行文件后,可以通过按钮的提示进行测试,以便读者感性认识消息的作用。

编写程序调用FindWindow()函数的时候,通常会使用其第2个参数,也就是窗口的标题。但是有些软件的窗口标题会根据不同的情况进行改变,那么程序中就不能在FindWindow()函数中直接通过窗口的标题来获得窗口的句柄了。而窗口的类名通常是不会变的,因此编程时可以指定窗口类名来调用FindWindow()函数以便获取窗口句柄。那么,如何能获取到窗口的类名称呢?这就是将要介绍的第1个开发辅助工具——Spy++。

Spy++是微软Visual Studio中提供的一个非常实用的小工具,它可以显示系统的进程、窗口等之间的关系,可以提供窗口的各种信息,可以对系统指定的窗口进行消息的监控等。它的功能非常多,这里演示如何用它来获取窗口的类名称。

打开“开始”菜单,在Visual Studio的菜单路径下找到Spy++,打开Spy++窗口,如图1-2所示。

图1-2 “Microsoft Spy++”窗口

选择工具栏中的“Find Window”按钮,如图1-3所示。

图1-3 “Find Window”按钮

单击“Find Window”按钮,出现如图1-4所示的窗口。

图1-4 Find Window窗口

在图1-4中,用鼠标左键单击“Finder Tool”后面的图标,然后拖曳到指定的窗口上,会显示出“Handle”(窗口句柄)“Caption”(窗口标题)和“Class”(窗口类名),其中“Class”是编程时要使用的“窗口类”名称。

“Hide Spy++”是一个比较实用的功能,它用来隐藏Spy++主窗口界面。选中该复选框后,拖曳“Finder Tool”后的图标时,图 1-2所示为窗口将被隐藏。这个功能的实用之处在于,有些应用软件有反Spy++的功能,隐藏Spy++主窗口有助于避免被反Spy++的软件检测到。为什么隐藏Spy++的“Find Window”窗口会有反检测的功能,反检测的原理是什么?原理很简单,目标程序也是通过调用FindWindow()函数来查找Spy++窗口的,如果有该窗口,就进行一些相应的处理。


 

注: 

通过Spy++找到的窗口句柄是不能在编程中使用的,每次打开窗口时,窗口的句柄都会改变。


将“Finder Tool”后的图标拖曳到记事本的标题处,Spy++的Find Window窗口显示的内容如图1-5所示。

图1-5 获取到信息的Find Window窗口

从图1-5中可以得到记事本程序的标题和类名称。当编写程序调用FindWindow()函数,不能通过程序的标题文本得到窗口的句柄时,可以通过窗口类名称得到窗口的句柄。

SendMessage()将指定的消息发送给指定的窗口,窗口接收到消息也有相应的行为发生。那么窗口接收到消息后的一系列行为是如何发生的?下面通过熟悉Windows的消息机制来理解消息处理背后的秘密。

Windows下的窗口应用程序都是基于消息机制的,操作系统与应用程序之间、应用程序与应用程序之间,大部分都是通过消息机制进行通信、交互的。要真正掌握Windows应用程序内部对消息的处理,必须分析实际的源代码。在编写一个基于消息的Windows应用程序前,先来比较DOS程序和Windows程序在执行时的流程。

1.DOS程序执行流程

在DOS下将编写完的程序进行执行,在执行时有较为清晰的流程。比如用C语言编写程序后,程序执行时的大致流程如图1-6所示。

图1-6 传统DOS程序执行流程

在图1-6中可以看出,DOS程序的流程是按照代码的顺序(这里的顺序并不是指程序控制结构中的顺序、分支和循环的意思,而是指程序运行的逻辑有明显的流程)和流程依次执行。大致步骤为:DOS程序从main()主函数开始执行(其实程序真正的入口并不是main()函数);执行的过程中按照代码编写流程依次调用各个子程序;在执行的过程中会等待用户的输入等操作;当各个子程序执行完成后,最终会返回main()主函数,执行main()主函数的return语句后,程序退出(其实程序真正的出口也并不是main()函数的return语句)。

2.Windows程序执行流程

DOS程序的执行流程比较简单,但是Windows应用程序的执行流程就比较复杂了。DOS是单任务的操作系统。在DOS中,通过输入命令,DOS操作系统会将控制权由Command.com转交给DOS程序从而执行。而Windows是多任务的操作系统,在Windows下同时会运行若干个应用程序,那么Windows就无法把控制权完全交给一个应用程序。Windows下的应用程序是如何工作的?首先看一下Windows应用程序内部的大致结构图,如图1-7所示。

图1-7 Windows应用程序执行原理图

图1-7可能看起来比较复杂,其实Windows应用程序的内部结构比该示意图更复杂。在实际开发Windows应用程序时,需要关注的部分主要是“主程序”和“窗口过程”两部分。但是从图1-7来看,主程序和窗口过程没有直接的调用关系,而在主程序和窗口过程之间有一个“系统程序模块”。“主程序”的功能是用来注册窗口类、获取消息和分发消息。而“窗口过程”中定义了需要处理的消息,“窗口过程”会根据不同的消息执行不同的动作,而不需要程序处理的消息则会交给默认的系统过程进行处理。

在“主程序”中,RegisterClassEx()函数会注册一个窗口类,窗口类中的字段中包含了“窗口过程”的地址信息,也就是把“窗口类”的信息(包括“窗口过程的地址信息”)告诉操作系统。然后“主程序”不断通过调用GetMessage()函数获取消息,再交由DispatchMessge()函数来分发消息。消息分发后并没有直接调用“窗口过程”让其处理消息,而是由系统模块查找该窗口指定的窗口类,通过窗口类再找到窗口过程的地址,最后将消息送给该窗口过程,由窗口过程处理消息。

相对一个简单的DOS程序来说一个简单的Windows应用程序要很长。下面的例子中只实现了一个特别简单的Windows程序,这个程序在桌面上显示一个简单的窗口,它没有菜单栏、工具栏、状态栏,只是在窗口中输出一段简单的字符串。虽然程序如此简单,但是也要编写100行左右的代码。考虑到初学的读者,这里将一部分一部分地逐步介绍代码中的细节,以减少代码的长度,从而方便初学者的学习。

1.Windows窗口应用程序的主函数——WinMain()

在DOS时代,或编写Windows下的命令行的程序,要使用C语言编写代码的时候都是从main()函数开始的。而在Windows下编写有窗口的程序时,要用C语言编写窗口程序就不再从main()函数开始了,取而代之的是WinMain()函数。

既然Windows应用程序的主函数是WinMain(),那么就从了解WinMain()函数的定义开始学习Windows应用程序的开发。WinMain()函数的定义如下:

int WINAPI WinMain(
  HINSTANCE hInstance,
  HINSTANCE hPrevInstance, 
  LPSTR lpCmdLine, 
  int nCmdShow 
);

该函数的定义取自MSDN中,在看到WinMain()函数的定义后,很直观地会发现WinMain函数的参数比main()函数的参数变多了。从参数个数上来说,WinMain()函数接收的信息更多了。下面来看每个参数的含义。

hInstance是应用程序的实例句柄。保存在磁盘上的程序文件是静态的,当被加载到内存中时,被分配了CPU、内存等进程所需的资源后,一个静态的程序就被实例化为一个有各种执行资源的进程了。句柄的概念随上下文的不同而不同,句柄是操作某个资源的“把手”。当需要对某个实例化进程操作时,需要借助该实例句柄进行操作。这里的实例句柄是程序装入内存后的起始地址。实例句柄的值也可以通过GetModuleHandle()参数来获得(注意系统中没有GetInstanceHandle()函数,不要误以为是hInstance就会有GetInstance×××()类的函数)。


 

注: 

句柄这个词在开发Windows程序时是非常常见的一个词。“句柄”一词的含义随上下文的不同而所有改变。比如,磁盘上的程序文件被加载到内存中后,就创建了一个实例句柄,这个实例句柄是程序装入内存后的“起始地址”,或者说是“模块的起始地址”。而在前面介绍的FindWindow()函数和SendMessage()函数中也提到了“句柄”这个词,而这时的“句柄”相当于某个资源的“把手”或“面板”。


拿SendMessage()函数举例来说,句柄相当于一个操作的面板,对句柄发送的消息相当于面板上的各个开关按键,消息的附加数据,相当于给开关按键送的各种参数,这些参数根据按键的不同而不同。

hPrevInstance是同一个文件创建的上一个实例的实例句柄。这个参数是Win16平台下的遗留物,在Win32下已经不再使用了。

lpCmdLine是主函数的参数,用于在程序启动时给进程传递参数。比如在“开始”菜单的“运行”中输入“notepad c:\boot.ini”,这样就通过记事本打开了C盘下的boot.ini文件。C:\Boot.ini文件是通过WinMain()函数的lpCmdLine参数传递给notepad.exe程序的。

nCmdShow是进程显示的方式,可以是最大化显示、最小化显示,或者是隐藏等显示方式(如果是启动木马程序的话,启动方式当然要由自己进行控制)。

主函数的参数都介绍完了。编写Windows的窗口程序,需要主函数中应该完成哪些操作是下面要讨论的内容。

2.WinMain()函数中的流程

编写Windows下的窗口程序,在WinMain()主函数中主要完成的任务是注册一个窗口类,创建一个窗口并显示创建的窗口,然后不停地获取属于自己的消息并分发给自己的窗口过程,直到收到WM_QUIT消息后退出消息循环结束进程。这是主函数中程序的执行脉络,程序中将注册窗口类、创建窗口的操作封装为自定义函数。

代码如下:

int WINAPI WinMain(
       HINSTANCE hInstance,
       HINSTANCE hPrevInstance,
       LPSTR lpCmdLine,
       int nCmdShow)
{
    MSG  Msg;
    BOOL bRet;

    // 注册窗口类
    MyRegisterClass(hInstance);

    // 创建窗口并显示窗口
    if ( !InitInstance(hInstance, SW_SHOWNORMAL) )
    {
       return FALSE;
    }

    // 消息循环
    // 获取属于自己的消息并进行分发
    while( (bRet = GetMessage(&Msg, NULL, 0, 0)) != 0 )
    { 
       if ( bRet == -1 )
       {
          // handle the error and possibly exit
          break;
       }
       else
       {
          TranslateMessage(&Msg); 
          DispatchMessage(&Msg); 
       }
    }

    return Msg.wParam;
}

在代码中,MyRegisterClass()和InitInstance()是两个自定义的函数,分别用来注册窗口类,创建窗口并显示更新创建的窗口。后面的消息循环部分用来获得消息并进行消息分发。它的流程如图1-7所示的“主程序”部分。

代码中主要是3个函数,分别是GetMessage()、TranslateMessage()和DispatchMessage()。这3个函数是Windows提供的API函数。GetMessage()的定义如下:

BOOL GetMessage(
  LPMSG lpMsg,  
  HWND hWnd,
  UINT wMsgFilterMin,
  UINT wMsgFilterMax 
);

该函数用来获取属于自己的消息,并填充MSG结构体。有一个类似于GetMessage()的函数是PeekMessage(),它可以判断消息队列中是否有消息,如果没有消息,可以主动让出CPU时间给其他进程。关于PeekMessage()函数的使用,请参考MSDN:

BOOL TranslateMessage(CONST MSG *lpMsg);

该函数是用来处理键盘消息的。它将虚拟码消息转换为字符消息,也就是将WM_KEYDOWN消息和WM_KEYUP消息转换为WM_CHAR消息,将WM_SYSKEYDOWN消息和WM_SYSKEYUP消息转换为WM_SYSCHAR消息:

LRESULT DispatchMessage(CONST MSG *lpmsg);

该函数是将消息分发到窗口过程中。

3.注册窗口类的自定义函数

在WinMain()函数中,首先调用了MyRegisterClass()这个自定义函数,需要传递进程的实例句柄hInstance作为参数。该函数完成窗口类的注册,分为两步:第一步是填充WNDCLASSEX结构体,第二步是调用RegisterClassEx()函数进行注册。该函数相对简单,但是,该函数中稍微复杂的是WNDCLASSEX结构体的成员较多。

代码如下:

ATOM MyRegisterClass(HINSTANCE hInstance)
{
   WNDCLASSEX WndCls;

   // 填充结构体为0
   ZeroMemory(&WndCls, sizeof(WNDCLASSEX));

   // cbSize是结构体大小
   WndCls.cbSize = sizeof(WNDCLASSEX);
   // lpfnWndProc是窗口过程地址
   WndCls.lpfnWndProc = WindowProc;
   // hInstance是实例句柄
   WndCls.hInstance = hInstance;
   // lpszClassName是窗口类类名
   WndCls.lpszClassName = CLASSNAME;
   // style是窗口类风格
   WndCls.style = CS_HREDRAW | CS_VREDRAW;
   // hbrBackground是窗口类背景色
   WndCls.hbrBackground = (HBRUSH)COLOR_WINDOWFRAME + 1;
   // hCursor是鼠标句柄
   WndCls.hCursor = LoadCursor(NULL, IDC_ARROW);
   // hIcon是图标句柄
   WndCls.hIcon = LoadIcon(NULL, IDI_QUESTION);
   // 其他
   WndCls.cbClsExtra = 0;
   WndCls.cbWndExtra = 0;

   return RegisterClassEx(&WndCls);
}

在代码中,WNDCLASSEX结构体的成员都介绍了。WNDCLASSEX中最重要的字段是lpfnWndProc,它将保存的是窗口过程的地址。窗口过程是对各种消息进程处理的“汇集地”,也是编写Windows应用程序的重点部分。代码中的函数都比较简单,主要涉及LoadCursor()、LoadIcon()和RegisterClassEx()这3个函数。由于这3个函数使用简单,通过代码就可以进行理解,这里不做过多介绍。

注册窗口类(提到窗口类,你是否想到了FindWindow()函数的第一个参数呢?)的重点是在后面的代码中可以根据该窗口类创建该种类型的窗口。代码中,在定义窗口类时指定了背景色、鼠标指针、窗口图标等,那么使用该窗口类创建的窗口都具有相同的窗口类型。

4.创建主窗口并显示更新

注册窗口类后,根据该窗口类创建具体的主窗口并显示和更新窗口。

代码如下:

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   HWND hWnd = NULL;

   // 创建窗口
   hWnd = CreateWindowEx(WS_EX_CLIENTEDGE,
                CLASSNAME,
                "MyFirstWindow",
                WS_OVERLAPPEDWINDOW,
                CW_USEDEFAULT, CW_USEDEFAULT,
                CW_USEDEFAULT, CW_USEDEFAULT, 
                NULL, NULL, hInstance, NULL);

   if ( NULL == hWnd )
   {
       return FALSE;
   }

   // 显示窗口
   ShowWindow(hWnd, nCmdShow);
   // 更新窗口
   UpdateWindow(hWnd);

   return TRUE;
}

在调用该函数时,需要给该函数传递实例句柄和窗口显示方式两个参数。这两个参数的第1个参数通过WinMain()函数的参数hInstance指定,第2个参数可以通过WinMain()函数的第3个参数指定,也可以进行自定义指定。程序中的调用代码如下:

   InitInstance(hInstance, SW_SHOWNORMAL);

在创建主窗口时调用了CreateWindowEx()函数,先来看看它的函数原型:

HWND CreateWindowEx(
  DWORD dwExStyle, 
  LPCTSTR lpClassName, 
  LPCTSTR lpWindowName,
  DWORD dwStyle,
  int x, 
  int y, 
  int nWidth, 
  int nHeight,
  HWND hWndParent,
  HMENU hMenu,
  HINSTANCE hInstance,
  LPVOID lpParam
);

CreateWindowEx()中的第2个参数是lpClassName,由注释可以知道是已经注册的类名。这个已经注册的类名就是WNDCLASSEX结构体的lpszClassName字段。

5.处理消息的窗口过程

按照如图1-7所示的流程,WinMain()主函数的部分已经都实现完成了。接下来看程序中关键的部分——窗口过程。从WinMain()主函数中看出,在WinMain()主函数中没有任何地方直接调用窗口过程,只是在注册窗口类时指定了窗口过程的地址。那么窗口类是由谁进行调用的呢?答案是由操作系统进行调用的。原因有二,首先窗口过程的地址是由系统维护的,注册窗口类时是将“窗口过程的地址”向操作系统进行注册。其次是除了应用程序本身会调用自己的窗口过程外,其他应用程序也会调用自己的窗口过程,比如前面的例子中调用SendMessage()函数发送消息后,需要系统调用目标程序的窗口过程来完成相应的动作。如果窗口过程由自己调用,那么窗口就要自己维护窗口类的信息,进程间消息的通信会非常繁琐,也会无形中增加系统的开销。

窗口过程的代码如下:

LRESULT CALLBACK WindowProc(
             HWND hwnd, 
             UINT uMsg, 
             WPARAM wParam,
             LPARAM lParam)
{
   PAINTSTRUCT ps;
   HDC hDC;
   RECT rt;

   char *pszDrawText = "Hello Windows Program.";

   switch (uMsg)
   {
   case WM_PAINT:
      {
          hDC = BeginPaint(hwnd, &ps);
          GetClientRect(hwnd, &rt);
          DrawTextA(hDC, 
                 pszDrawText, strlen(pszDrawText),&rt, 
                 DT_CENTER | DT_VCENTER | DT_SINGLELINE);
          EndPaint(hwnd, &ps);
          break;
      }
   case WM_CLOSE:
      {
          if ( IDYES == MessageBox(hwnd, 
               "是否退出程序", "MyFirstWin", MB_YESNO) )
          {
             DestroyWindow(hwnd);
             PostQuitMessage(0);
          }
          break;
      }
   default:
      {
          return DefWindowProc(hwnd, uMsg, wParam, lParam);
      }
   }

   return 0;
}

在WinMain()函数中,通过调用RegisterClassEx()函数进行了窗口类的注册,通过调用CreateWindowEx()函数创建了窗口,并且GetMessage()函数不停地获取消息,但是在主函数中没有对被创建的窗口做任何处理。那是因为真正对窗口行为的处理全部放在了窗口过程中。当WinMain()函数中的消息循环得到消息以后,通过调用DispatchMessage()函数将消息派发(实际不是由DispatchMessage()函数直接派发)给了窗口过程,从而由窗口过程对消息进行处理。

窗口过程的定义是按照MSDN上给出的形式进行定义的,MSDN上的定义形式如下:

LRESULT CALLBACK WindowProc(
  HWND hwnd,
  UINT uMsg,
  WPARAM wParam,
  LPARAM lParam
);

WindowProc是窗口过程的函数名,这个函数名可以随意改变,但是该窗口过程的函数名必须与WNDCLASSEX结构体中lpfnWndProc的成员变量的值一致。函数的第1个参数hwnd是窗口的句柄,第2个参数uMsg是消息值,第3个和第4个参数是对于消息值的附加参数。这4个参数的类型与SendMessage()函数的参数相对应。

上面WindowProc()窗口过程中只对两个消息进行了处理,分别是WM_PAINT和WM_CLOSE。这里为了演示因此只简单处理了两个消息。Windows中有上千种消息,那么多的消息不可能全部都由程序员自己去处理,程序员只处理一些程序中需要的消息,其余的消息就交给了DefWindowProc()函数进行处理。DefWindowProc()函数实际上是将消息传递给了操作系统,由操作系统来处理程序中没有处理的消息。比如,在调用CreateWindow()函数时,系统会发送消息WM_CREATE给窗口过程,但是这个消息可能对程序的功能并不需要进行特殊的处理,因此直接交由DefWindowProc()函数让系统进行处理。

DefWindowProc()函数的定义如下:

LRESULT DefWindowProc(
  HWND hWnd,
  UINT Msg,
  WPARAM wParam,
  LPARAM lParam
);

该函数的4个参数跟窗口过程的参数相同,只要将窗口过程的参数依次传递给DefWindowProc()函数就可以完成该函数的调用。在switch分支结构中的default位置直接调用DefWindowProc()函数就可以了。

WM_CLOSE消息是关闭窗口时发出的消息,在这个消息中需要调用DestoryWindow()函数来销毁窗口,并且调用PostQuitMessage()来退出消息循环,使程序退出。对于WM_PAINT消息,这里不进行介绍,涉及的几个API函数可以参考MSDN进行了解。

有的资料在介绍消息循环时会给出一个建议,就是把需要经常处理的消息放到程序靠上的位置,而将不经常处理的消息放到程序靠下的位置,从而提高程序的效率。其实,在窗口过程中往往会使用switch结构对消息进行判断(如果使用if和else结构进行消息的判断,那么常用的消息是要放到前面),而switch结构在编译器进行编译后会进行优化处理,从而大大提高程序的运行效率。关于switch结构的优化,我们将在其他章节进行介绍。

鼠标和键盘的操作也会被转换为相应的系统消息,窗口过程中在接收到鼠标或键盘消息后会进行相应的处理。通过前面的内容了解到,可以通过SendMessage()和PostMessage()发送消息到指定的窗口过程中,那么使用这两个函数来发送鼠标和键盘的相关消息就可以进行鼠标和键盘的模拟操作。除了SendMessage()和PostMessage()外,还可以通过keybd_event()和mouse_event()两个专用的函数进行鼠标和键盘按键的模拟操作。关于鼠标和键盘按键的模拟的用处就不多说了,想必读者都是知道的。

通过前面的介绍,我们已经明白,Windows的应用程序是基于消息机制的,对于鼠标和键盘的操作也会被系统转化为相应的消息。首先来学习如何通过发送消息进行鼠标和键盘的模拟操作。

1.鼠标、键盘按键常用的消息

无论是鼠标指针(或光标)的移动、单击,还是键盘的按键,通常在Windows应用程序中都会转换成相应的消息。在操作鼠标时,使用最多的是移动鼠标和单击鼠标键。比如,在教新手使用计算机时会告诉他,将鼠标指针(或光标)移动到“我的电脑”上,然后单击鼠标右键,在弹出的快捷菜单中用鼠标左键单击选择“属性”对话框。当移动鼠标光标的时候,系统中对应的消息是WM_MOUSEMOVE消息,按下鼠标左键时的对应的消息是WM_LBUTTONDOWN,释放鼠标左键时,对应的消息是WM_LBUTTONUP。在系统中,鼠标的消息有很多。在MSDN中查询到的鼠标消息如图1-8所示。

图1-8 鼠标相关消息

同样,在系统中也定义了键盘的按下与抬起的消息。键盘按下的消息是WM_KEY DOWN,与之对应的键盘抬起的消息是WM_KEYUP。除了这两个消息外,还有一个消息是比较常用的,这个消息在前面介绍消息循环时提到过,就是WM_CHAR消息。键盘的消息相对于鼠标要少很多,在MSDN中查询到的键盘消息如图1-9所示。

图1-9 键盘相关消息

2.PostMessage()函数对键盘按键的模拟

通过前面的介绍,我们已经知道,PostMessage()和SendMessage()这两个函数可以对指定的窗口发送消息。既然鼠标和键盘按键的操作被系统转换为相应的消息,那么就可以使用PostMessage()和SendMessage()通过按鼠标和键盘按键发送的消息来模拟它们的操作。对于模拟键盘按键消息,最好使用PostMessage()而不要使用SendMessage()。在很多情况下,SendMessage()是不会成功的。

现在编写一个简单的小工具,它通过PostMessage()函数模拟键盘发送(发送F5键的消息来模拟网页的刷新)的信息来刷新网页。首先打开VC6.0,创建一个MFC对话框工程,按照图1-10所示设置界面。

图1-10 模拟键盘刷新网页界面布局

按照图1-10所示的界面进行布局,然后为“开始”按钮设置控件变量。这个小程序在“IE浏览器标题”处输入要刷新的页面的标题,在“刷新频率”处输入一个刷新的时间间隔,单位是秒。

当了解程序的功能并且将程序的界面布置好以后,就可以开始编写程序的代码了。程序的代码分为两部分,第一部分是程序要处理“开始”按钮的事件,第二部分是要按照指定的时间间隔对指定的浏览器发送按F5键的消息来刷新网页。

首先来编写响应“开始”按钮事件的代码,双击“开始”按钮来编写它的响应事件。代码如下:

void CKeyBoardDlg::OnBtnStart() 
{
    // TODO: Add your control notification handler code here
    CString strBtn;
   int nInterval = 0;

   // 获取输入的浏览器标题
   GetDlgItemText(IDC_EDIT_CAPTION, m_StrCaption);
   // 获取输入的刷新频率
   nInterval = GetDlgItemInt(IDC_EDIT_INTERVAL, FALSE, TRUE);

   // 判断输入的值是否非法
   if ( m_StrCaption ==""|| nInterval == 0 )
   {
      return ;
   }

   // 获取按钮的标题
   m_Start.GetWindowText(strBtn);

   if ( strBtn == "开始" )
   {
      // 设置定时器
      SetTimer(1, nInterval * 1000, NULL);
      m_Start.SetWindowText("停止");
      GetDlgItem(IDC_EDIT_CAPTION)->EnableWindow(FALSE);
      GetDlgItem(IDC_EDIT_INTERVAL)->EnableWindow(FALSE);
   } 
   else
   {
      // 结束定时器
      KillTimer(1);
      m_Start.SetWindowText("开始");
      GetDlgItem(IDC_EDIT_CAPTION)->EnableWindow(TRUE);
      GetDlgItem(IDC_EDIT_INTERVAL)->EnableWindow(TRUE);
   }
}

在代码中,首先判断按钮的文本,如果是“开始”,则通过SetTimer()函数设置一个定时器;如果按钮的文本不是“开始”,则通过KillTimer()函数关闭定时器。

这里的SetTimer()和KillTimer()是MFC中CWnd类的两个成员函数,不是API函数。很多MFC中的类成员函数和API函数的写法是一样的,但是它们还是有区别的。比较一下SetTimer()在MFC中的定义和API函数的定义的差别。

MFC中的定义如下:

UINT SetTimer( 
     UINT nIDEvent, 
     UINT nElapse, 
     void (CALLBACK EXPORT* lpfnTimer)( 
           HWND, UINT, UINT, DWORD) );

API函数的定义如下:

UINT_PTR SetTimer(
  HWND hWnd,
  UINT_PTR nIDEvent,
  UINT uElapse,
  TIMERPROC lpTimerFunc 
);

从定义中可以看出,MFC中SetTimer()函数的定义比API中SetTimer()函数的定义少了一个参数,即HWND的窗口句柄的参数。在MFC中,窗口相关的成员函数都不需要指定窗口句柄,在MFC的内部已经维护了一个m_hWnd的句柄变量(如果想要查看或使用MFC内部维护的m_hWnd成员变量,可以直接使用它,也可以通过调用GetSafeHwnd()成员函数来得到它,推荐使用第二种方法)。

在按钮事件中添加定时器,那么定时器会按照指定的时间间隔进行相应的处理。定时器部分的代码如下:

void CKeyBoardDlg::OnTimer(UINT nIDEvent) 
{
    // 在此处添加处理程序代码

   HWND hWnd = ::FindWindow(NULL, m_StrCaption.GetBuffer(0));
   // 发送键盘按下消息
   ::PostMessage(hWnd, WM_KEYDOWN, VK_F5, 1);
   Sleep(50);
   // 发送键盘抬起消息
   ::PostMessage(hWnd, WM_KEYUP, VK_F5, 1);

   CDialog::OnTimer(nIDEvent);
}

关于定时器的处理非常简单,通过FindWindow()函数得到要刷新窗口的句柄,然后发送WM_KEYDOWN和WM_KEYUP消息来模拟键盘按键即可。其实在模拟的过程中,可以省去WM_KEYUP消息的发送,但是为了模拟效果更接近真实性,建议在模拟时将消息成对发送。

将写好的程序编译连接后运行起来看效果,在“IE浏览器标题”处输入浏览器的标题,这个标题可以通过Spy++获得,然后在“刷新频率”处输入1。然后单击“开始”按钮,观察浏览器每个1秒进行刷新一次。当单击“停止”按钮后,程序不再对浏览器进行刷新按键模拟。

到此,通过PostMessage()函数发送按F5键进行键盘按键模拟的程序就完成了。使用PostMessage()函数的好处是目标窗口可以在后台,而不需要窗口处于激活状态。可以将被刷新的浏览器最小化,然后运行刷新网页的小程序,在任务栏可以看到浏览器仍然在不断刷新。

在开发程序时,总是依靠发送消息是非常辛苦的事情,因为消息的类型非常多,并且不同消息的附件参数也因不同的消息类型而异。Windows几乎为每个常用的消息都提供了相应的API函数。为了不必记忆过多的消息,使用API函数进行开发是相对比较直观的。

1.鼠标键盘按键模拟函数

在使用Windows的系统消息进行模拟鼠标或键盘按键操作时,可能显得不直观,也不方便。微软公司在进行设计时已经考虑到了这点,因此在Windows下的大部分消息都可以直接使用对应的等价API函数,不必直接通过发送消息。比如可以用WM_GETTEXT消息去获取文本的内容,对应的函数有GetWindowText()。试想一下,如果程序中一眼看去都是SendMessage()与PostMessage()之类的函数,岂不是很吓人。

本节介绍两个函数,分别用来模拟鼠标和键盘的输入,它们分别是keybd_event()和mouse_event(),定义如下:

VOID keybd_event(
  BYTE bVk,
  BYTE bScan,
  DWORD dwFlags,
  ULONG_PTR dwExtraInfo
);
VOID mouse_event(
  DWORD dwFlags,
  DWORD dx,
  DWORD dy,
  DWORD dwData,
  ULONG_PTR dwExtraInfo
);

从函数的名称就能看出,这两个API函数分别对应的是键盘事件和鼠标事件,在程序里使用时,对于阅读代码的人来说就比较直观了。下面将使用keybd_event()和mouse_event()两个函数来完成上一小节编写的刷新网页的小工具。

2.网页刷新工具

keybd_event()和mouse_event()这两个API函数,从函数的参数上来看,不需要给它们传递窗口句柄当作参数。那么这两个函数在进行鼠标和键盘的模拟时就必须将目标窗口激活并处于所有窗口的最前端。因此在程序中首先要完成的是将目标窗口设置到最前面,并且处于激活状态。先来看一下程序的界面部分,如图1-11所示。

图1-11 模拟鼠标键盘

这次的窗口相比上个程序的窗口要简单些。在界面上有两个按钮,第1个按钮“模拟键盘”是通过keybd_event()来模拟按F5键从而刷新网页,第2个按钮“模拟鼠标”是通过mouse_event()来模拟鼠标右键,从而弹出浏览器的快捷菜单,再通过keybd_event()模拟按R键来刷新网页。

知道了程序要实现的功能,先来完成将目标窗口设置到最前面并处于激活状态的部分,代码如下:

VOID CSimInputDlg::FindAndFocus()
{
   GetDlgItemText(IDC_EDIT_CAPTION, m_StrCaption);

   // 判断输入是否为空
   if ( m_StrCaption == "" )
   {
      return ;
   }

   m_hWnd = ::FindWindow(NULL, m_StrCaption.GetBuffer(0));

   // 该函数将创建指定窗口的线程设置到前台
   // 并且激活该窗口
   ::SetForegroundWindow(m_hWnd);
}

这个自定义函数非常简单,分别调用了FindWindow()和SetForegroundWindow()两个API函数。FindWindow()函数在前面部分已经介绍过了。SetForegroundWindow()函数的使用比较简单,它会将指定的窗口设置到最前面并处于激活状态,该函数只有1个参数,是目标窗口的窗口句柄(这里的窗口句柄变量m_hWnd就是前面提到的由MFC提供的变量,该值也可以使用GetSafeHwnd()函数来进行获取,这点前面已经说过了,读者可以自行测试)。

“模拟键盘”按钮对应的代码如下:

void CSimInputDlg::OnBtnSimkeybd() 
{
    // 在此处添加处理程序代码
    // 找到窗口
    // 将其设置到前台并激活
   FindAndFocus();
   Sleep(1000);

   // 模拟F5三次
   keybd_event(VK_F5, 0, 0, 0);
   Sleep(1000);
   keybd_event(VK_F5, 0, 0, 0);
   Sleep(1000);
   keybd_event(VK_F5, 0, 0, 0);
}

在进行模拟键盘按键前,首先要调用自定义函数FindAndFocus()将浏览器设置到最前面并处于激活状态(在“模拟鼠标”按钮中同样要先调用FindAndFocus()自定义函数)。通过调用keybd_event()函数来模拟F5键进行了3次网页的刷新。

“模拟鼠标”按钮对应的代码如下:

void CSimInputDlg::OnBtnSimmouse() 
{
    // 在此处添加处理程序代码
    FindAndFocus();

   // 得到窗口在屏幕的坐标(x, y)
   POINT pt = { 0 };
   ::ClientToScreen(m_hWnd, &pt);

   // 设置鼠标位置
   SetCursorPos(pt.x + 36, pt.y + 395);

   // 模拟单击鼠标右键
   // 单击鼠标右键后,浏览器会弹出快捷菜单
   mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
   Sleep(100);
   mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);

   Sleep(1000);
   // 0x52 = R
   // 在弹出右键菜单后按下R键
   // 会刷新页面
   keybd_event(0x52, 0, 0, 0);
}

代码中用到了两个陌生的API函数,分别是ClientToScreen ()和SetCursorPos()。它们的定义如下:

BOOL ClientToScreen(
  HWND hWnd,       // handle to window
  LPPOINT lpPoint  // screen coordinates
);

ClientToScreen()函数的作用是将窗口区域的坐标转换为屏幕的坐标。更直接的解释是,得到指定窗口在屏幕中的坐标位置。

BOOL SetCursorPos(
  int X,  // horizontal position
  int Y   // vertical position
);

SetCursorPos()函数的作用是将鼠标光标移动到指定的坐标位置。

在程序中为什么不使用mouse_event()来移动鼠标光标的位置,而是使用SetCursorPos()的位置呢?在API函数中,与SetCursorPos()对应的一个函数是GetCursorPos(),而SetCursorPos()函数往往会与GetCursorPos()函数一起使用。因为在很多情况下,程序设置鼠标光标位置进行一系列操作后,仍需要将鼠标光标的位置设置回原来的位置,那么在调用SetCursorPos()前,就需要调用GetCursorPos()得到鼠标光标的当前位置,这样才可以在操作完成后把鼠标光标设置为原来的位置。由此也可以看出,很多API函数是成对出现的,有Set也有Get,这样在记忆的时候非常的方便。

在程序中调用SetCursorPos()函数时,参数中的x坐标和y坐标分别加了两个整型的常量,这里可能比较费解。这两个整型常量的作用是通过ClientToScreen()函数得到的是浏览器左上角的xy坐标,而浏览器的鼠标右键菜单必须在浏览器的客户区中才能激活,因此需要在左上角坐标的基础上增加两个偏移,代码里的两个整型常量就是一个偏移(这里的偏移值可以自己随意修改,只要保证鼠标能够落在浏览器窗口中即可)。

3.小结

对于鼠标和键盘按键的模拟在很多地方都会使用,比如有的病毒用模拟鼠标单击杀毒软件的警告提示,比如游戏辅助工具通过模拟鼠标进行快速单击……对于鼠标和键盘按键的模拟并不简单。在常规的情况下,可以通过上面介绍的内容来进行鼠标和键盘按键的模拟操作。但是对于有些情况就不行了,比如有些游戏过滤了PostMessage()函数发送来的消息,有些游戏hook了keybd_event()和mouse_event()函数,有些游戏使用了DX来响应鼠标和键盘……

在很多软件中需要多个进程协同工作,而不是单一的进程进行工作。那么多进程的协同工作就涉及进程间的通信。在Windows下,进程间的通信有多种实现的方法,比如管道、邮槽、剪贴板、内存共享……前面介绍了Windows的消息机制,本节主要介绍通过消息实现进程间的通信,通过知识的连贯性将前面的知识加以应用与提升。

通过消息进行进程间的通信,有一定的限制性。根据前面介绍的内容,Windows下有窗口的应用程序是基于消息驱动进行工作的,那么没有窗口的程序就不是基于消息驱动来进行工作的。对于非窗口的应用程序是无法通过消息进行进程间通信的。

通过消息实现进程间的通信在本节中介绍两种方法,一种是通过自定义消息进行进程间的通信,另一种是通过使用WM_COPYDATA消息进行进程间的通信。

消息分为两种,一种是系统已经定义的消息,另一种是用户自定义的消息。系统已经定义的消息是从0到0x3ff,用户自定义的消息可以从0x400开始。系统中提供了一个宏WM_USER,在进行自定义消息时,在WM_USER的基础上加一个值就可以了。下面来实现一个自定义消息完成进程间通信的程序例子。

1.实现自定义消息的步骤

根据前面的介绍,我们知道,通过自定义消息进行进程间通信,只有带有窗口的进程才能完成基于消息的进程间通信。既然是进程间通信,那么就需要至少编写两个程序,一个是接收消息的服务端,另一个是发送消息的客户端,并且这两个程序都需要有窗口。

先来介绍程序的功能,在发送消息的客户端,通过自定义消息给接收消息的服务端发送两个整型的数值。接收消息的服务端,将接收到的两个数值进行简单的加法运算。接收消息的服务端在VC下,使用MFC通过自定义消息来完成进程间的通信需要3个步骤,首先要定义一个消息,其次是添加自定义消息的消息映射,最后是添加消息映射对应的消息处理函数。

首先在服务端和客户端定义一个消息,具体如下:

#define WM_UMSG WM_USER + 1

然后是在接收消息的服务端添加消息映射,如下:

BEGIN_MESSAGE_MAP(CUserWMDlg, CDialog)
        //{{AFX_MSG_MAP(CUserWMDlg)
        ON_WM_SYSCOMMAND()
        ON_WM_PAINT()
        ON_WM_QUERYDRAGICON()
        ON_MESSAGE(WM_UMSG, RevcMsg)
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()

在这个消息映射中,ON_MESSAGE(WM_UMSG, RevcMsg)是自定义消息的消息映射。

最后在接收消息的服务端添加自定义消息的消息响应函数。根据消息映射可以得知,消息响应函数的函数名为RevcMsg(),定义如下:

VOID CUserWMDlg::RevcMsg(WPARAM wParam, LPARAM lParam)
{
     // ….
}

2.完成自定义消息通信的代码

对于如何完成自定义消息的介绍已经介绍完了,现在来看两个程序的窗口界面,如图1-12和图1-13所示。

图1-12 自定义消息服务端(接收端)

图1-13 自定义消息客户端(发送端)

知道了两个程序的作用以及窗口的界面,那么开始对它们分别进行编码。首先来看自定义消息服务端的代码,该部分的代码比较简单。前面已经介绍了如何定义消息,如何添加消息映射,如何添加消息响应函数。现在只需要完成消息响应函数的函数体即可。消息响应函数代码如下:

VOID CUserWMDlg::RevcMsg(WPARAM wParam, LPARAM lParam)
{
   int nNum1, nNum2, nSum;
   nNum1 = (int)wParam;
   nNum2 = (int)lParam;

   nSum = nNum1 + nNum2;

   CString str;
   str.Format("%d", nSum);

   SetDlgItemText(IDC_EDIT_REVCDATA, str);
}

在消息响应的函数中有两个参数,分别是WPARAM类型和LPARAM类型。这两个参数可以接收两个4字节的参数。这里代码中接收了两个整型数值,进行相加后显示在了窗口上的编辑框中。

在发送消息端,也需要定义相同的消息类型。这里不再重复介绍,只要把响应的定义复制粘贴即可。主要看发送消息的函数,代码如下:

void CUserWMCDlg::OnBtnSend() 
{
    // 在此处添加处理程序代码
   int nNum1, nNum2;

   nNum1 = GetDlgItemInt(IDC_EDIT_SENDDATA, FALSE, FALSE);
   nNum2 = GetDlgItemInt(IDC_EDIT_SENDDATA2, FALSE, FALSE);

   HWND hWnd = ::FindWindow(NULL, "自定义消息服务端");
   ::SendMessage(hWnd, WM_UMSG, (WPARAM)nNum1, (LPARAM)nNum2);
}

通过SendMessage()函数完成了发送,同样也非常简单。在SendMessage()函数中,通过第3个参数和第4个参数将两个整型值发送给了目标的窗口。

从自定义消息的例子中可以看出,自定义消息对于进程间的通信只能完成简单的数值型的传递,对于类型复杂的数据的通信就无法完成了。那么,通过消息是否能完成字符串等数据的通信传递呢?答案是肯定的。接下来看使用WM_COPYDATA消息完成进程间通信的例子。

自定义消息传递的数据类型过于简单,而通过WM_COPYDATA消息进行进程间的通信会更加灵活。但是由于SendMessage()函数在发送消息时的阻塞机制,在使用WM_COPYDATA时传递的消息也不宜过多。

1.WM_COPYDATA消息介绍

应用程序发送WM_COPYDATA消息可以将数据传递给其他应用程序。WM_COPYDATA消息需要使用SendMessage()函数进行发送,而不能使用PostMessage()消息。通过SendMessage()函数发送WM_COPYDATA消息的形式如下:

SendMessage( 
  (HWND) hWnd,
  WM_COPYDATA,
  (WPARAM) wParam,
  (LPARAM) lParam
);

第1个参数hWnd是接收消息的目标窗口句柄;第2个参数是消息的类型,也就是当前正在介绍的消息WM_COPYDATA;第3个参数是发送消息的窗口句柄;第4个参数是一个COPYDATASTRUCT结构体的指针。

COPYDATASTRUCT结构体的定义如下:

typedef struct tagCOPYDATASTRUCT { 
   ULONG_PTR dwData; 
   DWORD    cbData; 
   PVOID    lpData; 
} COPYDATASTRUCT, *PCOPYDATASTRUCT;

其中,dwData是自定义的数据,cbData用来指定lpData指向的数据的大小,lpData是指向数据的指针。

在程序中,发送WM_COPYDATA消息方仍然会通过调用FindWindow()函数来查找目标窗口的句柄,而接收消息方需要响应对WM_COPYDATA消息的处理。WM_COPYDATA不是自定义消息,在编程时不必像自定义消息那样需要自己定义消息和添加消息映射,这部分工作可以直接通过MFC辅助进行。

MFC添加WM_COPYDATA消息响应的方法如下:

首先在要响应WM_COPYDATA消息的窗口对应的类上单击鼠标右键,在弹出的快捷菜单中选择“Add Windows Message Handler”,如图1-14所示。选择该菜单项后会出现如图1-15所示的添加消息响应函数对话框。

图1-14 选择“Add Windows Message Handler”

图1-15 添加消息响应函数对话框

在“New Windows messages/events:”列中找到WM_COPYDATA消息,然后双击将它添加到“Existing message/event handlers:”列中。最后单击“Add Handler”按钮,MFC就自动生成了WM_COPYDATA的消息映射及消息响应函数。Windows其他常用的消息都可以通过该对话框辅助生成消息映射及消息响应函数。

2.WM_COPYDATA程序界面及介绍

对于WM_COPYDATA消息,前面已经介绍了,程序同样分为客户端程序和服务端程序。首先来看程序运行的效果,如图1-16所示。

图1-16 WM_COPYDATA的服务端与客户端界面

WM_COPYDATA的服务端会接收WM_COPYDATA消息,在接收到WM_COPYDATA消息进行处理后同样会发送一个WM_COPYDATA消息给客户端进行消息反馈。WM_COPYDATA的客户端会通过FindWindow()函数来查找WM_COPYDATA的服务端,并发送WM_COPYDATA消息,同样也会接收服务端发来的WM_COPYDATA消息并进行处理。

3.WM_COPYDATA客户端程序的实现

有了前面的介绍,现在我们就来完成程序的编码工作,首先来看WM_COPYDATA客户端。客户端的界面中有3个控件,分别是一个按钮控件、一个编辑框控件和一个列表框控件(为列表框控件定义一个控件变量:CListBox m_ListRec;)。

WM_COPYDATA客户端的代码如下:

void CCopyDataCDlg::OnBtnSend() 
{
    // 在此处添加处理程序代码
   // 查找接收WM_COPYDATA消息的窗口句柄
   HWND hWnd = ::FindWindow(NULL, "COPYDATA服务端");

   CString strText;
   GetDlgItemText(IDC_EDIT_SENDDATA, strText);

   // 设置COPYDATASTRUCT结构体
   COPYDATASTRUCT cds;
   cds.dwData = 0;
   cds.cbData = strText.GetLength() + 1;
   cds.lpData = strText.GetBuffer(cds.cbData);

   // m_hWnd是CWnd类中的一个成员函数
   // 表示该窗口的句柄
   ::SendMessage(hWnd, WM_COPYDATA, (WPARAM)m_hWnd, (LPARAM)&cds);
}

BOOL CCopyDataCDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) 
{
    // 在此处添加处理程序代码或者调用默认方法

   // 处理服务端发来的WM_COPYDATA消息
   CString strText;
   strText.Format("服务端在[%s]接收到该消息", pCopyDataStruct->lpData);

   m_ListRec.AddString(strText);

    return CDialog::OnCopyData(pWnd, pCopyDataStruct);
}

4.WM_COPYDATA服务端程序的实现

WM_COPYDATA服务端有两个控件,分别是一个列表框控件和一个按钮控件。为列表框控件定义一个控件变量:CListBox m_ListData。

WM_COPYDATA服务端的代码如下:

BOOL CCopyDataSDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT* pCopyDataStruct) 
{
    // 在此处添加处理程序代码或者调用默认方法
   CString strText;

   // 通过发送消息的窗口句柄获得窗口对应的进程号,即PID
   DWORD dwPid = 0;
   ::GetWindowThreadProcessId(pWnd->m_hWnd, &dwPid);

   // 格式化字符串并添加至列表框中
   strText.Format("PID=[%d]的进程发来的消息为:%s", 
             dwPid, pCopyDataStruct->lpData);
   m_ListData.AddString(strText);

   // 获取本地时间
   SYSTEMTIME st;
   GetLocalTime(&st);

   CString strTime;
   strTime.Format("%02d:%02d:%02d", st.wHour, st.wMinute, st.wSecond);

   // 将本地时间发送给客户端程序
   COPYDATASTRUCT cds;
   cds.dwData = 0;
   cds.cbData = strTime.GetLength() + 1;
   cds.lpData = strTime.GetBuffer(cds.cbData);

   // 注意SendMessage()函数的第3个参数为NULL
   ::SendMessage(pWnd->m_hWnd, WM_COPYDATA, NULL, (LPARAM)&cds);

    return CDialog::OnCopyData(pWnd, pCopyDataStruct);
}

void CCopyDataSDlg::OnBtnDelall() 
{
    // 在此处添加处理程序代码

   // 清空列表框内容
   while ( m_ListData.GetCount() )
   {
       m_ListData.DeleteString(0);
   }
}

在接收消息的服务端调用GetWindowThreadProcessId()通过发送消息的窗口得到了发送消息的进程PID号,并将接收消息的时间反馈给了发送消息的客户端。

5.小结

关于WM_COPYDATA的服务端和客户端的代码都有比较详细的注释,因此没有过多解释。这里需要强调一点,WM_COPYDATA消息需要两个附加消息,也就是SendMessage()函数的wParam和lParam参数都需要使用。wParam参数表示发送消息的窗口句柄,但是该参数可以省略,还可以通过类型转换传递其他数值型的数据。lParam参数是COPYDATASTRUCT结构体指针类型,不可以省略,否则接收WM_COPYDATA消息的服务端会无法响应。

VC6比起VS2005、VS2008之类的开发工具显得小巧轻便,非常适合入门学习,对于真正的开发也毫不逊色。这里介绍VC6开发环境下的两个工具,一个是比较简单且需要经常使用的“Error Lookup”,另一个是集成在VC中的调试器(前面介绍的SPY++也可以通过VC6的“ToolS”菜单栏找到)。除了这两个VC提供的工具外,我们还会介绍另外一个与Error Lookup工具相似的工具,即Windows Error Lookup Tool。

Error Lookup工具可以在VC6的“ToolS”菜单中找到,它可以对GetLastError()函数提供的出错代码进行解释,解释为可以理解的文字描述。下面通过一个非常简单的程序来解释该工具的使用。

例子代码如下:

#include <windows.h>
#include <stdio.h>

int main()
{
    HANDLE hFile = CreateFile("c:\\test.txt", GENERIC_READ, 
                 FILE_SHARE_READ, NULL, OPEN_EXISTING, 
                 FILE_ATTRIBUTE_NORMAL, NULL);

    if ( hFile == INVALID_HANDLE_VALUE )
    {
        printf("Err Code = %d \r\n", GetLastError());
    }

    return 0;
}

这段代码非常短小,主要是通过CreateFile()函数打开一个已经存在的文件,但是这里传递给函数的第1个参数“c:\test.txt”是一个不存在的文件,那么CreateFile()对c:\test.txt文件的打开必然会错误。当打开错误时,程序调用GetLastError()函数会得到一个错误码,并通过printf()进行输出。

编译运行这个程序,看到命令行中输出字符串“Err Code = 2”,说明GetLastError()函数得到的错误码为“2”。有了这个错误码,通过VC6的“ToolS”菜单打开“Error Lookup”工具,在 “Value”处输入“2”,然后单击“Look up”按钮,就可以看到错误码的解释为“系统找不到指定的文件”,如图1-17所示。

图1-17 “Error Lookup”工具

在平时写程序的时候,要养成对函数的返回值进行判断的习惯。在编写程序的时候,当调用CreateFile()函数时,指定文件的参数可能是由用户提供的。而当用户指定的文件不存在时,同样会报错。在代码中调用FormatMessage()函数可以将GetLastError()函数的错误码转换为错误描述。(提示:这里只是说明在代码中如何将GetLastError()的错误码转换为错误描述,建议在真正写程序时自行对用户的输入进行判断过滤,以保证程序的健壮性。)

Windows Error Lookup Tool工具是第三方的Windows错误码查看工具。该工具可以查看的错误码的类型有4类,分别是Win32、HRESULT、NTSTATUS、STOP。随着Windows Error Lookup Tool工具版本的更新,支持的错误码的数量也会不断增多。它相当于一个功能更强大的Error Lookup的增强版工具。

同样,将错误代码“2”输入该工具的编辑框中,可以看到给出的提示也是“系统找不到指定的文件”。该错误码的类型为“Win32”类型,此类型属于Win32 API定义的错误代码。除了Win32的错误码外,这里将编写另外一个程序例子来测试该软件。代码如下:

#include <stdio.h>

int main()
{
   int *p = NULL;

   *p = 3;

   return 0;
}

该代码在VC6下编辑完成后按F5调试运行,当程序执行到*p = 3时,程序会报错,如图1-18所示。

图1-18 错误代码为0xC0000005

调试提示的错误码为0xC0000005,将该错误码复制到Windows Error Lookup Tool中查看,如图1-19所示。

图1-19 错误类型为STATUS_ACCESS_VIOLATION

在图1-19中,错误的定义为STATUS_ACCESS_VIOLATION,意思是访问违例。在例子代码中对0地址进行了赋值,而0地址是禁止访问的地址,因此提示为访问内存违例。目前Windows Error Lookup Tool3.0.6版本没有对0xC0000005的错误码给出正确的描述,但是对其他绝大部分错误码都能给出正确的错误描述。(提示对于指针的赋值,一定要检查指针的有效性。在指针进行定义和指针指向空间释放时,一定要将其赋值为NULL。这样,当程序出错时,可以较容易地找到代码的错误位置。)

在编写代码的过程中,经常需要查找逻辑上的问题,或者是查找一些原因不明的问题。在这种情况下,就需要使用调试工具对编写的代码进行调试,以便能够找到代码中的问题。

1.调试器

调试的一般过程是让程序在调试的状态下运行。什么是调试状态呢?其实很简单,就是让程序在调试器的控制下运行。调试器可以对程序做多方面的控制,这里举几个简单的方面。

对于调试器的诸多功能,无法全面介绍,各种使用技巧及方法需要读者慢慢体会。下面的内容将针对上面的介绍来说明VC6中提供的调试器的使用。

2.被调试程序的代码

调试器具有的功能在前面已经进行了简单的说明。前面介绍调试器的功能不单单针对VC6提供的调试器,几乎任何调试器都支持以上功能,而且专业的调试器功能远不止如此。下面举例介绍说明VC6的调试器。

首先新建一个VC6的控制台应用程序,输入如下代码:

#include <iostream.h>

int main(int argc, char* argv[])
{
    // 定义3个整型的指针变量
    int *p = NULL;      // 32位的整型变量指针
    __int64 *q = NULL;  // 64位的整型变量指针
    int *m = NULL;      // 32位的整型变量指针

    // 使用new分配一个整型的内存空间
    // 用指针变量p指向该内存空间
    p = new int;
    if ( p == NULL )
    {
        return -1;
    }

    // 为指针变量p指向的内存空间赋值
    *p = 0x11223344;

    // q和m操作同p
    q = new __int64;
    if ( q == NULL )
    {
        return -1;
    }

    *q = 0x1122334455667788;

    m = new int;
    if ( m == NULL )
    {
        return -1;
    }

    *m = 0x11223344;

    // 释放3个变量指向的地址空间
    // 释放顺序依次是q、m、p
    delete q;
    q = NULL;

    delete m;
    m = NULL;

    delete p;
    p = NULL;

    return 0;
}

写完该程序后,按F7键进行编译连接,生成可执行文件。上面的步骤属于代码编辑、编译、连接的过程。接下来要完成的工作是对这段源代码生成的可执行文件进行调试,目的是熟悉VC6的调试器,以及熟悉VC6下Debug编译方式下生成的可执行文件是如何对“堆”空间进行管理的。


 

注: 

堆空间是在程序运行时由程序员自己申请的空间,该空间同样需要程序员自己进行释放。在C++语言中,使用new关键字申请堆空间,使用delete关键字可以对堆空间进行释放。C语言中的malloc()和free()函数也是申请和释放堆空间的函数。在程序中,除了有“堆”空间以外,还有另一种称为“栈”的内存空间,栈空间是由系统进行维护的空间。局部变量和函数的参数使用的都是栈空间,栈空间的分配和回收是由系统自动进行维护的。这里的“堆”与数据结构中的“堆排序”没有任何关系。


3.认识调试窗口

在编辑完以上的代码后,按F10键让程序处于调试状态,开始对编译生成的程序进行调试,程序的窗口界面如图1-20所示。

图1-20 VC的调试界面

VC的调试界面分为5个区域,(从左到右、从上到下)依次是调试工作区、寄存器窗口、调用栈窗口、监视窗口和内存窗口。除了调试工作区外,其余几个窗口都不是必需的。根据环境的不同,不是每个VC6在调试状态下都会出现这些窗口。除了这几个窗口外,还有其他关于调试方面的窗口。各种调试窗口的打开方式可以通过菜单进行,如图1-21所示。

图1-21 打开调试窗口的菜单

VC6的调试环境提供了6个调试窗口,均是常用的调试窗口。调试窗口的使用非常容易,这里不做过多的介绍。

程序在进入调试状态后,不可能始终通过单步方式让程序一步一步执行。调试器提供了多种调试运行方式,通过调试器控制可以使程序按照不同的方式运行。VC6提供了几种调试运行的方式,如图1-22所示。

图1-22 调试菜单

图1-22中的4种运行方式分别如下。

Step Into:这种方式称为单步步入方式,快捷键是F11键。单步步入的意思是当单步调试时,遇到函数调用时会进入被调用的函数体内。

Step Over:这种方式称为单步步过方式,快捷键是F10键。单步步过的意思是当单步调试时,遇到函数调用时不会进入被调用的函数体内。

Step Out:这种方式称为执行到函数返回处。当调试进入某个函数时,这个函数又不是调试的关键函数,可以通过该方式快速返回。

Run to Cursor:这种方式称为执行到光标处。当调试时明确知道要调试的地方时,可以使程序运行至光标指定的位置,这样会节省很多因为单步调试而浪费的时间。

除了上面几个调试命令外,再介绍3个调试的命令,分别是F9、F5和F7键。F9键是在光标指定的位置设置断点,当程序在调试状态下运行时遇到断点,会产生中断(程序在调试器中被中断后可以观察被调试程序的变量值,某块内存中的内容);F5键使程序进入调试状态运行,如果代码中有断点,则会在断点处产生中断,如果没有断点,程序会运行到界面启动或等待用户的交互,或者直接执行完程序自动结束调试状态;F7键是结束调试状态下运行的程序。

在调试程序时,尤其是调试代码量非常大的程序时,往往不可能通过单步执行一直来进行调试。通常情况是在某个或某几个关键的位置设置断点,然后让程序处于调试运行,当运行到断点处,程序会产生中断,这时再通过单步调试方法调试重要的代码部分,观察变量、内存、调用栈等数据的实时变化情况。一般调试时,都是调试部分代码的上下文,很少有从头开始调试的,那样效率就太低下了。

4.调试程序

前面的准备工作都已经完成了,接下来就来调试上面编辑的代码。按F10键,让程序处于调试状态,在监视窗口(Alt+F3组合键显示的Watch窗口)添加要监视的变量,分别是p、q、m、&p、&q、&m。当前调试的光标在main()函数的第一个花括号处,按F10键单步执行一步观察监视窗口,如图1-23所示。

观察如图1-23所示的Watch窗口,通过&p、&q和&m可以看出,3个指针变量pqm已经分配了变量的空间,分别是0x0012ff7c、0x0012ff78、0x0012ff74(如果没有Watch窗口,可以按照前面的介绍打开Watch窗口,如果在Watch窗口中没有内容,可以在Watch窗口中进行添加)。从这里可以看出,在主函数中先定义的变量的地址(局部变量使用的是栈地址)要大于后定义的变量的地址。由于在Win32系统下指针变量所占用的空间大小为4字节,通过3个地址值可以看出,3个变量的地址按照定义顺序依次紧挨。变量pqm的值为0xcccccccc,这是VC6 Debug编译方式下默认对局部变量初始化的值。

单步执行到p = new int;代码处,观察监视窗口,这时可以看到3个变量的值为0,因为3个变量经过初始化后值都被赋为NULL。

图1-23 Watch窗口的说明

在if( p == NULL )代码处按F10键,观察p指向地址的值,如图1-24所示。在VC6的Debug编译方式下,未进行赋值的堆空间的值为0xCDCDCDCD。

图1-24 未赋值的堆空间的值为0xCDCDCDCD

按F10键单步到q = new __int64;代码处,观察监视窗口和内存窗口(内存窗口调整为每行显示16字节),如图1-25所示。

图1-25 通过监视窗口的地址观察内存窗口

在监视窗口中,将&p、&q和&m进行修改,修改为(int *)&p、(__int *)&q和(int *)&m。这里简单说明一下,指针变量p的地址为0x0012ff7c,p指向的地址为0x00382e50,p指向的地址中的值为0x11223344。观察内存窗口,在0x00382e50处保存的值为44 33 22 11(相当于0x11223344。关于为什么顺序是反的,在后面的章节中会给出解释)。


 

注: 

有些C语言的书中说道,指针就是地址。这样的说法是不严密的,准确来说,指针是有类型的地址。 “*”操作需要根据指针的类型来进行取值。对于一个指针,要了解其4个方面,分别是指针的类型、指针的地址、指针指向的地址和指针指向地址的值。如果对这里的解释不明白,请复习C语言关于介绍指针的部分,这里不对C语言的语法知识进行过多的介绍。


按F10键单步执行到delete q;代码处,将p指向的地址减0x20字节,即0x00382e50 – 0x20 = 0x00382e30,然后在内存窗口中观察,如图1-26所示。

图1-26 内存窗口

现在来分析图1-26中的内容,通过监视窗口可以看出p指向的空间为0x00382e50,q指向的空间为0x00382e98,m指向的空间为0x00382ee0。这3个变量指向的空间比较近。再来观察内存窗口, 0x00382e30地址处的值为“98 07 38 00 78 2e 38 00”,这里是两个地址,分别是0x00380798和0x00382e78;0x00382e78地址处的值为“30 2e 38 00 c0 2e 38 00”,这里也是两个地址,分别是0x00382e30和0x00382ec0。0x00382e30是不是看着比较眼熟?这个值就是内存窗口中第一个地址的位置。0x00382ec0地址处的值为“78 2e 38 00 00 00 00 00”,这里同样是两个地址,分别是0x00382e78和0x00000000。0x00382e78是不是看着比较眼熟?整理一下这几个地址,如图1-27所示。从图1-27中可以看出,使用new申请的堆空间是通过双向链表进行链式管理的。图1-27所示为最后一个节点的0x00000000表示链表的结尾。

图1-27 堆的链式管理

明白了链表是链式管理后,接着我们分析其他相关数据。当使用new申请的空间不再使用时,会使用delete释放空间,那么delete要释放多大的空间呢?堆空间的首地址处是管理双向链表的指针,在首地址偏移0x10的位置记录了堆空间的大小,第一个堆空间的首地址是0x00382e30,偏移0x10的位置是0x00382e40,在0x00382e40地址保存的值为4。其余几个用new申请的空间的大小通过这种方式也可以找到。

在堆空间偏移0x18的位置记录堆的一个序号,程序中通过new申请的第1块堆空间的序号为30,第2块为31,第3块为32。

在图1-26中,每个数值的前后(对pqm赋的值)都有4个“FD FD FD FD 44 33 22 11 FD FD FD FD”,前后的FD是用来在调试时检测溢出的。当为指向整型地址的p变量赋值超过4字节时,就会覆盖数值后面的FD;当调试程序时,通过查看FD的值,就可以观察到赋值溢出了。

关于堆的管理结构就介绍这么多,继续按F10键单步执行,执行到q = NULL语句处,观察内存窗口,如图1-28所示。

图1-28 释放q指向的内存后的内存布局

通过图1-28可以看出,释放后的堆空间会被赋值为“EE FE”。观察堆链表的指针的变化,第1块堆的后继链表指针指向了第3块堆,第3块堆的前驱链表指针指向了第1块堆。关于链表的具体操作,需要学习和阅读关于数据结构的知识。


 

提示: 

VC默认提供2种编译方式,分别为DEBUG和RELEASE。以上堆管理方法为DEBUG编译方式,RELEASE编译方式并不是该种管理方法。


作为黑客编程的基础,本章介绍了关于Windows消息的知识。Windows中存在各种各样的消息,学习和掌握Windows的消息可以更好地掌握Windows的编程。无论是应用程序编程还是黑客编程,这里都是以Windows为平台在进行开发,那么其中的基础必然相同。

在编写程序时,需要各种各样的辅助工具来协助代码的编写。当编写的程序出现问题时,需要对程序进行调试。关于开发的辅助工具,本章介绍了Spy++、Error Lookup。对于调试工具,通过调试堆的管理方法介绍VC6的集成调试工具。对于前面的部分,读者需要进行深刻的理解,对于本章后面的部分需要实际动手实验。本章作为全书的基础,在后续的各章中会用到本章的知识。

本章的知识中,至少掌握消息、消息通信的相关知识,至于VC6下Debug方式对“堆”内存空间的管理并不是必须要掌握,重点是掌握VC6提供的调试器的使用方法。


相关图书

CMake构建实战:项目开发卷
CMake构建实战:项目开发卷
代码审计——C/C++实践
代码审计——C/C++实践
零基础学Qt 6编程
零基础学Qt 6编程
C++现代编程技术写给工程师的C++教程
C++现代编程技术写给工程师的C++教程
C++ Templates(第2版)中文版
C++ Templates(第2版)中文版
C++设计模式
C++设计模式

相关文章

相关课程