Home po、p、v 命令;LLDB 的自定义 Data Formatter;在 LLDB 中使用 Python 脚本
Post
Cancel

po、p、v 命令;LLDB 的自定义 Data Formatter;在 LLDB 中使用 Python 脚本

目录

前言

WWDC 2019 / 429 - LLDB: Beyond “po” 介绍了 Xcode 11LLDB 的常用功能及其原理,演讲者来自 Debugging Technologies Team ,内容包含:

  1. LLDB 中的 popv 命令;
  2. LLDB 中自定义 Data Formatter
  3. Python 脚本在 LLDB 中的使用。

本文做一个摘要和总结。

示例 project:https://github.com/Bob-Playground/LLDB-Demo

LLDB 常用命令 po、p、v

LLDB 常用命令一:po

示例代码:

1
2
3
4
5
6
7
8
struct Trip {
    var name: String
    var destinations: [String]
}

let cruise = Trip(
    name: "Mediterranean Cruise",
    destinations: ["Sorrento", "Capri", "Taormina"])

po 的常见用法

po 常见用法是打印变量:

(lldb) po cruise
▿ Trip
  - name : "Mediterranean Cruise"
  ▿ destinations : 3 elements
    - 0 : "Sorrento"
    - 1 : "Capri"
    - 2 : "Taormina"

po 也可以调用对象的方法:

(lldb) po cruise.name.uppercased()
"MEDITERRANEAN CRUISE"
(lldb) po cruise.destinations.sorted()
▿ 3 elements
  - 0 : "Capri"
  - 1 : "Sorrento"
  - 2 : "Taormina"

po 还可以计算表达式,等等。

po 是 expression 命令的 alias

po 不是 LLDB 中的 first-class 命令。

po 实际上是 expression 命令的一个 aliaspo cruise 等效于:

expression -O -- cruise

参数说明:-O--object-description 的简写。详细的参数说明可在 LLDB 内通过 help expression 查看(在 Xcode ConsoleSystem Console 中都可以),如:

help expression

创建自定义的 alias

我们也可以创建自己的 alias

command alias my_po expression -O --

使用自己创建的 my_po

my_po cruise

po 的原理

po 的执行流程如下,假如用户输入了 po view

此例中,LLDB 为 po 生成两次代码并编译、执行,最后展示相应的 description。

如果想自定义 description,可以添加一个遵守 CustomDebugStringConvertible 协议的 extension

1
2
3
extension Trip: CustomDebugStringConvertible {
    var debugDescription: String { "Trip description" }
}

更多信息请查看 CustomDebugStringConvertible 协议的相关文档。

对于 Objective-C,则可覆盖 debugDescription 方法或 description 属性来实现自定义输出。

LLDB 常用命令二:p

po 和类似的命令是 p

(lldb) p cruise
(Travel.Trip) $R0 = {
  name = "Mediterranean Cruise"
  destinations = 3 values {
    [0] = "Sorrento"
    [1] = "Capri"
    [2] = "Taormina"
  }
}

po 输出不同的是,p 的输出中多了一个 $R0。像这种$R+数字的组合,实际上是 LLDB 生成的变量,可以在后续的调试中使用,如:

(lldb) po $R0.name
"Mediterranean Cruise"

p 是 expression 命令的 alias

p 不是 LLDB 中的 first-class 命令。

实际上,p 也是 expression 命令的 aliasp cruise 等效于:

expression cruise

p 的原理

p 的前半部分过程与 po 一样,生成获取对象的代码并获取对象。不一样的地方是,p 拿到 result 后,会对 result动态类型解析(Dynamic type resolution)。我们来看一个例子,把之前的代码稍作改动:

1
2
3
4
5
6
7
8
9
10
protocol Activity {}

struct Trip: Activity {
    var name: String
    var destinations: [String]
}

let cruise: Activity = Trip(
    name: "Mediterranean Cruise",
    destinations: ["Sorrento", "Capri", "Taormina"])

我们添加了一个 Activity 协议,并让 Trip 遵守此协议。创建的 cruise 变量声明为遵守 Activity 协议的类型。

再输入 p cruisep cruise.name

可以看到,p cruise 的输出和之前一样,打印出了 cruise 的真实类型 Trip。这是因为 LLDB 在拿到 result 后对其做了动态类型解析

要注意的是,p 只对 result动态类型解析,所以上面的 p cruise.name 会报错。分析:在 LLDB 生成代码时,只能从源码知道 cruise 是遵守 Activity 的协议的类型,而 Activity 协议中并没有 name 成员,所以 LLDB 生成的代码编译时就报错了,无法走到动态类型解析这一步。

想要通过 p 打印 name,就必须先将 cruise 强转为 Trip 类型:p (cruise as! Trip).name

在流程的最后,result 会传递给 Formatter Subsystem 处理,以输出可读性更强的内容。如果想看原始内容,可在 expression 命令后使用 --raw 参数:

expression --raw -- cruise

LLDB 常用命令三:v

在上面的例子中,我们需要强制转换 cruise 的类型,才能打印 name。其实 LLDB 的 v 命令,可以更便捷地完成这项任务:

从输出我们可以看到,v cruise 的输出和 p cruise 类似。但是,p cruise.name 报错了,而 v cruise.name 能正常打印 name。

v 是 frame variable 命令的 alias

实际上,v 是 Xcode 10.2 引入的 aliasv cruise 等效于:

frame variable cruise

v 的原理

从上图可以看出,v 并不生成代码来编译和执行,而是先直接从内存中读取值,再进行 动态类型解析。如果有 subfields,则循环这两步,直到拿到最终的值。

p 相同的是,result 会传递给 Formatter Subsystem 处理,以输出可读性更强的内容。

由于不需要编译和执行代码,v 的速度也比 pop 快很多。但是,这也决定了 v 只能读取值,而无法调用方法或计算表达式

po,p,v 的使用场景

小结:

  • pop 可使用语言的全部特性。LLDB 根据用户的输入,生成代码编译运行。可以进行调用方法、计算表达式等操作。
  • po 可以获得对象的 descriptionpv 能使用 Data Formatter Subsystem 处理 result
  • p 可以对 result动态类型解析
  • v 直接从内存读取变量,速度快,并且可以对读取的值递归地动态类型解析,但不能用于调用方法、计算表达式等。

自定义 Data Formatter

LLDB 有一个 Data Formatter Subsystem,允许开发者为他们的变量自定义显示选项。1

Filters

如果一个类型的成员变量很多,而我们只想看其中某个变量的值,则可为这个类型添加一个 Filter

如果我们只想看 Tripname 属性,添加 Filter

type filter add Travel.Trip --child name

再打印,就只显示 name 属性了:

(lldb) v cruise
(Travel.Trip) cruise = (name = "Mediterranean Cruise")

删除 Filter

type filter delete Travel.Trip

Summary Strings

Xcode Variables 界面中会显示变量的 Summary

可以看出,name (String) 和 destinations (Array) 这两个系统类型的变量都显示了 Summary ,而自定的 Trip 类型的 cruise 变量没有显示 Summary

我们可以为 Trip 类型添加自定义的 Summary,比如显示旅程的起点和终点。添加 Summary

type summary add Travel.Trip --summary-string "${var.name} from ${var.destinations[0]} to ${var.destinations[2]}"

v 命令一样,要使用 ${var.name} 这种格式来访问变量。

再调用 v cruise

(lldb) v cruise
(Travel.Trip) cruise = "Mediterranean Cruise" from "Sorrento" to "Taormina"

下次进入断点时,Xcode Variables 界面中也会显示 cruise 变量的 Summary

删除 Summary

type summary delete Travel.Trip

上述例子有个问题:由于 Formatter 无法访问计算变量(computed variables),如数组的元素总数,所以数组 index 只能硬编码

Python 脚本在 LLDB 中的使用

Xcode 11 开始,LLDB Scripting 开始使用 Python3

要解决上述的硬编码问题,可使用 LLDB’s Python API 来添加 Formatter

优势:

  • 可以使用 Python 进行任意的计算
  • Full access to LLDB’s Python API

简介

LLDB’s Python API : 即 LLDB Scripting Bridge API

LLDB Scripting Bridge API 中的常用类型:

在 Xcode Console 的交互界面中使用 Python

Xcode Console 中输入 script 命令进入到 Python 交互界面:

1
2
3
(lldb) script
Python Interactive Interpreter. To exit, type 'quit()', 'exit()'.
>>>
  • 调用 lldb.frame 可获取当前 Frame (栈桢),返回值的类型是 SBFrame
  • 调用 FindVariable 可以获取到指定的变量,返回值的类型是 SBValue
1
2
3
4
5
6
7
8
9
10
>>> cruise = lldb.frame.FindVariable("cruise")
>>> print(cruise) # 为什么在 Xcode 12.5 中打印的是 No value ?
(Travel.Trip) cruise = {
  name = "Mediterranean Cruise"
  destinations = 3 values {
    _buffer = {
      _storage = (rawValue = 0x0000600002c22260 -> 0x00007fff873fddd8 libswiftCore.dylib`InitialAllocationPool + 72)
    }
  }
}

调用 SBValueGetChildMemberWithName 方法,可以获取到 cruisedestinations 属性,返回值的类型是 SBValue

1
2
3
4
5
6
7
>>> destinations = cruise.GetChildMemberWithName("destinations")
>>> print(destinations)
([String]) destinations = 3 values {
  [0] = "Sorrento"
  [1] = "Capri"
  [2] = "Taormina"
}

获取数组的总数:

1
>>> count = destinations.GetNumChildren()

获取出发地的名称:

1
2
3
>>> begin = destinations.GetChildAtIndex(0)
>>> print(begin)
(String) [0] = "Sorrento"

获取目的地的名称:

1
2
3
>>> end = destinations.GetChildAtIndex(count - 1)
>>> print(end)
(String) [2] = "Taormina"

格式化打印:

1
2
>>> print("Trip from {} to {}".format(begin, end))
Trip from (String) [0] = "Sorrento" to (String) [2] = "Taormina"

使用 GetSummary 方法优化格式化打印:

1
2
>>> print("Trip from {} to {}".format(begin.GetSummary(), end.GetSummary()))
Trip from "Sorrento" to "Taormina"

加载 Python 脚本

可将上述操作写入名为 Trip.py 的脚本中,在其中添加 SummaryProvider 方法:

1
2
3
4
5
6
7
8
9
10
def SummaryProvider(value, _):
	destinations = value.GetChildMemberWithName("destinations")
	count = destinations.GetNumChildren()
	if count == 0:
		return "Empty trip"

	begin = destinations.GetChildAtIndex(0).GetSummary()
	end = destinations.GetChildAtIndex(count - 1).GetSummary()
	
	return "Trip with {} stops from {} to {}".format(count, begin, end)

将脚本放入 ~/.lldb 目录下,在 Xcode Console 中加载脚本:

command script import ~/.lldb/Trip.py

Xcode Console 中使用脚本提供的 SummaryProvider 方法,为 Trip 类型添加 Summary

type summary add Travel.Trip --python-function Trip.SummaryProvider

再次输入 v cruise,就得到了自定义格式化的输出:

(lldb) v cruise
(Travel.Trip) cruise = Trip with 3 stops from "Sorrento" to "Taormina"

除了 Xcode ConsoleXcode Variables 界面中也显示了自定义 Formatter 的内容:

Synthetic Children

https://lldb.llvm.org/use/variable.html#synthetic-children

Trip.py 脚本中添加 Python class

1
2
3
4
5
6
7
8
9
10
11
12
13
// Trip.py
class ExampleSyntheticChildrenProvider:
    def __init__(self, value, _):
        ...
    
    def num_children(self):
        ...
    
    def get_child_at_index(self, index):
        ...
    
    def get_child_index(self, name):
        ...

Xcode Console 中再次加载 Trip.py 脚本会使其 reload :

command script import ~/.lldb/Trip.py

Xcode Console 中添加 Synthetic Children

type synthetic add Travel.Trip --python-class Trip.ExampleSyntheticChildrenProvider

在 Xcode Console 中添加的 Formatter 的有效期

Xcode Console 中添加的 FiltersSummary StringsSynthetic ChildrenFormatter 的有效期:

有效

  • 重新 Run 项目
  • 关闭 Project 后重新打开

失效

  • Xcode 退出(Quit) 后,在 Xcode Console 中添加的 Formatter 就失效了,如果要再次使用,需要重新添加。

脚本的自动加载

对于一些需要长期使用的 Formatter ,每次启动 Xcode 后,都要在 Xcode Console 中手动加载脚本和手动添加 Formatter ,很繁琐。

LLDB 启动时,会先执行 ~/.lldbinit ,因此,可将加载脚本和添加 Formatter 的命令写入到 ~/.lldbinit 中,实现脚本自动加载:

# ~/.lldbinit

# Load Trip.py
command script import ~/.lldb/Trip.py

# Register Trip summary provider
type summary add Travel.Trip --python-function Trip.SummaryProvider

# Register Trip child provider
type synthetic add Travel.Trip --python-class Trip.ExampleSyntheticChildrenProvider

参考资料

WWDC 2019 / 429

其他资料

Reference

-- Missing configuration options! --