lldb 调试 linux下 .net Core 总结及开源扩展 yinuo

相信很多朋友在跟随微软.net core 从windows平台迁移至linux平台的过程中遇到很多别扭的地方,
这里我只聊聊 运行时 调试的那些事儿。

  • 首先从工具上来讲Windows上的windbg肯定是运行时的首选调试工具(因为有对应版本的SOS.dll),在linux平台运行时调试需要切换到lldb (Only lldb is supported by the SOS plugin. gdb can be used to debug the coreclr code but with no SOS support.)
    调试器的原理和功能基本一样,但细节到某个功能的命令自然会有区别,尤其是熟练了其中1个的命令之后(比如之前在看汇编的时候是Intel格式,现在要适用AT&T格式)…

这里先总结一些个人常用的命令在 windbg下和lldb下的对比:

  • 非托管命令:
非托管命令 lldb windbg
列出当前模块 image list lmf
当前线程 thread list ~
当前线程栈回溯 thread backtrace kp
所有线程栈回溯 thread backtrace all ~* kp
切换线程 thread select 2 ~2s kp
查看寄存器 re r r
查看内存(8字节) memory read –size 8 –format x <address> dq <address>

LLDB同GDB的命令对比:https://lldb.llvm.org/lldb-gdb.html

  • 托管命令:

这里先介绍下自己写的开源lldb调试.net Core扩展模块 Yinuo
在使用lldb调试linux .net Core程序的过程中,有很多不适应的地方,比如遍历并查看所有线程的托管栈回溯 在windbg下可以~*e !clrstack 在lldb里虽然有bt allclrstack 但是却只能手动切换单个线程再回溯,没有办法结合到一起,还有一个原因是lldb内命令输出的内容颜色统一,不太好区分重点关注的点,比如线程回溯比较关注方法名,托管对象转储比较关注内部对象地址等等,lldb的好处是支持python或者c++接口,可以通过接口方式写lldb的扩展来辅助我们调试过程,提高调试效率。
下面介绍下调试扩展 Yinuo 的加载过程:

  • 以下的软件环境 CentOS7(x64),lldb-3.6.0,python-2.7.5

  • 首先git下载模块 git clone https://github.com/espider/yinuo
    目录没有要求,但要记得,因为加载模块的时候要知道在哪。

  • 启动lldb 并附加被调试的进程 (lldb) attach -p PID

  • 加载Yinuo调试模块 (lldb) command script import ~/yinuo/ynlldb.py 之前git下来的目录里的python文件

  • 成功加载ynlldb.py后可以 help 看下当前注册进来的命令,都以 yn_ 为前缀,加载模块的时候会判断当前的.net Core版本号,并自动加载对应版本的调试插件libsosplugin.so

接下来介绍下当前注册进lldb的辅助调试命令

  • yn_heap_dump
    查看当前托管堆信息命令,以色块和色块的比例直观的感受 Gen 0,1,2 LOH 在同一个堆内的比例 以及其实际大小(这里的比例按托管堆的地址空间计算,并没有排除Free的和Gen0没有使用的地址空间,由于比例可能相去甚远所以有可能看不到某个堆的色块)

  • yn_object_dump
    转储托管对象,可以根据类型的方法表、类型名、对象地址 进行批量或者单个转储,同时计算对象(按方法表或类型的话针对每1个单独对象)所属托管堆的位置Gen 0,1,2,LOH,并统计4类堆内的数量。
    支持选项和参数
    • –methodtable/-m 转储此方法表的所有对象,后跟方法表地址;
    • –type/-t 转储此类型名的所有对象,后跟类型名(同方法表选项互斥 2选1);
    • –offset/-o 转储对象的同时是否深入转储其内部偏移对象,后跟该对象的偏移量(目前只支持1级内部偏移)
    • –address/-a 转储单个对象,后跟对象地址
    • –dumpobj/-d 是否转储对象,默认为True,如果不转储则只返回对象地址;
    • –gen/-g 是否显示对象所在的堆Gen0,1,2,LOH,默认True;

例如想要查看 类型Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Frame`1[[Microsoft.AspNetCore.Hosting.Internal.HostingApplication+Context, Microsoft.AspNetCore.Hosting]] 方法表地址 00007ff8711df620 可以这样写转储命令:

这里会对输出内容做下颜色处理,比如我们比较关心其内部成员的Value这列,如果是地址的话会显示成黄色,方便调试时候的快速定位。

如果想要转储某个对象可以这样写,根据对象地址:

发现对象内的某个成员比较感兴趣,例如 刚刚的对象内偏移 0xe0 位置是 RawTarget

想对其进行偏移转储 可以这样写:

  • yn_thread_clrstack
    显示某个线程或者所有线程的托管栈回溯,不指定选项 –thread/-t 的话默认显示当前线程的托管栈回溯, -t 跟线程index可以回溯指定线程,或者 跟 all,来批量显示所有线程的托管栈回溯,参数 -a 可选(此为SOS命令clrstack可选参数)

这里也对输出的内容做了颜色处理,比如IP指令指针列和CallSite是我们比较关注的,这里分别用黄色和绿色标注。

  • yn_thread_pe
    显示某个线程或者所有线程托管异常,选项同 yn_thread_clrstack 一样

  • yn_transfer
    此命令只用于转移执行其他lldb命令,因为yinuo项目调试的时候会在当前目录生成一个log文件(ynlldb.log)会把所有执行的yinuo命令及输出写入日志便于以后的查询,使用例如:yn_transfer dumpheap -stat 会执行 dumpheap -stat 并把结果输出到终端和日志文件里。
  • Yinuo 项目 License 采用 BSD,大家有兴趣可以自己调整或者联系我共同维护,实现自己的调试命令比较简单,git项目内的 commandlist 目录,所有自动注册的调试命令都在这里以,自命名.py文件即可,内容例子及说明如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#!/usr/bin/python
# coding:utf-8
import lldb
import commandlist.ynbase as yn
from util.colorstyle import *
from util.exportcontent import *
"""这里把自定义类型注册进来等待加载的时候自动注册"""
def register_lldb_commands():
return [
YNTransfer()
]
"""自定义类型继承自yn.YNCommand即可"""
class YNTransfer(yn.YNCommand):
""" transfer lldb command and exe it for log """
def __init__(self):
pass
"""这个是注册到lldb里的命令名字"""
def name(self):
# register function name in lldb
return 'yn_transfer'
"""如果有选项的话在这里定义,基于python argparse模块实现的 """
def options(self):
return [
]
"""描述信息"""
def description(self):
return 'Transfer lldb command for log,e.g. arguments: dumpheap -stat'
"""这里是实际命令的具体实现了"""
def run(self, options, arguments):
target = lldb.debugger.GetSelectedTarget()
if target:
if arguments:
YNTransfer.handle_command(arguments)
else:
export_content(' no arguments in yn_transfer.')
else:
export_content(' no target in current debugger.')
@staticmethod
def handle_command(args):
(ci, result) = yn.run_log_command(
" " + (" ".join(args) if len(args) > 0 else ''))
success = result.Succeeded()
if success:
output = result.GetOutput()
contents = output.strip()
lines = contents.splitlines(False)
for i in range(len(lines)):
export_content(' %s' % lines[i])
else:
export_content(
' error="%s"' %
use_style_level(
important_level['high2'],
result.GetError()))
export_content(
' %s ' %
use_style_level(
important_level['low2'],
'-------------'))

参考文档:

https://github.com/dotnet/coreclr/blob/master/Documentation/building/debugging-instructions.md
https://lldb.llvm.org/python-reference.html
https://github.com/llvm-mirror/lldb
https://github.com/facebook/chisel