2023年1月

前面有个案例最终查明原因是System.Convert.ToInt16的调用导致溢出异常:

0:000> !PrintException /d 4ee2e8f4
Exception object: 4ee2e8f4
Exception type:   System.OverflowException
Message:          值对于 Int32 太大或太小
InnerException:   <none>
StackTrace (generated):
    SP       IP       Function
    001EC094 1D3D8831 mscorlib_ni!System.Convert.ToInt32(Double)+0xc4bc19
当时核对了代码,代码里明明调用的是System.Convert.ToInt16(float value),为什么这里却抛出异常是调用System.Convert.ToInt32(Double)引起的呢。

要想查明原因,只有查看源代码。那我们看看DotNet48RTM的源代码:

在工程mscorlib的代码..\DotNet48RTM\Source\ndp\clr\src\BCL\system\convert.cs我们可以找到相关代码

public static short ToInt16(float value) {
            return ToInt16((double)value);
        }

可知ToInt16(float value)调用的是ToInt16(double value) ,那么ToInt16(double value) 的代码如下:

public static short ToInt16(double value) {
            return ToInt16(ToInt32(value));
        }

可知ToInt16(double value)调用的是ToInt32(double value),ToInt32(double value)的代码如下:

 public static int ToInt32(double value) {
            if (value >= 0) {
                if (value < 2147483647.5) {
                    int result = (int)value;
                    double dif = value - result;
                    if (dif > 0.5 || dif == 0.5 && (result & 1) != 0) result++;
                    return result;
                }
            }
            else {
                if (value >= -2147483648.5) {
                    int result = (int)value;
                    double dif = value - result;
                    if (dif < -0.5 || dif == -0.5 && (result & 1) != 0) result--;
                    return result;
                }
            }
           throw new OverflowException(Environment.GetResourceString("Overflow_Int32"));
        }

此时,我们也就明白了为什么抛出的是“值对于 Int32 太大或太小”的异常了,同时,我也比较担忧着个性能的问题。

大多数的应用程序都使用多线程技术。对应Windows应用程序,为了能够使用户界面保持快速响应,经常需要把费时的任务放在与主应用程序独立的线程上运行。此时,多个线程的并发执行调试变得很困难,特别是在多个线程访问同一个类和方法时。Threads能够帮助我们减轻复杂度。

打开窗口

一般来说当程序遇到断点进入调试模式,会自动打开Threads窗口,如果没有,我们可以通过下面的方式打开:

  • 通过菜单栏打开
  • 通过快捷键
    Ctrl+Alt+H

窗口的使用

线程窗口包含其中每行描述一个单独的线程在应用程序中的表。 默认情况下,该表列出应用程序中的所有线程,但可以筛选列表以仅显示感兴趣的线程。 每个列说明了不同类型的信息。 您还可以隐藏某些列。 如果显示所有列,显示以下各列,从左到右:

  • 标志:在此未标记的专栏中,可以标记要特别注意的线程。 有关如何标记一个线程的信息,请参阅如何:标记线程和取消标记线程。

  • 当前线程:在此未标记的列,黄色箭头指示当前线程。 概述箭头指示非当前线程的当前调试器上下文。

  • ID:显示每个线程的标识号。

  • 托管 ID:显示托管线程的托管的标识号。

  • 类别:显示为用户界面线程、 远程过程调用处理程序或工作线程的线程的类别。 一个特殊类别标识应用程序的主线程。

  • 名称:如果有的话,或按名称标识每个线程<无名称 >。

  • 位置:显示线程正在其中运行。 可以展开此位置以显示线程的完整调用堆栈。

  • 优先级:(默认情况下隐藏) 的高级的列,显示系统已分配给每个线程的优先级。

  • 关联掩码:高级的列 (默认情况下隐藏),显示了每个线程的处理器关联掩码。 在多处理器系统中,关联掩码确定线程可以在哪些处理器上运行。

  • 挂起项计数:高级的列 (默认情况下隐藏),显示挂起项计数。 此计数确定线程是否可以运行。 

  • 进程名称:(默认情况下隐藏) 的高级的列,显示每个线程所属的进程。 在调试多个进程时,此列中的数据很有用。

  • 进程 ID:(默认情况下隐藏) 的高级的列,显示每个线程所属的进程 ID。

  • 传输限定符:高级的列 (默认情况下隐藏) 唯一标识调试器连接到的计算机。

在顶部的工具栏线程窗口中,选择然后,选中或清除要显示或隐藏的列的名称。

在“线程”窗口中,可以用图标标记来标记要格外关注的线程 。在“线程”窗口中,可以选择显示所有线程或仅显示标记的线程 。

当冻结线程时,系统不会启动线程的执行,即使提供了资源。在本机代码中,您可以挂起或继续线程通过调用 Windows 函数SuspendThreadResumeThread或者,致电 MFC 函数CWinThread::SuspendThread并cwinthread:: Resumethread。 如果您调用SuspendThreadResumeThread,则挂起项计数中所示线程窗口将会更改。 如果冻结或解冻本机线程不会更改挂起项计数。 线程不能在本机代码中执行,除非它线程解冻并且其挂起项计数为零。在托管代码中,当冻结或解冻线程时,将更改挂起项计数。 如果在托管代码中冻结线程,其挂起项计数为 1。 当本机代码中冻结线程时,其挂起项计数为 0,除非使用SuspendThread调用。

在顶部的工具栏线程窗口中,选择冻结线程解冻线程此操作仅影响在“线程”窗口中选中的线程 。

 

黄色箭头指示当前线程 (和执行指针的位置)。 带有卷尾的绿色箭头指示非当前线程具有当前的调试器上下文。

若要切换到另一个线程请按照以下步骤之一操作:

  • 双击任一线程。

  • 右击一个线程,然后选择切换到线程

分组线程时,表中将显示每组的标题。 标题包含组说明(如“辅助线程”或“未标记的线程”)和树控件 。 每组的成员线程显示在组标题下。 如果你想要隐藏组的成员线程,使用树控件折叠组。

因为分组优先于排序,所以您可以先按类别(以此为例)分组线程,再按每个类别中的 ID 对其进行排序。

排序线程

  1. 在顶部的工具栏线程窗口中,选择任意列顶部的按钮。

    线程现在按该列中的值进行排序。

  2. 如果你想要反转排序顺序,请再次选择相同的按钮。

    在列表顶部显示的线程现在显示在底部。

分组线程

  • 在中线程窗口工具栏中,选择分组依据列表,然后选择要分组线程所依据的条件。

对组内线程排序

  1. 在顶部的工具栏线程窗口中,选择分组依据列表,然后选择要分组线程所依据的条件。

  2. 在中线程窗口中,选择任意列顶部的按钮。

    线程现在按该列中的值进行排序。

展开或折叠所有组

在顶部的工具栏线程窗口中,选择展开组折叠组

 

您可以搜索匹配的指定的字符串中的线程线程窗口。 在搜索线程时,窗口将显示匹配的任何列中的搜索字符串的所有线程。 信息包括在“位置”列中调用堆栈顶部显示的线程位置 。 默认情况下,不搜索整个调用堆栈。

搜索特定线程

  1. 在“线程”窗口顶部的工具栏中,转到“搜索”框,执行下列操作之一 :


    • 输入搜索字符串,然后按Enter


    - 或 -


    • 选择下拉列表旁边搜索框并选择上一次搜索的搜索字符串。
  2. (可选)若要在搜索中包括整个调用堆栈,请选择“搜索调用堆栈” 。


寄存器是处理器(CPU)中的特殊区域,用于存储处理器需要当前处理的少量数据。 编译或解释源代码时会生成一些指令,这些指令根据需要将数据从内存移动到寄存器或反之。 相对于访问内存数据,访问寄存器数据非常快。那些允许处理器将数据保留在寄存器并多次访问的代码,比起那些需要处理器不断加载和卸载寄存器的代码执行速度快得多。 为了方便编译器将数据保存在寄存器中并实现其他优化,应避免使用全局变量而尽可能地依靠局部变量。 称以此方式编写的代码具有良好的引用局部性。 在某些语言中,如 C/C++ 中,程序员可以声明寄存器变量,它告诉编译器在所有时间尽可能地将该变量保留在寄存器中。

寄存器可分为两类:通用寄存器和专用寄存器。 通用寄存器保存用于一般操作(如将两数相加或引用数组中的元素)的数据。 专用寄存器具有特定用途和专门的意义。 堆栈指针寄存器是一个好例子,处理器利用它跟踪程序的调用堆栈。 程序员可能不会直接处理堆栈指针,但它对于程序正确运行至关重要。 没有堆栈指针,处理器在函数调用完成后将不知道返回何处。

大多数通用寄存器只保存一个数据元素。 例如单个整数、浮点数或某一数组元素。 一些新的处理器具有较大的寄存器,称为向量寄存器,可以保存一个小的数组。 由于它们可以保存较多数据,向量寄存器对于涉及数组的运算处理得非常快。 向量寄存器最初是用在昂贵的、高性能超级计算机上,但现在也用于微处理器,它们在密集的图形操作中具有很大优势。一个处理器通常有两组通用寄存器,一组针对浮点运算进行优化,一组针对整数运算进行优化。 因此,它们被称为浮点寄存器和整数寄存器。托管代码在运行时被编译为用来访问微处理器的物理寄存器的本机代码。 对于公共语言运行时或本机代码,“寄存器”窗口可以显示这些物理寄存器信息。 而对于脚本或 SQL 应用程序,“寄存器”窗口不会显示寄存器信息,因为脚本或 SQL 都是不支持寄存器概念的语言

通过“寄存器”窗口不仅可以看到寄存器的内容,还可以完成更多任务。 当本机代码处于中断模式时,可以单击寄存器内容并更改其值。 这不是可以随意做的事。 除非理解正在编辑的寄存器和它所包含的内容,草率编辑很可能导致程序崩溃或其他不良后果。

打开窗口

  • 通过菜单栏
  • 快捷键
    Alt+5

窗口的使用

查看寄存器值

在调试期间,窗口显示寄存器内容。为了减少混乱,“寄存器”窗口将寄存器组织成组,具体情况随平台和处理器类型的不同而不同。默认显示通用寄存器组。 您可以显示或隐藏寄存器组。在窗口区域右键单击弹出如下菜单

 

大家根据自己的需要在上面菜单里勾选相应的菜单就显示相应的寄存器组,去掉勾选就隐藏。

单步执行时,特别是在汇编模式下,我们可以看到被修改的寄存器值会变红。

编辑寄存器值

  1. 在“寄存器”窗口中,使用 Tab 或鼠标可以将插入点移到要更改的值的位置。 在开始键入之前,光标必须位于要覆盖的值之前。

  2. 键入新值。

 

“反汇编”窗口显示与编译器所创建的指令对应的汇编代码。 如果你正在调试托管的代码,这些程序集指令对应于在实时 (JIT) 编译器,而非 Microsoft 中间语言 (MSIL) 由 Visual Studio 编译器创建所创建的本机代码。在调试的环境下,我们可以很方便地通过反汇编窗口查看程序生成的反汇编信息。

打开窗口

  • 通过菜单栏
  • 通过源代码窗口右键菜单
  • 快捷键
    Alt+8

窗口的使用

除汇编指令外,“反汇编”窗口还可显示下列可选信息:

  • 每条指令所在的内存地址 对于本机应用程序,它是实际的内存地址。 对于 Visual Basic 或C#,它是从该函数的开头的偏移量。

  • 程序集代码派生于的源代码。

  • 代码字节,实际的计算机或 MSIL 指令的字节表示形式的即。

  • 内存地址的符号名。

  • 对应于源代码的行号。

汇编语言指令组成助记键,这是指令名称的缩写和符号的变量、 寄存器以及常量。 每个机器语言指令由一个汇编语言助记符代表还可以后跟一个或多个符号表示。程序集代码严重依赖于处理器寄存器,或者,对于托管代码,公共语言运行时注册。 可以使用反汇编窗口中的连同Register窗口中,它允许你检查寄存器内容。若要在其原始的数字格式,而不是作为程序集语言,请查看计算机代码的说明,请使用内存窗口或 select代码字节的快捷菜单中反汇编窗口。

左边距中的黄色箭头标记当前执行点。 对于本机代码中,执行点对应于 CPU 的程序计数器。 该位置显示程序中将要执行的下一条指令。

地址栏的使用

  • 1、直接输入地址

  • 2、输入函数名

在Visual Studio中使用调试器浏览代码时,有许多选项,包括设置断点、单步执行和使用Run-to-Cursor。在Visual Studio 2017中,引入了Run to Click,这是一种更容易调试代码的新方法——点击式风格。不再需要设置临时断点或多次执行步骤来执行代码并在所需的行上停止。现在,可以获得运行到光标(Ctrl+Shift+F10)的所有好处,而无需通过上下文菜单进行搜索,也无需将手从鼠标上移开以获得双手快捷组合。在VS中使用任何编程语言进行调试,同时包括C++、VB、C++和Python。

点击调试

当在调试器下的中断状态停止时,在鼠标悬停的代码行旁边会微妙地显示一个浅绿色的“RunExecutiontohere”标志符号。

 

将鼠标移到图示符并单击按钮。您的代码将运行并在下一次在您的代码路径中被命中时停止在该行上。

 

如果在调试代码中的数据提示来检查变量时,您可以自然地用一只手操作鼠标,这一点尤其有用。您可以快速运行以单击一行代码,检查该行上的变量,然后继续调试,同时将焦点和注意力放在该位置。运行以在同一方法、不同方法和循环内单击!

特别注意事项

记住,Run to Click将运行正在调试的应用程序的执行,直到到达该行代码为止。

  • 如果单击一行不会被命中的代码,则应用程序将执行finish。
  • 如果您单击一行代码以继续应用程序,等待其他用户输入,则一旦输入触发了该代码路径,您将中断执行运行的位置以单击。
  • 如果运行以单击某行,并且执行路径触发断点,则调试器将在路径中的任何断点处停止。当您点击“continue”(F5)时,执行将继续,并且您将在触发run单击的行上停止(就像您在该行上设置了断点一样)。

总结

运行到单击可以帮助您更快地找到要检查的代码,从而提高工作效率。