stand on the shoulders of giants

Debug Knowledge Base 1

【转载+整理】

符号文件的重要性

编译器将源代码编译成二进制代码,所产生的符号文件就相当于代码行和二进制的中间解释器。以下都离不开符号文件的帮助:
1. 设置断点,相当于将源代码行的行号转换成对应的机器代码的地址;
2. 相反的,查看程序堆栈,就是调试器使用映射关系将堆栈里面的地址转换成包含这个地址的函数名。
3. 对于机器来说,所有的变量都只是一个内存地址,程序在读取变量值的时候,只不过按照变量所属的类型来读取指定大小的内存而已
4. 我们在立即窗口里面调用一个函数的时候,调试器需要将函数名翻译成对应的机器代码的起始地址

异常处理机制

异常分为硬件异常,软件异常。

硬件异常:当CPU运行到一些非法的指令,例如除零错误,访问内存页失败等指令,CPU会生成一个硬件异常,不同的异常有固定的异常代码作为标识符,异常产生以后CPU暂时不能继续执行后续的指令—因为后续的指令有可能也是无效的。
当然不能让整个计算机系统就这么当掉,因此CPU内置了一个异常处理表—这个异常处理表只有运行在内核模式的代码才能访问,操作系统在启动的时候初始化这个异常处理表,为每一个异常注册一个异常处理程序,因此这个表看起来就像:

0xC0000005      AccessViolationExceptionHandler()
……  
0x80000003      LaunchOrNotifyDebugger()    
这个就是Windbg每次debug程序的时候第一个“异常”中断,其实是为了调试!

例如:
Executable search path is:
ModLoad: 01150000 01158000   Windbg2.exe
ModLoad: 77300000 77480000   ntdll.dll
ModLoad: 71920000 7196a000   C:\Windows\SysWOW64\MSCOREE.DLL
ModLoad: 75110000 75210000   C:\Windows\syswow64\KERNEL32.dll
ModLoad: 76740000 76786000   C:\Windows\syswow64\KERNELBASE.dll
(1340.103c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=cba30000 edx=000ee188 esi=fffffffe edi=77323ade
eip=773a0995 esp=001ef9bc ebp=001ef9e8 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrpDoDebuggerBreak+0x2c:
773a0995 cc              int     3

CPU产生异常以后,根据异常代码在异常处理表里面查找对应的异常处理程序,通过调用异常处理程序执行了合适的处理之后再继续执行其他的代码。

软件异常:处理CPU自定义的硬件异常以外,异常处理表里面还有一些空的表项,这样操作系统可以通过添加自定义的异常代码和相应的异常处理程序实现软件异常。在Windows操作系统中,程序可以通过调用Win32的RaiseException函数来触发软件异常。因此实际上C++和.NET里面的异常都是通过调用RaiseException来实现的,然而异常处理表不是很大,只能保存不超过256个异常处理程序信息,因此Windows只定义了几个通用的异常码来表示C++ 和.NET异常。

0xE06D7363     表示C++ 异常
0xE0434f4D      表示CLR(或者说.NET)异常

对于CPU来说,硬件异常、软件异常、C++ 异常和.NET异常的触发和处理的方式都是一样的,这种方式在Windows中叫做结构化异常处理(SEH)

当程序里面有一个异常发生以后:

1. SEH首先通过回溯堆栈内容,查找程序内部是否有一个异常处理块(就是我们通常知道的catch 块);如果找到一个异常处理块可以处理这个异常,那么Windows将异常处理块所在的函数下面的堆栈释放,并且执行这个异常处理块里面的代码。
2. 如果Windows没有发现任何一个异常处理块处理掉这个异常的话,也就是到程序入口(main)函数也没有找到一个合适的异常处理块的话,Windows会使用它自带的异常处理块处理这个异常;
3. Windows自带的异常块会检查你的程序是否附加了一个调试器,如果是的话,Windows中断程序并将控制权交给调试器。
4. 如果没有调试器附加到你的程序上,Windows启动注册在注册表里面的默认验尸调试器,一般情况下,Windows的默认调试器是Dr Watson,这个调试器的工作就是弹出著名的“调试还是终止,这个问题”对话框(或者是“是否将错误报告发送给微软?”对话框):
image

FirstChance和SecondChance

如果异常发生,Windows会先检查你的程序是否正在被调试,如果有一个调试正附加在你的程序上,Windows向调试发送一个调试消息,通知调试器程序里面有一个异常发生,询问调试器是否要中断程序执,默认情况下调试器会忽略这条消息—因为我们纯洁的调试器总是相信程序员都能写出优良的代码出来。
这个步骤在调试术语中叫做第一次机会(First Chance)--第一次在异常发生的现场观察触发异常的环境。
这个就是我们经常在Windbg里看到的First Chance

而第2步以后,操作系统自己来处理这个异常的时候,在调试术语中叫做第二次机会(Second Chance),由于这个时候程序实际上已经挂了(不会有任何生命活动了)—不是病危,所以这个时候即使你将调试器附加上去来检查触发异常的环境时,就像法医在检查人非正常死亡的原因一样—验尸调试。

如果我们在程序中

using (StreamReader reader = new StreamReader("notexists.txt"))
{
       Console.WriteLine(reader.ReadToEnd());
}

在Debug中会看到:
(1340.103c): CLR exception - code e0434f4d (first chance)  !!!被忽略!!!
(1340.103c): CLR exception - code e0434f4d (!!! second chance !!!) !!!Second Chance!!!
CLR exception type: System.IO.FileNotFoundException
    "Could not find file 'C:\Program Files (x86)\Debugging Tools for Windows (x86)\notexists.txt'."
eax=001eef84 ebx=e0434f4d ecx=00000001 edx=00000000 esi=001ef00c edi=00679b50
eip=7674b727 esp=001eef84 ebp=001eefd4 iopl=0         nv up ei pl nz ac pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000216
KERNELBASE!RaiseException+0x58:
7674b727 c9              leave

所以如果程序中有许多简单粗暴的try … catch (Exception e) { … }对通常Exception都catch了,可能会有很莫名其妙的bug,比较难发现。
比如:

代码
using System;

public class Class1
{
   
public static void Main()
    {
        Console.WriteLine(Calculate(
"12345.6789 + 987654321l"));
        Console.Read();
    }

   
private static double Calculate(string expression)
    {
       
string[] numbers = expression.Split('+');

       
return RedundantParseForDemoOnly(numbers[0].Trim()) +
               RedundantParseForDemoOnly(numbers[
1].Trim());
    }

   
private static double RedundantParseForDemoOnly(string number)
    {
       
try
        {
           
return double.Parse(number);
        }
       
catch
        {
           
return 0;
        }
    }
}

 

结果是12345.6789,注意第二个数字最后是小写的L,如果程序复杂,这样的问题被catch后是很难发现的,一步步单步调试是很不合适的。
如果调试器能忽略Try catch, 在出现Exception的地方中断,就很容易发现问题了。那么当我们在调试的时候,如何让调试器停在first chance的catch呢?

设置调试器不要忽略first chance异常

1. 在Windbg里面
使用下面的命令来通知调试器不要忽略first chance异常:
                        sxe 异常代码
例如在调试.NET程序的时候,可以使用命令sxe 0xE0434f4D来使调试器在catch块执行之前中断程序的执行。
可以使用命令      sxd 异常代码
来启用忽略first chance异常的功能。
例如上例,Windbg忽略first chance异常,windbg输出first chance异常后程序结束:
(1694.17c8): CLR exception - code e0434f4d (first chance)
(1694.17c8): CLR exception - code e0434f4d (first chance)
eax=00000000 ebx=00000000 ecx=00000000 edx=00000000 esi=77602100 edi=776020c0

设置sxe 0xE0434f4D后, Windbg输出first chance异常后停止:
(14ac.1600): CLR exception - code e0434f4d (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=001ff03c ebx=e0434f4d ecx=00000001 edx=00000000 esi=001ff0c4 edi=00409b50

2. 在Visual Studio里面,
选择“Debug”菜单里的“Exceptions…”菜单项,打开“Exceptions…”对话框设置异常断点:
Break when an exception is  当“Thrown”一列的复选框被选中以后,则无论程序是否有try…catch块捕捉该异常,只要扔出了指定的异常,则调试器都会中断程序的执行。如上例,如果你勾选了System.FormatException则程序调试的时候,会自动停在:

 image

另外VS还支持自定义的异常中断,选择Add… 输入自定义的异常
image

代码
using System;
using System.IO;

namespace TestNamespace
{
   
public class TestException : Exception
    {
    }

   
public class ExceptionBreakpoint
    {
       
static void Main()
        {      
           
// 在“New Exception”中添加TestNamespace.TestException异常
           
// 并且在“Throw”中勾上“TestNamespace.TestException”复选框
           
// 下面的语句才会导致程序中断,并且跳入到调试器当中
            try
            {
               
for (int i = 0; i < 10; ++i)
                {
                   
if (i == 5)
                       
throw new TestException();

                    Console.WriteLine(i);
                }
            }
           
catch (Exception e)
            {
                Console.WriteLine(e.Message);
                Console.Write(e.StackTrace);
            }

            
//显然,下面的语句总能导致程序中断,并且跳入到调试器中
            using (StreamReader reader = new StreamReader("notexists.txt"))
                Console.WriteLine(reader.ReadToEnd());

            Console.Read();
        }
    }
}

 

posted @ 2010-09-19 18:40  DylanWind  阅读(3659)  评论(0编辑  收藏  举报