欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

iOS——调试工具LLDB学习

程序员文章站 2022-04-09 18:02:13
一、前言 LLDB是个开源的内置于XCode的具有REPL(read-eval-print-loop)特征的Debugger,其可以安装C++或者Python插件。在日常的开发和调试过程中给开发人员带来了非常多的帮助。了解并熟练掌握LLDB的使用是非常有必要的。这篇文章将会带着大家一起了解在iOS开 ......

一、前言

  lldb是个开源的内置于xcode的具有repl(read-eval-print-loop)特征的debugger,其可以安装c++或者python插件。在日常的开发和调试过程中给开发人员带来了非常多的帮助。了解并熟练掌握lldb的使用是非常有必要的。这篇文章将会带着大家一起了解在ios开发中lldb调试器的使用。

二、lldb基础

2.1 lldb基本语法

  lldb的基本语法如下

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

  举个例子,假设我们给main方法设置一个断点,我们使用下面的命令:
iOS——调试工具LLDB学习
  这个命令对应到上面的语法就是:

1. command: breakpoint 表示断点命令 
2. action: set 表示设置断点 
3. option: -n 表示根据方法name设置断点 
4. arguement: mian 表示方法名为mian
 
 

2.2 lldb的基本使用

 2.2.1 help命令  

  lldb其中内置了非常多的功能,选择去硬背每一条指令并不是一个明智的选择。我们只需要记住一些常用的指令,在需要的时候通过help命令来查看相关的描述即可。

(lldb) help
debugger commands:
  apropos           -- list debugger commands related to a word or subject.
  breakpoint        -- commands for operating on breakpoints (see 'help b' for
                       shorthand.)
  bugreport         -- commands for creating domain-specific bug reports.
  command           -- commands for managing custom lldb commands.
  disassemble       -- disassemble specified instructions in the current
                       target.  defaults to the current function for the
                       current thread and stack frame.
  expression        -- evaluate an expression on the current thread.  displays
                       any returned value with lldb's default formatting.
  frame             -- commands for selecting and examing the current thread's
                       stack frames.
  gdb-remote        -- connect to a process via remote gdb server.  if no host
                       is specifed, localhost is assumed.
  gui               -- switch into the curses based gui mode.
  help              -- show a list of all debugger commands, or give details
                       about a specific command.
......

  我们要查看某一个命令改如何使用时,可以使用 help <command> 来获取对应命令的使用方法。

(lldb) help expression
     evaluate an expression on the current thread.  displays any returned value
     with lldb's default formatting.  expects 'raw' input (see 'help
     raw-input'.)

syntax: expression <cmd-options> -- <expr>

command options usage:
  expression [-aflortgp] [-f <format>] [-g <gdb-format>] [-a <boolean>] [-i <boolean>] [-t <unsigned-integer>] [-u <boolean>] [-l <source-language>] [-x <boolean>] [-v[<description-verbosity>]] [-j <boolean>] [-d <none>] [-s <boolean>] [-d <count>] [-p <count>] [-y[<count>]] [-v <boolean>] [-z <count>] -- <expr>
  expression [-aflortgp] [-a <boolean>] [-i <boolean>] [-t <unsigned-integer>] [-u <boolean>] [-l <source-language>] [-x <boolean>] [-j <boolean>] [-d <none>] [-s <boolean>] [-d <count>] [-p <count>] [-y[<count>]] [-v <boolean>] [-z <count>] -- <expr>
  expression [-r] -- <expr>
  expression <expr>

       -a ( --show-all-children )
            ignore the upper bound on the number of children to show.

       -d <count> ( --depth <count> )
            set the max recurse depth when dumping aggregate types (default is
            infinity).

。。。。。。

examples:

    expr my_struct->a = my_array[3]
    expr -f bin -- (index * 8) + 5
    expr unsigned int $foo = 5
    expr char c[] = \"foo\"; c[0]
     
     important note: because this command takes 'raw' input, if you use any
     command options you must use ' -- ' between the end of the command options
     and the beginning of the raw input.

 2.2.2 expression命令

  expression命令的作用是执行一个表达式,并将表达式返回的结果输出。expression的完整语法是这样的 :

expression <cmd-options> -- <expr>

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

  说expression是lldb里面最重要的命令都不为过。因为他能实现2个功能。

  1. 执行某个表达式。 我们在代码运行过程中,可以通过执行某个表达式来动态改变程序运行的轨迹。 假如我们在运行过程中,突然想把self.view颜色改成红色,看看效果。我们不必写下代码,重新run,只需暂停程序,用expression改变颜色,再刷新一下界面,就能看到效果

    // 改变颜色
     (lldb) expression -- self.view.backgroundcolor = [uicolor redcolor]
     // 刷新界面
     (lldb) expression -- (void)[catransaction flush]
  2. 将返回值输出。 也就是说我们可以通过expression来打印东西。 假如我们想打印self.view

    (lldb) expression self.view
    (uiview *) $0 = 0x00007f8ed7418480
    (lldb) expression -- self.view
    (uiview *) $1 = 0x00007f8ed7418480

 2.2.3 p & print & call & po 命令

  一般情况下,我们直接用expression还是用得比较少的,更多时候我们用的是p、print、call。这三个命令其实都是 expression -- 的别名(--表示不再接受命令选项,详情见前面原始(raw)命令这一节):

  • print: 打印某个东西,可以是变量和表达式

  • p: 可以看做是print的简写

  • po:oc里所有的对象都是用指针表示的,所以一般打印的时候,打印出来的是对象的指针,而不是对象本身。如果我们想打印对象。我们需要使用命令选项:-o。为了更方便的使用,lldb为expression -o –定义了一个别名:po。p打印的是当前对象的地址而po则会调用对象的description方法,做法和nslog是一致的
  • call: 调用某个方法

  表面上看起来他们可能有不一样的地方,实际都是执行某个表达式(变量也当做表达式),将执行的结果输出到控制台上。所以你可以用p调用某个方法,也可以用call打印东西 e.g: 下面代码效果相同:

(lldb) expression -- self.view
(uiview *) $5 = 0x00007fb2a40344a0
(lldb) p self.view
(uiview *) $6 = 0x00007fb2a40344a0
(lldb) print self.view
(uiview *) $7 = 0x00007fb2a40344a0
(lldb) call self.view
(uiview *) $8 = 0x00007fb2a40344a0
(lldb) e self.view
(uiview *) $9 = 0x00007fb2a40344a0
(lldb) expression -o -- self.view
<uiview: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = w+h; layer = <calayer: 0x7fb2a4018c80>>
(lldb) po self.view
<uiview: 0x7fb2a40344a0; frame = (0 0; 375 667); autoresize = w+h; layer = <calayer: 0x7fb2a4018c80>>

2.2.4 thread backtrace命令

  有时候我们想要了解线程堆栈信息,可以使用thread backtrace thread backtrace作用是将线程的堆栈打印出来。我们来看看他的语法 :

thread backtrace [-c <count>] [-s <frame-index>] [-e <boolean>]

/*
   * thread backtrace后面跟的都是命令选项,实际上这些命令选项我们一般不需要使用。
    -c:设置打印堆栈的帧数(frame)
    -s:设置从哪个帧(frame)开始打印
    -e:是否显示额外的回溯
*/

e.g: 当发生crash的时候,我们可以使用thread backtrace查看堆栈调用。从下面的结果中,我们可以看到crash发生在-[viewcontroller viewdidload]中的第23行,只需检查这行代码是不是干了什么非法的事儿就可以了。

(lldb) thread backtrace
* thread #1: tid = 0xdd42, 0x000000010afb380b libobjc.a.dylib`objc_msgsend + 11, queue = 'com.apple.main-thread', stop reason = exc_bad_access (code=exc_i386_gpflt)
    frame #0: 0x000000010afb380b libobjc.a.dylib`objc_msgsend + 11
  * frame #1: 0x000000010aa9f75e tlldb`-[viewcontroller viewdidload](self=0x00007fa270e1f440, _cmd="viewdidload") + 174 at viewcontroller.m:23
    frame #2: 0x000000010ba67f98 uikit`-[uiviewcontroller loadviewifrequired] + 1198
    frame #3: 0x000000010ba682e7 uikit`-[uiviewcontroller view] + 27
    frame #4: 0x000000010b93eab0 uikit`-[uiwindow addrootviewcontrollerviewifpossible] + 61
    frame #5: 0x000000010b93f199 uikit`-[uiwindow _sethidden:forced:] + 282
    frame #6: 0x000000010b950c2e uikit`-[uiwindow makekeyandvisible] + 42

 此外,lldb还为backtrace专门定义了一个别名:bt,他的效果与thread backtrace相同,如果你不想写那么长一串字母,直接写下bt即可

2.2.5 thread return命令

  debug的时候,也许会因为各种原因,我们不想让代码执行某个方法,或者要直接返回一个想要的值。这时候就该thread return上场了。thread return可以接受一个表达式,调用命令之后直接从当前的frame返回表达式的值。

thread return [<expr>]

  e.g: 我们有一个somemethod方法,默认情况下是返回yes。我们想要让他返回no。我们只需在方法的开始位置加一个断点,当程序中断的时候,输入命令即可,效果相当于在断点位置直接调用return no;,不会执行断点后面的代码。

iOS——调试工具LLDB学习

(lldb) thread return no

 2.2.6 thread其他不常用的命令

  thread 相关的还有其他一些不常用的命令,这里就简单介绍一下即可,如果需要了解更多,可以使用命令help thread查阅

  • thread jump: 直接让程序跳到某一行。由于arc下编译器实际插入了不少retain,release命令。跳过一些代码不执行很可能会造成对象内存混乱发生crash。

  • thread list: 列出所有的线程

  • thread select: 选择某个线程

  • thread until: 传入一个line的参数,让程序执行到这行的时候暂停

  • thread info: 输出当前线程的信息

2.2.7 c & n & s & finish命令

  一般在调试程序的时候,我们经常用到下面这4个按钮:

iOS——调试工具LLDB学习

  用触摸板的孩子们可能会觉得点击这4个按钮比较费劲。其实lldb命令也可以完成上面的操作,而且如果不输入命令,直接按enter键,lldb会自动执行上次的命令。按一下enter就能达到我们想要的效果,有木有顿时感觉逼格满满的!!! 我们来看看对应这4个按钮的lldb命令:

  • c/ continue/ thread continue: 这三个命令效果都等同于上图中第一个按钮的。表示程序继续运行

  • n/ next/ thread step-over: 这三个命令效果等同于上图第二个按钮。表示单步运行

  • s/ step/ thread step-in: 这三个命令效果等同于上图第三个按钮。表示进入某个方法

  • finish/ step-out: 这两个命令效果等同于第四个按钮。表示直接走完当前方法,返回到上层frame

 2.2.8 frame命令

  前面我们提到过很多次frame(帧)。可能有的朋友对frame这个概念还不太了解。随便打个断点,我们在控制台上输入命令bt,可以打印出来所有的frame。如果仔细观察,这些frame和左边红框里的堆栈是一致的。平时我们看到的左边的堆栈就是frame。

iOS——调试工具LLDB学习  

  • frame variable:平时debug的时候我们经常做的事就是查看变量的值,通过frame variable命令,可以打印出当前frame的所有变量
    (lldb) frame variable
    (viewcontroller *) self = 0x00007fa158526e60
    (sel) _cmd = "text:"
    (bool) ret = yes
    (int) a = 3
  • frame variable 参数:如果我们要需要打印指定变量,也可以给frame variable传入参数。不过frame variable只接受变量作为参数,不接受表达式,也就是说我们无法使用frame variable self.string,因为self.string是调用string的getter方法。所以一般打印指定变量,我更喜欢用p或者po。
    (lldb) frame variable self->_string
    (nsstring *) self->_string = nil
  • frame info: 查看当前frame的信息
    (lldb) frame info
    frame #0: 0x0000000101bf87d5 tlldb`-[viewcontroller text:](self=0x00007fa158526e60, _cmd="text:", ret=yes) + 37 at viewcontroller.m:38
  • frame select: 选择某个frame,当我们选择frame 1的时候,他会把frame1的信息和代码打印出来。不过一般我都是直接在xcode左边点击某个frame,这样更方便
    (lldb) frame select 1
    frame #1: 0x0000000101bf872e tlldb`-[viewcontroller viewdidload](self=0x00007fa158526e60, _cmd="viewdidload") + 78 at viewcontroller.m:23
            
             - (void)viewdidload {
                 [super viewdidload];
                [self text:yes];
                nslog(@"1");
                nslog(@"2");
                nslog(@"3");

2.2.9 breakpoint命令

  • breakpoint set:设置断点,lldb提供了很多种设置断点的方式
    • 使用-n根据方法名设置断点
      //我们想给所有类中的viewwillappear:设置一个断点
      (lldb) breakpoint set -n viewwillappear:
          breakpoint 13: 33 locations.
    • 使用-f指定文件
      // 我们只需要给viewcontroller.m文件中的viewdidload设置断点
      (lldb) breakpoint set -f viewcontroller.m -n viewdidload
          breakpoint 22: where = tlldb`-[viewcontroller viewdidload] + 20 at viewcontroller.m:22, address = 0x000000010272a6f4
    • 使用-l指定文件某一行设置断点
      //我们想给viewcontroller.m第38行设置断点
      (lldb) breakpoint set -f viewcontroller.m -l 38
      breakpoint 23: where = tlldb`-[viewcontroller text:] + 37 at viewcontroller.m:38, address = 0x000000010272a7d5
    • 使用-c设置条件断点
      //text:方法接受一个ret的参数,我们想让ret == yes的时候程序中断
      (lldb) breakpoint set -n text: -c ret == yes
      breakpoint 7: where = tlldb`-[viewcontroller text:] + 30 at viewcontroller.m:37, address = 0x0000000105ef37ce
    • 使用-o设置单次断点
      //如果刚刚那个断点我们只想让他中断一次
      (lldb) breakpoint set -n text: -o
      'breakpoint 3': where = tlldb`-[viewcontroller text:] + 30 at viewcontroller.m:37, address = 0x000000010b6f97ce 
  • breakpoint command add:就是给断点添加命令的命令。多次对同一个断点添加命令,后面命令会将前面命令覆盖
     //假设我们需要在viewcontroller的viewdidload中查看self.view的值 我们首先给-[viewcontroller viewdidload]添加一个断点
    (lldb) breakpoint set -n "-[viewcontroller viewdidload]"
    'breakpoint 3': where = tlldb`-[viewcontroller viewdidload] + 20 at viewcontroller.m:23, address = 0x00000001055e6004
     
    /*
        可以看到添加成功之后,这个breakpoint的id为3,然后我们给他增加一个命令:po self.view
        -o完整写法是--one-liner,表示增加一条命令。3表示对id为3的breakpoint增加命令。 添加完命令之后,每次程序执行到这个断点就可以自动打印出self.view的值了
    */
    (lldb) breakpoint command add -o "po self.view" 3
     
    /*
        如果我们一下子想增加多条命令,比如我想在viewdidload中打印当前frame的所有变量,但是我们不想让他中断,也就是在打印完成之后,需要继续执行。我们可以这样玩
        输入breakpoint command add 3对断点3增加命令。他会让你输入增加哪些命令,输入’done’表示结束。这时候你就可以输入多条命令了
    */
    (lldb) breakpoint command add 3
    enter your debugger command(s).  type 'done' to end.
    > frame variable
    > continue
    > done
  • breakpoint command list:查看某个断点已有的命令
    //我们查看一下刚刚的断点3已有的命令
    (lldb) breakpoint command list 3
    'breakpoint 3':
        breakpoint commands:
          frame variable
          continue
  • breakpoint command delete:有增加就有删除,breakpoint command delete可以让我们删除某个断点的命令
  • breakpoint list:查看已经设置了哪些断点
  • breakpoint disable/enable:有的时候我们可能暂时不想要某个断点,可以使用breakpoint disable让某个断点暂时失效,使用breakpoint enable再次让他生效。
  • breakpoint delete:如果我们觉得这个断点以后再也用不上了,可以用breakpoint delete直接删除断点
    //删除断点4
    (lldb) breakpoint delete 4
    1 breakpoints deleted; 0 breakpoint locations disabled.
     
    //如果我们想删除所有断点,只需要不指定breakpoint delete参数即可
    (lldb) breakpoint delete
    about to delete all breakpoints, do you want to do that?: [y/n] y
    all breakpoints removed. (1 breakpoint)
     
    //删除的时候他会提示你,是不是真的想删除所有断点,需要你再次输入y确认。如果想直接删除,不需要他的提示,使用-f命令选项即可
    (lldb) breakpoint delete -f
    all breakpoints removed. (1 breakpoint)

  实际平时我们真正使用breakpoint命令反而比较少,因为xcode已经内置了断点工具。我们可以直接在代码上打断点,可以在断点工具栏里面查看编辑断点,这比使用lldb命令方便很多。不过了解lldb相关命令可以让我们对断点理解更深刻。 如果你想了解怎么使用xcode设置断点,可以阅读这篇文章《xcode中断点的威力》

2.2.10 watchpoint命令

  breakpoint有一个孪生兄弟watchpoint。如果说breakpoint是对方法生效的断点,watchpoint就是对地址生效的断点。如果我们想要知道某个属性什么时候被篡改了,我们该怎么办呢?有人可能会说对setter方法打个断点不就行了么?但是如果更改的时候没调用setter方法呢? 这时候最好的办法就是用watchpoint。我们可以用他观察这个属性的地址。如果地址里面的东西改变了,就让程序中断

  • watchpoint set:用于添加一个watchpoint。只要这个地址中的内容变化了,程序就会中断。
  • watchpoint set variable:一般情况下,要观察变量或者属性,使用watchpoint set variable命令即可。watchpoint set variable传入的是变量名。需要注意的是,这里不接受方法,所以不能使用watchpoint set variable self.string,因为self.string调用的是string的getter方法
    (lldb) watchpoint set variable self->_string
    watchpoint created: watchpoint 1: addr = 0x7fcf3959c418 size = 8 state = enabled type = w
        watchpoint spec = 'self->_string'
        new value: 0x0000000000000000
  • watchpoint set expression:如果我们想直接观察某个地址,可以使用watchpoint set expression

    //我们先拿到_model的地址,然后对地址设置一个watchpoint
    (lldb) p &_model
    (modek **) $3 = 0x00007fe0dbf23280
    (lldb) watchpoint set expression 0x00007fe0dbf23280
    watchpoint created: watchpoint 1: addr = 0x7fe0dbf23280 size = 8 state = enabled type = w
        new value: 0
  • watchpoint command add:和breakpoint一样给watchpoint添加命令

    //设置一个watchpoint
    (lldb) watchpoint set variable _string
    watchpoint created: watchpoint 1: addr = 0x7fe4e1444760 size = 8 state = enabled type = w
        watchpoint spec = '_string'
        new value: 0x0000000000000000
     
    //可以看到这个watchpoint的id是1。我们可以用watchpoint command add -o添加单条命令
    watchpoint command add -o 'bt' 1
    
    //我们也可以一次添加多条命令
    (lldb) watchpoint command add 1
    enter your debugger command(s).  type 'done' to end.
    > bt
    > continue
    > done
  • watchpoint command list:列出某个watchpoint所有的command

  • watchpoint command delete:删除某个watchpoint所有的command

  • watchpoint list:查看当前所有watchpoint

  • watchpoint disable/enable:使某个watchpoint失效/生效

  • watchpoint delete:删除watchpoint,删除单个或多个,用法同breakpoint delete