posts - 319, comments - 22, trackbacks - 0, articles - 11
  C++博客 :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理
导读:


版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
http://chenshine.blogbus.com/logs/4414354.html


刚 进入计算机相关专业领域时,大家最先用过的调试器大多会是Turbo C。它虽然古老但用过的人却很多,然而严格的讲,Turbo C是一个集成开发环境,虽然拥有独立的编译器,链接器,却没有独立的调试器,这和Visual Studio一样。如果你做过DOS,早期Windows下的汇编,也许你会对Debug,CodeView等调试工具熟悉,但这些工具太老 了,Debug甚至不能调试32位程序,介绍它们与这篇文章的主旨不符,如果你对它们感兴趣,可以去查阅相关资料。本文主要是介绍与调试技术相关的理论知 识以及常用调试器的使用,Windows设备驱动程序与内核的调试等。具体的讲解方法是理论结合实践,并且是站在给新手看的角度,循序渐进的,用一个一个 调试会话向你展示每个重要命令的使用。



目录

1。调试器
2。调试信息
3。用户模式程序的调试
4。驱动程序与内核的调试

1 调试器

1。1 概览

调 试器,与编译器,链接器一样,都属于基础软件,它们在制作上都有很大的难度,但尽管如此,现实中还是有不少专业级的调试器,微软官方的就有 cdb,ntsd,WinDbg,kd等,还有SoftIce,OllyDbg等来自于其它公司的优秀调试器。本文不可能对这几个调试器一一介绍,一个是 限于篇幅,另一个是上面列举的这几个调试器无论是哪一个都需要你花很长的时间去完全掌握(在市面上有很多书籍甚至专门讲解某一个调试器的使用,比如 SoftIce)。虽然本文不会讲解SoftIce的详细使用,但我还是要对它进行简短的介绍,因为它太有名了,甚至比Windows出生的早。

1。2 SoftIce

SoftIce 是NuMega公司生产的调试器,产于80年代后期,直到今天为止,这个软件已经变革过好几次了,因为处理器的体系结构在更新,操作系统也在更新,所以调 试器也必须相应的更新。它之所以有名,那是与历史有关的,在Intel推出386 Cpu的时候,NuMega立即让SoftIce支持了386 Cpu的新特性,同时吸引了大量黑客的使用,使它在黑客界产生了一定的地位(功能强大的调试器可以对软件的保护机制进行破解,如突破序列号之类的)。在 Windows推出时,所有的调试器都不适用这种新的系统软件体系结构了,唯一能用的就是微软自己生产的调试器,却很拙劣,这时NuMega又对 SoftIce进行了变革,不仅让它可以在新的系统上很好的工作,甚至还可以调试Windows的内核,这是当时是不敢想象的。也正是因为它很好的性能, 卓越的功能,它成为了黑客们的宠儿,并且培养了几代的黑客。读完本文后,你自己应该有能力去探索它,或许你也可以在这几款调试器里找到适合自己的。

1。3 微软的调试工具

本 文主要介绍的是微软官方的调试器,并且MS微软也最积极,调试工具包一直在更新中。在微软的调试器里,大家可能最为熟悉Visual Studio Debugger,就是你在Visual C++里使用的那个,它并不是一个单独可运行的程序,而是内嵌在Visual Studio中的,它的功能相对于上面几个调试器来说要弱很多,这里并不会对它进行介绍,如果你想全方面的了解它的话,你可以去参考MSDN,那里专门有 几章讲解它,是一份很好的文档化的资源(而且还是中文的)。其它的微软调试工具cdb,ntsd,WinDbg,kd都在微软的一个安装包里,被称作Debugging Tools for Windows(10 多M的大小,请通过这个链接下载并安装,一会儿就要用到)。这些调试器在下文中会分别介绍,这里先做一个简单的区别,cdb与ntsd几乎是一样的程序, 唯一的不同是cdb是一个CUI程序,即Console程序,而ntsd是GUI程序,但ntsd并没有产生窗口,而是又分配了一个Console窗口, 这个Console窗口就相当于是cdb。它与真正的cdb执行完全一样的功能(官方如是说,然而实际上还有一些个不同)。在命令行中启动它们时下面两句 命令有一样的效果(C:/Progs/Debug是调试工具包的安装位置):

C:/Progs/Debug/>start cdb
C:/Progs/Debug/>ntsd

cdb 与ntsd是用来调试用户模式应用程序的。kd调试器也是一个命令行程序,不过正如其名kernel debugger所描述的一样,它是内核调试器的,是驱动程序开发者,系统Hacker的最爱。WinDbg是一个称职的GUI程序,它有菜单,有工具 栏,还有多个子窗口,可以分别显示源代码,调用栈,命令等,它既可以调试ring 0程序,也可以调试ring 3程序,其实它只是一个壳而已,当调试ring 3级程序时它实际上是用cdb/ntsd,而当调试ring 0级程序时是用kd。WinDbg的到来吸引了很多的人使用,你也将会发现,它确实是一款优秀的调试器。

2 调试信息

调试器之所以能够工作,完全是依赖于编译和链接程序时所生成的调试信息,当然调试信息是具有一定的格式的。

2。1 格式

微 软的调试信息格式经过了几代变化,最终形成了Program DataBase这种格式,并且这种格式还在进行版本上的更新,VS.Net所用的新的Program DataBase版本与Visual C++ 6.0所用的老的版本是不兼容的,并且你也可以用编译器和链接器明确指定你想要生成的调试信息格式,这一点下文中有阐述。

2。2 内容

关 于调试信息我们还必须知道两件事,一是调试信息包括哪些内容,二是调试信息储存在什么地方。其实调试信息所应该包括的内容正是调试信息格式变化的原因,从 COFF格式,到CodeView格式,到Program DataBase格式,调试信息变得越来越丰富了,并且是只多不少。Program DataBase格式的调试信息中主要包括了所有全局函数,static 函数,全局变量,static变量,局部变量,函数形参的名字及其位置,源代码与可执行文件中指令的映射信息,每个函数与变量的类型,以及FPO信息,编 辑和继续信息。编辑与继续信息主要用于在Visual Studio中调试时,可以在调试的同时编辑源代码,并在接下来的调试中得到体现。Program DataBase格式的调试信息包含了这么多的内容,所以用这种调试信息来调试程序时,你将能够得到更多,更准确,更深入的反馈。

2。3 存储位置

调 试信息的存储位置是与其格式相关的,Program DataBase格式的调试信息存储在一个单独的文件里,扩展名为pdb。像以前的CodeView格式的调试信息即可存储在单独的文件里,又可存储在编 译时所生成的obj文件中。知道了这些知识后,我们就可以正确地生成调试信息了。在后面的内核调试中我们还要继续谈到它。

3 用户模式程序的调试

根 据上面的讲述,我们可以用cdb或WinDbg来进行ring 3程序的调试,这里先讲解cdb。cdb允许你启动某个被调试程序(以下称debugee)的一个新的实例来进行调试,即先创建cdb,然后cdb再把你 所指定的程序创建为一个新进程进而对其调试,也允许debugee在已经运行的情况下被cdb attach上。cdb还可以对Crash Dump(程序崩溃时的内存Copy,下文将会说明)进行调试,只需要加上-z选项,后面再加上Crash Dump的文件名即可。这几种调试方式下面将会一一讲述。

3。1 调试前的准备

第 2节中对调试信息进行了理论上的说明,接下来我们来看看在实际中应该如何操作。首先我们的程序必须要经过编译,链接,并且在编译和链接时还要指定一些选项 以正确地生成调试信息。这里所使用的编译器是cl.exe,链接器是link.exe,都是微软官方的,Visual Studio就是用的这两个(CTRL+F5就是顺序调用cl和link的快捷键),如果你安装了Visual C++或Visual Studio,就会有它们,另外一种选择是安装SDK,也能够得到它们。本文所用的cl.exe和link.exe是Visual C++ 6.0的版本。若要生成调试信息,编译时我们需要加上的选项应是/Zi,而链接时则要加上/debug。在下面的调试中,我们将用C语言来写程序,所以你 有必要知道用C语言写出来的程序与用汇编写出来的有什么不同。首先,每个程序都有一个入口函数,它的地址需要在链接时指定,并被链接器放到最终可执行文件 的头部,通常用汇编语言写的程序,有选择的,你可以在源代码中指定入口函数,而用C语言写的程序则需要在链接的时候来指定入口函数,说到这你可能不以为 然,“C语言写的程序的入口函数不就是main吗?“。实际上,控制台C程序的入口函数默认情况下是C运行时的启动函数:mainCRTStartup。 然后由这个函数调用你写的main函数,所以可执行文件的头部存放的入口地址是mainCRTStartup的地址,而不是你写的main函数地址。 mainCRTStartup主要做了一些为了正确执行C/C++程序的初始化工作。它已经由微软写好了,由链接器自动链接到可执行文件中。如果你在程序 中不使用C语言的库以及一些ANSI规定的全局变量,只是单纯地使用C语言这种语法,那么你也可以不链接mainCRTStartup,直接指定你自己写 的某个函数为入口函数,这也是我们在下文中所使用的方法,具体的做法是在链接时加入如下选项:/subsystem:console /entry:你的入口函数名称。这个入口函数应该是不带参数的。现在我们来总结一下上面所讲的内容,假定你的源程序名为exam.c,你想指定的入口函 数为main,那么应该如下生成可执行程序:

C:/Pro/>cl /Zi exam.c /link /debug /subsystem:console /entry:main
另 外,就算你不链接mainCRTStartup(它会调用很多的Win32 API),也不在源程序中调用任何的系统函数。那么系统还是会把一些动态链接库,如kernel32.dll,Ntdll.dll等动态链接到你的程序所 对应的进程里,即把它们映射到你的程序对应的进程地址空间里。这是因为在你所指定的入口函数运行之前,还会有一系列的 Kernel32.dll,Ntdll.dll中的函数要运行。即在用户空间中你指定的入口函数,例如上面的main,根本不是第一个运行的函数。那么那 些函数是做什么的呢,通过大量的反汇编和调试能够推断出它们是做一些进程,线程在用户空间的初始化,设置一些异常桢等。在下面的调试中我们将会用一些手段 来研究它们。这些函数是操作系统的一部分,因些我们必须从官方网站下载它的符号文件,当然不下载也行,那么你将面临的会是一大堆的警告。说到下载,没有比 这更简单的了,你不需要预先的下载,只需要添加一个环境变量_NT_SYMBOL_PATH即可,而真正的下载工作由调试器来做,这个环境变量的值与已存 在的PATH环境变量类似,是由分号分隔的一系列的路径组成。这些路径应该包括你的调试信息文件(pdb文件)所在地,如前面的C:/Pro/。如果要下 载系统文件如Kernel32.dll,Ntdll.dll的pdb文件,你仅仅需要再加一个这样的路径:SRV*D:/Symbols*http: //msdl.microsoft.com/download/symbols,其中D:/Symbols是可更换的,你可以换成任何一个其它的路径,这 个D:/Symbls是用来存放从后面的URL路径所下载下来的系统调试信息文件的。其实你也可以预先下载系统调试信息文件到一个路径里,然后在 _NT_SYMBOL_PATH里指定那个目录,但这样一来有两个缺点,一是你必须得进行版本的匹配,做这件事简直太乏味了,二是你一次需要把整个操作系 统的调试信息文件都下载下来,可能会需要1G的空间。而通过前面设置环境变量的方式,调试器会根据需要只下载这次调试会话所需要的调试信息文件,并且它会 自动给你匹配版本。由于我们将要编写的源代码都在C:/Pro/文件夹中,生成的pdb文件也在C:/Pro/中,所以我们的 _NT_SYMBOL_PATH环境变量应该如下设置,假设你希望系统pdb文件下载到D:/Symbols:
C:/Pro/>set _NT_SYMBOL_PATH=SRV*D:/Symbols*http://msdl.microsoft.com/download/symbols;C:/Pro/
最后,我们需要在命令行中启动cdb调试器,但当前目录通常是源代码文件夹C:/Pro/,为了避免如下冗余的键入:

C:/Pro/>C:/Progs/Debug/cdb example1.exe
应该为cdb,ntsd,WinDbg设置PATH环境变量:

C:/Pro/>set PATH=%PATH%;C:/Progs/Debug/
这里的C:/Progs/Debug是Debugging Tools for Windows的安装目录。以后就可以这样来调试了:

C:/Pro/>cdb example1.exe

3。2 cdb启动新实例的调试

3。2。1 编写一源程序,启动cdb

首先我编写了下面的C程序,先用这个程序来介绍一些基本的命令,并且来验证一下调试信息中是否确切包含了上面所说的那些内容:

[example1.c]
int 		gVar;
static int sgVar;
int Inc(int Param);
static int sDec(int sParam);
void main(void)
{
int lVar;
static int slVar;
lVar = 3;
slVar = 4;
gVar = Inc(lVar);
sgVar = sDec(slVar);
}
int Inc(int Param)
{
return (Param+1);
}
int sDec(int sParam)
{
return (sParam-1);
}
对这个程序用上面所讲的方法编译链接:
C:/Pro/>cl /Zi example1.c /link /debug /subsystem:console /entry:main
接下来启动cdb调试器:

C:/Pro/>cdb example1.exe

Microsoft (R) Windows Debugger
Copyright (c) Microsoft Corporation. All rights reserved.

CommandLine: example1.exe
Symbol search path is: SRV*D:/Symbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00406000 example1.exe
ModLoad: 7c920000 7c9b4000 ntdll.dll
ModLoad: 7c800000 7c91c000 C:/WINDOWS/system32/kernel32.dll
(c94.c24): Break instruction exception - code 80000003 (first chance)
eax=00251eb4 ebx=7ffd4000 ecx=00000001 edx=00000002 esi=00251f48 edi=00251eb4
eip=7c921230 esp=0012fb20 ebp=0012fc94 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
ntdll!DbgBreakPoint:
7c921230 cc int 3
0:000>
cdb 输出了此时主线程的上下文信息(这个例子中也只有这一个线程)以及程序断点信息后便会进入一个新的提示符:0:000>。以后我们会一直工作在这种 类型的提示符上,下面在这个提示符上输入一个命令来加载所有的调试信息文件,此时可能会慢一些,因为要下载系统DLL的pdb文件,所以要确保你能上网。

0:000>.reload /f

看到这个以"."号做为前缀的命令,可能你会觉得怪怪的,但实际上还有用"!"号做前缀的呢,用"."号做前缀的命令表示元命令,而用"!“号做前缀的命令表示扩展命令,这里只做一个简单的区分即可。

其 实这时我们所要调试的程序已经运行起来了,不过停在了某处,主线程处于冻结状态。这一点和Linux的Gnu调试器GDB不一样,对于GDB调试器,你先 要设置一个断点,然后再键入运行命令(如果自己不手动设置一个断点,那么程序将会一直运行直到结束,你根本没有调试的机会。),这时程序才处于运行状态。 没有运行和运行之后处于冻结状态是两个完全不同的概念。那么我们这个example1.exe停在了什么地方呢。下面我将介绍一个很重要的命令,用它我们 可以来研究这方面的问题。

3。2。2 查看堆栈及用户空间的初始化

用kb命令可以来查看堆栈,它显示栈上一些重要的信息。

0:000>kb
ChildEBP RetAddr Args to Child
0012fb1c 7c95edc0 7ffdf000 7ffd4000 00000000 ntdll!DbgBreakPoint
0012fc94 7c941639 0012fd30 7c920000 0012fce0 ntdll!LdrpInitializeProcess+0xffa
0012fd1c 7c92eac7 0012fd30 7c920000 00000000 ntdll!_LdrpInitialize+0x183
00000000 00000000 00000000 00000000 00000000 ntdll!KiUserApcDispatcher+0x7
这个命令所列出来的信息后面还要详细介绍,这里先关注一下函数的调用关系。从上面的列表可以看到有四个函数,这四个函数都是ntdll模块里的,从下至上函数依次被调用。要注意这是当前线程的用户栈里所有的东西,就四个函数,KiUserApcDispatcher是第一个,上面已经提到过,在你指定的入口函数执行之前还有很多的函数被调用,KiUserApcDispatcher是第一个被调用的,接下来它再调用_LdrpInitialize_LdrpInitialize调用LdrpInitializeProcess,再调用DbgBreakPoint。至于是谁调用的KiUserApcDispatcher,我现在只能简单的告诉你是操作系统调度例程调度的结果。深入地探讨它就离开本文的主题了。现在我们可以肯定的是程序停在了DbgBreakPoint里,因为它是栈上最后一个函数。从cdb调试器的输出可以看到example1.exe停在了DbgBreakPoint函数里的int 3语句上,int 3是一个异常,它将会通知操作系统挂起这个线程,并且通知调试器,这是操作系统对调试的一个支持。其实DbgBreakPoint函数只有一条语句,那就是int 3。敏感点的人可能会想到,这样一来,所有的程序,无论是否被调试,在运行的时候最初都会执行DbgBreakPoint函数了,这也太傻了吧?情况并非如此,当我们键入cdb example1.exe时,cdb在启动example1.exe的时候会加一个特殊的标记,在这种情况下LdrpInitializeProcess才会调用DbgBreakPoint,这又是操作系统对调试的一个支持。


本文转自
http://chenshine.blogbus.com/logs/4414354.html

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理