2023年1月

有时在调试时,您希望知道某个特定进程是否使用 /LARGEADDRESSAWARE标志链接,从而能够使用高于2Gb边界的用户模式地址。我的解决方案是:
!address -summary将显示进程的有效用户模式地址空间:

0:022> !address –summary
..
       Tot: 7fff0000 (2097088 KB) Busy: 278fd000 (648180 KB) <<< 2Gb for non-large-address-aware EXE or large address aware EXE on x86 system without /3Gb in boot.ini


或者


Tot: bd7f0000 (3104704 KB) Busy: 23dee000 (587704 KB) <<< 3Gb for large-address-aware EXE on x86 system with /3Gb in boot.ini


或则


Tot: ffff0000 (4194240 KB) Busy: 268b2000 (631496 KB) <<< 4Gb for large-address-aware EXE running with WoW64 on x64 system


但是,由于第一种情况不明确,要实际查看EXE是否与/LargeAddressware链接,请执行以下操作:


    0:000> !dlls -c inetinfo    <<< inetinfo is the module name of the EXE in this case]
    Dump dll containing 0x01000000:

    0x00081eb0: C:WINDOWSsystem32inetsrvinetinfo.exe
          Base   0x01000000  EntryPoint  0x0100326e  Size        0x00006000
          Flags  0x00004000  LoadCount   0x0000ffff  TlsIndex    0x00000000
                 LDRP_ENTRY_PROCESSED
    0:000> .shell -i - -ci "!dlls -f 0x00081eb0" FIND "characteristics"
         12F characteristics
特征字段是:0x12f&0x20==0x20。这是IMAGE_FILE_LARGE_ADDRESS_AWARE的值。–有关此定义和相关定义,请参阅平台SDK中的winnt.h。
所以这个EXE可以识别大地址。
注意,上面使用的.shell命令(用于shell到另一个EXE,在本例中是“FIND”)是我一直用来过滤调试器命令输出的内容。非常方便。

如果您使用了WinDBG源代码调试功能,您可能会发现,每当您在调试中遇到一个与尚未打开的WinDBG文件中的源代码相关的点时,都会打开另一个源代码窗口,这很烦人。这导致了大量的源窗口和一组不断重新排列的窗口。

如果您进入View->Options,并将“打开此多个后重用”选项设置为1,而不是默认值(0):

 

然后你会发现它总是重用同一个源窗口来显示当前相关的源代码。
这意味着您可以将源窗口拖到WinDBG主窗口之外的某个位置(例如,拖到另一个监视器上),每次打开一个新的源文件时,它都会在那里打开,而不会扰乱WinDBG主区域中窗口的排列。

 

人们会问“为什么本机调试器不能调试托管代码?”.
原因是CLR提供了许多在典型的本地C++应用程序中所获得的酷服务,例如:运行在虚拟机/JITEN、动态类布局、类型系统、垃圾收集、反射等等。每一个都对调试器提出了特殊的挑战。换句话说,一个完成了所有这些功能的本地应用程序根本无法与传统的本地调试器进行调试。

1) 本机调试可以在硬件级别抽象,但是托管调试需要在IL级别抽象。托管代码不能仅仅被压缩成C/C++本地调试范例。一个原因是这可能会限制CLR执行IL的选项。例如,尽管目前(从v2.0起)jit-IL,我们还是希望为诸如解释IL、推销很少使用的jitted代码、甚至重新jitting代码等事情敞开大门。如果ICorDebug对所有内容都使用本机代码偏移量,它将无法调试解释的IL。
2) 托管调试需要很多信息,直到运行时才可用。对于托管代码,编译器只生成IL,真正的调试信息直到运行时才得到解析。例如,JIT将在运行时将IL编译为本机代码,加载程序将在运行时动态确定大多数类的布局。类型系统可以在运行时创建新类型. 对于本机代码,这都是在编译时确定的。托管调试器需要某种方法在运行时获取所有这些信息。一些解决方案包括:

a、 让CLR在运行时在信息确定时创建辅助pdb。这可能是一个巨大的性能命中率,如果没有附加调试器,我们不愿意这样做。但是如果我们在没有附加调试器的情况下不这样做,那么如果以后调试器附加了调试器,它可能就不可用了。
b、 让托管调试器检查相关的CLR数据结构(直接从进程外或通过进程内运行的“helper”线程)。这里的一个重要警告是确保当CLR数据结构处于不一致状态时,调试器不会请求此类信息。CLR当前使用帮助线程。

3) 托管调试器需要与垃圾回收器(GC)协调。CLR有一个标记-清除压缩GC。这意味着GC将移动对象来整理堆碎片,并在整个过程中相应地更新所有引用(“GC根”)。这会从几个方面影响调试:

a、 调试对象在GC期间暂时处于不一致的状态。调试器必须与GC协调,以确保在此窗口期间不会检查调试对象。
b、 调试器可以让用户更改变量的值。此更新必须与GC的更新相协调。
c、 没有方便的对象标识。在本机代码中,对象的原始指针值唯一地标识该对象,因为对象不会四处移动。

Interop-Debugging,互操作调试,又称混合调试。

一般调试背景
当调试进程时,它会生成调试事件,调试器可以监听和响应这些事件。这些事件包括CreateProcess、LoadModule、异常、ExitThread、断点等。
调度调试事件时,调试对象将停止,直到调试器继续调试事件为止。当调试对象停止时,调试器可以在窗口期间检查调试对象。一旦debugger继续运行,它将自由运行,直到下一个调试事件。调试事件以每个线程为基础。
托管+本机调试都共享这个概念模型,尽管它们有不同的调试事件集,以及在停止和运行之间循环的不同方法。
调试器使用调试事件和检查API来实现所有调试操作。本文只涉及解释互操作调试如何路由托管+本机调试事件。如何在这些事件的基础上构建调试器不在本文的讨论范围之内。

本机调试:
本机调试由操作系统实现。操作系统提供用于监听和继续调试事件(WaitForDebugEvent和ContinueDebugEvent)的调试API。在调试事件中,操作系统冻结调试对象。本机调试API非常小。只有少数本机调试事件。
本机调试的关键属性:

-本机调试完全是进程外的(oop)。调试器不需要来自调试对象的额外合作(在操作系统支持之外)。
-我们将本机调试对象停止状态称为“冻结”。操作系统已停止冻结进程,并且在恢复之前不执行任何用户代码。
-本机调试事件可以在进程自由运行的任何时候出现。
-对win32调试API的所有调用都必须在同一线程上进行。我们称这个线程为W32ET,它变成了一个非常特殊的b/c,它决不能阻塞。

 

托管调试

托管调试完全是由CLR在用户模式下实现的,因此操作系统不知道何时进行纯托管调试。
CLR在为来自托管调试API的请求提供服务的每个托管进程中都有一个特殊线程(称为帮助线程)。CLR中专门用于托管调试的部分称为Left-Side(LS)。驻留在调试器进程中的ICorDebug的实现称为right-side(RS)。LS和RS通过各种用户模式进程间通信(IPC)机制进行通信,例如命名事件和共享内存块。
托管调试接口(ICorDebug)比其本机接口丰富得多。关键属性:

-托管调试是一个进程内模型。帮助线程必须存在并在调试对象的进程中运行,以便托管调试正常工作。
-我们将托管调试对象停止状态称为“已同步”。从操作系统的角度来看,同步进程是实时的,但是所有托管线程都被CLR停止。
-托管调试事件完全是在用户模式下创建和调度的。
-托管调试事件可以建立在本机调试事件之上。
-托管调试操作只能在托管停止状态下发生(这需要助手线程正在运行)

Managed vs. Native operations.

托管调试和本机调试是两个不同的世界。对于任何调试操作,查看它是在托管世界还是在本机世界中进行都非常重要。CLR调试服务只实现托管调试操作,不实现任何本机调试操作。同样,本机调试API不提供任何托管调试支持。
例如,最终用户只想到“步进”,但实际上有两个离散的操作,“托管步进”和“本机步进”,有两个完全不相交的实现。托管单步执行是通过CLR的ICorDebug API实现的;而本机单步执行是通过使用Win32调试事件的非CLR本机调试库实现的。
本机调试API级别很低,而托管调试API非常丰富。例如,本机执行控制(例如步进和断点)完全在异常之上实现,并且在本机调试API中没有显式的支持。托管调试API显式具有断点和步进器功能。
抽象级别的这种差异阻止了托管+本机调试操作之间的代码共享。

那么什么是互操作调试?
最终用户对互操作调试的看法是能够在单个调试会话中调试应用程序的托管+本机部分。这包括在托管+本机代码和运行混合调用堆栈之间单步执行的能力。托管+本机调试事件完全不相交,由调试器中的不同组件处理(我们称之为托管调试引擎和本机调试引擎)。
如果托管+本机调试器只是天真地同时附加到同一进程上,它们将相互干扰。互操作调试器确保这两个不相交的模型协同工作。
从接口的角度来看,互操作调试是将同一进程上的托管调试接口和本机调试接口公开给单个调试器。这意味着调试器的本机调试引擎和托管调试引擎可以同时运行,只需稍加修改。从理论上讲,这意味着可以轻松地扩展一个只进行托管调试和只进行本机调试的调试器来执行互操作调试。
理想情况下,本机+管理的调试引擎将彼此充分合作,以向最终用户呈现统一的模型。实际上,调试引擎之间的这种通信可能需要对调试器的设计进行重大的更改和规划。

 

对于纯托管和纯本机调试,只能将1个调试器附加到进程。
为什么?
本机调试器从托管调试器下面窃取调试事件。这会混淆托管调试器并导致其崩溃。本机调试器无法与托管调试器协调。

执行此操作时遇到的问题:
在windows中,操作系统强制一次只能将一个本机调试器附加到调试对象(已调试进程的DebugActiveProcess将失败)。仅托管调试服务具有类似的检查,以强制一次只能将一个托管调试器附加到调试对象。仅托管调试实际上与纯本机调试是分开的。(这与使用辅助线程有关)。如果一个进程只被管理调试,操作系统不会将其视为只进行本机调试。
如果允许同时将1个托管调试器和1个本机调试器附加到应用程序。这样就可以绕过检查。更糟糕的是,我们无法完全检测到这种情况。本机调试api对托管调试一无所知,因此不能强制执行。托管调试api的进程内部分可以调用IsDebuggerPresent来检查是否附加了本机调试器,但是本机调试器可以在这些调用之间附加。即使在检测到这种情况时,托管调试器也没有一种好的响应方式。

为什么有人想这么做?
实际上有一些场景可以激励将多个调试器附加到一个应用程序上。

1) 获得更多的功能-仅本机调试器只能调试应用程序的本机部分。同样,仅托管调试器只能调试应用程序的仅托管部分。那么,如果你想调试一个同时使用托管代码和本机代码的应用程序呢?一种解决方案是附加两个调试器并同时使用它们。(我认为这是您在其他类似场景中必须做的;例如调试VB6调用本机COM对象)。我们开发了互操作调试(也称为“混合模式”)来显式地启用这个场景。它允许单个调试器调试应用程序的托管部分和本机部分。VisualStudio支持混合模式调试。
2)

正在调试服务器。一些服务器应用程序(如ASP.Net+SQL)可以代表多个用户运行代码。理想情况下,每个用户只需同时附加一个调试器并调试服务器上运行的部分代码。我们希望在未来的CLR版本中通过每appdomain调试(而不是仅仅是每个进程的调试)来支持这些场景。