【问题标题】:How does the debugger get type information about an object initialized to null?调试器如何获取初始化为 null 的对象的类型信息?
【发布时间】:2012-01-13 01:36:41
【问题描述】:

如果一个对象被初始化为null,它就是not possible to get the type information,因为引用没有指向任何东西。

但是,当我调试并将鼠标悬停在变量上时,它会显示类型信息。只显示了静态方法,但它似乎仍然知道类型。即使在发布版本中。

调试器是否使用其他信息而不是某种类型的反射来找出数据类型?它怎么比我知道的多?如果它知道这一点,为什么它不能showing the datatype in a NullReferenceException

【问题讨论】:

    标签: c# .net debugging


    【解决方案1】:

    您似乎将引用的类型与其指向的值的类型混淆了。引用的类型嵌入到 DLL 元数据中,调试器可以轻松访问。关联的 PDB 中还存储了其他信息,调试器利用这些信息来提供更好的体验。因此,即使对于空引用,调试器也可以确定类型和名称等信息。

    至于NullReferenceException。它是否还可以告诉您它正在查询字段/方法的类型......可能。我不熟悉 CLR 这一部分的内部结构,但似乎没有内在的原因导致它不能这样做。

    但我不确定 CLR 的额外成本是否值得。我对空引用异常缺乏信息感到沮丧。但除了涉及的类型之外,我还想要名字!我不在乎它是IComparable,我想知道它是leftCustomer

    名称是 CLR 并不总是可以访问的东西,因为其中很大一部分存在于 PDB 中,而不是元数据中。因此它不能为他们提供极大的可靠性(或速度)

    【讨论】:

    • 啊,我当然忘记了 PDB。但是,对于局部变量,它如何存储在 DLL 元数据中?还是您的意思是 ILASM?
    • @Abel 对于本地名称存储在 PDB 中,类型在元数据中(IIRC 所有常量都存储在 PDB 中)。如果您对分解编译一些代码感到好奇,请删除 PDB 并在反射器中调出反汇编。这很好地表明了 CLR 可用的内容。
    • 我实际上经常使用 Reflector,确实,类型信息在那里,但这是 ILASM 反编译的,其中包含类型(但既然你说“元数据”,我想是时候研究 CLI Annotated标准更彻底;)。
    • @Abel hmm 我的错误陈述。元数据可能是错误的词。虽然本地变量的类型嵌入在方法体的 IL 中(否则 CLR 将无法验证它们的用法)。注意:refelctor 读取 PDB 信息,因此它的默认呈现比 CLR 更接近调试器
    • 所以,如果我自己尝试这个,我将不得不使用Assembly.GetExecutingAssembly 之类的东西,获取 IL 等并获取必要的信息。可能是一个无用的练习,但我只是想知道。
    【解决方案2】:

    Jared 的回答当然是正确的。只是添加一点:

    当我调试并将鼠标悬停在变量上时,它会显示类型信息

    没错。你有一个碗。碗标有“水果”。碗是空的。碗里的水果是什么类型的?你不能说,因为碗里没有水果。但这并不意味着你对一无所知。你知道碗里可以装任何水果。

    当您将鼠标悬停在一个变量上时,调试器可以告诉您变量本身或其内容。

    调试器是否使用其他信息而不是某种类型的反射来找出数据类型?

    当然。调试器不仅需要知道此引用所引用的事物的类型是什么,还需要知道对可以存储在此变量中的内容施加了哪些限制。运行时知道有关对特定存储位置施加什么限制的所有信息,并且运行时可以将这些信息告知调试器。

    它怎么比我知道的多?

    我拒绝问题的前提。调试器代表您运行;它不能做任何你自己做不到的事情。如果你不知道某个特定变量的类型限制是什么,那并不是因为你缺乏找出来的能力。你只是还没看过。

    如果它知道这一点,为什么它不能在 NullReferenceException 中显示数据类型?

    想想当你取消引用 null 时实际发生了什么。例如,假设您这样做:

    Fruit f = null;
    string s = f.ToString();
    

    ToString 可能在 Fruit 中被重载。抖动必须生成什么代码?假设局部变量 f 存储在堆栈位置。抖动说:

    • 将与 f 关联的堆栈指针偏移处的内存地址的内容复制到寄存器 1
    • 虚函数表将是,比方说,该指针顶部的八个字节,ToString 将是,比方说,该表顶部的四个字节。 (我只是在编造这些数字;我不知道真正的偏移量是多少。)所以,首先在寄存器 1 的当前内容中添加 8。
    • 现在取消引用寄存器 1 的当前内容以将 vtable 的地址放入寄存器 2 中
    • 现在向寄存器 2 添加四个字节
    • 现在我们有了一个指向 ToString 方法的指针...

    但是等一下,让我们再次遵循这个逻辑。第一步将零放入寄存器 1,因为 f 包含空值。第二步增加了八。第三步解引用指针 0x00000008,虚拟内存系统发出异常,指出刚刚触及了非法内存页面。 CLR 处理异常,确定异常发生在前 64 K 内存上,并猜测有人刚刚取消引用空指针。因此它会创建一个空引用异常并抛出它。

    虚拟内存系统肯定不知道它取消引用指针 0x00000008 的原因是因为有人试图调用 f.ToString()。该信息在过去丢失了;内存管理器的工作是在你触摸了你无权触摸的东西时告诉你; 为什么你试图触摸你不拥有的记忆不是它的工作。

    CLR 可以维护一个单独的辅助数据结构,这样每次您访问内存时,它都会记录您尝试这样做的原因。这样,异常可以包含更多信息,描述异常发生时您正在做什么。想象一下为每次对内存的访问维护这种数据结构的成本!托管代码很容易比现在慢十倍,而正确代码和损坏代码所承担的成本一样高。为了什么?告诉您您可以轻松弄清楚的内容:您取消引用了哪个包含 null 的变量。

    该功能不值得,因此 CLR 不这样做。没有技术原因不能这样做。只是不实用。

    【讨论】:

    • 您好 Eric,感谢您抽出宝贵时间做出如此彻底的回应。 vtable 的故事总是让我想起 COM,它在很大程度上以相同的方式工作(就像 C++ 一样)。我认为 NRE on 位于 0x0 地址,而不是附近。但是如果我遵循你的逻辑,你是说如果我有 16K 方法,并且我访问最后一个方法,我会遇到未定义的行为(因为试图取消引用高于 64K 边界的东西)? // 我知道你在 CLR 上工作过,但可以肯定的是,这是一个设计选择,但如果你选择不同,你可能会想出一个有效的算法;)。
    • @Abel:在我给出的示例中,崩溃是在尝试找到指向 vtable 的指针时发生的。该 vtable 中是否有 16K 条目无关紧要;如果我们在找到 vtable 之前就崩溃了,我们就崩溃了。一个有趣的问题是当不涉及 vtable 时会发生什么?如果一个对象的字段值超过 64K,该怎么办?
    • @Abel:但是,空引用异常是虚拟地址空间底部 64 K 中的任何异常。大多数 null ref 异常都涉及在取消引用之前对 null 进行算术运算,因此任何小指针取消引用都可能是由 null 指针取消引用引起的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-23
    • 1970-01-01
    • 2011-07-26
    • 1970-01-01
    • 1970-01-01
    • 2018-08-15
    相关资源
    最近更新 更多