iOS Developer寒哥管理的技术专题iOS

advanced LLDB debugging

2015-10-21  本文已影响1053人  纵横而乐

作为session debugging with lldb的后续篇,session 413 advanced debugging with lldb的目的在于为了让玩转LLDB以尽调试之能事来进行一些提纲挈领的引导。值得注意的是lldb的功能不仅限于为你提供流畅的调试服务,以帮你从烦心的bug中解脱出来,而且能够作为你强有力的助手去探寻app华丽UI之下汹涌的暗流,而这一切都是为了制作出更加可靠的app。

众所周知LLDB是作为GDB的替代者身份出现的,而其设计之初就是从底层基础即可弹性扩展以适应今后的开发,从xcode5开始,LLDB就开始作为内置调试器跟随xcode发布。

除了对原有调试功能的优化之外,现在的LLDB相比于以前更加增强了数据监控功能:包括对标准系统类型,foundation类型,C++标准库类型及unicode text等信息在观察的时候体验 需要再一次更新了,以前你可以搜寻到这些信息,但现在你想看到的信息就在你的眼前。

表达式解析器也得到了更新,虽然随着语言特性的更新它也一直在更新,而我们遵从一个原则性的指导:即我们希望用声明来作为解析器。在LLDB中有一个健全的编译器,正常的源代码也能够在LLDB中正常工作,任何新的语言特性都会在LLDB中得到支持。

一个讨巧的事情是在以前如果你输入了一个表达式之后,需要显式地对result进行类型转换为其类型,以供LLDB在当前的上下文中进行探索定位。而现在LLDB可以更多地对表达式进行推理,从而省去了很多显式类型转换的书写。

用LLDB之前你要知道的

最佳的调试体验需要对调试器有很全面的了解,那就先来了解下吧。除了之后在着重介绍的观察技巧比如:assertion,logging,static analysis,runtime memory tools之外,也需要重点关注单元测试,因为单元测试可以告诉你应用的哪些地方是有问题的。除了这些之外,xcode的debug配置也是很重要的,通常包括将生成调试信息的选项开启,及关闭优化等,以供调试的顺利进行。

避免常见的错误

如果想断点到某种场景,则最佳的做法不是先随意打个断点然后一直单步到出问题的那行代码,而是充分利用LLDB的特性以一次性定位到你所感兴趣的代码。同时可以自定义你所想看到的数据,因为LLDB默认是只输出系统类型的,而并不认识自定义的数据类型,所以需要告诉LLDB你所关注的自定义数据类型及其数据。同时为了避免重新构建所带来的时间开销,你需要学会编写调试代码,以改变应用的执行路径,并修改数据,比如初始化还未初始代的数据。

同时需要注意的是,你所输入并执行的表达式会改变应用原本的执行路径,所以对此所带来的副作用你需要有清晰的把控。

正确的调试过程

step 1 搞清楚希望通过LLDB知道的是什么

step 2 在可疑的地方停下来

step 3 在正在执行的代码中一步步执行

step 4 观察数据并验证猜想

什么时候用LLDB,LLDB能在调试的时候怎样帮助你

发现bug时,LLDB并不是你的第一选择,而应该是debug-Only assertion,它能帮助你知道应用中是否发生了不可能发生的事情,以及各组件之间传递的参数是否与约定的一致。而同时需要注意的是不要在assertion中做对应用逻辑有影响的操作,因为一旦构建发布版本,这些assertion都会被屏幕,你在其中执行的操作也就不会进行了。

而对于在运行中各处都看起来很正常,最终去呈现了错误的结果的情况,log会是一个非常有用的工具。而此处的log指的并不是NSLog,而是apple system log(ASL),其可以通过console观察到。ASL可以通过log的level来区分log的严重程序,比如ASL_LEVEL_EMERG和ASL_LEVEL_DEBUG。同时还可以附带使用hash tag以方便log的查找,比如为特定的业务添加特定的业务字符串等。而鉴于log可能被滥用,所以ASL可以通过开关提供某些log是否应该进行。正常的开关方式可以是可以使用NSUserDefaults等,或者也可以在shell中设置一个变量,程序在运行的时候读取此值以决定是否进行log。

Xcode可以为程序做的,-Weverything和静态代码分析器可以在代码运行前的编译阶段即发现可能存在的问题。可以在之前的session: What's New In LLVM和What's New In the LLVM Compiler中找到相关内容。而在运行期间可以通过Guard Malloc发现堆上缓存overrun的问题,通过zombie objects获取对释放了的对象进行方法调用的问题。具体可以参见Advanced Memory Analysis with Instruments

LLDB的正确打开方式

现在正式开始LLDB之旅,它有两种操作方式,一种是通过xcode上的按钮,另一种是通过console中的lldb调试语言。LLDB命令有3种形式,discoverable,abbreviated及alias形式,分别是演示如下:expression --object-description -- foo, e -0 -- foo,po foo。表达的是一个意思,但书写繁琐程度递减,而且可以定义自己的alias 形式的命令。

断点的设置方式有多种:

如果知道代码行号,可以直接在代码界面点击代码行首处,命令像这样: b MyCode.m:4(breakpoint set --file MyCode.m --line 4)

如果不知道代码行号,则可以使用这样的命令断点在某方法处:b "-[MyClass method:]"(breakpoint set --name "-MyClass method:]")

如果想在任何对象收到特定selector时断点,可以这样b method:(breakpoint set --selector method:)

省时的命令

鉴于在app及xcode之间因为多次中断而进行的多次切换,可以在断点发生之后执行特定的命令,比如查看希望查看的数据之后再立即恢复执行,可以这样

b "-[MyClass method:]"

br co a/breakpoint command add

>p rect/expression rect

>bt/thread backtrace

>c/process continue

>DONE

这些命令也可以在xcode面板中通过点击及输入完成同样的功能。

条件断点

如果不希望断点频繁触发,可以通过条件断点来达到此目的,比如想在某个特定对象析构的消息处断点,可以这样:

p id $myModel = self/expression id $myModel = self

b "-[MyClass dealloc]"/breakpoint set  "-[MyClass dealloc]"

br m -c "self == $myModel"/breakpoint modify --condition "self == $myModel"

通过watchpoint监控特定内存空间

watchpoint的应用场景在于有人会修改某个变量值,你对此很关心但只知道变量的地址,其行为方式是如果变量被访问则watchpoint会暂停app的运行,设置watchpoint的方式是

w s v self->_needsSynchronization/watchpoint set variable self->_needsSynchronization

受限于CPU的支持程度,在intel平台上,提供了4个slot供watchpoint使用,所以同时只可设置最多4个watchpoint,arm上是2个

watchpoint也可以在xcode的控制面板中进行操作,只需要在变量区域中右击某变量选择菜单中的watchpoint选项即可

避免不断单步的高招

LLDB可以在两种场景下暂停程序的执行:

1 执行到程序的具体某行代码的时候

   这种场景的实用命令是thread until linenum,避免一步步单步执行到希望的行,在xcode中这个功能对应的操作是右击代码行选择continue to this line。

2 函数返回之后

LLDB维护调试操作的上下文

如果单步过程中命中�函数中的断点,则直接continue会继续单步到下一行

LLDB维护调试操作的上下文

如上图中在单步到第9行时,恰好其中removeDuplicates这个selector中也存在一个断点,此时选择stepover,会停到这个selector中的断点上,但这时候不要惊慌,使用continue命令此时会恰好断点在第10行上。

LLDB中手动执行代码

很多时候你可能会发现,要想让你希望执行的代码执行一遍会很困难,比如单元测试的时候某个用例就是无法进行测试,这种情况下你需要用Clang(['kl^n])直接调用这份代码,调用方式是直接在命令行表达式中输入你希望执行的代码并执行,比如

b "-[ModelDerived removeDuplicates]"

e -i false -- [self removeDuplicates]/expression --ignore-breakpoints false -- [self removeDuplicates]

先在方法上打断点,然后在LLDB中执行此函数,选择不要忽略断点,你会发现执行此expression之后会断点到removeDuplicates,接着即可对其进行执行。

然而有一点需要特别注意的是,通过LLDB执行的表达式代码是在你的进程中执行的,所以需要对此所带来的后果有自己的认知。

检查数据以找寻事情的缘由

在上述的操作之后,我们已经可以断点到我们希望断点的位置了,接着就是检查数据寻找事情发生的原因了,这部分有3方面内容:

在LLDB命令行中检查数据

  查看局部变量: frame variable

  执行任意代码: expression (x+35) 其会通过app使用的编译器进行编译并在你的app中执行

  p @"hello" 兼容expression的语法,执行表达式并输出结果

  po @"hello" 执行任意代码并输出结果的description 

LLDB实用数据格式

  需要先搞清楚raw data和data的区别,raw data就是内存中所存储的数据,但它并不易读,对你来说可能太复杂,或者并不是你理解的数据类型,又或者它的数据量很大。如果想对raw data有个直观的印象,只需要在xcode的变量区域选择show raw values就可以在观察任意一个栈帧的时候看到raw data了,而此时切换到show types就可以看到规整而有意义的数据呈现形式了,这就是 LLDB 数据格式所要达到的目的。

对于内置的系统库STL,CoreFoundation,Foundation,其中的数据都已经添加了Data formatter,在调试的时候显示都很规整

对于程序员自定义的数据类型的data formatter,苹果构建了可扩展的data formatter子系统,这意味着程序员也可以为自定义的类型添加data formatter。

自定义data formatter

数据类型的data formatter包括两部分:综述summary,用于呈现数据的关键描述;所组成的子数据即synthetic children

array右端的summary和3个数组synthetic children信息

以使用python定义summary为例,summary会将一种数据类型与一个python函数映射起来,基础的映射是通过类型名,更多其它规则可以参见http://lldb.llvm.org/varformats.html

这个python函数会在此类型的数据在展示的时候被调用,LLDB会将一个SBValue传递给它,SBValue是LLDB对象模型的一部分,可以将其简单地想象成为一个变量,这个python函数最终会返回一个字符串,这个字符串即会被当做summary

SBValue

之前提到SBValue可以当做一个变量来对待,可以询问其name,data type,summary(如果有的话),是否有children,有多少children,是否可以详述每个child的信息,每个child的信息其实也是一个sbvalue,所以整个是一个递归的过程。如果值是一个比如数字这样的标量,整数,浮点数等,也可以询问其value。

对自定义类进行summary

def MyClass_Summary(value,unused)://其中value是一个SBValue

//由于是自己定义的数据,可先获取其中的成员变量,成员变量也是SBValue

member1 = value.GetChildMemberWithName("_member1")

member2 = value.GetChildMemeberWithName("_member2")

member1Summary = member1.GetSummary()

member2Summary = member2.GetSummary()

#当然也可以做任何你想做的事情,这里仅仅只是简单地组合两个成员的summary

return member1Summary + " " + member2Summary

完成了这个python函数之后,变量区域中仍然不能正确显示MyClass的自定义数据类型,因为你还需要在LLDB中执行:

ty su a MyClass -F MyClass_Summary/type summary add MyClass --python-function MyClass_Summary

审视不透明的数据

先介绍下用于数据分析的expression,可以通过如下形式定义一个持久有效的结构体:

 expression struct $NotOpaque{int item1;float item2;char* item3;}

对于第3方库提供的对象,你可能连其数据类型都不知道,更不会知道其中成员变量的定义,可能通过google之后,可以发现其具体的定义,这时候,就需要使用上述expression再结合summary,即可以在展示的时候使用自定义的data formatter了

扩展LLDB

自定义LLDB命令

通过python脚本,可以为调试器添加新特性,实现自定义的操作,自动化的操作过程

比如计算递归的层数,想想LLDB怎么也算是个强大的程序,数数对它来说应该不是什么难事,更加说相比于你手工一个栈帧一个栈帧地数了。

LLDB 对象模型

LLDB的强大在于它所使用的LLDB对象模型,我们称其为"SB"(scripting bridge),这是个python API,xcode用其来构建debugger的UI,这意味着对你可以完全地通过LLDB脚本使用SB的所有功能,同时其也有一套对调试session的描述:

LLDB对象模型中的调试模型

对于上述调试界面相信大家都比较熟悉,LLDB对象模型对其的描述是这样:SBTarget即是调试中的target,接着在点击了xcode中的运行按钮之后,这个target成为了一个活着的实体,对这个实体,你可以输入,点哪,点哪,点,这即是在机器底层上运行的进程称为SBProcess,进程有着很多用来完成任务的thread,即SBThread,而SBThread会不停地执行function,每个function都会而每次function调用都会对应栈上的一帧,即SBFrame,现在我们已经了解到了描述程序运行中的所有对象,接下来看看怎样完成我们想完成的任务。

首先需要知道的是python命令是如何执行的呢,python函数是与LLDB中的命令一一对应的,LLDB看到这个命令的时候即会调用相应的python命令,python命令的原型是这样

def MyCommand_Impl(debugger,user_input,result,unused):

第一个参数debugger是一个SBDebugger

user_input是用户输入的python 字符串

result是SBCommandReturnObject,是用来反馈给LLDB的,反馈执行成功与否等信息

添加自定义的命令的方式如下:

co sc a foo -f foo/command script add foo --python-function foo

计算递归深度 完整的计算递归深度的python代码

断点操作

断点的痛点在于它会不停在中断程序的执行,条件断点会好一点,有了断点action,我们可以只在自己关注的场景停下来

断点action是将断点与一个python函数联系起来,断点命中的时候会调用此python函数,而其可以返回false以勾选断点编辑界面中的continue选择框以让LLDB继续运行

此python函数的原型是

def break_on_deep_traversal(frame,bp_loc,unused):

frame 类型为SBFrame

bp_loc类型为SBBreakpointLocation

绑定python函数的命令是

br co a -s p -F foo 1 /breakpoint command add --script python --python-function foo 1

以递归过深时需要停止 这个功能为例

根据递归深度判断是否需要暂停程序执行

lldbinit

苹果提供了这么多可以定制的功能,如果一旦重启xcode所有的自定义都回到解放前,我肯定欲哭无泪,不过别担心,自定义可以固化在一个LLDB的配置文件中位于~/.lldbinit,在每个debug session启动时,都会加载此文件,在里面可以很方便地调整调试器设置,在里面也可以加载常用的脚本。如果只想在用xcode启动lldb的时候才进行这些加载,则你需要的文件是~/.lldbinit-Xcode,在.lldbinit文件中可以进行操作比如command script import 添加自定义的python文件,及命名自定义名字的lldb命令 command script add -f pythonmodule.funcname cmdname

后注:

Debug-only Assertions(NS_BLOCK_ASSERTIONS在开发版中屏蔽assert)

NSAssert(_dictionary != nil,@"_dictionary should be initialized")

其实在Xcode 7.1中设置条件断点时,需要显式地将表达式转换为BOOL型,比如添加条件为 [view clipsToBounds],未添加显式类型转换时,断点时会报错:

error: no known method '-clipsToBounds:'; cast the message send to the method's return type

解决方案是�添加显式类型转换

上一篇下一篇

猜你喜欢

热点阅读