iOS LLDB调试学习使用

之前使用Xcode调试在LLDB中只用了打印、看日志输入、再复杂点就是po一个属性的值,前段时间看了篇关于LLDB的文章,感觉之前完全白白浪费了Xcode提供这么好的调试工具。这段时间学习实验,此文供记录学习。

LLDB 概述

LLDB全称 " Low Level Debugger ", 是由苹果出品。标准的 LLDB 提供了一组广泛的命令,旨在与熟悉的 GDB 命令兼容。 除了使用标准配置外,还可以很容易地自定义 LLDB 以满足实际需要,带给我们更丰富的流程控制和数据检测的调试功能。

LLDB 作用

  • 允许你在程序运行的特定时暂停它;
  • 查看变量的值;
  • 执行自定的指令;
  • 按照你所认为合适的步骤来操作程序的进展;

LLDB控制台

Xcode中内嵌了LLDB控制台,在Xcode中代码的下方,我们可以看到LLDB控制台。快捷键是command + shift + y。LLDB控制台平时会输出一些log信息。如果我们想输入命令调试,必须让程序进入暂停状态。


iOS LLDB调试学习使用_第1张图片
LLDB控制台.png

LLDB 语法

 [ [...]]  [-options [option-value]] [argument [argument...]]
(命令)和(子命令):LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
:执行命令的操作
:命令选项
:命令的参数
[]:表示命令是可选的,可以有也可以没有

举个例子,假设我们给main.m中16行设置一个断点,我们使用下面的命令:

breakpoint set -f main.m -l 16

与上面语法结构对应的是:

command: breakpoint 添加断点命令
action: set 表示设置断点
option: -f 表示在某文件添加断点
arguement: mian.m 表示要添加断点的文件名为mian.m
option: -l 表示某一行
arguement: 16 表示第16行

上面的print命令只是LLDB调试中的一个很简单但很常用的命令,除此之外还有很多有可能用到的命令:

apropos           -- 列出与单词或主题相关的调试器命令
  breakpoint        -- 在断点上操作的命令 (详情使用'help b'查看)
  bugreport         -- 用于创建指定域的错误报告
  command           -- 用于管理自定义LLDB命令的命令
  disassemble       -- 拆分当前目标中的特定说明。 默认为当前线程和堆栈帧的当前函数
  expression        -- 求当前线程上的表达式的值。 以LLDB默认格式显示返回的值
  frame             -- 用于选择和检查当前线程的堆栈帧的命令
  gdb-remote        -- 通过远程GDB服务器连接到进程。 如果未指定主机,则假定为localhost
  gui               -- 切换到基于curses的GUI模式
  help              -- 显示所有调试器命令的列表,或提供指定命令的详细信息
  kdp-remote        -- 通过远程KDP服务器连接到进程。 如果没有指定UDP端口,则假定端口41139
  language          -- 指定源语言
  log               -- 控制LLDB内部日志记录的命令
  memory            -- 用于在当前目标进程的内存上操作的命令
  platform          -- 用于管理和创建平台的命令
  plugin            -- 用于管理LLDB插件的命令
  process           -- 用于与当前平台上的进程交互的命令
  quit              -- 退出LLDB调试器
  register          -- 命令访问当前线程和堆栈帧的寄存器
  script            -- 使用提供的代码调用脚本解释器并显示任何结果。 如果没有提供代码,启动交互式解释器。
  settings          -- 用于管理LLDB设置的命令
  source            -- 检查当前目标进程的调试信息所描述的源代码的命令
  target            -- 用于在调试器目标上操作的命令
  thread            -- 用于在当前进程中的一个或多个线程上操作的命令
  type              -- 在类型系统上操作的命令
  version           -- 显示LLDB调试器版本
  watchpoint        -- 在观察点上操作的命令


缩写命令 (使用 'help command alias'查看更多信息):

  add-dsym  -- ('target symbols add')  通过指定调试符号文件的路径,或使用选项指定下载符号的模块,将调试符号文件添加到目标的当前模块中的一个
  attach    -- ('_regexp-attach')  通过ID或名称附加到进程
  b         -- ('_regexp-break')  使用几种简写格式之一设置断点
  bt        -- ('_regexp-bt')  显示当前线程的调用堆栈。通过数字参数设置最多显示帧数。参数“all”显示所有线程
  c         -- ('process continue')  继续执行当前进程中的所有线程
  call      -- ('expression --')  计算当前线程上的表达式,使用LLDB的默认格式显示返回的值
  continue  -- ('process continue')  继续执行当前进程中的所有线程
  detach    -- ('process detach')  脱离当前目标进程
  di        -- ('disassemble')  拆分当前目标中的特定说明。 默认为当前线程和堆栈帧的当前函数
  dis       -- ('disassemble')  同上
  display   -- ('_regexp-display')  在每次停止时计算表达式(请参阅'help target stop-hook')
  down      -- ('_regexp-down')  选择一个新的堆栈帧。默认为移动一个帧,数字参数可以指定值
  env       -- ('_regexp-env')  查看和设置环境变量的简写
  exit      -- ('quit')  退出LLDB调试器
  f         -- ('frame select')  从当前线程中通过索引选择当前堆栈帧(参见'thread backtrace')
  file      -- ('target create')  使用参数作为主要可执行文件创建目标
  finish    -- ('thread step-out')  完成当前堆栈帧的执行并返回后停止。 默认为当前线程
  image     -- ('target modules')  用于访问一个或多个目标模块的信息的命令
  j         -- ('_regexp-jump')  将程序计数器设置为新地址
  jump      -- ('_regexp-jump')  同上
  kill      -- ('process kill')  终止当前目标进程
  l         -- ('_regexp-list')  使用几种简写格式之一列出相关的源代码
  list      -- ('_regexp-list')  同上
  n         -- ('thread step-over')  源级单步执行、步进调用,默认当前线程
  next      -- ('thread step-over')  同上
  nexti     -- ('thread step-inst-over')  指令级单步执行、步进调用,默认当前线程
  ni        -- ('thread step-inst-over')  同上
  p         -- ('expression --')  计算当前线程上表达式的值,以LLDB默认格式显示返回值
 parray    -- ('expression -Z %1   --')  同上
  po        -- 计算当前线程上的表达式。显示由类型作者控制的格式的返回值。
  poarray   -- ('expression -O -Z %1    --')  计算当前线程上表达式的值,以LLDB默认格式显示返回值
  print     -- ('expression --')  同上
  q         -- ('quit')  退出LLDB调试器
  r         -- ('process launch -X true --')  在调试器中启动可执行文件
  rbreak    -- ('breakpoint set -r %1')  在可执行文件中设置断点或断点集
  repl      -- ('expression -r  -- ')  E计算当前线程上表达式的值,以LLDB默认格式显示返回值
  run       -- ('process launch -X true --')  在调试器中启动可执行文件
  s         -- ('thread step-in')  源级单步执行、步进调用,默认当前线程
  si        -- ('thread step-inst')  指令级单步执行、步进调用,默认当前线程
  sif       -- 遍历当前块,如果直接步入名称与TargetFunctionName匹配的函数,则停止
  step      -- ('thread step-in')  源级单步执行、步进调用,默认当前线程
  stepi     -- ('thread step-inst')  指令级单步执行、步进调用,默认当前线程
  t         -- ('thread select')  更改当前选择的线程
  tbreak    -- ('_regexp-tbreak')  使用几种简写格式之一设置单次断点
  undisplay -- ('_regexp-undisplay')  每次停止时停止显示表达式(由stop-hook索引指定)
  up        -- ('_regexp-up')  选择较早的堆栈帧。 默认为移动一个帧,数值参数可以指定任意数字
  x         -- ('memory read')  从当前目标进程的内存中读取

上面的命令不需要都记住,记住常用的几个如: p, po, call, breakpoint, call, expression 等 ,其他需要的时候再查,通过“help”命令显示所有调试命令的列表,或查询指定命令的详细信息。

LLDB 常用命令

LLDB拥有大量有用的调试工具。

获取变量值:expression, e, print, po, p
获取执行环境+特定语言命令:bugreport, frame, language
执行流程控制:process, breakpoint, thread, watchpoint
其他:command,platform,gui

下面列一些常用的命令:

expression

expression命令的作用是执行一个表达式,并将表达式返回的结果输出。

expression命令的格式

expression  -- 

:命令选项,一般情况下使用默认的即可,不需要特别标明。
--: 命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,--可以省略
: 要执行的表达式

例如
expression -O -- testStr
-O 是命令选项
testStr 是执行的表达式

再例如
expression testStr 和 expression -- testStr
两个命令功能是一致的,没有命令选项时可以省略--

expression最基本的功能是打印和修改变量的值,你可以在运行时执行几乎任何表达式或命令。

// 改变颜色

(lldb) expression self.view.backgroundColor = UIColor.brown    //改变颜色
(lldb) expression CATransaction.flush()   //刷新
(lldb) 

// 改变属性值

(lldb) expression testStr   // 打印testStr
(String) $R0 = "123"
(lldb) expression testStr = "abc"  //修改testStr的值
(lldb) expression testStr
(String) $R2 = "abc"  //testStr值调试期间变成abc

expression 拥有大约30个命令选项

下面列出了几个比较常用的选项:

-D ( --depth ) - 设置打印聚合类型的递归深度(默认无限递归)。
-O ( --object-desctiption ) - 打印description方法。
-T ( --show-types ) - 显示每个变量的类型。
-f ( --format ) - 设置输出格式。
-i ( --ignore-breakpoints ) - 运行表达式时忽略表达式内的断点。

p、 print、 e、 call

实际上call、 p、print这三个指令都是 expression 指令的别名, 实际上的运行效果是一样的,举例说明,请看一下代码:

(lldb) call testStr
(String) $R8 = "abc"
(lldb) p testStr
(String) $R9 = "abc"
(lldb) print testStr
(String) $R10 = "abc"
(lldb) e testStr
(String) $R11 = "abc"

po

po self 命令与 expression -O -- self功能类似。

breakpoint

开发调试的第一步就是设置断点,我们用的最多的就是breakpoint命令,但我们很少在LLDB 中使用breakpoint命令,大多在Xcode GUI界面中设置断点。除了直接直接出发暂停调试外,我们还可以进一步的设置。


iOS LLDB调试学习使用_第2张图片
image.png
  • 添加condition,一般用于多次调用的函数或者循坏的代码中,在作用域内达到某个条件,才会触发程序暂停
  • 忽略次数,这个很容易理解,在忽略触发几次后再触发暂停
  • 添加Action,为这个断点添加子命令、脚本、shell命令、声效(有个毛线用)等Action,我的理解是一个脚本化的功能,我们可以在断点的基础上添加一些方便调试的脚本,提高调试效率。
  • 自动继续,配合上面的添加Action,我们就可以不用一次又一次的暂停程序进行调试来查询某些值(大型程序中断一次还是会有卡顿),直接用Action将需要的信息打印在控制台,一次性查看即可。

除去在代码中直接点击添加断点外,我们也可以在 BreakPoint Navigation页面下直接添加相关的断点。我们常用的有 Exception Breakpoint 与 Symbolic Breakpoint

breakpointNav.png
iOS LLDB调试学习使用_第3张图片
breakPointException.png
  • Add Exception Breakpoint
    Exception Breakpoint为异常断点。在某些情况下,TableView的数据源与UI操作不一致,或者容器插入了nil的指针,将消息传至野指针,都会导致程序的crash,并且LLDB输出的信息不是很友好。加上异常断点,能够使程序在抛出异常的栈自动暂停,可直接定位导致抛出异常的代码。在一般的开发流程中,都建议开启这个异常断点,反正你总是会crash的嘿嘿。
  • Add Symbolic Breakpoint
    Symbolic Breakpoint 为符号断点。有时候,我们并不清楚程序会在什么情况下调用某一个函数,那我们可以通过符号断点来获取调用该函数时的程序堆栈。当然,在自己实现的类,我们也可以在该函数实现的地方打上断点,但如果需要定位其他框架提供的API的调用,就只能使用符号断点啦。

当然,LLDB的breakpoint命令也可以实现上述的功能,因为不常用,所以这里就简单列举一些用法。 breakpoint set -n trigger //在所有类的trigger函数实现中打上断点

    breakpoint set -f ViewController.m -n trigger //在ViewController.m中的trigger方法打上断点 
    breakpoint set -f ViewController.m -l 50 //在ViewController.m的50行打上断点 
    breakpoint set -f ViewController.m -n trigger: -c testCondition > 5 //在ViewController.m中的trigger方法打上断点并添加condition, testCondition大于5时触发断点 
    breakpoint set -n trigger -o //单次断点 
    breakpoint command add -o "frame info" 3 //在设置的三号断点加入子命令frame info 
    breakpoint list // 列出所有断点 
    breakpoint delete 3 //删除3号断点

watchpoint

通过watchpoint 来查看某个属性是否有变化,watchpoint是对地址生效的断点,用watchpoint观察属性的地址变化情况。

(lldb) watchpoint set variable self.testArr  //观察一个名为testArr的属性值
Watchpoint created: Watchpoint 1: addr = 0x7f81b4607478 size = 8 state = enabled type = w
    declare @ '/LLDBDemo/ViewController.swift:16'
    watchpoint spec = 'self.testArr'
    new value: 1 value
屏幕快照 2018-05-12 上午11.11.54.png

按钮点击方法中修改了testArr的值,

(lldb) watchpoint set variable self.testArr  //观察一个名为testArr的属性值
Watchpoint created: Watchpoint 1: addr = 0x7f81b4607478 size = 8 state = enabled type = w
    declare @ '/LLDBDemo/ViewController.swift:16'
    watchpoint spec = 'self.testArr'
    new value: 1 value

Watchpoint 1 hit:    //检测到属性变化
old value: 1 value
new value: 2 values

thread

可以使用 thread backtrace(或 bt )命令打印线程堆栈信息:

iOS LLDB调试学习使用_第4张图片
屏幕快照 2018-05-12 上午11.22.27.png
iOS LLDB调试学习使用_第5张图片
屏幕快照 2018-05-12 上午11.22.19.png

thread backtrace 后面可以添加命令选项:

-c:设置打印堆栈的帧数(frame)
-s:设置从哪个帧(frame)开始打印
-e:是否显示额外的回溯

Debug 的时候,也许会因为各种原因,我们不想让代码执行某个方法,或者要直接返回一个想要的值。可以使用 thread return 命令:

thread return '要返回的值'

thread return 不让代码执行某个方法,可以在某个方法的开始位置设置一个断点,当程序运行到断点的位置时直接返回我们设置的返回值。
但我实验了,总是提示错误:

error: Error returning from frame 0 of thread 1: We only support setting simple integer and float return types at present..

有了解的同学帮忙指导下。

参考链接:
使用 LLDB 调试 APP
iOS调试-LLDB学习总结

你可能感兴趣的:(iOS LLDB调试学习使用)