NetRoc's Blog

N-Tech

 

《Windows高级调试》书评

NetRoc

从事了数年的软件开发,我觉得工作中最考脑筋的有两个方面:软件设计和软件调试。通常技术人员个人能力的高低,很大程度上也是看这两方面的能力。以往讨论软件设计的书都已经很多,但是软件调试却似乎一直没有得到足够的关注。当我个人试图从程序员的视角转换出来,更多的从全局的眼光来观察软件生命周期中的方方面面 时,很高兴能够读到《Windows高级调试》这本好书。

《Windows高级调试》(后面简称AWD)是一本实用化的书。整本书都是联系软件调试中的实际操作来讲述的。第二部分中分类讲解了各种常见的BUG问题的调试方法,每一部分都是按照如何识别问题、如何分析原因、如何调试确认这样的流程。这是非常符合我们在实际的调试工作中的思维方式的。

AWD是一本包含作者很多调试经验的书。令我印象非常深刻的是第二章中的一句话:"由于扩展命令!dreg的参数是非常长的,因此我们经常将这些参数从之前的调试会话中复制过来。一种常见的情况是,在每次分析调试会话之前,首先在文件中保存一组命令,然后再将根据需要将这些命令复制到调试会话中。"就像是一位调试经验丰富的朋友在谈话中偶尔传达给大家的经验之谈。虽然看起来不起眼,但是却常常能对工作起到很好的帮助。实际上,之前我自己在调试的时候就是像作者所说的这样做的。

刚刚接触软件调试和有丰富调试经验的开发人员都能从AWD中找到自己需要的东西。新手可以从作者讲解的分析思路中学到当遇到实际问题的时候应该从什么方面去思考。书中也还介绍了一些东西,需要相当调试经验才能理解它的作用,例如第6章对堆内存分配机制的内容。通常来说很多问题并不需要了解堆分配的细节就能解决,但是有一些特殊情况,例如通过工具和命令不能自动分析,必须手动分析堆管理器的内部数据时。关于软件调试的内容,有很多是可以马上用到手上的,但是也有很多需要在读完之后在漫长的实践和经验积累中才能慢慢体会到作用的。

如果想全面了解软件调试技术,单看AWD这一本书是不够的。这本书比较遗憾的是没有系统的介绍调试器和各种调试工具的使用。很多命令和工具是穿插在调试场景中介绍的。不过这方面的内容也足以写一本几百页的书了,AWD的篇幅可能没有办法面面俱到。关于调试器和调试工具的使用,建议读者也专门找一些资料来学习,例如WinDbg的帮助文档就是非常好的资源。

2009-07-10

posted @ 2009-07-10 14:48 NetRoc 阅读(557) | 评论 (1)编辑 收藏

Microsoft Detours 2.1简介

NetRoc

http://www.DbgTech.net/

本文从主站点转贴过来的,附件和pdf请访问http://www.DbgTech.net/下载

一、简介

《Windows高级调试》第一章中提到了一个基于Microsoft Detours库的内存泄露检查工具LeakDiag。本文对这个库进行一些介绍。

一句话来说,Detours是一个用来在二进制级别上对程序中的函数(Function)或者过程(Procedure)进行修改的工具库。一般我们将这种技术称为"Hook"。Detours的实现原理是将目标函数的前几个字节改为jmp指令跳转到自己的函数地址,以此接管对目标函数的调用,并插入自己的处理代码。在现实中,这种技术可以应用在很多场景下。比如Hook某些Windows API,在实际调用到系统函数前进行一些过滤工作;软件中使用到了一些没有源代码的第三方库,但是又想增强其中某些函数的功能,等等。

图1 Hook前后的程序执行流程对比。

 

图2 Hook前后目标函数和跳板代码的改变

 

Detours相对其他一些Hook库和自己实现的代码来说,通常有以下这些优点:

  • 考虑全面,代码非常稳定,并且经过了微软自己众多产品的验证。
  • 可以简单的用纯C/C++代码实现对类的成员函数的Hook。
  • 购买版权之后的Detours Professional还可以支持x64和IA64处理器。以此为基础编写的代码拥有更强的可移植性。
  • 使用简单,不需要了解汇编指令以及技术细节。

二、使用方法

一般来说,使用Detours的代码都具有固定的模式。Detours 1.5和Detours 2.1的接口函数变了很多,这里按照2.1版本对基本的使用方法进行说明。

常用的函数有下面几个:

  • DetourTransactionBegin() :开始一次Hook或者Unhook过程。
  • DetourUpdateThread() :列入一个在DetourTransaction过程中要进行update的线程。这个函数的作用稍微有一些复杂,会在后面专门说明。
  • DetourAttach() :添加一个要Hook的函数。
  • DetourDetach () :添加一个要Unhook的函数。
  • DetourTransactionCommit() :执行当前的Transaction过程。在这个函数中才会真正进行Hook或者Unhook操作。前面三个函数都只是做一些记录工作。

在使用的时候,这几个函数的调用步骤基本上也是按照上面列出来的顺序。举例来说,现在想Hook掉API函数MessageBoxA,将消息框弹出的消息修改掉,可以按下面的方法做。

进行Hook的步骤:

  1. 首先需要定义目标函数的原型。如果目标函数是Windows API,可以到MSDN中查阅,但是需要注意ANSI版本和Unicode版本的区别。如果没有确切的原型声明,或者目标函数是通过逆向工程找出来的,那么需要定义一个和目标函数原型兼容的声明,即参数个数和调用约定要相同。如MessageBoxA的原型是:

    int MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

    使用typedef定义如下:

    typedef int (WINAPI *pfnMessageBoxA)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

  2. 声明一个指向目标函数的函数指针:

    pfnMessageBoxA g_pMessageBoxA = ::MessageBoxA;

  3. 编写Hook函数的代码,用于替换目标函数。
  4. 调用DetourTransactionBegin开始一次Detours事务。
  5. 对进程中每个可能调用到目标函数的线程,都需要使用DetourUpdateThread加入到update队列中。这是因为Hook时修改目标函数的前几个字节,如果某个线程刚好执行到这几个字节的位置时,粗暴的修改掉会造成该线程出现异常。Detours事务处理时,会先枚举并暂停update队列中所有线程,获取它们的指令指针,如果发现这种情况,则将指令指针修改到跳板代码的对应字节上。这样就避免出现崩溃的问题。
  6. 对每个需要Hook的函数,调用DetourAttach加入到事务列表中。
  7. 调用DetourTransactionCommit进行实际的Hook操作。

Unhook的过程和上面的流程基本一样,只是第6步改为调用DetourDetach函数。

Hook MessageBoxA的完整示例代码如下:

//Hook函数的向前声明

int WINAPI Hook_MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

 

//目标函数原型声明

typedef int (WINAPI *pfnMessageBoxA)( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);

 

//指向目标函数的指针

pfnMessageBoxA g_pMessageBoxA = ::MessageBoxA;

 

BOOL StartHook()

{//开始Hook

    DetourTransactionBegin();

 

    //只有一个线程,所以GetCurrentThread

    DetourUpdateThread( GetCurrentThread());

 

    //添加MessageBoxA的Hook

    if( DetourAttach( &(PVOID&)g_pMessageBoxA, Hook_MessageBoxA) != NO_ERROR)

    {

        printf( "Hook MessageBoxA fail.\n");

    }

 

    //完成事务

    if( DetourTransactionCommit() != NO_ERROR)

    {

        printf( "DetourTransactionCommit fail\n");

        return FALSE;

    }

    else

    {

        printf( "DetourTransactionCommit ok\n");

        return TRUE;

    }

}

 

BOOL StopHook()

{//停止Hook

    DetourTransactionBegin();

    DetourUpdateThread( GetCurrentThread());

 

    if( DetourDetach( &(PVOID&)g_pMessageBoxA, Hook_MessageBoxA) != NO_ERROR)

    {

        printf( "Hook MessageBoxA fail.\n");

    }

 

    if( DetourTransactionCommit() != NO_ERROR)

    {

        printf( "DetourTransactionCommit fail\n");

        return FALSE;

    }

    else

    {

        printf( "DetourTransactionCommit ok\n");

        return TRUE;

    }

}

 

int WINAPI Hook_MessageBoxA( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType)

{

    //需要调用原函数时,可以直接使用前面定义的指针变量

    return g_pMessageBoxA( hWnd, "MessageBox after hook.", "TestDetours", MB_OK);

}

在附件的示例代码中还包含了Hook类成员函数的代码。流程和上面基本一致,只是需要用一些强制转换来对付编译器的类型检查。

另外,Detours还包含一系列其他函数,如果需要使用的话,可以参考Detours安装目录下的示例。

三、使用Detours的注意事项

总体来说,Detours库的代码是非常稳定的,但是如果使用方法不对,会造成一些问题。有下面一些地方需要特别注意:

  1. 一定要枚举线程并调用DetourUpdateThread函数。否则可能出现很低几率的崩溃问题,这种问题很难被检查出来。
  2. 如果Hook函数在DLL中,那么绝大多数情况下不能在Unhook之后卸载这个DLL,或者卸载存在造成崩溃的危险。因为某些线程的调用堆栈中可能还包含Hook函数,这时卸载掉DLL,调用堆栈返回到Hook函数时内存位置已经不是合法的代码了。
  3. Detours库设计时并没有考虑到卸载的问题,这是因为钩子的卸载本身是不安全的。当Detours库代码存在于DLL中的时候,即使Unhook了所有函数,清理了所有自己使用到的函数,还是会占用一些内存。卸载这个DLL会造成内存泄露,特别是反复的进行加载DLL->Hook->Unhook->卸载DLL的过程,会让这个问题变得非常严重。后面会用一篇专题文章来讨论Detours内存泄露问题的调试和解决。
  4. 有一些非常短的目标函数是无法Hook的。因为jmp指令需要占用一定空间,有些函数太过短小,甚至不够jmp指令的长度,自然是没有办法Hook掉的。

Detours不支持9x内核的Windows系统。因为9x内核下的内存模型和NT内核下有非常大的差别。

posted @ 2009-05-28 22:42 NetRoc 阅读(5425) | 评论 (0)编辑 收藏

《Windows高级调试》介绍

做程序员的人肯定听说过这句话:“世界上最痛苦的事情是加班,比加班更痛苦的事情是天天加班。”相信很多工作的朋友都体验过这种加班加得天昏地暗的日子吧?
    天哪,我怎么才能少加点班?我想多一点自己的时间!
    从我个人的经历来看,加班最常见的原因,就是程序发现严重BUG。一边是上级不断催着要解决问题,一边却找不出来出错原因,经常忙得焦头烂额。事实上,如果再花少许时间学习一些调试方法和调试工具的使用,会让我们花在调试BUG上的时间大大缩短。可能以前花几天时间还找不出来的问题,其实只需要半个小时就搞定了。
    《Advanced Windows Debugging》这本书就是为了解决这个问题而问世的,但是因为大家只能看到英文版,让很多人望而生畏。很幸运的是,机械工业出版社即将推出它的中文版,更多人可以不因为语言的限制而错过这样一本好书了。
    更幸运的是,机械工业出版社的编辑朋友联系到DbgTech,我们得以提前获得了这本书的一些资料。DbgTech立足于调试技术的宣传和推广,希望以此能为国产软件的稳定性和可靠性提高做出微薄的贡献。这本书可以说是目前市面上能找到的最好的一本为开发人员量身打造的调试书籍。包含对各种常用调试工具的使用介绍;内存溢出、资源泄露、同步、进程间通信等最典型BUG问题的调试介绍。我们希望以此为契机,跟大家共同学习这本书中的内容。后面会陆续推出一些典型的调试案例的文章,大家阅读的时候如果有心得体会或者疑问,也欢迎参加讨论!
    后面本书的一些内容节选,更详细的内容请下载附件中的文档。
    欢迎到我们的站点访问:DbgTech

posted @ 2009-05-19 12:27 NetRoc 阅读(1266) | 评论 (2)编辑 收藏

关于Windows系统调用实现的笔记

NetRoc

http://www.DbgTech.net/

从Win XP开始,Windows的系统调用都是通过sysenter指令进入KiFastCallEntry或者_KiFastCallEntry2。最近稍微看了一下这部分的wrk代码实现,作了些笔记。

  1. KiFastCallEntry中如何引用SSDT和SSDT Shadow

从sysenter进入KiFastCallEntry后,系统取得SSDT和SSDT Shadow的地址并不是直接通过内核导出的KeServiceDescriptorTable和未导出的KeServiceDescriptorTableShadow变量来引用的。而是从PCR取得_KTHREAD地址,通过KTHREAD中的ServiceTable来获得服务表地址的。I386处理器的KiFastCallEntry相关代码片段如下:

mov ebx, PCR[PcSelfPcr] ; 取得PRCB地址

push KGDT_R3_TEB OR RPL_MASK ; Push user mode FS

mov esi, [ebx].PcPrcbData+PbCurrentThread ; 获得当前线程地址

……

add edi, [esi]+ThServiceTable ; 计算出服务表的地址

通过这里,我们可以知道系统调用所使用的的服务表来自_KTHREAD这种"动态"数据,而非KeServiceDescriptorTable这种相对"静态"的数据。这一点在很多情况下都有较强的利用价值。比如XXX……

  1. nt!_KTHREAD中ServiceTable字段的设置

NtCreateThread内部是通过PspCreateThread创建线程的。进行了线程的相关结构内存分配后,会调用KeInitThread初始化这些结构。KeInitThread中就会设置nt!_KTHREAD的ServiceTable字段。如下:

#if defined(_AMD64_)

Thread->ServiceTable = KeServiceDescriptorTable[SYSTEM_SERVICE_INDEX].Base;

Thread->KernelLimit = KeServiceDescriptorTable[SYSTEM_SERVICE_INDEX].Limit;

#else

Thread->ServiceTable = (PVOID)&KeServiceDescriptorTable[0];

#endif

I386下,线程的初始服务表就是KeServiceDescriptorTable。这可能会被改变。后面如果这个线程调用了ShadowTable中的服务,ServiceTable会被转变成ShadowTable。KiFastCallEntry会判断系统调用的服务号,如果发现是GUI服务,则调用PsConvertToGuiThread将线程转换成GUI线程。

PsConvertToGuiThread中如下处理:

#if defined(_AMD64_)

Thread->Tcb.Win32kTable = KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Base;

Thread->Tcb.Win32kLimit = KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Limit;

#else

Thread->Tcb.ServiceTable = (PVOID)&KeServiceDescriptorTableShadow[0];

#endif

  1. SSDT和SSDT Shadow的初始化

两个SDT的初始化在KiInitSystem函数中:

KeServiceDescriptorTable[0].Base = &KiServiceTable[0];

KeServiceDescriptorTable[0].Count = NULL;

KeServiceDescriptorTable[0].Limit = KiServiceLimit;

KeServiceDescriptorTable[0].Number = KiArgumentTable;

for (Index = 1; Index < NUMBER_SERVICE_TABLES; Index += 1) {

KeServiceDescriptorTable[Index].Limit = 0;

……

RtlCopyMemory(KeServiceDescriptorTableShadow,

KeServiceDescriptorTable,

sizeof(KeServiceDescriptorTable));

ShadowTable的第一项被在这个时候初始化成和KeServiceDescriptorTable一样。而第二项会在后面的初始化过程中通过调用KeAddSystemServiceTable被设置。

  1. ShadowTable的设置时机

上面提到了,由于KiInitSystem的初始化,KeServiceDescriptorTableShadow[0]和KeServiceDescriptorTable[0]总是一样的,但是KeServiceDescriptorTableShadow[1]会被设置成win32k的服务。这应该是win32k加载之后调用KeAddSystemServiceTable添加的。

  1. AMD64和i386中的区别

AMD64下的实现和i386不同,AMD64下的ShadowTable在_KTHREAD中专门有一个字段,ServiceTable保存KeServiceDescriptorTable,Win32kTable保存KeServiceDescriptorTableShadow[WIN32K_SERVICE_INDEX].Base。详见PsConvertToGuiThread等函数。

  1. 有什么意义?

从上面记录的这些实现来看,很直接导致的结果就是,系统服务表可以比较容易的替换掉。这种替换有下面这些特点:

  • 只会影响指定的线程,对整个系统来说影响小。
  • 由于和传统SSDT Hook一样只需要改掉一个指针,比inline hook更加稳定。
  • 传统的SSDT Hook被用得太滥,大家都在搞,某些人还敢卸载掉并且恢复Hook之前的指针,很容易造成崩溃。这种方法因为只针对单独线程,被使用得也少,相对来说比传统SSDT Hook和Hook KiFastCallEntry都要安全。
  • 对可能转变成GUI线程的线程进行替换就需要特别的处理了。比如构造自己的ShadowTable并Hook掉PsConvertToGuiThread之类的方法。

没有什么隐蔽性,呵呵。干坏事的同学慎用。当然,用的时候再改,用过之后马上改回去,或者配合内存隐藏之类的技术还是不错滴。干好事的同学也可以用,但是不适合对所有系统线程都用上去,因为要枚举所有线程,或者Hook线程创建函数之类比较麻烦,到不如直接去干KiFastCallEntry。用来保护自己线程的服务表安全倒是不错。

posted @ 2009-04-08 00:18 NetRoc 阅读(2385) | 评论 (0)编辑 收藏

Windows调试工具入门-7

 

 

驱动程序的源码调试

 

 

http://www.DbgTech.net

  1. 准备

Windows调试工具优于目前的其他内核调试器很重要的一点,就是能够非常方便的对自己编写的驱动程序进行源码调试。为了能够更好的说明,我们首先需要做一些准备工作,分别编写一个测试驱动程序和一个应用程序来使用驱动的功能。

  1. 驱动程序

首先实现一个最简单的驱动程序,除了DriverEntry等框架代码之外,我们添加一个IRP_MJ_READ的Dispatch例程,当应用程序调用ReadFile时返回一个值递增的字节。另外,实现两个DeviceIoControl Code,一个调用DbgPrint向调试器显示信息并返回,另一个访问非法指针造成崩溃。代码片断如下,完整的代码和编译出来的文件可以在附件中获取:

NTSTATUS DispatchRead (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)

{

    PIO_STACK_LOCATION pstIrpStack;

    PUCHAR pbyUserBuffer;

ULONG ulSize;

    BOOLEAN blRtn = FALSE;

    static UCHAR s_byCounter = 0;

 

    pstIrpStack = IoGetCurrentIrpStackLocation( Irp);

    pbyUserBuffer = (PUCHAR)Irp->UserBuffer;

ulSize = pstIrpStack->Parameters.Read.Length;

 

    if ( ulSize == 1)

    {

        *pbyUserBuffer = s_byCounter++;

        blRtn = TRUE;

    }

 

    if ( blRtn)

    {

        Irp->IoStatus.Status = STATUS_SUCCESS;

        Irp->IoStatus.Information = 1L;

        IoCompleteRequest( Irp, 0 );

        return STATUS_SUCCESS;

    }

    else

    {

        Irp->IoStatus.Status = STATUS_UNSUCCESSFUL;

        Irp->IoStatus.Information = 0L;

        IoCompleteRequest( Irp, 0 );

        return STATUS_UNSUCCESSFUL;

    }

}

 

NTSTATUS DispatchIoCtrl (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)

{

    NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;

    PUCHAR pucCrash = NULL;

    

    PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);

    //////////////////////////////////////////////////////////////////////////

    switch(irpStack->Parameters.DeviceIoControl.IoControlCode)

    {

    case IOCTL_TEST_CTRL_CODE2:

        //崩溃

        *pucCrash = 1;

        break;

    case IOCTL_TEST_CTRL_CODE1:

        DbgPrint( "Received IOCTL_TEST_CTRL_CODE1\n");

        ntStatus = STATUS_SUCCESS;

        break;

    default:

            ntStatus = STATUS_SUCCESS;    

    }

    //////////////////////////////////////////////////////////////////////////

    //

    if(ntStatus == STATUS_SUCCESS)

        Irp->IoStatus.Information = irpStack->Parameters.DeviceIoControl.OutputBufferLength;

    else

        Irp->IoStatus.Information = 0;

    Irp->IoStatus.Status = ntStatus;

    

    IoCompleteRequest( Irp, 0 );

        

    return ntStatus;

}

  1. 应用程序

应用程序主要用来控制驱动设备对象,显示返回的结果。

  1. 开始源码调试

    1. 设置

系列前面的几篇文章已经介绍过对符号路径和源码路径的设置。调试自己编写的驱动时,如果是主控机上编译,在目标机上运行,那么一般都不需要专门设置路径WinDbg就能找到正确的符号和源文件。如果驱动不是在主控机上编译的,或者编译之后移动了源码或符号文件就必须要进行设置。在这里我是这样设置的:

  1. 加载驱动程序

这里我们不自己写程序加载,而是通过附件中的KmdManager.exe工具来进行。

选择要加载的驱动程序之后,首先点击Register就能在注册表中注册该驱动程序的服务。然后点击Run加载并运行。按下Run之后,驱动的DriverEntry就会被调用,所以如果我们要想在驱动入口点中断下来的话,就要在这之前设置断点。Stop和Unregister按钮用来停止和卸载驱动服务。注意操作时要按照Register->Run->Stop->Unregister的顺序,并且Stop之前要在应用层关闭所有已打开的句柄,否则可能只有重起机器才能卸载了。

  1. 设置断点

初学者调试驱动时常常会问的一个问题就是,如何在DriverEntry运行之前中断下来。我们在实际的调试工作中也经常需要调试DriverEntry。这需要用到以前介绍过的未定断点,因为一般来说DriverEntry 运行之前,我们的驱动还没有被加载进来,这时在调试器是找不到这个驱动的任何符号的。

首先在调试器命令窗口输入bu SrcDbgKnlDrv!DriverEntry命令,F5运行起来。在目标机中用KmdManager.exe工具Register并Run。这时会发现目标机中断到了调试器中。

如果前面的路径设置没有问题的话,WinDbg中会自动弹出源码,可以看到目前中断在DriverEntry函数入口处。

从这时起,就可以使用各个WinDbg的调试窗口了,比如查看局部变量、查看内存等等,也可以打开各个源文件直接针对源码设置断点。

  1. 查看驱动设备的信息

运行到DriverEntry时,SrcDbgKnlDrv.sys已经被成功加载到系统中了。DriverEntry运行完成之后就能够看到我们的驱动创建的设备了。

使用gu命令,执行到DriverEntry返回处。使用!drvobj和!devobj命令可以查看驱动程序对象和设备对象的信息。

kd> !drvobj SrcDbgKnlDrv

Driver object (8219f5f0) is for:

\Driver\SrcDbgKnlDrv

Driver Extension List: (id , addr)

 

Device Object list:

82138030

kd> !devobj SrcDbgKnlDrv

Device object (82138030) is for:

SrcDbgKnlDrv \Driver\SrcDbgKnlDrv DriverObject 8219f5f0

Current Irp 00000000 RefCount 0 Type 00000022 Flags 000000c0

Dacl e10361f4 DevExt 00000000 DevObjExt 821380e8

ExtensionFlags (0000000000)

Device queue is not busy.

如果我们的设备有附加到某个设备栈上的话,可以用!devstack扩展命令显示设备栈的信息。

kd> !devstack 82138030

!DevObj !DrvObj !DevExt ObjectName

> 82138030 \Driver\SrcDbgKnlDrv00000000 SrcDbgKnlDrv

这里看到的输出说明SrcDbgKnlDrv没有附加到任何设备栈。

通过!devhandles命令可以查看设备被打开的句柄。目前在这个地方使用的话,由于没有句柄被打开,还看不到什么有用的信息,在后面进行演示。

  1. Dispatch例程的调试

Dispatch例程是驱动程序响应应用层请求的地方。通常来说,驱动程序和应用程序的各种交互就是通过Dispatch例程实现。因此,要跟踪应用层请求处理的情况,可以在这些Dispatch例程上设置断点。

我们的测试程序只处理了IRP_MJ_READ和IRP_MJ_DEVICE_CONTROL两个请求。来试着调试一下。

接着上面中断下来的调试会话,首先用bp SrcDbgKnlDrv!DispatchRead命令和bp SrcDbgKnlDrv!DispatchIoCtrl命令在程序的两个Dispatch例程上下断。F5运行起来。在目标机打开SrcDbgKnlApp程序点击Open,会打开一个SrcDbgKnlDrv.sys驱动设备的句柄。这时中断目标机,使用!devhandles扩展命令就能看到有用的信息了:

kd> !devhandles 82138030

 

Checking handle table for process 0x823b97c0

Handle table at e1002000 with 241 Entries in use

省略掉一部分……

Checking handle table for process 0x82164da0

Handle table at e1137000 with 34 Entries in use

PROCESS 82164da0 SessionId: 0 Cid: 05e4 Peb: 7ffd6000 ParentCid: 05d0

DirBase: 07480240 ObjectTable: e11c1cd0 HandleCount: 34.

Image: SrcDbgKnlAppD.exe

 

008c: Object: 821cb028 GrantedAccess: 0012019f

命令显示EPROCESS地址为0x82164da0的进程打开了一个\\.\SrcDbgKnlDrv设备的句柄。再使用!process 82164da0扩展命令可以看到,这个进程正是我们的SrcDbgKnlApp。在调试驱动程序的时候,这是一种查看哪些应用程序在使用某个驱动设备对象的方法。

接下来继续,F5运行,点击SrcDbgKnlApp的Read按钮,可以看到中断到了DispatchRead例程上。这里就可以用单步、查看局部变量等等普通的方法调试我们的程序运行情况了。对于驱动程序来说,我们还可以做一些其他事情,例如查看IRP的信息,查看IRQL等。如:

kd> !irp @@(Irp)

Irp is active with 1 stacks 1 is current (= 0x8238d2d0)

No Mdl: No System Buffer: Thread 82191da8: Irp stack trace.

cmd flg cl Device File Completion-Context

>[ 3, 0] 0 0 82138030 821cb028 00000000-00000000

     \Driver\SrcDbgKnlDrv

            Args: 00000001 00000000 00000000 00000000

kd> !irql

nt!_KPRCB.DebuggerSavedIRQL not found, error : 0x4.

Saved IRQL not available prior to Windows Server 2003

!irql命令只能对Windows 2003之后的系统使用,我的机器上是Windows XP,因此这里提示了错误。

另外,也可以使用kd> !devobj @@(DeviceObject)这样的命令形式,直接用C++表达式将局部变量作为命令参数来使用。

bc*命令清除断点,F5运行。每次点击Read都可以看到从驱动中读取出来的数据。

  1. 驱动异常和内核崩溃转储文件的源码调试

当驱动程序中发生异常时,如果有调试器附加上去,一般会中断到调试器中。如果没有附加调试器,系统通常会自动为我们生成内核dump文件。如果异常发生在我们的驱动中,那么也可以用WinDbg源码调试直观的看出问题所在。

在SrcDbgKnlApp程序中点击Crash。由于现在是在调试会话中,可以在调试器命令窗口中看到异常的信息:

kd> g

Access violation - code c0000005 (!!! second chance !!!)

SrcDbgKnlDrv!DispatchIoCtrl+0x3d:

f8d615ad c60101 mov byte ptr [ecx],1

源码窗口中可以看到崩溃处的代码:

pucCrash的值为NULL,造成了崩溃:

kd> ?? pucCrash

unsigned char * 0x00000000

""

如果是实际工作时进行调试的话,这时可以继续分析崩溃的原因。

再来尝试一下没有附加调试器的情况。重起目标机,在我的电脑->属性->高级中设置一下内核dump的类型和生成的路径:

再使用刚才的方法,加载驱动程序后,用SrcDbgKnlApp让目标机崩溃。发生蓝屏之后重起,在设置的路径下就可以找到dump文件了。这里我们生成的是minidump。将它拷贝到主控机上来,通过WinDbg菜单打开。如果驱动不是在本机上编译的话,因为还没有设置各种路径,现在看到的只有汇编代码。我们使用一下!analyze -v命令能够看到没有符号时对异常的分析结果,可以看到出问题的模块是SrcDbgKnlDrv.sys:

接下来设置好可执行映像路径、符号路径和源码路径,使用.reload命令重新加载符号,会自动弹出源码窗口,调试器中也会更新显示符号信息。如果再次使用!analyze -v,能够看到非常详细的分析报告。这时还可以查看异常的每个堆栈帧中的源码和符号化信息。对于调试Dump文件中的BUG来说,使用源码可以大大提高调试效率。

关于内核模式Dump文件的调试这里就不再介绍了。Windows调试工具的帮助文档中有相关技术较详细的资料。

posted @ 2009-02-19 22:44 NetRoc 阅读(1199) | 评论 (0)编辑 收藏

Windows调试工具入门-7

 

 

驱动程序的源码调试

 

http://www.DbgTech.net

  1. 准备

Windows调试工具优于目前的其他内核调试器很重要的一点,就是能够非常方便的对自己编写的驱动程序进行源码调试。为了能够更好的说明,我们首先需要做一些准备工作,分别编写一个测试驱动程序和一个应用程序来使用驱动的功能。

  1. 驱动程序

首先实现一个最简单的驱动程序,除了DriverEntry等框架代码之外,我们添加一个IRP_MJ_READ的Dispatch例程,当应用程序调用ReadFile时返回一个值递增的字节。另外,实现两个DeviceIoControl Code,一个调用DbgPrint向调试器显示信息并返回,另一个访问非法指针造成崩溃。代码片断如下,完整的代码和编译出来的文件可以在附件中获取:

NTSTATUS DispatchRead (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)

{

    PIO_STACK_LOCATION pstIrpStack;

    PUCHAR pbyUserBuffer;

ULONG ulSize;

    BOOLEAN blRtn = FALSE;

    static UCHAR s_byCounter = 0;

 

    pstIrpStack = IoGetCurrentIrpStackLocation( Irp);

    pbyUserBuffer = (PUCHAR)Irp->UserBuffer;

ulSize = pstIrpStack->Parameters.Read.Length;

 

    if ( ulSize == 1)

    {

        *pbyUserBuffer = s_byCounter++;

        blRtn = TRUE;

    }

 

    if ( blRtn)

    {

        Irp->IoStatus.Status = STATUS_SUCCESS;

        Irp->IoStatus.Information = 1L;

        IoCompleteRequest( Irp, 0 );

        return STATUS_SUCCESS;

    }

    else

    {

        Irp->IoStatus.Status = STATUS_UNSUCCESSFUL;

        Irp->IoStatus.Information = 0L;

        IoCompleteRequest( Irp, 0 );

        return STATUS_UNSUCCESSFUL;

    }

}

 

NTSTATUS DispatchIoCtrl (IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)

{

    NTSTATUS ntStatus = STATUS_UNSUCCESSFUL;

    PUCHAR pucCrash = NULL;

    

    PIO_STACK_LOCATION irpStack = IoGetCurrentIrpStackLocation(Irp);

    //////////////////////////////////////////////////////////////////////////

    switch(irpStack->Parameters.DeviceIoControl.IoControlCode)

    {

    case IOCTL_TEST_CTRL_CODE2:

        //崩溃

        *pucCrash = 1;

        break;

    case IOCTL_TEST_CTRL_CODE1:

        DbgPrint( "Received IOCTL_TEST_CTRL_CODE1\n");

        ntStatus = STATUS_SUCCESS;

        break;

    default:

            ntStatus = STATUS_SUCCESS;    

    }

    //////////////////////////////////////////////////////////////////////////

    //

    if(ntStatus == STATUS_SUCCESS)

        Irp->IoStatus.Information = irpStack->Parameters.DeviceIoControl.OutputBufferLength;

    else

        Irp->IoStatus.Information = 0;

    Irp->IoStatus.Status = ntStatus;

    

    IoCompleteRequest( Irp, 0 );

        

    return ntStatus;

}

  1. 应用程序

应用程序主要用来控制驱动设备对象,显示返回的结果。

  1. 开始源码调试

    1. 设置

系列前面的几篇文章已经介绍过对符号路径和源码路径的设置。调试自己编写的驱动时,如果是主控机上编译,在目标机上运行,那么一般都不需要专门设置路径WinDbg就能找到正确的符号和源文件。如果驱动不是在主控机上编译的,或者编译之后移动了源码或符号文件就必须要进行设置。在这里我是这样设置的:

  1. 加载驱动程序

这里我们不自己写程序加载,而是通过附件中的KmdManager.exe工具来进行。

选择要加载的驱动程序之后,首先点击Register就能在注册表中注册该驱动程序的服务。然后点击Run加载并运行。按下Run之后,驱动的DriverEntry就会被调用,所以如果我们要想在驱动入口点中断下来的话,就要在这之前设置断点。Stop和Unregister按钮用来停止和卸载驱动服务。注意操作时要按照Register->Run->Stop->Unregister的顺序,并且Stop之前要在应用层关闭所有已打开的句柄,否则可能只有重起机器才能卸载了。

  1. 设置断点

初学者调试驱动时常常会问的一个问题就是,如何在DriverEntry运行之前中断下来。我们在实际的调试工作中也经常需要调试DriverEntry。这需要用到以前介绍过的未定断点,因为一般来说DriverEntry 运行之前,我们的驱动还没有被加载进来,这时在调试器是找不到这个驱动的任何符号的。

首先在调试器命令窗口输入bu SrcDbgKnlDrv!DriverEntry命令,F5运行起来。在目标机中用KmdManager.exe工具Register并Run。这时会发现目标机中断到了调试器中。

如果前面的路径设置没有问题的话,WinDbg中会自动弹出源码,可以看到目前中断在DriverEntry函数入口处。

从这时起,就可以使用各个WinDbg的调试窗口了,比如查看局部变量、查看内存等等,也可以打开各个源文件直接针对源码设置断点。

  1. 查看驱动设备的信息

运行到DriverEntry时,SrcDbgKnlDrv.sys已经被成功加载到系统中了。DriverEntry运行完成之后就能够看到我们的驱动创建的设备了。

使用gu命令,执行到DriverEntry返回处。使用!drvobj和!devobj命令可以查看驱动程序对象和设备对象的信息。

kd> !drvobj SrcDbgKnlDrv

Driver object (8219f5f0) is for:

\Driver\SrcDbgKnlDrv

Driver Extension List: (id , addr)

 

Device Object list:

82138030

kd> !devobj SrcDbgKnlDrv

Device object (82138030) is for:

SrcDbgKnlDrv \Driver\SrcDbgKnlDrv DriverObject 8219f5f0

Current Irp 00000000 RefCount 0 Type 00000022 Flags 000000c0

Dacl e10361f4 DevExt 00000000 DevObjExt 821380e8

ExtensionFlags (0000000000)

Device queue is not busy.

如果我们的设备有附加到某个设备栈上的话,可以用!devstack扩展命令显示设备栈的信息。

kd> !devstack 82138030

!DevObj !DrvObj !DevExt ObjectName

> 82138030 \Driver\SrcDbgKnlDrv00000000 SrcDbgKnlDrv

这里看到的输出说明SrcDbgKnlDrv没有附加到任何设备栈。

通过!devhandles命令可以查看设备被打开的句柄。目前在这个地方使用的话,由于没有句柄被打开,还看不到什么有用的信息,在后面进行演示。

  1. Dispatch例程的调试

Dispatch例程是驱动程序响应应用层请求的地方。通常来说,驱动程序和应用程序的各种交互就是通过Dispatch例程实现。因此,要跟踪应用层请求处理的情况,可以在这些Dispatch例程上设置断点。

我们的测试程序只处理了IRP_MJ_READ和IRP_MJ_DEVICE_CONTROL两个请求。来试着调试一下。

接着上面中断下来的调试会话,首先用bp SrcDbgKnlDrv!DispatchRead命令和bp SrcDbgKnlDrv!DispatchIoCtrl命令在程序的两个Dispatch例程上下断。F5运行起来。在目标机打开SrcDbgKnlApp程序点击Open,会打开一个SrcDbgKnlDrv.sys驱动设备的句柄。这时中断目标机,使用!devhandles扩展命令就能看到有用的信息了:

kd> !devhandles 82138030

 

Checking handle table for process 0x823b97c0

Handle table at e1002000 with 241 Entries in use

省略掉一部分……

Checking handle table for process 0x82164da0

Handle table at e1137000 with 34 Entries in use

PROCESS 82164da0 SessionId: 0 Cid: 05e4 Peb: 7ffd6000 ParentCid: 05d0

DirBase: 07480240 ObjectTable: e11c1cd0 HandleCount: 34.

Image: SrcDbgKnlAppD.exe

 

008c: Object: 821cb028 GrantedAccess: 0012019f

命令显示EPROCESS地址为0x82164da0的进程打开了一个\\.\SrcDbgKnlDrv设备的句柄。再使用!process 82164da0扩展命令可以看到,这个进程正是我们的SrcDbgKnlApp。在调试驱动程序的时候,这是一种查看哪些应用程序在使用某个驱动设备对象的方法。

接下来继续,F5运行,点击SrcDbgKnlApp的Read按钮,可以看到中断到了DispatchRead例程上。这里就可以用单步、查看局部变量等等普通的方法调试我们的程序运行情况了。对于驱动程序来说,我们还可以做一些其他事情,例如查看IRP的信息,查看IRQL等。如:

kd> !irp @@(Irp)

Irp is active with 1 stacks 1 is current (= 0x8238d2d0)

No Mdl: No System Buffer: Thread 82191da8: Irp stack trace.

cmd flg cl Device File Completion-Context

>[ 3, 0] 0 0 82138030 821cb028 00000000-00000000

     \Driver\SrcDbgKnlDrv

            Args: 00000001 00000000 00000000 00000000

kd> !irql

nt!_KPRCB.DebuggerSavedIRQL not found, error : 0x4.

Saved IRQL not available prior to Windows Server 2003

!irql命令只能对Windows 2003之后的系统使用,我的机器上是Windows XP,因此这里提示了错误。

另外,也可以使用kd> !devobj @@(DeviceObject)这样的命令形式,直接用C++表达式将局部变量作为命令参数来使用。

bc*命令清除断点,F5运行。每次点击Read都可以看到从驱动中读取出来的数据。

  1. 驱动异常和内核崩溃转储文件的源码调试

当驱动程序中发生异常时,如果有调试器附加上去,一般会中断到调试器中。如果没有附加调试器,系统通常会自动为我们生成内核dump文件。如果异常发生在我们的驱动中,那么也可以用WinDbg源码调试直观的看出问题所在。

在SrcDbgKnlApp程序中点击Crash。由于现在是在调试会话中,可以在调试器命令窗口中看到异常的信息:

kd> g

Access violation - code c0000005 (!!! second chance !!!)

SrcDbgKnlDrv!DispatchIoCtrl+0x3d:

f8d615ad c60101 mov byte ptr [ecx],1

源码窗口中可以看到崩溃处的代码:

pucCrash的值为NULL,造成了崩溃:

kd> ?? pucCrash

unsigned char * 0x00000000

""

如果是实际工作时进行调试的话,这时可以继续分析崩溃的原因。

再来尝试一下没有附加调试器的情况。重起目标机,在我的电脑->属性->高级中设置一下内核dump的类型和生成的路径:

再使用刚才的方法,加载驱动程序后,用SrcDbgKnlApp让目标机崩溃。发生蓝屏之后重起,在设置的路径下就可以找到dump文件了。这里我们生成的是minidump。将它拷贝到主控机上来,通过WinDbg菜单打开。如果驱动不是在本机上编译的话,因为还没有设置各种路径,现在看到的只有汇编代码。我们使用一下!analyze -v命令能够看到没有符号时对异常的分析结果,可以看到出问题的模块是SrcDbgKnlDrv.sys:

接下来设置好可执行映像路径、符号路径和源码路径,使用.reload命令重新加载符号,会自动弹出源码窗口,调试器中也会更新显示符号信息。如果再次使用!analyze -v,能够看到非常详细的分析报告。这时还可以查看异常的每个堆栈帧中的源码和符号化信息。对于调试Dump文件中的BUG来说,使用源码可以大大提高调试效率。

关于内核模式Dump文件的调试这里就不再介绍了。Windows调试工具的帮助文档中有相关技术较详细的资料。

posted @ 2009-02-19 22:30 NetRoc 阅读(656) | 评论 (0)编辑 收藏

Windows调试工具入门4 - WinDbg内核调试配置

从4开始,我们将有一个系列来介绍WinDbg的内核调试技术。这部分由小喂主笔。感谢小喂专门抽出空余时间来共享技术:)
http://www.dbgtech.net/forum/read.php?tid=50&toread=1

posted @ 2008-12-18 12:09 NetRoc 阅读(921) | 评论 (0)编辑 收藏

Windows调试工具入门-3

     摘要: 基本调试操作     http://www.DbgTech.net 调试器命令窗口 简介 使用Windows调试工具进行调试,大部分和调试器之间的交互都是通过调试器命令窗口来进行的。命令的输入、输出都是在调试器命令窗口中显示出来。对WinD...  阅读全文

posted @ 2008-12-13 12:36 NetRoc 阅读(2446) | 评论 (4)编辑 收藏

Windows调试工具入门-2

NetRoc

http://www.DbgTech.net

本篇介绍Windows调试工具的基本设置和基本操作方法。这里我们会用一个测试程序一步一步说明如何使用WinDbg开始调试工作。首先用VC建立一个名为TestDebug1的控制台项目,并生成它。

  1. 符号、源码和可执行映像路径设置

使用WinDbg开始调试工作之前,最重要的就是配置好各种环境了。这使得调试器可以正确识别调试目标中的各种变量、函数等等,使得我们能够进行符号化调试或者源码调试,而不是只能在一堆汇编代码中转圈。

首先来看一下未设置环境之前的样子。使用刚才说的TestDebug1项目,为了对比更清晰,用Release进行编译,链接选项中选中生成map文件和调试信息,如下:

在C/C++选项卡中设置如下:

程序代码如下:

#include "stdafx.h"

#include <stdio.h>

int main(int argc, char* argv[])

{

    printf( "TestDebug1.cpp");

    return 0;

}

编译之后,将Release目录下的TestDebug1.pdb剪切到其他目录下(如果没有这样做,由于编译出来的程序中包含了符号文件路径,调试器可以直接使用exe中的信息找到pdb文件,而不需要设置路径)。在map文件中可以看到像下面这样的内容:

0001:00000000 _main 00401000 f TestDebug1.obj

说明main函数位于401000地址处。

通过WinDbg的File->Open Executeable菜单打开TestDebug1.exe,可以在调试器命令窗口中看到下面的内容:

可以看到,调试器自动中断下来的位置并不是程序入口点,这是由WinDbg实现造成的,这里先不管它。

调试器命令窗口中可以看到,我们还没有设置符号路径,所以WinDbg目前还找不到TestDebug1.exe的任何符号文件。如果想在main函数下断,这时就不能使用符号,而只能直接使用main的地址。

使用命令bp 00401000在main函数设置断点,然后F5执行就可以中断到main的入口处了。断点设置和基本操作我们将在后面介绍。可以在反汇编窗口中看到这样的内容:

由于没有加载任何符号,所以我们看到的都是一堆反汇编代码和地址。在上一篇中已经介绍过,WinDbg不像OllyDbg这些调试器一样拥有强大的反汇编分析能力,所以仅仅靠这些看起来一团乱麻的反汇编代码,调试工作是很难开展下去的。

  • 符号路径的设置

    要想在WinDbg中看到程序中的符号,必须通过命令或者WinDbg菜单设置符号路径。如果还设置了Microsoft公共符号存储的话,我们不但能够看到自己程序中的符号,还能够看到Windows平台代码中的符号,这对于调试会提供很好的帮助。

    所谓符号路径,就是包含了程序符号信息的符号文件所在的目录路径。通常我们接触到的符号文件都是以pdb作为后缀名的。TestDebug1.exe项目如果在项目设置的Link选项中选中了生成调试信息的话(如上图中的Generate debug info),那么可以在Debug或者Release目录中找到它的符号文件TestDebug1.pdb。

    我们通过WinDbg的File->Symbol File Path…菜单,或者命令.sympath设置符号路径为TestDebug1.pdb所在的目录。例如刚才我把生成的pdb文件移动到桌面上了,所以在我的机器上就设置为:

    完成之后在命令窗口输入.reload命令,我们可以看到反汇编窗口的内容发生改变:

    这里就已经可以看到TestDebug1.exe中的函数、变量名这样的符号了。而我们也可以通过bp main这样的命令直接使用符号来操作调试器。

    另外,在Local、Watch等窗口中也可以直接使用符号名查看到变量的值、在Call Stack窗口中可以看到函数名,等等。

  • 源码路径的设置

    通过上面的设置,我们可以对程序进行符号化调试。如果拥有程序的代码,还可以通过设置源码路径来进行源码级调试。

    继续上面的工作,我们通过WinDbg的File->Source File Path…菜单或者.srcpath命令设置源代码保存的路径,比如我的机器上是这样:

    确定之后,如果当前指令指针在源文件的代码范围内,就会自动跳出源文件窗口。如果没有跳出,那么可以通过File->Open Source File…菜单手动打开源文件。由于刚才设置的断点还没有删除,所以在源码窗口也能口看到设断的行被高亮了:

    之后就基本上可以完全通过源码窗口进行设置断点、查看变量、跟踪代码等操作。比只有符号的时候方便了很多。

  • 可执行映像路径的设置

    可执行映像路径一般在调试dump文件时才用得上。需要将这个路径设置成要调试的exe、dll、sys等可执行文件的路径。可以通过File->Image File Path…菜单或者.exepath命令设置。

  • 使用微软公共符号存储

    除了使用自己程序的符号之外,调试时还可以使用微软提供的Windows系统代码的符号。这需要修改一下我们设置的符号路径。最方便的办法是使用.symfix命令。

    现在我们来看一下kernel32.dll中的代码,在反汇编窗口的Offset栏中填入kernel32!OpenProcess,在我的机器上代码如下:

    注意位于764e8ccf处的那个call,现在只能看到调用了kernel32某个偏移处的地址。

    使用命令.symfix+ d:\Symbols命令,注意加号要紧靠前面的文本。d:\Symbols是用来保存下载的符号文件的目录,可以修改成自己需要的路径。再来打开符号路径窗口,我们可以看到调试器自动添加了一些内容:

    自己在源码路径中加入这些新的内容也可以实现相同的效果。详细的原理请参考WinDbg帮助文档关于符号服务器设置的部分内容。

    接下来再次使用.reload命令重新加载符号,第一次使用到的符号文件会从网上自动下载下来,所以可能有时候会等待一会。完成之后,可以看到反汇编窗口中出现了新的符号内容:

    764e8cd8处指令中可以看到这是调用了kernel32导入的函数NtOpenProcess。

    微软提供的Windows符号是我们研究Windows实现的必备利器。首先,符号化的名字有助于调试过程中的记忆和对各种信息的识别;其次,通过名字就常常可以猜测出来函数或变量的作用,很大的方便调试。在各种调试应用中,都强烈建议添加微软公共符号的引用。

  • 设置环境变量

    上面介绍的各种路径都可以通过环境变量来进行设置。将一些常用的路径保存在环境变量中,就可以避免每次在新的工作空间中进行调试时都要重新设置的麻烦。另外,Visual Studio 2008也共享一些环境变量的设置,这样在使用IDE调试的时候也能方便的查看到各种符号了。常用的有下面几个:

环境变量

作用

_NT_SOURCE_PATH = Path

指定包含调试目标的源代码的路径。Path可以包含后跟一个冒号(:)的驱动器符。用分号分隔多个目录(;)。

_NT_SYMBOL_PATH = Path

指定包含符号文件的目录树的根目录。Path可以包含后跟一个冒号(:)的驱动器符。用分号分隔多个目录(;)。

_NT_EXECUTABLE_IMAGE_PATH = Path

指定包含二进制可执行文件的路径。Path可以包含后跟一个冒号(:)的驱动器符。用分号分隔多个目录(;)。

_NT_DEBUG_LOG_FILE_OPEN = Filename

(仅CDB和KD) 指定调试器用来记录输出的日志文件。

_NT_DEBUG_LOG_FILE_APPEND = Filename

(仅CDB和KD) 指定调试器用来添加输出的日志文件。新的内容每次会添加到这个文件末尾,而不是覆盖整个文件。

如果设置了符号路径的环境变量的话,可能在初期使用VS 2008调试MFC这样的有较多导入库的程序时会下载很多符号文件,使得启动调试的速度变慢。不过经过一段时间,大部分需要的符号都缓存到本地之后速度就会快起来。

  1. 配置日志文件

进行调试时,有时候调试器命令窗口会变得很杂乱,所以常常想用.cls命令清空它。但是这样会无法再看到之前调试过程中输出的结果。另外,有时候想保存下整个调试过程的详细记录以备后面"回味"。这时,就需要用到日志文件了。可以将调试器命令窗口中出现过的所有内容都自动记录到日志文件中。

创建日志文件:

  • (仅CDB 和KD) 启动调试器之前,设置_NT_DEBUG_LOG_FILE_OPEN环境变量。
  • 启动调试器时,使用-logo 命令行选项。 如-logo d:\logs\mylogfile.txt
  • 使用.logopen命令。如.logopen /t d:\logs\mylogfile.txt
  • (仅WinDbg) 使用Edit->Open/Close Log File菜单命令。

将日志添加到已有的文件末尾:

  • (仅CDB 和KD) 启动调试器之前,设置_NT_DEBUG_LOG_FILE_APPEND环境变量。
  • 启动调试器时,使用-loga命令行选项。如-loga d:\logs\mylogfile.txt
  • 使用.logappend命令。 如. logappend/t d:\logs\mylogfile.txt
  • (仅WinDbg) 使用Edit->Open/Close Log File菜单命令,然后选择Append。

关闭日志文件:

  • 使用.logclose命令
  • (仅WinDbg) 使用Edit->Open/Close Log File菜单命令,然后选择Close Open Log File。
  1. 设置工作空间

工作空间(Workspace)是用来保存WinDbg中工作环境的工具。例如习惯的窗口布局方式、符号路径、异常处理的设置等等,都可以通过工作空间保存下来,在下次调试的时候就不用再次设置了。

相关的设置都可以通过WinDbg菜单来完成,有下面几个:

  • Open Workspace:这里只能打开自己通过SaveAs保存的工作空间。
  • Save Workspace:按默认的方式保存当前的工作空间。下次再打开相同的调试目标时,就会自动打开这个Workspace。
  • Save Workspace As:可以自己设置工作空间的名字,这样就能通过Open Workspace来手动打开。
  • Clear Workspace:可以选择保存工作空间时要保存哪些设置。
  • Delete Workspace:删除当前保存的工作空间。这里可以查看到所有默认保存和另存为的工作空间,用来进行清理是很方便的。
  • Save Worlspace in File和Open Workspace in File:将工作空间保存到文件或者从文件打开。可以把自己的工作空间保存下来,这样通过U盘之类的就能在多台机器之间方便的使用相同的设置了。

    在没有调试目标的时候调整WinDbg的窗口布局等等设置的话,会保存为默认的工作空间。下一次打开新目标的时候,就会使用这个设置。通常我们可以设定一个默认的工作空间,然后为各个单独的任务保存另外的设置。

posted @ 2008-12-03 18:11 NetRoc 阅读(1028) | 评论 (0)编辑 收藏

Windows调试工具入门—1

NetRoc

http://www.DbgTech.net

  1. 引子

Debugging Tools for Windows是微软发布的一套用于软件调试的工具包(后面如果没有指明,那么我会使用WinDbg来作为这一套调试工具的简称)。我第一次接触是在三年前的一个内核驱动项目,由于进行了IDT中键盘鼠标中断的Hook,使用Softice调试时造成会造成影响,只得使用WinDbg通过串口进行双机调试。自此之后这个Windows平台下最为强大的调试工具一直是开发过程中的必备。这里我毫不掩饰的说"最强",可能很多通过逆向工作而接触调试的朋友不会认同,但是我相信随着对WinDbg了解的加深,以及对这套工具在软件开发中应用的了解,他们也会和我有一样的观点。

一直以来,软件调试技术在软件开发者中都没有得到足够的普及和重视,互联网上能找到的系统描述的资料也较少。随着国内软件行业整体的发展和进步,这些技术慢慢开始得到推广。2008年出版的有关调试的数据比以往都要多。我有幸拜读了Raymond的《软件调试》,以及熊力的《Windows用户态程序高效排错》,获益良多。 这几年的工作中也积累了一些关于Windows调试工具的知识,希望能够将这些东西进行一些分享。因此,利用几个月空闲时间翻译了WinDbg文档中上半部调试器配置、使用和命令介绍的内容,同时准备写一些关于WinDbg调试工具的初级文章。希望能够为对调试技术感兴趣而又苦于没有资料的朋友提供一些帮助。

特别感谢我的前同事小喂。虽然他第一条串口线还是我焊的,但是他对于WinDbg的使用和了解程度很快就超过了我。在相当长时间的共事和讨论中,让我学到了很多。

  1. Windows调试工具的简介和组成

WinDbg是专门为Windows NT系列操作系统设计的调试器,最早是作为Windows NT 3.1的工具发布的。其后也一直跟随NT操作系统的发展而不断发展完善。如果用一句话来概括,可以说WinDbg是为了软件开发而存在的调试工具。软件包中的调试器和小工具的各种功能都是为了配合软件的开发而设计的,并且覆盖到了Windows平台下各种不同类型项目的调试(传统的SDK或MFC应用程序、.NET平台应用、COM应用、软硬件驱动程序等等)。

Windows调试工具包中的调试器包括WinDbg、KD、CDB和NTSD。其中, KD用于内核调试;CDB和NTSD用于用户态调试,在功能和使用上几乎完全一致;WinDbg是内核调试器和用户态调试器的综合体,由于功能完善并且具有图形界面,所以是最常用的工具。它们能够在x86、Itanium和x64机器上的所有NT平台操作系统中运行。

另外,工具包中还有一些小工具,下面是常用的几个:

  • KDbgCtrl:用于控制和配置内核调试的一些参数。例如是否只有当发生异常时才会启用内核调试、设置DbgPrint缓冲区大小、如何处理用户模式异常等等。
  • ADPlus:这是一个VB脚本,可以为一个或多个进程自动创建内存dump。
  • SymStore:用于创建符号存储。当需要创建自己的符号存储时就要用到它了。
  • SymProxy:用于在网络中创建单独的HTTP符号服务器,以供所有调试器使用。该工具特别适合企业级应用的环境,可以将多个符号存储通过单一的接入点提供使用。
  • DbgSrv、KdSrv、Remote.exe:用于远程调试。
  • GFlags:用于编辑Global Flags。
  • UMDH:用于对用户模式堆分配的情况进行转储和分析。
  • USBView:这是WinDbg 6.10.3版本才加入到软件包中的工具,可以查看当前连接到系统中的USB设备信息。

另外,Application Verifier虽然没有包含在软件包中,但是也是一个非常强大的工具。可以对程序运行时的很多状态进行监控,以发现一些普通调试难以找到的错误。下面是Application Verifier配置界面的一个截图:

Application Verifier可以在这个页面下载:http://go.microsoft.com/fwlink/?linkid=108353

  1. Windows调试器和其他熟知的调试器比较

可能很多已经习惯使用SoftICE、OllyDbg、IDE调试器的朋友会提出这样的疑问:在这么多调试器中,为什么要选择WinDbg?它究竟有什么特点?

设想一下下面几个场景:

  • 公司的软件针对企业级用户,该客户在地球另一半的美国。有一天客户抱怨了一个BUG,但是从抓取的dump又没办法看出个所以然,想进行动态调试查找原因。公司预算有限,不能让你过去出差顺便旅游、对方公司有防火墙,不允许外部连接,等等等等。。。怎么办?
  • 项目规模很大,涉及到的模块多,版本也多,并且是由不同部门开发的。这些部门可能遍布五湖四海。如何在调试其中某个模块时,能够快速获得它的符号和源文件,而不用每次都从一大堆不同版本的文件中辛苦找寻?调试到某个阶段,突然发现这不是自己的模块出现问题,如何快速知道这个问题应该找谁解决?项目某些重要模块有保密需要,如何控制调试人员访问符号和源文件的权限?
  • 驱动程序怎么才能源码调试?SoftICE不支持新系统,我要在Vista上调试怎么办?
  • 软件中包含一个Windows服务组件,但是每次还没有登陆到桌面之前就崩溃了,怎么进行动态调试?
  • 我想调试Explorer,调试IE,调试CSRSS,调试……,但是调试器一附加上去,系统就会出问题。怎么办?
  • 公司发布的软件,有用户反馈和XXX安全软件冲突老是造成系统崩溃,但是搭建环境之后却又没有办法重现;对方是个普通用户,鼠标都抓得不太稳。用户很火大,闹着要抓个老虎到公司来找你上司做俯卧撑,后果很严重,怎么办?

在现实环境中,有很多复杂的调试场景,我们需要专业级的调试器来解决这些问题。而WinDbg恰恰提供了这种商业软件环境下的专业级软件调试功能,它和其他很多我们熟知的调试器的区别也在于此。

我们将WinDbg和其他调试器分作内核调试器和用户态调试器两类来进行比较。

内核调试方面:

 

WinDbg

SoftICE

原理

Windows操作系统内置调试支持

Hook中断,接管系统

系统和平台支持

x86、Itanium和x64机器上的所有NT平台操作系统

x86,由于已停止更新,新版本操作系统中支持不佳,老系统中也常常遇到兼容性问题

符号和源码支持

完美支持符号调试和源码调试,可直接使用微软公共符号

支持符号调试和源码调试,但是需要先转换符号格式

远程调试

通过和远程工具、转发器的配合,实现各种灵活的远程调试方式,以支持不同的网络环境

通过Virtual SoftICE支持基于网络的远程调试

硬件需求

通过串口、1394、USB 2.0接口的双机调试;通过Pipe连接的虚拟机调试;或者功能有诸多限制的本地内核调试

单机或者通过Virtual SoftICE的双机调试

用户界面

由于是双机调试,调试器只是主控机上运行的一个普通软件。拥有GUI界面,可以同时进行其他应用。

单机调试时完全接管系统,字符界面,操作不是很方便。

扩展性

支持脚本和插件,并且软件包本身提供了大量非常有用的插件

支持插件

由于SoftICE已经停止更新,WinDbg可以说是现在Windows平台上唯一好用的进行内核调试的工具,并且随着新版本的不断推出,不断地添加对新版操作系统的支持以及完善功能。强大的符号支持,方便的源码调试,使得内核级调试能够事半功倍。

用户态调试方面:

 

Windows调试工具包

OllyDbg

Visual Studio调试器

原理

Windows的用户程序调试支持

Windows的用户程序调试支持

Windows的用户程序调试支持

系统和平台支持

主要基于NT系统,9x内核下支持不佳并且需要安装附加模块

主要支持NT系统,9x下也可以使用

新版本的VisualStudio不支持在9x系统下安装。VC6之前可以在9x下调试

符号和源码支持

完美支持符号调试和源码调试,可直接使用微软公共符号

支持符号调试和源码调试

支持。VS2008开始可以直接使用微软公共符号

远程调试

通过和远程工具、转发器的配合,实现各种灵活的远程调试方式,以支持不同的网络环境

不支持

较新版本Visual Studio中支持

无源码调试

反汇编分析能力较弱,GUI界面偏弱,无源码时调试比较困难

强大的代码分析能力,无符号和源码时也能很好的进行调试

无源码调试的支持很弱,使用不便

用户界面

GUI界面不是很丰富,大量操作需要通过命令

GUI界面强大,能够实现大多数调试操作

介于WinDbg和OllyDbg之间。

扩展性

支持脚本和插件,并且软件包本身提供了大量非常有用的插件

支持脚本和插件,有大量可用的资源

支持插件扩展

Dump文件调试

支持,分析功能强大

不支持

支持,但是不够强大

.NET调试

通过SOS.dll支持,进行高级调试比较方便

不能直接支持

功能强大易用,绝大多数情况下都能解决问题

由于WinDbg功能相当复杂,有很多方面并不能一一比较,例如非侵入式调试、通过WinDbg控制CDB和NTSD来调试系统服务、创建和分析Dump文件等等。

总体来说,WinDbg更适合作为软件项目开发和维护过程中的调试工具使用,而OllyDbg更适合逆向工程。

  1. 何时使用Windows调试工具

 

根据我个人对WinDbg的使用经验来说,它更适合作为开发维护的辅助工具来使用。

如果要进行用户态的逆向工程,推荐使用OllyDbg、IDA这些拥有强大汇编程序分析能力的工具。

WinDbg更适用于以下这些场合:

  • 商业软件的Debug和客户支持

 

  • 内核驱动的调试,以及对驱动进行逆向工程时进行动态调试
  • 研究Windows本身的内核或者软件
  • 疑难BUG的调试,如死锁、COM调用、资源泄露、堆栈或者堆溢出
  • 以性能优化为目的的调试
  • 对调试目标基本不造成影响的非侵入式调试

posted @ 2008-11-27 17:27 NetRoc 阅读(2679) | 评论 (0)编辑 收藏

仅列出标题
共10页: 1 2 3 4 5 6 7 8 9 Last 

导航

统计

常用链接

留言簿(6)

随笔档案(99)

文章分类(35)

文章档案(32)

Friends

Mirror

搜索

最新评论

阅读排行榜

评论排行榜