iOS动态调试

什么是动态调试

通过动态下断点的方式,动态调试程序的方法,查看程序的内存信息、参数、调用堆栈等信息的一种调试方法

LLDB

什么是LLDB

LLDB 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.

LLDB命令格式

<command> [<subcommand> [<subcommand>...]] <action> [-options [option-value]] [argument [argument...]]
  • command、subcommand:LLDB调试命令的名称。命令和子命令按层级结构来排列:一个命令对象为跟随其的子命令对象创建一个上下文,子命令又为其子命令创建一个上下文,依此类推。
  • action:命令操作,想在前面的命令序列的上下文中执行的一些操作。
  • options:命令选项,行为修改器(action modifiers)。通常带有一些值。
  • argument:命令参数,根据使用的命令的上下文来表示各种不同的东西。
    []:表示命令是可选的,可以有也可以没有。

举个例子:
命令:breakpoint set -n main 对应到上面的语法就是:

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

LLDB常用命令

  • help

    • 查看某指令的用法
    • breakpoint set -n testhelp breakpoint set
  • expression --

    • 执行表达式
    • 计算以及生成一个表达式
    (lldb) expr (int)printf ("Print nine: %d.\n", 4 + 5) 
    Print nine: 9.
    (int) $0 = 15
    
    • 创建一个变量并分配值
    (lldb) expr int $val = 10
    (lldb) expr $val
    (int) $val = 10
    
    • exp 打印值、修改值
    (lldb) expr width
    (CGFloat) $0 = 10
    (lldb) expr width = 2
    (CGFloat) $1 = 2
    (lldb) po width
    2
    
    (lldb) p width
    (CGFloat) $3 = 2
    
    • ppoexpr 的关系

    p是expr --的简写,它的工作是把接收到参数在当前环境中进行编译,然后打印出来
    po是expr -o --的简写,它所做的操作和p相同。如果接收到的参数是一个指针,那么它会调用对象的description方法并打印;如果接收到的参数是一个core foundation对象,那么它会调用CFShow方法并打印。如果这两个方法都调用失败,那么po打印出和p相同的内容。

    (lldb) expr -- person
    (Person *) $0 = 0x000000010053b7f0
    (lldb) p person
    (Person *) $1 = 0x000000010053b7f0
    (lldb) expr -o -- person
    <Person: 0x10053b7f0>
    
    (lldb) po person
    <Person: 0x10053b7f0>
    
    • p 做进制转换
    //默认打印为10进制
    (lldb) p 10
    (int) $0 = 10
    //转16进制
    (lldb) p/x 10
    (int) $1 = 0x0000000a
    //转8进制
    (lldb) p/o 10
    (int) $2 = 012
    //转二进制
    (lldb) p/t 10
    (int) $3 = 0b00000000000000000000000000001010
    //字符转10进制数字
    (lldb) p/d 'A'
    (char) $4 = 65
    //10进制数字转字符
    lldb) p/c 66
    (int) $5 = B\0\0\0
    
  • call

    • 方法调用,可以"理解"为expression
  • thread

    • thread backtracebt
      • 打印线程的堆栈 信息
    (lldb) thread backtrace 1
    thread #1, queue = 'com.apple.main-thread', stop reason = step over
    * frame #0: 0x0000000100000e02 TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:30:5
    frame #1: 0x00007fff6b47fcc9 libdyld.dylib`start + 1
    (lldb) thread backtrace -c 1
    * thread #1, queue = 'com.apple.main-thread', stop reason = step over
    frame #0: 0x0000000100000e02 TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:30:5
    (lldb) bt 10
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    * frame #0: 0x0000000100000df1 TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:28:13
    frame #1: 0x00007fff6b47fcc9 libdyld.dylib`start + 1
    
    • thread return[]

      • 让函数返回某个值,不会执行断点后面的代码
    • thread contunuecontinuec

      • 程序继续运行,过掉当前断点
    • thread step-overnextn

      • 单步运行,把自函数当作整体一步执行
    • thread step-insteps

      • 单步运行,遇到子函数会进入子函数
    • thread step-inst-overnextini

      • 汇编级别单步运行,把自函数当作整体一步执行
    • thread step-inststepisi

      • 汇编单步运行,遇到子函数会进入子函数
    • thread list

      • 列出当前线程列表
      (lldb) thread list
      Process 4500 stopped
      * thread #1: tid = 0x33d9a, 0x0000000100000e02 TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:30:5, queue = 'com.apple.main-thread', stop reason = step over
      
    • thread select

      • 选取某个线程作为后续命令的默认线程
      (lldb) thread select 1
      
  • frame

    • frame select
      • 根据下标选择堆栈列表中的某帧,此时会跳转到汇编页面,即使没有设置Always Show Disassembly
      (lldb) bt
      * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
      * frame #0: 0x0000000100000deb TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:37:5
      frame #1: 0x00007fff6ce68cc9 libdyld.dylib`start + 1
      (lldb) frame select 1
      frame #1: 0x00007fff6ce68cc9 libdyld.dylib`start + 1
      libdyld.dylib`start:
      ->  0x7fff6ce68cc9 <+1>: movl   %eax, %edi
      0x7fff6ce68ccb <+3>: callq  0x7fff6ce7c82e            ; symbol stub for: exit
      0x7fff6ce68cd0 <+8>: hlt    
      0x7fff6ce68cd1 <+9>: nop  
      
    • frame info
      • 显示当前帧信息
      (lldb) bt
      * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
      * frame #0: 0x0000000100000deb TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:37:5
      frame #1: 0x00007fff6ce68cc9 libdyld.dylib`start + 1
      (lldb) frame info
      frame #0: 0x0000000100000deb TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:37:5
      (lldb) frame select 1
      frame #1: 0x00007fff6ce68cc9 libdyld.dylib`start + 1
      libdyld.dylib`start:
      ->  0x7fff6ce68cc9 <+1>: movl   %eax, %edi
      0x7fff6ce68ccb <+3>: callq  0x7fff6ce7c82e            ; symbol stub for: exit
      0x7fff6ce68cd0 <+8>: hlt    
      0x7fff6ce68cd1 <+9>: nop    
      (lldb) frame info
      frame #1: 0x00007fff6ce68cc9 libdyld.dylib`start + 1
      
    • frame v
      • frame v 变量名
      • frame v -r 变量名
      • frame v -R 变量名
      • frame v -a 变量名
    (lldb) fr v b
    (Int??) b = nil
    (lldb) fr v -r b
    (Int??) b = nil
    (lldb) fr v -R b
    (Swift.Optional<Swift.Optional<Swift.Int>>) b = some {
      some = none {
        some = {
            _value = 0
        }
      }
    }
    (lldb) fr v -a b
    (Int??) b = nil
    
  • up、down

    • up
      • 移动当前帧(序号加1)
    • down
      • 移动当前帧(序号减1)
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    * frame #0: 0x0000000100000ddf TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:36:5
    frame #1: 0x00007fff6ce68cc9 libdyld.dylib`start + 1
    (lldb) up
    frame #1: 0x00007fff6ce68cc9 libdyld.dylib`start + 1
    libdyld.dylib`start:
    ->  0x7fff6ce68cc9 <+1>: movl   %eax, %edi
    0x7fff6ce68ccb <+3>: callq  0x7fff6ce7c82e            ; symbol stub for: exit
    0x7fff6ce68cd0 <+8>: hlt    
    0x7fff6ce68cd1 <+9>: nop    
    (lldb) up
    error: Already at the top of the stack.
    (lldb) down
    frame #0: 0x0000000100000ddf TestWeak`main(argc=1, argv=0x00007ffeefbff530) at main.m:36:5
    33       person.age = 5;
    34       
     35       CGFloat width = 10;
    -> 36       testFunction();
            ^
    37       NSLog(@"hello");
    38       return 0;
    39   }
    (lldb) down
    error: Already at the bottom of the stack.
    
  • beakpoint

    • breakpoint set
      • 设置断点
      • breakpoint set -a 函数地址
      • breakpoint set -n 函数名
      • breakpoint set -r 正则表达式
      • breakpoint set -s 动态库 -n 函数名
    (lldb) b -[Person getInt]
    Breakpoint 2: where = TestWeak`-[Person getInt] + 30 at main.m:30:24, address = 0x0000000100001a6e
    (lldb) b +[Person aaa]
    Breakpoint 3: where = TestWeak`+[Person aaa] + 23 at main.m:27:5, address = 0x0000000100001a37
    (lldb) c
    Process 2344 resuming
    (lldb) c
    Process 2344 resuming
    
    • breakpoint list
      • 列出所有的断点,展示的断点信息后的数字为断点编号
    (lldb) breakpoint list
    Current breakpoints:
    1: file = '/Users/liu_david/Desktop/TestWeak/TestWeak/main.m', line = 39, exact_match = 0, locations = 1, resolved = 1, hit count = 1
    
    1.1: where = TestWeak`main + 41 at main.m:39:5, address = 0x0000000100001d19, resolved, hit count = 1 
    
    2: file = '/Users/liu_david/Desktop/TestWeak/TestWeak/main.m', line = 30, exact_match = 0, locations = 1, resolved = 1, hit count = 0
    
    2.1: where = TestWeak`-[Person getInt] + 30 at main.m:30:24, address = 0x0000000100001a6e, resolved, hit count = 0 
    
    3: file = '/Users/liu_david/Desktop/TestWeak/TestWeak/main.m', line = 27, exact_match = 0, locations = 1, resolved = 1, hit count = 0
    
    3.1: where = TestWeak`+[Person aaa] + 23 at main.m:27:5, address = 0x0000000100001a37, resolved, hit count = 0 
    
    • breakpoinst disable 断点编号

      • 禁用
    • breakpoinst enable 断点编号

      • 启用
    • breakpoinst delete 断点编号

      • 删除断点
    • breakpoint command add 断点编号

      • 给断点预先设置需要执行的命令,触发断点时执行
    • breakpoint command list 断点编号

      • 查看某个断点设置的命令
    • breakpoint command delete 断点编号

      • 删除某个断点设置的命令
  • watchpoint

    • 内存断点,内存数据发生改变的时候触发
    • watchpoint set variable 变量
    watchpoint set variable self->age
    
    • watchpoint set expression 地址
    watchpoint set expression &(self->age)
    
    • watchpoint list
      • 列出所有的断点,展示的断点信息后的数字为断点编号
    • watchpoint disable 断点编号
      • 禁用
    • watchpoint enable 断点编号
      • 启用
    • watchpoint delete 断点编号
      • 删除
    • watchpoint command add 断点编号
      • 给断点预先设置需要执行的命令,触发断点时执行
    • watchpoint command list 断点编号
      • 查看某个断点设置的命令
    • watchpoint command delete 断点编号
      • 删除某个断点设置的命令
  • image

    • image lookup

      • image lookup -t 类型
        • 查找某个类型的信息
      (lldb) image lookup -type Person
      Best match found in /Users/liu_david/Library/Developer/Xcode/DerivedData/TestWeak-egnzdbndwsiikvcheqmcxvkqnwbw/Build/Products/Debug/TestWeak:
      id = {0x10000002b}, name = "Person", byte-size = 24, decl = main.m:12, compiler_type = "@interface Person : NSObject{
      BOOL _isMan;
      short _age;
      NSString * _name;
      }
      @property(nonatomic, copy, readwrite, getter = name, setter = setName:) NSString *name;
      @property(nonatomic, assign, readwrite, getter = age, setter = setAge:) short age;
      @property(nonatomic, assign, readwrite, getter = isMan, setter = setIsMan:) BOOL isMan;
      @end"
      
      • image lookup -a 地址
        • 根据内存地址查找在模块中的位置
      2020-09-16 00:41:45.605355+0800 TestWeak[2106:87796] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 4 beyond bounds [0 .. 2]'
      *** First throw call stack:
      (
      0   CoreFoundation                      0x00007fff32e79b57 __exceptionPreprocess + 250
      1   libobjc.A.dylib                     0x00007fff6bcc05bf objc_exception_throw + 48
      2   CoreFoundation                      0x00007fff32f2859e -[__NSCFString characterAtIndex:].cold.1 + 0
      3   CoreFoundation                      0x00007fff32deab70 +[NSNull null] + 0
      4   TestWeak                            0x0000000100001b8f -[Person getInt] + 287
      5   TestWeak                            0x0000000100001d73 main + 99
      6   libdyld.dylib                       0x00007fff6ce68cc9 start + 1
      7   ???                                 0x0000000000000001 0x0 + 1
      )
      libc++abi.dylib: terminating with uncaught exception of type NSException
      (lldb) image lookup --address 0x0000000100001b8f
      Address: TestWeak[0x0000000100001b8f] (TestWeak.__TEXT.__text + 335)
      Summary: TestWeak`-[Person getInt] + 287 at main.m:31:13
      (lldb) image lookup -a 0x0000000100001b8f
      Address: TestWeak[0x0000000100001b8f] (TestWeak.__TEXT.__text + 335)
      Summary: TestWeak`-[Person getInt] + 287 at main.m:31:13
      
      • image lookup -n 符号或者函数名
        • 查找某个符号或者函数的位置
      (lldb) image lookup getInt
      error: invalid combination of options for the given command
      (lldb) image lookup -name getInt
      1 match found in /Users/liu_david/Library/Developer/Xcode/DerivedData/TestWeak-egnzdbndwsiikvcheqmcxvkqnwbw/Build/Products/Debug/TestWeak:
      Address: TestWeak[0x0000000100001a70] (TestWeak.__TEXT.__text + 48)
      Summary: TestWeak`-[Person getInt] at main.m:29
      1 match found in /usr/lib/libicucore.A.dylib:
      Address: libicucore.A.dylib[0x000000000003b046] (libicucore.A.dylib.__TEXT.__text + 239078)
      Summary: libicucore.A.dylib`icu::ResourceBundle::getInt(UErrorCode&) const
      1 match found in /System/Library/Frameworks/Security.framework/Versions/A/Security:
      Address: Security[0x000000000012260e] (Security.__TEXT.__text + 1184206)
      Summary: Security`Security::Context::getInt(unsigned int, int) const
      1 match found in /System/Library/Frameworks/SceneKit.framework/Versions/A/SceneKit:
      Address: SceneKit[0x000000000024f396] (SceneKit.__TEXT.__text + 2414070)
      Summary: SceneKit`getInt(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >&, int&, bool, bool)
      1 match found in /usr/lib/libTelephonyUtilDynamic.dylib:
      Address: libTelephonyUtilDynamic.dylib[0x0000000000012e10] (libTelephonyUtilDynamic.dylib.__TEXT.__text + 69904)
      Summary: libTelephonyUtilDynamic.dylib`ctu::cf::map_adapter::getInt(__CFString const*, int) const
      1 match found in /System/Library/PrivateFrameworks/CorePrediction.framework/Versions/A/CorePrediction:
      Address: CorePrediction[0x00000000000475c4] (CorePrediction.__TEXT.__text + 286980)
      Summary: CorePrediction`-[CPMLEvalutionResult getInt]
      
    • image list

      • image list
        • 列出所家在的模块信息
      (lldb) image list
      [  0] 9D17C7F5-7D6B-387D-81E5-1C7ED33709BE 0x0000000100000000 /Users/liu_david/Library/Developer/Xcode/DerivedData/TestWeak-egnzdbndwsiikvcheqmcxvkqnwbw/Build/Products/Debug/TestWeak 
      [  1] F9D4DEDC-8296-3E3F-B517-9C8B89A4C094 0x0000000100009000 /usr/lib/dyld 
      [  2] 7C69F845-F651-3193-8262-5938010EC67D 0x00007fff35437000 /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation 
      [  3] 6DF81160-5E7F-3E31-AA1E-C875E3B98AF6 0x00007fff6bcad000 /usr/lib/libobjc.A.dylib 
      [  4] C0C9872A-E730-37EA-954A-3CE087C15535 0x00007fff69e4d000 /usr/lib/libSystem.B.dylib 
      [  5] C0D70026-EDBE-3CBD-B317-367CF4F1C92F 0x00007fff32d7a000 /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation 
      [  6] B6124448-7690-34AE-8939-ED84AAC630CE 0x00007fff6a04f000 /usr/lib/libauto.dylib 
      
      • image list -o -f
        • 打印出模块的偏移地址、全路径
    • x

      • 内存读取
      (lldb) x person
      0x10053a6b0: 5d 22 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  ]"..............
      0x10053a6c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
      (lldb) x/4gx person
      0x10053a6b0: 0x001d80010000225d 0x0000000000000000
      0x10053a6c0: 0x0000000000000000 0x0000000000000000
      (lldb) x/3wx person
      0x10053a6b0: 0x0000225d 0x001d8001 0x00000000
      (lldb) x &width
      0x7ffeefbff4f0: 00 00 00 00 00 00 00 00 b0 a6 53 00 01 00 00 00  ..........S.....
      0x7ffeefbff500: 30 f5 bf ef fe 7f 00 00 01 00 00 00 00 00 00 00  0...............
      (lldb) x/4gx &width
      0x7ffeefbff4f0: 0x0000000000000000 0x000000010053a6b0
      0x7ffeefbff500: 0x00007ffeefbff530 0x0000000000000001
      (lldb) x/4go
      0x7ffeefbff510: 03777735757772440
      0x7ffeefbff518: 03777755321776311
      0x7ffeefbff520: 00
      0x7ffeefbff528: 01
      

      x是读取内存的命令,x/4gx中第一个x是读取内存命令,后面的g是每次读取8字节,x的意思是16进制显示结果,4表示连续打印4段

      • 对于g,常用的大小格式为b对应byte 1字节,h对应half word 2字节,w对应word 4字节,g对应giant word 8字节
      • 对于x,我们还可以用o对应8机制,b对应2进制,x对应16进制,f对应浮点,d对应10进制

上面介绍的是OC用法,swift请参考这里

Xcode动态调试原理

一张图片

  • debugserver一开始存放在Mac的Xcode中

    • /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/DeviceSupport/9.1/DeveloperDiskImage.dmg/usr/bin/debugserver
  • 当Xcode中识别到手机设备时,Xcode会自动将debugserver安装到iPhone上

    • 手机中的路径为/Developer/usr/bin/debugserver
  • Xcode调试局限性

    • 一般只能调试通过Xcode安装的APP

动态调试任意APP

debugserver的权限问题

  • 默认情况下,/Developer/usr/bin/debugserver缺少一定的权限,只能调试通过Xcode安装的APP,无法调试其他APP
  • 如果希望调试其他APP,需要对debugserver重签名,签上2个调试权限
    • get-task-allow
    • task_for_pid-allow

如何给debugserver签上权限

  • iPhone上的/Developer目录是只读的,无法直接对/Developer/usr/bin/debugserver文件签名,需要先把debugserver复制到Mac
  • 通过ldid命令导出文件以前的签名权限
ldid -e debugserver > debugserver.entitlements
  • 给debugserver.plist文件加上get-task-allow和task_for_pid_allow权限
    一张图片

  • 通过ldid命令重新签名

ldid -Sdebugserver.entitlements debugserver
  • 将签好权限的debugserver放到/usr/bin目录
  • 也可使用codesign签名
# 查看权限信息
$ codesign -d --entitlements - debugserver
# 签名权限
$ codesign -f -s - --entitlements debugserver.entitlements debugserver
# 或者简写
$ codesign -fs- --entitlements debugserver.entitlements debugserver

让debugserver附加到某个APP进程

debugserver *:端口号 -a 进程
  • *.端口号

    • 使用iPhone的某个端口启动debugserver服务
  • -a 进程

    • 输入APP的进程信息(ID或者名称)

在Mac上启动LLDB,远程连接iPhone上的debugserver

  • 启动LLDB
$ lldb
(lldb)
  • 连接debugserver服务
(lldb) process connect connect://手机IP地址:debugserver端口号
  • 使用LLBDB c命令让程序继续运行
(lldb) c
  • 接着就可以使用LLDB调试APP

通过debugserver启动APP

$ debugserver -x auto *:端口号 APP的可执行路径