美文网首页iOS-学习笔记iOS DeveloperDevSupport
xcode调试利器 LLDB-深度命令文档

xcode调试利器 LLDB-深度命令文档

作者: JackTheBest | 来源:发表于2016-12-08 17:51 被阅读582次

    什么是 LLDB?

    LLDB(Low Level Debugger) is a next generation, high-performance debugger. It is built as a set of reusable components which highly leverage existing libraries in the larger LLVM Project, such as the Clang expression parser and LLVM disassembler.

    LLDB is the default debugger in Xcode on Mac OS X and supports debugging C, Objective-C and C++ on the desktop and iOS devices and simulator.

    All of the code in the LLDB project is available under the standard LLVM License, an open source "BSD-style" license.

    基础

    LLDB 是一个有着 REPL 的特性的调试器,这说明他和Linux数据库有一样的"套路"。当一般不是手动把help命令禁止掉的时候,就可以通过help查找所有想要使用的命令。

    现在iOSxcode,安卓android studio 都有LLDB模块支持。学好LLDB,各种难搞的问题都能找到一些可以原由。

    print

    你可能还注意到了,结果中有个 $3。 实际上你可以使用它来指向这个结果。试试 print $3 + 1,你会看到 100。任何以美元符开头的东西都是存在于 LLDB 的命名空间的,它们是为了帮助你进行调试而存在的。

    lldb_001.png

    expression

    如果想改变一个值怎么办?你或许会猜 modify。其实这时候我们要用到的是 expression 这个方便的命令。

    lldb_002.png lldb_003.png

    这不仅会改变调试器中的值,实际上它改变了程序中的值。从现在开始,我们将会偷懒分别以pe来代替printexpression

    什么是 print 命令

    考虑一个有意思的表达式:p count = 18。如果我们运行这条命令,然后打印 count 的内容。我们将看到它的结果与 expression count = 18 一样。

    和 expression 不同的是,print 命令不需要参数。比如 e -h +17 中,你很难区分到底是以 -h 为标识,仅仅执行 +17 呢,还是要计算 17 和 h 的差值。连字符号确实很让人困惑,你或许得不到自己想要的结果。

    幸运的是,解决方案很简单。用 -- 来表征标识的结束,以及输入的开始。如果想要 -h 作为标识,就用 e -h -- +17,如果想计算它们的差值,就使用 e -- -h +17。因为一般来说不使用标识的情况比较多,所以 e -- 就有了一个简写的方式,那就是 print。

    输入 help print

    'print' is an abbreviation for 'expression --'.   
    (print是 `expression --` 的缩写)
    

    打印对象

    (lldb) p @[ @"foo", @"bar" ]
    (NSArray *) $8 = 0x00007fdb9b71b3e0 @"2 objects" 
    

    实际上,我们想看的是对象的 description 方法的结果。我么需要使用 -O (字母 O,而不是数字 0) 标志告诉 expression 命令以 对象 (Object) 的方式来打印结果。

    (lldb) e -O -- $8
    <__NSArrayI 0x7fdb9b71b3e0>(
    foo,
    bar
    )
    

    幸运的是,e -o -- 有也有个别名,那就是 po (print object 的缩写),我们可以使用它来进行简化:

    (lldb) po $8
    <__NSArrayI 0x7fdb9b71b3e0>(
    foo,
    bar
    )
    (lldb) po @"lunar"
    lunar
    (lldb) p @"lunar"
    (NSString *) $13 = 0x00007fdb9d0003b0 @"lunar"
    

    Thread Return

    调试时,还有一个很棒的函数可以用来控制程序流程:thread return 。它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。这意味这函数剩余的部分不会被执行。这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。但是在函数的开头执行这个命令,是个非常好的隔离这个函数,伪造返回值的方式 。

    断点

    我们都把断点作为一个停止程序运行,检查当前状态,追踪 bug 的方式。但是如果我们改变和断点交互的方式,很多事情都变成可能。

    想象把断点放在函数的开头,然后用 thread return 命令重写函数的行为,然后继续。想象一下让这个过程自动化,听起来不错,不是吗?

    创建断点

    在上面的例子中,我们通过在源码页面器的滚槽 16 上点击来创建断点。你可以通过把断点拖拽出滚槽,然后释放鼠标来删除断点 (消失时会有一个非常可爱的噗的一下的动画)。你也可以在断点导航页选择断点,然后按下删除键删除。

    要在调试器中创建断点,可以使用breakpoint set命令。

    (lldb) breakpoint set -f main.m -l 16
    Breakpoint 1: where = DebuggerDance`main + 27 at main.m:16, address = 0x000000010a3f6cab
    

    也可以在一个符号 (C 语言函数) 上创建断点,而完全不用指定哪一行

    (lldb) b isEven
    Breakpoint 3: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
    (lldb) br s -F isEven
    Breakpoint 4: where = DebuggerDance`isEven + 16 at main.m:4, address = 0x000000010a3f6d00
    

    如果你 Xcode 的 UI 上右击任意断点,然后选择 "Edit Breakpoint" 的话,会有一些非常诱人的选择。

    lldb_004.png

    你也可以添加多个行为,可以是调试器命令,shell 命令,也可以是更直接的打印:

    lldb_005.png

    UI操作

    更新UI

    (lldb) e id $myView = (id)0x7f82b1d01fd0
    
    然后在调试器中改变它的背景色:
    (lldb) e (void)[$myView setBackgroundColor:[UIColor blueColor]]
    
    

    但是只有程序继续运行之后才会看到界面的变化。因为改变的内容必须被发送到渲染服务中,然后显示才会被更新。

    渲染服务实际上是一个另外的进程 (被称作 backboardd)。这就是说即使我们正在调试的内容所在的进程被打断了,backboardd 也还是继续运行着的。

    这意味着你可以运行下面的命令,而不用继续运行程序:

    (lldb) e (void)[CATransaction flush]
    

    Push 一个 View Controller

    (lldb) e id $nvc = [[[UIApplication sharedApplication] keyWindow] rootViewController]
    
    (lldb) e id $vc = [UIViewController new]
    (lldb) e (void)[[$vc view] setBackgroundColor:[UIColor yellowColor]]
    (lldb) e (void)[$vc setTitle:@"Yay!"]
    (lldb) e (void)[$nvc pushViewContoller:$vc animated:YES]
    
    (lldb) caflush // e (void)[CATransaction flush]
    

    查找按钮的 target

    (lldb) po [$myButton allTargets]
    {(
        <MagicEventListener: 0x7fb58bd2e240>
    )}
    (lldb) po [$myButton actionsForTarget:(id)0x7fb58bd2e240 forControlEvent:0]
    <__NSArrayM 0x7fb58bd2aa40>(
    _handleTap:
    )
    

    观察实例变量的变化

    假设你有一个 UIView,不知道为什么它的 _layer 实例变量被重写了 (糟糕)。因为有可能并不涉及到方法,我们不能使用符号断点。相反的,我们想监视什么时候这个地址被写入。

    首先,我们需要找到 _layer 这个变量在对象上的相对位置:

    (lldb) p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
    (ptrdiff_t) $0 = 8
    

    现在我们知道 ($myView + 8) 是被写入的内存地址:

    (lldb) watchpoint set expression -- (int *)$myView + 8
    Watchpoint created: Watchpoint 3: addr = 0x7fa554231340 size = 8 state = enabled type = w
        new value: 0x0000000000000000
    
    

    这被以 wivar $myView _layer 加入到 Chisel 中。

    非重写方法的符号断点

    假设你想知道 -[MyViewController viewDidAppear:] 什么时候被调用。如果这个方法并没有在MyViewController 中实现,而是在其父类中实现的,该怎么办呢?试着设置一个断点,会出现以下结果

    (lldb) b -[MyViewController viewDidAppear:]
    Breakpoint 1: no locations (pending).
    WARNING:  Unable to resolve breakpoint to any actual locations.
    

    因为 LLDB 会查找一个符号,但是实际在这个类上却找不到,所以断点也永远不会触发。你需要做的是为断点设置一个条件 [self isKindOfClass:[MyViewController class]],然后把断点放在 UIViewController 上。正常情况下这样设置一个条件可以正常工作。但是这里不会,因为我们没有父类的实现。

    viewDidAppear: 是苹果实现的方法,因此没有它的符号;在方法内没有 self 。如果想在符号断点上使用 self,你必须知道它在哪里 (它可能在寄存器上,也可能在栈上;在 x86 上,你可以在 $esp+4 找到它)。但是这是很痛苦的,因为现在你必须至少知道四种体系结构 (x86,x86-64,armv7,armv64)。想象你需要花多少时间去学习命令集以及它们每一个的调用约定,然后正确的写一个在你的超类上设置断点并且条件正确的命令。幸运的是,这个在 Chisel 被解决了。这被成为 bmessage:

    (lldb) bmessage -[MyViewController viewDidAppear:]
    Setting a breakpoint at -[UIViewController viewDidAppear:] with condition (void*)object_getClass((id)$rdi) == 0x000000010e2f4d28
    Breakpoint 1: where = UIKit`-[UIViewController viewDidAppear:], address = 0x000000010e11533c
    

    LLDB 和 Python

    LLDB 有内建的,完整的 Python 支持。在LLDB中输入 script,会打开一个 Python REPL。你也可以输入一行 python 语句作为 script 命令 的参数,这可以运行 python 语句而不进入REPL:

    (lldb) script import os
    (lldb) script os.system("open http://www.objc.io/")
    

    这样就允许你创造各种酷的命令。把下面的语句放到文件 ~/myCommands.py 中:

    def caflushCommand(debugger, command, result, internal_dict):
      debugger.HandleCommand("e (void)[CATransaction flush]")
    

    然后再 LLDB 中运行:

    command script import ~/myCommands.py
    

    常用命令

    frame variable

    查看当前栈帧里帧参数和本地变量

    register read

    查看寄存器的值

    register read $arg1 $arg2

    查看两个变量

    memory read

    查看内存地址的命令

    (lldb) memory read --size 8 --format A --count 10 0x0000000100004630
    

    memory write

    内存值的修改

    up down

    stack frame 跳栈帧

    bt

    显示所有的调用栈帧。该命令可用来显示函数的调用顺序。

    disassemble --frame

    disassemble相关指令用于输出某段程序的汇编代码 汇编当前栈帧的代码。

    使用disassemble -b则会输出带字节信息的汇编代码:

    Refrence

    相关文章

      网友评论

      • fuyoufang:正文中:
        p (ptrdiff_t)ivar_getOffset((struct Ivar *)class_getInstanceVariable([MyView class], "_layer"))
        我测试的时候修改下面内容才可以运行:
        p (ptrdiff_t)ivar_getOffset((Ivar)class_getInstanceVariable([MyView class], "_layer"))

      • Gibsion:好东西,先mark了
      • ldldlkdldld:知道一个内存地址,能否通过lldb查看那块内存到底存了什么吗?
      • 小行为:请问这个lldb一般都什么情况用呢 我现在一直都没用过只知道打断点的时候弹出这个
        小行为:@JackTheBest 好吧 现在code还用的不熟悉呢 遇到再说吧
        JackTheBest:@小行为 打断点也算lldb的功能的一种。比如说你想看某个view的 contentinset,比如说你想监听某个值在什么时候变化 甚至在什么时候被读取。都可以用lldb。lldb功能很强大寄存器,内存 栈帧的信息都可以看。建议先好好写code 等你遇到问题 需要debug的时候 自然会需要lldb。

      本文标题:xcode调试利器 LLDB-深度命令文档

      本文链接:https://www.haomeiwen.com/subject/mbpsmttx.html