一:什么是时间旅行

简而言之就是把程序的执行流拍成vlog,这样就可以对 vlog 快进或者倒退,还可以分享给别人做进一步的分析,是不是想都不敢想。

很开心的是 windbg preview 版本中已经实现了,叫做 时间旅行调试 TTD,相比传统的 静态分析 不知道好多少倍。

为了能提起大家兴趣,我就举二个例子吧。

二:二个有趣的例子

1. 查看程序都触发了第几代垃圾回收

为了方便说明,我就用诱导GC手工触发,然后再观察都触发了哪一代的 GC ,参考代码如下:


static void Main(string[] args)
{
List<string> list = new List<string>(); //1. 第一次触发GC
GC.Collect(); Console.WriteLine("触发full gc"); //2. 第二次触发GC
GC.Collect(0); Console.WriteLine("触发 0 代 gc"); //3.第二次触发GC
GC.Collect(1); Console.WriteLine("触发 1 代 gc");
}

接下来用 windbg 的 launch executeable (advanced) 来附加程序,勾选 Record,然后在弹框中将 vlog 保存到指定目录,最后点击 Record 就可以啦!

运行完后,windbg 会自动加载我的 D:\test\ConsoleApp104.run 的 vlog 文件,因为 gc 触发的底层函数是coreclr!WKS::GCHeap::GarbageCollectGeneration ,所以我们用 bp 给它下一个断点,运行多次 g 命令。


0:000> bp coreclr!WKS::GCHeap::GarbageCollectGeneration
Bp expression 'coreclr!WKS::GCHeap::GarbageCollectGeneration' could not be resolved, adding deferred bp
0:000> g
Time Travel Position: 3079F:63E
eax=00000001 ebx=00000002 ecx=00000002 edx=00000008 esi=00000002 edi=00000002
eip=02fc4256 esp=0057f204 ebp=0057f214 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
coreclr!WKS::GCHeap::GarbageCollectGeneration:
02fc4256 55 push ebp
0:000> g
Time Travel Position: 34661:AF
eax=00000001 ebx=00000002 ecx=00000000 edx=00000008 esi=00000000 edi=00000002
eip=02fc4256 esp=0057f1f8 ebp=0057f208 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
coreclr!WKS::GCHeap::GarbageCollectGeneration:
02fc4256 55 push ebp
0:000> g
Breakpoint 0 hit
Time Travel Position: 346A5:2CD
eax=00000001 ebx=00000002 ecx=00000001 edx=00000008 esi=00000001 edi=00000002
eip=02fc4256 esp=0057f1f8 ebp=0057f208 iopl=0 nv up ei pl nz na po nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
coreclr!WKS::GCHeap::GarbageCollectGeneration:
02fc4256 55 push ebp

从输出中可以很清楚的看到,命中了三次 GarbageCollectGeneration 回收,而且从上面的 ecx 寄存器看依次是 2,0,1,对应着 gc 的2代回收,0代回收, 1代回收, 这比只有一个静态的 dump 是不是有优势的多,要知道我这里的 ConsoleApp101.run 文件是可以分发给别人分析的哦。

2. 查看新生成的线程曾今都执行了什么代码

这个例子源自朋友遇到的一个问题,他的程序跑着跑着,发现 ThreadPool 中有400多待命的工作线程,线程栈大概如下:


0:011> k
# ChildEBP RetAddr
00 0564fc6c 7531f0ca ntdll!NtRemoveIoCompletion+0xc
01 0564fc6c 78480b69 KERNELBASE!GetQueuedCompletionStatus+0x2a
02 0564fcb8 7847d92b coreclr!CLRLifoSemaphore::WaitForSignal+0x29 [d:\a\_work\1\s\src\vm\synch.cpp @ 654]
03 0564fd08 7847cf04 coreclr!CLRLifoSemaphore::Wait+0x13b [d:\a\_work\1\s\src\vm\synch.cpp @ 897]
04 0564fdd4 783f2910 coreclr!ThreadpoolMgr::WorkerThreadStart+0x234 [d:\a\_work\1\s\src\vm\win32threadpool.cpp @ 2121]
05 0564ff70 7703fa29 coreclr!Thread::intermediateThreadProc+0x50 [d:\a\_work\1\s\src\vm\threads.cpp @ 2110]
06 0564ff80 772175f4 KERNEL32!BaseThreadInitThunk+0x19
07 0564ffdc 772175c4 ntdll!__RtlUserThreadStart+0x2f
08 0564ffec 00000000 ntdll!_RtlUserThreadStart+0x1b

因为给我的是 静态dump,所以我无法寻找 11号线程 曾今执行了什么托管代码,因为时间不能倒流,但现在有了 TTD,真的可以让时间倒流啦。。。太有意思了,哈哈,既然能倒流,那就一定有办法找到破绽。

为了方便讲解,写一个简单例子。


static void Main(string[] args)
{
Task.Factory.StartNew(() =>
{
Console.WriteLine("我是 task 线程");
}); Console.ReadLine();
}

接下来我们一起探究下最后生成的 Thread WorkThread 曾今都执行了什么? 深挖思路大概是这样的。

先将进度条拉到底,然后用 !bpmd System_Private_CoreLib System.Threading.Tasks.Task.InnerInvoke 下一个断点,最后将时间倒流,看谁命中了这个 task。


0:000> g
TTD: End of trace reached.
(4f20.4d0c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 36F51:0
eax=00070053 ebx=00000000 ecx=8a60f857 edx=77237170 esi=7845e6c0 edi=00000000
eip=771a7000 esp=0602fe90 ebp=0602ff70 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
771a7000 ea09701a773300 jmp 0033:771A7009 0:009> !bpmd System_Private_CoreLib System.Threading.Tasks.Task.InnerInvoke
MethodDesc = 06A29704
Setting breakpoint: bp 05A915C7 [System.Threading.Tasks.Task.InnerInvoke()]
Adding pending breakpoints... 0:009> g-
Breakpoint 1 hit
Time Travel Position: 32DF4:AC
eax=05a915c0 ebx=00000000 ecx=0349a864 edx=0349a864 esi=0349a864 edi=0349a7c8
eip=05a915c7 esp=066afa14 ebp=066afa1c iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
System_Private_CoreLib!System.Threading.Tasks.Task.InnerInvoke()$##6002185+0x7:
05a915c7 8b7e04 mov edi,dword ptr [esi+4] ds:002b:0349a868=0349a800 0:008> !clrstack
OS Thread Id: 0x44a8 (8)
Child SP IP Call Site
066AFA14 05a915c7 System.Threading.Tasks.Task.InnerInvoke() [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2437]
066AFA24 05a915bb System.Threading.Tasks.Task+c.<.cctor>b__274_0(System.Object) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2427]
066AFA2C 05a91567 System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(System.Threading.Thread, System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object) [/_/src/System.Private.CoreLib/shared/System/Threading/ExecutionContext.cs @ 289]
066AFA5C 05a912d1 System.Threading.Tasks.Task.ExecuteWithThreadLocal(System.Threading.Tasks.Task ByRef, System.Threading.Thread) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2389]
066AFABC 05a911d7
066AFACC 05a9118b System.Threading.Tasks.Task.ExecuteFromThreadPool(System.Threading.Thread) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2312]
066AFAD0 05a90e58 System.Threading.ThreadPoolWorkQueue.Dispatch() [/_/src/System.Private.CoreLib/shared/System/Threading/ThreadPool.cs @ 663]
066AFB1C 05a90c6f
066AFD10 784fa0ef [DebuggerU2MCatchHandlerFrame: 066afd10]

熟悉 Task 的朋友应该知道: System.Threading.Tasks.Task.InnerInvoke 的下一步就是执行我的回调函数,而此时 回调函数 还没有被 JIT 编译,这时候我们可以在 bp clrjit!CILJit::compileMethod 中去拦截 JIT 对此方法的编译,然后从 compileMethod 中提取 mt


0:008> bp clrjit!CILJit::compileMethod
0:008> g
Breakpoint 1 hit
Time Travel Position: 32E36:C18
eax=7933ad50 ebx=066af5c8 ecx=792c8770 edx=066af5c8 esi=7932d164 edi=00cbbf90
eip=792c8770 esp=066af3ec ebp=066af44c iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
clrjit!CILJit::compileMethod:
792c8770 55 push ebp 0:008> kb
# ChildEBP RetAddr Args to Child
00 066af44c 78428db2 7933ad50 066af5c8 066af4f0 clrjit!CILJit::compileMethod [d:\a\_work\1\s\src\jit\ee_il_dll.cpp @ 294]
... 0:008> dp 066af4f0 L1
066af4f0 06a2ae04 0:008> !dumpmd 06a2ae04
Method Name: ConsoleApp1.dll!Unknown
Class: 032fa0dc
MethodTable: 06a2ae14
mdToken: 06000005
Module: 02c5d7d0
IsJitted: no
Current CodeAddr: ffffffff
Version History:
ILCodeVersion: 00000000
ReJIT ID: 0
IL Addr: 00000000
CodeAddr: 00000000 (MinOptJitted)
NativeCodeVersion: 00000000

很奇怪的是提取的 md 目前还不能显示完全名字,不过没关系,我们继续 g ,然后再重复执行一下命令。


0:009> g
TTD: End of trace reached.
(4f20.4d0c): Break instruction exception - code 80000003 (first/second chance not available)
Time Travel Position: 36F51:0
eax=00070053 ebx=00000000 ecx=8a60f857 edx=77237170 esi=7845e6c0 edi=00000000
eip=771a7000 esp=0602fe90 ebp=0602ff70 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
771a7000 ea09701a773300 jmp 0033:771A7009
0:009> !dumpmd 06a2ae04
Method Name: ConsoleApp1.Program+<>c.<Main>b__0_0()
Class: 032fa0dc
MethodTable: 06a2ae14
mdToken: 06000005
Module: 02c5d7d0
IsJitted: yes
Current CodeAddr: 06133300
Version History:
ILCodeVersion: 00000000
ReJIT ID: 0
IL Addr: 00000000
CodeAddr: 06133300 (MinOptJitted)
NativeCodeVersion: 00000000

当时间线结束的时候,我们终于看到了,原来 Task 执行的是 ConsoleApp1.Program+<>c.<Main>b__0_0() 方法,那这个方法逻辑是什么呢? 可以用 ILSpy 查看。

总的来说,要复现还是挺考验基本功的。

windbg的时间旅行实现对 C# 程序的终极调试的更多相关文章

  1. 在应用程序中实现对NandFlash的操作

    以TC58NVG2S3ETA00 为例: 下面是它的一些物理参数: 图一 图二 图三 图四 图五 图6-0 图6-1 说明一下,在图6-1中中间的那个布局表可以看做是实际的NandFlash一页数据的 ...

  2. 用 Python 脚本实现对 Linux 服务器的监控

    目前 Linux 下有一些使用 Python 语言编写的 Linux 系统监控工具 比如 inotify-sync(文件系统安全监控软件).glances(资源监控工具)在实际工作中,Linux 系统 ...

  3. 51单片机实现对24C02进行页写、顺序读取并显示验证

    //************************************************************************************* //**程序名称:51单 ...

  4. Android平台中实现对XML的三种解析方式

    本文介绍在Android平台中实现对XML的三种解析方式. XML在各种开发中都广泛应用,Android也不例外.作为承载数据的一个重要角色,如何读写XML成为Android开发中一项重要的技能. 在 ...

  5. 使用JWT来实现对API的授权访问

    目录 什么是JWT JWT的结构 Header Payload Signature 解码后的JWT JWT是怎样工作的 在JAVA里使用JWT 引入依赖 JWT Service 生成JWT 解码JWT ...

  6. 使用morphia实现对mongodb的聚合查询

    morphia是谷歌的一个针对mongodb的数据化持久框架: 关于mongodb的介绍不在这里展示,直接进入主题:采用morphia实现对mongodb的聚合查询 这里获取所有学生的分数总和 spr ...

  7. C# - VS2019 通过DataGridView实现对Oracle数据表的增删改查

    前言 通过VS2019建立WinFrm应用程序,搭建桌面程序后,通过封装数据库操作OracleHelper类和业务逻辑操作OracleSQL类,进而通过DataGridView实现对Oracle数据表 ...

  8. 在VS2015中用C++创建DLL并用C#调用且同时实现对DLL的调试

    from:http://m.blog.csdn.net/article/details?id=51075023 在VS2015中先创建C#项目,然后再创建要编写的动态库DLL项目,这样做的好处是整个解 ...

  9. JDOM方法实现对XML文件的解析

    首先要下载JDOM.jar包,下载地址:http://download.csdn.net/detail/ww6055/8880371 下载到JDOM.jar包之后导入到工程中去. 实例程序: book ...

随机推荐

  1. Java 中,throw 和 throws 有什么区别?

    throw 用于抛出 java.lang.Throwable 类的一个实例化对象,意思是说你可以通 过关键字 throw 抛出一个 Error 或者 一个 Exception,如:throw new ...

  2. idea常见设置一

    1.顶部执行栏调整 原: 设置:勾选view==>toolbar 后: 这个一看其实没有什么太大的作用,但是你看这个按钮 这个按钮就是File==>Settings,这样会稍微方便点,只要 ...

  3. vulnhub靶机djinn:1渗透笔记

    djinn:1渗透笔记 靶机下载地址:https://www.vulnhub.com/entry/djinn-1,397/ 信息收集 首先我们嘚确保一点,kali机和靶机处于同一网段,查看kali i ...

  4. Rust 中的数据布局-repr

    repr(Rust) 首先,所有类型都有一个以字节为单位的对齐方式,一个类型的对齐方式指定了哪些地址可以用来存储该值.一个具有对齐方式n的值只能存储在n的倍数的地址上.所以对齐方式 2 意味着你必须存 ...

  5. 8.5_线性控制器设计_轨迹跟踪(Follow a Desired Path)

    平衡点时所有的导数都为0 simulink中建立状态方程模型

  6. html5中常被忘记的标签,属性

    placeholder placeholder是input中的属性,就是默认输入的text,当用户输入时,text会被清空. 用法 <input type ="text" p ...

  7. java的命令行参数到底怎么用,请给截图和实际的例子

    8.2 命令行参数示例(实验) public class Test {    public static void main(String[] args){        if(args.length ...

  8. java基础-File

    File类 * File更应该叫做一个路径, 文件路径或者文件夹路径    * 路径分为绝对路径和相对路径  * 绝对路径是一个固定的路径,从盘符开始  * 相对路径相对于某个位置,在eclipse下 ...

  9. Blazor组件自做五 : 使用JS隔离封装Google地图

    Blazor组件自做五: 使用JS隔离封装Google地图 运行截图 演示地址 正式开始 1. 谷歌地图API 谷歌开发文档 开始学习 Maps JavaScript API 的最简单方法是查看一个简 ...

  10. Java习题

    public class ClassTest{ String str = new String("hello"); char[] ch = {'a','b','c'}; publi ...