03-探究iOS底层原理|LLDB【命令结构、查询命令、断点设置、流程控制、模块查询、内存读写、chisel插件】

3,844 阅读16分钟

前言

之前,我们在探索动画及渲染相关原理的时候,我们输出了几篇文章,解答了iOS动画是如何渲染,特效是如何工作的疑惑。我们深感系统设计者在创作这些系统框架的时候,是如此脑洞大开,也 深深意识到了解一门技术的底层原理对于从事该方面工作的重要性。

因此我们决定 进一步探究iOS底层原理的任务 ,本文探索的底层原理围绕“LLDB【命令结构、查询命令、断点设置、流程控制、模块查询、内存读写、chisel插件】”展开

一、概述

工欲善其事必先利其器,我们要探索iOS的底层原理,需要掌握一定的前知识,如:

本文的核心目标就是对LLDB进行简单介绍 且 对其常用的命令进行适当的实践

1.什么是LLDB?

LLDB官网上介绍LLDB如下: image.png

  • LLDB是Xcode在macOS上的默认调试器,支持在桌面、iOS设备和模拟器上调试C、Objective-C和c++。
  • LLDB 是作为一组可重用组件构建的,高度利用了较大的LLVM项目中的现有库,如Clang表达式解析器和LLVM反汇编器。
  • LLDB 是一个有着 REPL 的特性和 C++ 、Python 插件的开源调试器

2.LLDB 命令结构

<command> [<subcommand> [<subcommand>...]] <action> [-options [option- value]] [argument [argument...]]
对应:

  • 1 <command>: 命令
  • 2 <subcommand>: 子命令
  • 3 <action>: 命令操作
  • 4 [-options [option- value]]: 命令选项
  • 5 [argument [argument...]]: 命令参数`

其中:

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

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

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

3.LLDB原始命令

LLDB支持不带命令选项的原始命令

  • 原始命令会将命令后面的所有东西当做参数(arguement)处理

  • 但很多原始命令也可以带命令选项,当你使用命令选项的时候,需要在命令选项后面加 -- 区分命令选项和参数。  如:expression (就是 p/print/call)、expression -o(就是 po),打印一个UIView对象地址:

image.png

 前者是计算其地址的值,后者调用了对象的 description 方法,其中体现了唯一匹配原则:

  • 假如根据前n个字母已经能唯一匹配到某个命令
    • 则只写前n个字母等效于写下完整的命令
    • 再用设置断点的命令举例,下面两条命令等效:

image.png image.png image.png 更多命令结构的介绍及用法,请参考 LLDB 入门
接下来介绍点 LLDB 常用命令

4.LLDB查询命令

4.1 apropos

 当我们并不能完全记得某个命令的时候,使用 apropos 通过命令中的某个关键字就可以找到所有相关的命令信息。

  • 比如: 我们想使用stop-hook的命令,但是已经不记得stop-hook命令是啥样了: image.png 我们也可以查看官方文档的介绍: LLDB command map

4.2help指令 查询 指令用法

除了 apropos、查看官方网文档LLDB command map,我们也可以通过 help命令 快速查找 LLDB指令的用法,示例如下:

help breakpoint
help breakpoint set 

image.png

二.断点设置指令|breakpointwatchpoint

 通过调试器断点指令操作断点:罗列 断点列表使断点 失效/生效删除/设置 断点

1.breakpoint 相关指令

breakpoint(可简写成br)

1.1 breakpoint set

= br set

设置断点

  • br set -a 函数地址:给函数设置断点 image.png image.png
  • br set -n 函数名:给函数设置断点
  • breakpoint set -n test:给全局的test方法设置断点
  • breakpoint set -n touchesBegan:withEvent::给全局的touchesBegan:withEvent:方法设置断点 image.png
  • breakpoint set -n "-[ViewController touchesBegan:withEvent:]":给 ViewControllertouchesBegan:withEvent:方法设置断点
  • breakpoint set -r 正则表达式正则表达式匹配上的方法设置断点
    • 此处跟上正则表达式会将所有匹配到的方法都加上断点
  • breakpoint set -s 动态库 -n 函数名:将指定动态库指定函数打上断点
  • 其它: breakpoint 支持按文件名、函数名、行数、正则等各种条件筛选设置断点。详情参考官方文档:image.png 示例: br set -r testSel:遍历整个项目中包含 testSel 这个字符的所有方法并设置断点

image.png

1.2 breakpoint list

= br list :列出所有的断点,每个断点都有单独的编号 image.png image.png

image.png

1.3 breakpoint disable 断点ID

= br disable 断点ID : 禁用断点 image.png

1.4 breakpoint enable 断点ID

= br enable 断点ID : 启用断点

image.png

1.5 breakpoint delete 断点ID

= br delete 断点ID : 删除断点

1.6 breakpoint command add 断点ID

= br command add 断点ID : 给指定断点编号的断点预先设置需要执行1条或者多条命令,到触发断点时,就会按顺序执行预先设置的命,设置多条命令时,用Done表示设置结束

示例: 假设我们需要在ViewController的viewDidLoad中查看self.view的值 我们首先给-[ViewController viewDidLoad]添加一个断点
这里写图片描述

可以看到添加成功之后,这个breakpoint的id为3,然后我们给他增加一个命令:po self.view
这里写图片描述

-o完整写法是–one-liner,表示增加一条命令。3表示对id为3的breakpoint增加命令。 添加完命令之后,每次程序执行到这个断点就可以自动打印出self.view的值了

如果我们一下子想增加多条命令,比如我想在viewDidLoad中打印当前frame的所有变量,但是我们不想让他中断,也就是在打印完成之后,需要继续执行。我们可以这样玩:
这里写图片描述

输入breakpoint command add 5:对 断点编号为5的断点 增加命令。它会让我们输入增加哪些命令,输入’DONE’表示结束。这时候我们就可以输入多条命令了

image.png

image.png

1.7 breakpoint command list 断点编号

= br command list 断点ID :查看某个编号的断点所有预先设置的命令

image.png

1.8 breakpoint command delete 断点编号

= br command delete 断点ID:删除指定编号断点的所有预设命令 image.png

2. 内存断点watchpoint

watchpoint 可简写为 wa
给指定的内存下断点,当内存中的数据发生改变时会触发 image.png

2.1 watchpoint set variable 变量

= wa set variable 变量:对指定的变量设置内存断点,当变量值改变的时候会触发

2.2 watchpoint set expression 内存地址

= wa set expression 内存地址:对指定内存地址设置断点,作用和watchpoint set variable相同

image.png

2.3 watchpoint list

= wa disable list:列出所有的内存断点

image.png

2.4 watchpoint disable 断点ID

= wa disable 断点ID:禁用内存断点

2.5 watchpoint enable 断点ID

= wa enable 断点ID:启用内存断点

2.6 watchpoint delete 断点ID

= wa delete 断点ID:删除内存断点

2.7 watchpoint command add 断点ID

= wa command add 断点ID:给指定断点编号的内存断点预先设置需要执行的命令,到触发内存断点时,就会按顺序执行预先设置的命令

2.8 watchpoint command list 断点编号

= wa command list 断点ID:查看某个编号的内存断点所有预先设置的命令

2.9 watchpoint command delete 断点编号

= wa command delete 断点ID:删除指定编号内存断点的所有预设命令

3.target stop-hook

image.png

image.png

  • target stop-hook add -o "frame variable":添加每次程序 stop 时都希望执行的命令:frame variable(打印当前栈内的所有变量)
  • target stop-hook、watchpoint 的增删改查命令与 breakpoint 的基本相同

image.png

4.其它断点

其它断点玩法需自定义插件支持,在第五段介绍。

三.流程控制

image.png Xcode断点流程控制按钮 如图:

image.png 以下指令从左到右依次表示:指令全称、指令简称、极简指令:

  • 第一个按钮:= 指令process continue/continue/c: 让程序跳过断点继续运行

  • 第二个按钮:单步运行,将子函数当做整体一步执行

    • 源码级别 调试: = thread step-over/next/n
    • 汇编级别 调试: = thread step-inst-over/nexti/ni
  • 第三个按钮:单步运行,遇到子函数会进入子函数

    • 源码级别 调试: = thread step-in/step/s
    • 汇编级别 调试: = thread step-inst-over/stepi/si
  • 第四个按钮:thread step-out/finish: 退出当前帧栈

image.png

image.png

si、ni和s、n指令类似,但是s、n是源码级别,si、ni是汇编指令级别。

  • 每一句OC代码会有一条或多条汇编指令构成
  • s、n指令表示一步一步执行每一句OC代码
  • 而si、ni表示一步一步执行汇编指令

其他命令:

  • thread return:它有一个可选参数,在执行时它会把可选参数加载进返回寄存器里,然后立刻执行返回命令,跳出当前栈帧。
    • 这意味这函数剩余的部分不会被执行。

    • 这会给 ARC 的引用计数造成一些问题,或者会使函数内的清理部分失效。

    • 但是在函数的开头执行这个命令,是个非常好的隔离目标函数、伪造返回值的方式。

四.image模块查询指令(模块:可执行文件&共享库等)

image.png 这些命令在逆向及定位错误时使用频率非常高。

4.1 image list

  1. image list: 列出所有所加载的模块信息
(lldb) image list
[  0] 927601BD-5A00-319C-B8AF-8D501BB5D849 0x0000000100c01000 /Users/pinba/Library/Developer/Xcode/DerivedData/LLDBDemo-dkmvkvkytmalsoensevrvixujugm/Build/Products/Debug-iphonesimulator/LLDBDemo.app/LLDBDemo 
[  1] EEA931D0-403E-3BC8-862A-CBA037DE4A74 0x000000010d352000 /usr/lib/dyld 
[  2] 75369F31-702D-364A-95C3-8AFA9DD4B3A2 0x0000000100c0d000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim 
...... 
  1. image list -o -f: 打印出模块的偏移地址、全路径
lldb) image list -o -f
[  0] 0x0000000000c01000 /Users/pinba/Library/Developer/Xcode/DerivedData/LLDBDemo-dkmvkvkytmalsoensevrvixujugm/Build/Products/Debug-iphonesimulator/LLDBDemo.app/LLDBDemo
[  1] 0x000000010d352000 /usr/lib/dyld
[  2] 0x0000000100c0d000 /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
...... 

4.2 image lookup

  1. image lookup --address 内存地址: 根据内存地址查找在模块中的位置
    • = image lookup -a 内存地址
(lldb) image lookup -a 0x00000001088b7cdb
      Address: LLDBDemo[0x0000000100000cdb] (LLDBDemo.__TEXT.__text + 187)
      Summary: LLDBDemo`-[ViewController touchesBegan:withEvent:] + 123 at ViewController.m:26:15
 
  1. image lookup -v --address 内存地址:查找完整的源代码行信息
    • = image lookup -v -a 内存地址
  2. image lookup --type 类型:查找某个类型的信息
    • = image lookup -t 类型
(lldb) image lookup -t NSUInteger
Best match found in /Users/pinba/Library/Developer/Xcode/DerivedData/LLDBDemo-dkmvkvkytmalsoensevrvixujugm/Build/Products/Debug-iphonesimulator/LLDBDemo.app/LLDBDemo:
id = {0x1000000ab}, name = "NSUInteger", byte-size = 8, decl = NSObjCRuntime.h:13, compiler_type = "typedef NSUInteger"
     typedef 'NSUInteger': id = {0x1000000b8}, name = "long unsigned int", qualified = "unsigned long", byte-size = 8, compiler_type = "unsigned long"
 
  1. image lookup -n 符号或函数名: 查找某个符号或者函数的位置
(lldb) image lookup -n "-[ViewController touchesBegan:withEvent:]"
2 matches found in /Users/pinba/Library/Developer/Xcode/DerivedData/LLDBDemo-dkmvkvkytmalsoensevrvixujugm/Build/Products/Debug-iphonesimulator/LLDBDemo.app/LLDBDemo:
        Address: LLDBDemo[0x0000000100000c60] (LLDBDemo.__TEXT.__text + 64)
        Summary: LLDBDemo`-[ViewController touchesBegan:withEvent:] at ViewController.m:22        Address: LLDBDemo[0x0000000100000c60] (LLDBDemo.__TEXT.__text + 64)
        Summary: LLDBDemo`-[ViewController touchesBegan:withEvent:] at ViewController.m:22
 

五、expression指令

expression指令被用来执行一个表达式

expression self.view.backgroundColor = [UIColor redColor]
//或者
expression -- self.view.backgroundColor = [UIColor redColor] 
  • expression
    • =printpcall 效果等同
  • expression -o
    • =po 效果等同
(lldb) expression -o -- self.view
<UIView: 0x7f8f77c1dde0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x600002098d20>>
(lldb) po self.view
<UIView: 0x7f8f77c1dde0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x600002098d20>> 

expression后的 -- 表示命令选项结束符,表示所有的命令选项已经设置完毕,如果没有命令选项,--可以省略。如果expression之后有命令选项,则--不能省略。

六.内存读写指令

1.格式

  • x是16进制
  • f是浮点
  • d是10进制

2.字节大小

  • b:byte 1字节
  • h:half word 2字节
  • w:word 4字节
  • g:giant word 8字节

3.读取内存

格式:memory read/数量格式字节数 内存地址

  • x/数量-格式-字节大小 内存地址
  • x/3xw 0x10010

4. 修改内存中的值

格式:memory write 内存地址 数值

  • memory write 0x0000010 10

七.寄存器相关的指令

  1. register read:显示当前线程的通用寄存器。 image.png
  2. register write rax 123:将一个新的十进制值“123”写入当前线程寄存器“rax”。

八.其他常用指令

8.1 thread backtrace

= bt :指令的作用是打印线程的堆栈信息

8.2 frame variable

打印当前栈帧的变量

(lldb) frame variable
(ViewController *) self = 0x00007f8f7b0091e0
(SEL) _cmd = "touchesBegan:withEvent:"
(__NSSetM *) touches = 0x00006000020c13e0 1 element
(UITouchesEvent *) event = 0x00006000011c43c0
(lldb) frame variable self
(ViewController *) self = 0x00007f8f7b0091e0 

8.3 LLDB小技巧

  • 每次敲Enter键,都会自动执行上次的命令
  • 绝大部分的指令都可以使用缩写
(lldb) breakpoint list
(lldb) br li
(lldb) br l

(lldb) breakpoint set -n test
(lldb) br s -n test

九.LLDB第三方插件

有一些公司(如Facebook)专门为LLDB写了一些插件,便于提供给开发者更多便捷的调试

推荐插件: facebook 开源的 LLDB 插件 chisel 快捷命令: 它提供的快捷 命令清单及说明

9.1 安装

brew install chisel

9.2 配置

  • 如果没有创建 .lldbinit 文件,则在终端创建文件
$ cd ~
$ touch .lldbinit 
//open .lldbinit 
  • 编辑 .lldbinit 文件
    • 1、找到 chisel 的安装路径
      $ brew list chisel
      
    • 2、找到fbchisellldb.py的路径
      /opt/homebrew/Cellar/chisel/2.0.1/libexec/fbchisellldb.py
      
    • 3、编辑$ vim ~/.lldbinit , 并添加以下内容:
      command script import /opt/homebrew/Cellar/chisel/2.0.1/libexec/fbchisellldb.py
      

9.3 使用

最后wq保存,重启Xcode,就可以使用Chisel了。

9.4 快捷命令

命令命令描述iOSOS X
pviewsPrint the recursive view description for the key window.YESYES
pvcPrint the recursive view controller description for the key window.YESNO
visualizeOpen a UIImage, CGImageRef, UIView, CALayer, NSData (of an image), UIColor, CIColor, or CGColorRef in Preview.app on your Mac.YESNO
fvFind a view in the hierarchy whose class name matches the provided regex.YESNO
fvcFind a view controller in the hierarchy whose class name matches the provided regex.YESNO
show/hideShow or hide the given view or layer. You don't even have to continue the process to see the changes!YESYES
mask/unmaskOverlay a view or layer with a transparent rectangle to visualize where it is.YESNO
border/unborderAdd a border to a view or layer to visualize where it is.YESYES
caflushFlush the render server (equivalent to a "repaint" if no animations are in-flight).YESYES
bmessageSet a symbolic breakpoint on the method of a class or the method of an instance without worrying which class in the hierarchy actually implements the method.YESYES
wivarSet a watchpoint on an instance variable of an object.YESYES
presponderPrint the responder chain starting from the given object.YESYES
............

总结

通过通篇介绍,我们了解了:

  • LLDB命令结构
  • 查询命令
  • 断点设置
  • 流程控制
  • 模块查询
  • 内存读写
  • 第三方LLDB插件:chisel

专题系列文章

1.前知识

2. 基于OC语言探索iOS底层原理

3. 基于Swift语言探索iOS底层原理

关于函数枚举可选项结构体闭包属性方法swift多态原理StringArrayDictionary引用计数MetaData等Swift基本语法和相关的底层原理文章有如下几篇:

其它底层原理专题

1.底层原理相关专题

2.iOS相关专题

3.webApp相关专题

4.跨平台开发方案相关专题

5.阶段性总结:Native、WebApp、跨平台开发三种方案性能比较

6.Android、HarmonyOS页面渲染专题

7.小程序页面渲染专题