LLDB 调试和实践
LLDB
LLDB官方文档教程
- Xcode4.0开始,编译器改用LLVM,调试器从gdb改为LLDB
- LLDB全称Low Level Debugger,轻量级的高性能调试器,默认内置于Xcode中
- 查看已设置的所有断点信息:breakpoint list
- 查看设置断点的帮助文档:help breakpoint set
- -n用来给方法名设置断点,范围是所有相同方法名的文件
- -f用来指定哪个文件,一般是带.m的文件
- -l 数字可以指定哪一行
- -c表示条件断点判断
- -o表示提供一次断点
- 更多help
- help thread
- help breakpoint set
- 设置观察点,类似KVO功能:watchpoint set var self->_testA(当变量发生变化时便会断下来)在观察页面断点,然后输入命令设置观察点,之后继续运行便可开启观察了
- 无论网络返回什么值,变量jumpUrl都固定为某一value:在代码行左边设置断点,然后Edit Breakpoint,Condition为空,Action选择Debugger Command,内容填入:expression jumpUrl = @"https://www.baidu.com" 再勾选上Options的Automatically cotinue即可固定jumpUrl的值
- 列举所有断点:breakpoint list
- 删除断点:breakpoint delete index
- 禁用/开启断点:breakpoint disable/enable index
- 给所有方法名为xx的函数设置断点(C或OC方法都行)
- breakpoint set -name xx
- br s -n xx
- b xx
- 给特定类的某个方法添加断点:breakpoint set -name "-[ClassName test:]"
- 给OC方法添加断点:breakpoint set --selector 方法名(等价于breakpoint set -S 方法名)
- 给某行代码添加断点:breakpoint set -f file.m -l 行数
- 当程序断点调试完后,可用命令continue,让程序继续运行
- 若某方法不想被执行,可以不用注释代码重新运行程序,在方法开始位置加断点,待程序执行到该位置,执行命令:
thread return [<expr>]
,断点后的内容不会执行,Xcode会出现堆栈向上回溯现象,然后点击LLDB continue按钮继续运行,程序不会执行到方法的内容。 - call:调用。提供一个按钮,在按钮方法中设置断点,控制台上键入命令:call [self.view setBackgroundColor:[UIColor redColor]],回车,然后继续执行程序,可以看到控制器背景颜色被修改了
- 进制打印。p默认按十进制输出,p/x按十六进制,p/t按二进制,p/o按八进制
- 打印字符的ASCII:p/d 'A'
- 有时无法打印frame,用:p (CGRect)[self.view frame]
- 打印沙盒路径:po NSHomeDirectory()
- 关于p、po、expersion
- expression完整语法:
expression <cmd-options> -- <expr>
- print简写为p,功能相当于expression --,--的意思是命令的参数终止,跟在--后面的都是命令的表达式、数据变量等。p用来处理基本数据类型
- print object简写为po,功能相当于expression -O -- anObj。po用来处理oc对象类型
- expression完整语法:
- print a输出
$0 = 20
。$
表示变量或对象的引用,我们可以用$0
来操作该变量或对象:
(lldb) print a
(NSInteger) $0 = 20
(lldb) exp $0 = 100
(NSInteger) $9 = 100
(lldb) p a
(NSInteger) $10 = 100
- 流控制命令
- 继续:process continue, continue, c
- 下一步:thread step-over, next, n
- 进入:thread step-in, step, s
- 跳出:thread step-out, finish, f
- 给指定函数地址func_addr位置设置断点:breakpoint set -a func_addr
- frame表示堆栈帧
- thread info:显示断点所在当前线程的当前堆栈信息,即调用对象,方法名称和传入参数
- thread backtrace可看到当前线程(主线程)的堆栈信息,即堆栈帧列表,以frame #编号形式陈列
- frame variable:打印断点所在作用域的所有变量,包括调用对象,方法名称,传入参数和所有临时变量
- frame select number:进入所在堆栈帧查看信息。若该堆栈帧在方法体中,会打印出具体的方法体代码。另外可通过命令down让帧向上走(Xcode左边的show Debug navigator可看到指示帧),命令up让帧向下走来控制帧移动。
接手新项目,不看代码直接锁定控制器
老生常谈的符号断点 for Xcode ,找出你想要的ViewController
- 用符号断点(Symbolic breakpoint)来拦截UIViewController的viewDidLoad(只能是viewDidLoad,因每个控制器几乎都实现了viewDidLoad),可让程序暂停在viewDidLoad的堆栈顺序里,Symbolic中做如下设置:
-[UIViewController viewDidLoad]
- 优化
但这么操作很麻烦,总是要暂停。所以,更好的方式是把断点加在BaseViewController里,然后勾上断点的automatically continue after evaluating actions
选项,Action选项选择Debugger command,填写命令是po self
,就把当前的ViewController名称打印在控制台。
(大多数项目里的ViewController都是继承自BaseViewController,而BaseViewController又是继承自UIViewController的。不设置到UIViewController,是因为控制台里使用po命令找不到self这个标志) - 更新20190710——由于公司项目没有基类控制器BaseViewController,经过大量命令尝试,发现了一个巧妙的方法,也是通过添加Symbolic断点来打印控制器名称,程序无需停下来。Symbolic中Action选择Debugger command,命令为
thread return
thread return
po self
continue
注意千万不能勾选automatically continue,否则app会卡死,continue本身就表示遇到断点也继续执行。
报出exc_bad_access错误
- 这是由于访问已释放的对象(僵尸对象)导致的,定位错误位置,用如下方式:
- Xcode的Product->Scheme->Edit Scheme,设置Run(Debug)的Diagnostics选项的Memory Management,勾选Zombie Object。
其实就是开启僵尸对象检测,默认情况下不要开启,否则系统会检测指针访问的对象是否是僵尸对象,影响效率。记得检测完后关闭它。 - 原理:对象释放时,retainCount为0,使用内置的Zombie对象,替代原来被释放的对象,无论向该对象发送什么消息,都会触发异常,抛出调试信息。
- Xcode的Product->Scheme->Edit Scheme,设置Run(Debug)的Diagnostics选项的Memory Management,勾选Zombie Object。
- 注意:访问已释放对象有时候正常有时候会崩溃,非常危险。之所以有时候正常,是当僵尸对象所在内存空间系统还没分配给别人时,这个时候还是可以访问的,因为对象数据还在。详见:iOS-野指针与僵尸对象
实战
定位点击事件位置的几种方法
点击事件涉及几种情况:tableViewCell、collectionViewCell、UIButton、UIImageView或UIView的手势tab点击,每一种拦截方式都不同,但只要懂得锁定这几种方式的技巧,就能快速找到点击事件的代码位置。
- 先暂停LLDB运行,控制台输入命令"br s -r . -s projectName",然后点击待观察按钮,之后点击LLDB continue execution按钮继续运行,便能在Xcode左边的堆栈列表中找到代码位置。发现该命令是给整个工程的所有方法打了断点,从而实现点击定位(这会引起很多问题,很久之前找到的方式,不要用了,看下面的)。
- 【表格cell点击定位】先暂停LLDB运行,控制台输入命令breakpoint set -S 方法名(注意:方法名是取@selecter()括号中的内容),该命令等价于
breakpoint set --selector 方法名
表格cell点击一般有tableViewCell或collectionViewCell点击,方法名为tableView:didSelectRowAtIndexPath: 或collectionView:didSelectItemAtIndexPath:。若断点设置成功,会输出如下:
Breakpoint 1: 77 locations
以上1表示方法名称标识号,77表示成功设置了77处涉及该方法名称的断点。也可用命令 breakpoint list查看已设置的所有断点
之后点击LLDB continue execution按钮继续运行,然后点击视图便可在Xcode左边的堆栈列表中找到代码位置。如果不需要了,可以重新运行Xcode 或删除该断点:
breakpoint delete 1 【#这里1是上述方法名称的标识号】
更多用法可用命令help breakpoint set查看。
- 【按钮点击定位】添加Symbol断点,Edit breakpoint的Symbol填写-[UIControl sendAction:to:forEvent:] 或用LLDB在控制台添加断点。点击目标按钮,堆栈会停在sendAction:to:forEvent中,LLDB命令
register read
,可查看到21个寄存器的信息,如下所示
(lldb) register read
General Purpose Registers:
rax = 0x00007fc8efc72db0
rbx = 0x00007fc8efc72db0
rcx = 0x00007fc8efc72db0
rdx = 0x000000010e603878 "itemClick:"
rdi = 0x00007fc8efc76a50
rsi = 0x0000000111354ce4 "sendAction:to:forEvent:"
rbp = 0x00007ffee16bb7a0
...
fs = 0x0000000000000000
gs = 0x0000000000000000
在rdx后面可看到点击事件的方法名称itemClick:,此时不要激动,我们还需要找到方法调用者,因为工程中可能存在多个同名方法。怎么操作?答案是LLDB命令po $rax,打印的便是调用者对象,如
(lldb) po $rax
<HomeNavigationGridView: 0x7fc8efc72db0; frame = (0 0; 414 193.2); layer = <CALayer: 0x604000431820>>
如此我们便可知道有[HomeNavigationGridView itemClick:],从而定位到按钮点击事件在程序中的代码位置。
- 【手势点击定位】对于用手势UITapGestureRecognizer添加的点击事件,根据上述步骤操作,用命令-[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:]来定位代码。这个命令是给手势事件打断点在堆栈中获取到的。
之后用LLDB命令po $rax
或po $rdx
便可获取到完整的调用者和方法名称,如
(lldb) po $rax
(action=informationTap:, target=<HotInformationView 0x7fbf72c43e00>)
(lldb) po $rdx
<UITapGestureRecognizer: 0x6000001f1800; state = Ended; view = <HotInformationItem 0x7fbf72e1e970>; target= <(action=informationTap:, target=<HotInformationView 0x7fbf72c43e00>)>>
由此可知有[HotInformationView informationTap:HotInformationItem],从而定位到手势事件的代码
-
可能遇到的问题
。如果该点击事件用来跳转,且跳转后的控制器中含有UIWebView或WKWebView控件,由于该控件内部封装了一些手势,会影响我们的断点定位,可能导致定位失败,一直卡在UIWebView或WKWebView控件内部封装的手势堆栈中。
解决方式:控制器跳转定位。一般控制器跳转都会经过pushViewController: animated:方法,- 若项目继承了UINavigationController自定义导航栏控制器,则可以在自定义导航栏控制器中重写pushViewController: animated:,然后给该方法打断点,则点击时发生控制器跳转会在该方法中停下来,通过左边的堆栈列表信息可以回溯到上一方法,即点击事件和控制器跳转所在位置。
- 若项目没有自定义导航栏控制器,则可以填写Symbolic断点,Edit breakpoint的Symbolic填写命令-[UINavigationController pushViewController:animated:]也能在左边的堆栈列表信息中回溯到上一方法。
-
资料
- XCode LLDB调试小技巧基础篇提高篇汇编篇——精品1.2k,底部有详细的参数--进制对照表
- 小笨狼与LLDB的故事——1.2k
- iOS开发断点调试高级技巧
- Xcode关于LLDB的使用
-
逆向使用lldb调试命令(cycript)——Xcode的
Debug View Hierarchy
能够可视化视图层级,选中按钮,在右边的Show the Object Inspector能看到按钮类型和地址,target,Action(Selector)。选中UIImageView,在Accessibility--Identifier一栏能看到图片名称。 - LLDB调试利器及高级用法
上一篇: 向您发送 lldb 详情!
下一篇: 深入了解 GDB 和 LLDB
推荐阅读
-
实践中的软件测试技术:设计、工具和管理 - 第 2 章第 2.3 节 使用状态转换图设计测试用例
-
腾讯技术分享:GIF动画技术细节和手机QQ动态表情压缩技术实践
-
SEATA 容器部署 NACOS 注册和配置中心、数据库存储实践记录
-
前端安全保护实践:XSS、CSRF 防御和同源策略详情(反应案例)
-
Plankton Embedded:用于飞思卡尔 Kinetis 开发板的 OpenSDA 调试器(第 1 部分)--背景和架构
-
stm32 HardFault_Handler 调试和问题查找方法 - Freescale
-
飞思卡尔 16 位微控制器(II)--CodeWarrior 开发环境设置和新项目、调试项目
-
35 岁实现财务*,腾讯程序员手握2300万提前退休?-1000万房产、1000万腾讯股票、加上300万的现金,一共2300万的财产。有网友算了一笔账,假设1000万的房产用于自住,剩下1300万资产按照平均税后20-50万不等进行计算,大约花上26-60年左右的时间才能赚到这笔钱。也就是说,普通人可能奋斗一辈子,才能赚到这笔钱。在很多人还在为中年危机而惶惶不可终日的时候,有的人的35岁,就已经安全着陆,试问哪个打工人不羡慕?但问题是有这样财富积累必然有像样的实力做靠山。没有人可以不劳而获。 看到这里,肯定有人说,那么对于普通人来说,卷可能真就成了唯一的出路。但是卷也有轻松的卷,“偷懒”的卷法,对于程序员而言,刨除掉一时无法改掉的开会传统占用的大部分时间,如何把有限的时间和精力放在真正重要的架构设计、需求设计上,而不是重复的造*,编码、改bug、手动测试。因此在科技改变生活的今天,学会使用AI工具成为程序员们的必备技能。 以全栈式全自动的软件开发工具飞算SoFlu软件机器人为例,作为全球首款面向微服务架构设计和最佳实践的软件机器人,SoFlu软件机器人改变了原来手工编码的作业模式,通过可视化拖拽方式以及参数配置就能实现等同于编写复杂代码的业务逻辑,在设计业务逻辑时就完成了微服务应用开发,做到“业务即图,图即代码”,实现“软件开发,十倍提效”。 而作为飞算SoFlu软件机器人的一个重要组成部分,FuncGPT(慧函数)支持所有类型函数创建。通过自然语言描述Java函数需求,实时生成高质量、高可读性的Java函数代码。生成代码可直接复制到IDEA,或一键导入Java全自动开发工具函数库。 FuncGPT(慧函数)具有五大核心能力: ● 自然语言:通过自然语言即可生成函数,降低软件开发门槛。 ● 秒级函数开发:全自动秒级开发函数,秒级完成,效率千倍提升。 ● 一站式函数开发应用:函数生成即可直接应用,效率、安全有保障。 ● 质量可靠:生成的函数代码符合业界规范、具备良好的可读性,符合最优解。 ● 灵活生成:可以根据开发人员的具体需求生成并支持调整和修改。 以“将文件流转换为File对象”这个需求为例,以下是FuncGPT(慧函数)秒级生成的代码: / 类名:FileConverter // 函数名:convertStreamToFile // 函数功能:将文件流转换为File对象 // POM依赖包:无 import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; public class FileConverter { /** * 将文件流转换为File对象 * @param inputStream 文件流 * @param filePath 文件路径 * @param fileName 文件名 * @return 转换后的File对象 * @throws IOException 如果发生I/O异常 */ public static File convertStreamToFile(InputStream inputStream, String filePath, String fileName) throws IOException { File file = new File(filePath + File.separator + fileName); // 创建File对象 try (FileOutputStream outputStream = new FileOutputStream(file)) { // 创建文件输出流 byte buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { // 从文件流读取数据并写入文件 outputStream.write(buffer, 0, bytesRead); } } return file; // 返回转换后的File对象 } } // 函数示例 // 将文件流转换为File对象示例 // 入参:inputStream,文件流 // 入参:filePath,文件路径 // 入参:fileName,文件名 // 出参:file,转换后的File对象 // 调用示例: // InputStream inputStream = new FileInputStream("example.txt"); // String filePath = "C:\\Users\\User\\Documents"; // String fileName = "example.txt"; // File file = FileConverter.convertStreamToFile(inputStream, filePath, fileName); // System.out.println(file.getAbsolutePath); // 输出结果:例如,将文件流转换为File对象后,文件的绝对路径为:C:\Users\User\Documents\example.txt // 则输出结果为:C:\Users\User\Documents\example.txt 通过分析,不难发现以上代码:
-
茁壮成长首选监测方案的定时数据库选择和登陆实践
-
机器人系统 ros2-开发实践 04-ROS2 中 tf2 的定义和示例说明