作為session debugging with lldb的后續(xù)篇惫撰,session 413 advanced debugging with lldb的目的在于為了讓玩轉(zhuǎn)LLDB以盡調(diào)試之能事來(lái)進(jìn)行一些提綱挈領(lǐng)的引導(dǎo)。值得注意的是lldb的功能不僅限于為你提供流暢的調(diào)試服務(wù)顶籽,以幫你從煩心的bug中解脫出來(lái)葵腹,而且能夠作為你強(qiáng)有力的助手去探尋app華麗UI之下洶涌的暗流高每,而這一切都是為了制作出更加可靠的app屿岂。
眾所周知LLDB是作為GDB的替代者身份出現(xiàn)的,而其設(shè)計(jì)之初就是從底層基礎(chǔ)即可彈性擴(kuò)展以適應(yīng)今后的開(kāi)發(fā)鲸匿,從xcode5開(kāi)始爷怀,LLDB就開(kāi)始作為內(nèi)置調(diào)試器跟隨xcode發(fā)布。
除了對(duì)原有調(diào)試功能的優(yōu)化之外带欢,現(xiàn)在的LLDB相比于以前更加增強(qiáng)了數(shù)據(jù)監(jiān)控功能:包括對(duì)標(biāo)準(zhǔn)系統(tǒng)類型运授,foundation類型,C++標(biāo)準(zhǔn)庫(kù)類型及unicode text等信息在觀察的時(shí)候體驗(yàn) 需要再一次更新了乔煞,以前你可以搜尋到這些信息吁朦,但現(xiàn)在你想看到的信息就在你的眼前。
表達(dá)式解析器也得到了更新渡贾,雖然隨著語(yǔ)言特性的更新它也一直在更新逗宜,而我們遵從一個(gè)原則性的指導(dǎo):即我們希望用聲明來(lái)作為解析器。在LLDB中有一個(gè)健全的編譯器剥啤,正常的源代碼也能夠在LLDB中正常工作锦溪,任何新的語(yǔ)言特性都會(huì)在LLDB中得到支持不脯。
一個(gè)討巧的事情是在以前如果你輸入了一個(gè)表達(dá)式之后府怯,需要顯式地對(duì)result進(jìn)行類型轉(zhuǎn)換為其類型,以供LLDB在當(dāng)前的上下文中進(jìn)行探索定位防楷。而現(xiàn)在LLDB可以更多地對(duì)表達(dá)式進(jìn)行推理牺丙,從而省去了很多顯式類型轉(zhuǎn)換的書(shū)寫(xiě)。
用LLDB之前你要知道的
最佳的調(diào)試體驗(yàn)需要對(duì)調(diào)試器有很全面的了解复局,那就先來(lái)了解下吧冲簿。除了之后在著重介紹的觀察技巧比如:assertion,logging亿昏,static analysis峦剔,runtime memory tools之外,也需要重點(diǎn)關(guān)注單元測(cè)試角钩,因?yàn)閱卧獪y(cè)試可以告訴你應(yīng)用的哪些地方是有問(wèn)題的吝沫。除了這些之外,xcode的debug配置也是很重要的递礼,通常包括將生成調(diào)試信息的選項(xiàng)開(kāi)啟惨险,及關(guān)閉優(yōu)化等,以供調(diào)試的順利進(jìn)行脊髓。
避免常見(jiàn)的錯(cuò)誤
如果想斷點(diǎn)到某種場(chǎng)景辫愉,則最佳的做法不是先隨意打個(gè)斷點(diǎn)然后一直單步到出問(wèn)題的那行代碼,而是充分利用LLDB的特性以一次性定位到你所感興趣的代碼将硝。同時(shí)可以自定義你所想看到的數(shù)據(jù)恭朗,因?yàn)長(zhǎng)LDB默認(rèn)是只輸出系統(tǒng)類型的屏镊,而并不認(rèn)識(shí)自定義的數(shù)據(jù)類型,所以需要告訴LLDB你所關(guān)注的自定義數(shù)據(jù)類型及其數(shù)據(jù)冀墨。同時(shí)為了避免重新構(gòu)建所帶來(lái)的時(shí)間開(kāi)銷闸衫,你需要學(xué)會(huì)編寫(xiě)調(diào)試代碼,以改變應(yīng)用的執(zhí)行路徑诽嘉,并修改數(shù)據(jù)蔚出,比如初始化還未初始代的數(shù)據(jù)。
同時(shí)需要注意的是虫腋,你所輸入并執(zhí)行的表達(dá)式會(huì)改變應(yīng)用原本的執(zhí)行路徑骄酗,所以對(duì)此所帶來(lái)的副作用你需要有清晰的把控。
正確的調(diào)試過(guò)程
step 1 搞清楚希望通過(guò)LLDB知道的是什么
step 2 在可疑的地方停下來(lái)
step 3 在正在執(zhí)行的代碼中一步步執(zhí)行
step 4 觀察數(shù)據(jù)并驗(yàn)證猜想
什么時(shí)候用LLDB,LLDB能在調(diào)試的時(shí)候怎樣幫助你
發(fā)現(xiàn)bug時(shí)悦冀,LLDB并不是你的第一選擇趋翻,而應(yīng)該是debug-Only assertion,它能幫助你知道應(yīng)用中是否發(fā)生了不可能發(fā)生的事情盒蟆,以及各組件之間傳遞的參數(shù)是否與約定的一致踏烙。而同時(shí)需要注意的是不要在assertion中做對(duì)應(yīng)用邏輯有影響的操作,因?yàn)橐坏?gòu)建發(fā)布版本历等,這些assertion都會(huì)被屏幕讨惩,你在其中執(zhí)行的操作也就不會(huì)進(jìn)行了。
而對(duì)于在運(yùn)行中各處都看起來(lái)很正常寒屯,最終去呈現(xiàn)了錯(cuò)誤的結(jié)果的情況荐捻,log會(huì)是一個(gè)非常有用的工具。而此處的log指的并不是NSLog寡夹,而是apple system log(ASL)处面,其可以通過(guò)console觀察到。ASL可以通過(guò)log的level來(lái)區(qū)分log的嚴(yán)重程序菩掏,比如ASL_LEVEL_EMERG和ASL_LEVEL_DEBUG魂角。同時(shí)還可以附帶使用hash tag以方便log的查找,比如為特定的業(yè)務(wù)添加特定的業(yè)務(wù)字符串等智绸。而鑒于log可能被濫用野揪,所以ASL可以通過(guò)開(kāi)關(guān)提供某些log是否應(yīng)該進(jìn)行。正常的開(kāi)關(guān)方式可以是可以使用NSUserDefaults等传于,或者也可以在shell中設(shè)置一個(gè)變量囱挑,程序在運(yùn)行的時(shí)候讀取此值以決定是否進(jìn)行l(wèi)og。
Xcode可以為程序做的沼溜,-Weverything和靜態(tài)代碼分析器可以在代碼運(yùn)行前的編譯階段即發(fā)現(xiàn)可能存在的問(wèn)題平挑。可以在之前的session: What's New In LLVM和What's New In the LLVM Compiler中找到相關(guān)內(nèi)容。而在運(yùn)行期間可以通過(guò)Guard Malloc發(fā)現(xiàn)堆上緩存overrun的問(wèn)題通熄,通過(guò)zombie objects獲取對(duì)釋放了的對(duì)象進(jìn)行方法調(diào)用的問(wèn)題唆涝。具體可以參見(jiàn)Advanced Memory Analysis with Instruments
LLDB的正確打開(kāi)方式
現(xiàn)在正式開(kāi)始LLDB之旅,它有兩種操作方式唇辨,一種是通過(guò)xcode上的按鈕廊酣,另一種是通過(guò)console中的lldb調(diào)試語(yǔ)言。LLDB命令有3種形式赏枚,discoverable,abbreviated及alias形式亡驰,分別是演示如下:expression --object-description -- foo, e -0 -- foo饿幅,po foo凡辱。表達(dá)的是一個(gè)意思,但書(shū)寫(xiě)繁瑣程度遞減栗恩,而且可以定義自己的alias 形式的命令透乾。
斷點(diǎn)的設(shè)置方式有多種:
如果知道代碼行號(hào),可以直接在代碼界面點(diǎn)擊代碼行首處磕秤,命令像這樣: b MyCode.m:4(breakpoint set --file MyCode.m --line 4)
如果不知道代碼行號(hào)乳乌,則可以使用這樣的命令斷點(diǎn)在某方法處:b "-[MyClass method:]"(breakpoint set --name "-MyClass method:]")
如果想在任何對(duì)象收到特定selector時(shí)斷點(diǎn),可以這樣b method:(breakpoint set --selector method:)
省時(shí)的命令
鑒于在app及xcode之間因?yàn)槎啻沃袛喽M(jìn)行的多次切換市咆,可以在斷點(diǎn)發(fā)生之后執(zhí)行特定的命令汉操,比如查看希望查看的數(shù)據(jù)之后再立即恢復(fù)執(zhí)行,可以這樣
b "-[MyClass method:]"
br co a/breakpoint command add
>p rect/expression rect
>bt/thread backtrace
>c/process continue
>DONE
這些命令也可以在xcode面板中通過(guò)點(diǎn)擊及輸入完成同樣的功能床绪。
條件斷點(diǎn)
如果不希望斷點(diǎn)頻繁觸發(fā)客情,可以通過(guò)條件斷點(diǎn)來(lái)達(dá)到此目的其弊,比如想在某個(gè)特定對(duì)象析構(gòu)的消息處斷點(diǎn)癞己,可以這樣:
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"
通過(guò)watchpoint監(jiān)控特定內(nèi)存空間
watchpoint的應(yīng)用場(chǎng)景在于有人會(huì)修改某個(gè)變量值,你對(duì)此很關(guān)心但只知道變量的地址梭伐,其行為方式是如果變量被訪問(wèn)則watchpoint會(huì)暫停app的運(yùn)行痹雅,設(shè)置watchpoint的方式是
w s v self->_needsSynchronization/watchpoint set variable self->_needsSynchronization
受限于CPU的支持程度,在intel平臺(tái)上糊识,提供了4個(gè)slot供watchpoint使用绩社,所以同時(shí)只可設(shè)置最多4個(gè)watchpoint,arm上是2個(gè)
watchpoint也可以在xcode的控制面板中進(jìn)行操作赂苗,只需要在變量區(qū)域中右擊某變量選擇菜單中的watchpoint選項(xiàng)即可
避免不斷單步的高招
LLDB可以在兩種場(chǎng)景下暫停程序的執(zhí)行:
1 執(zhí)行到程序的具體某行代碼的時(shí)候
? ?這種場(chǎng)景的實(shí)用命令是thread until linenum愉耙,避免一步步單步執(zhí)行到希望的行,在xcode中這個(gè)功能對(duì)應(yīng)的操作是右擊代碼行選擇continue to this line拌滋。
2 函數(shù)返回之后
LLDB維護(hù)調(diào)試操作的上下文
如果單步過(guò)程中命中函數(shù)中的斷點(diǎn)朴沿,則直接continue會(huì)繼續(xù)單步到下一行
如上圖中在單步到第9行時(shí),恰好其中removeDuplicates這個(gè)selector中也存在一個(gè)斷點(diǎn),此時(shí)選擇stepover赌渣,會(huì)停到這個(gè)selector中的斷點(diǎn)上魏铅,但這時(shí)候不要驚慌,使用continue命令此時(shí)會(huì)恰好斷點(diǎn)在第10行上坚芜。
LLDB中手動(dòng)執(zhí)行代碼
很多時(shí)候你可能會(huì)發(fā)現(xiàn)览芳,要想讓你希望執(zhí)行的代碼執(zhí)行一遍會(huì)很困難,比如單元測(cè)試的時(shí)候某個(gè)用例就是無(wú)法進(jìn)行測(cè)試鸿竖,這種情況下你需要用Clang(['kl^n])直接調(diào)用這份代碼沧竟,調(diào)用方式是直接在命令行表達(dá)式中輸入你希望執(zhí)行的代碼并執(zhí)行,比如
b "-[ModelDerived removeDuplicates]"
e -i false -- [self removeDuplicates]/expression --ignore-breakpoints false -- [self removeDuplicates]
先在方法上打斷點(diǎn)缚忧,然后在LLDB中執(zhí)行此函數(shù)屯仗,選擇不要忽略斷點(diǎn),你會(huì)發(fā)現(xiàn)執(zhí)行此expression之后會(huì)斷點(diǎn)到removeDuplicates搔谴,接著即可對(duì)其進(jìn)行執(zhí)行魁袜。
然而有一點(diǎn)需要特別注意的是,通過(guò)LLDB執(zhí)行的表達(dá)式代碼是在你的進(jìn)程中執(zhí)行的敦第,所以需要對(duì)此所帶來(lái)的后果有自己的認(rèn)知峰弹。
檢查數(shù)據(jù)以找尋事情的緣由
在上述的操作之后,我們已經(jīng)可以斷點(diǎn)到我們希望斷點(diǎn)的位置了芜果,接著就是檢查數(shù)據(jù)尋找事情發(fā)生的原因了鞠呈,這部分有3方面內(nèi)容:
在LLDB命令行中檢查數(shù)據(jù)
? 查看局部變量: frame variable
? 執(zhí)行任意代碼: expression (x+35) 其會(huì)通過(guò)app使用的編譯器進(jìn)行編譯并在你的app中執(zhí)行
? p @"hello" 兼容expression的語(yǔ)法,執(zhí)行表達(dá)式并輸出結(jié)果
? po @"hello" 執(zhí)行任意代碼并輸出結(jié)果的description?
LLDB實(shí)用數(shù)據(jù)格式
? 需要先搞清楚raw data和data的區(qū)別右钾,raw data就是內(nèi)存中所存儲(chǔ)的數(shù)據(jù)蚁吝,但它并不易讀,對(duì)你來(lái)說(shuō)可能太復(fù)雜舀射,或者并不是你理解的數(shù)據(jù)類型窘茁,又或者它的數(shù)據(jù)量很大。如果想對(duì)raw data有個(gè)直觀的印象脆烟,只需要在xcode的變量區(qū)域選擇show raw values就可以在觀察任意一個(gè)棧幀的時(shí)候看到raw data了山林,而此時(shí)切換到show types就可以看到規(guī)整而有意義的數(shù)據(jù)呈現(xiàn)形式了,這就是 LLDB 數(shù)據(jù)格式所要達(dá)到的目的。
對(duì)于內(nèi)置的系統(tǒng)庫(kù)STL,CoreFoundation,Foundation,其中的數(shù)據(jù)都已經(jīng)添加了Data formatter券腔,在調(diào)試的時(shí)候顯示都很規(guī)整
對(duì)于程序員自定義的數(shù)據(jù)類型的data formatter亿笤,蘋果構(gòu)建了可擴(kuò)展的data formatter子系統(tǒng),這意味著程序員也可以為自定義的類型添加data formatter。
自定義data formatter
數(shù)據(jù)類型的data formatter包括兩部分:綜述summary,用于呈現(xiàn)數(shù)據(jù)的關(guān)鍵描述;所組成的子數(shù)據(jù)即synthetic children
以使用python定義summary為例明也,summary會(huì)將一種數(shù)據(jù)類型與一個(gè)python函數(shù)映射起來(lái),基礎(chǔ)的映射是通過(guò)類型名,更多其它規(guī)則可以參見(jiàn)http://lldb.llvm.org/varformats.html
這個(gè)python函數(shù)會(huì)在此類型的數(shù)據(jù)在展示的時(shí)候被調(diào)用诡右,LLDB會(huì)將一個(gè)SBValue傳遞給它安岂,SBValue是LLDB對(duì)象模型的一部分,可以將其簡(jiǎn)單地想象成為一個(gè)變量帆吻,這個(gè)python函數(shù)最終會(huì)返回一個(gè)字符串域那,這個(gè)字符串即會(huì)被當(dāng)做summary
SBValue
之前提到SBValue可以當(dāng)做一個(gè)變量來(lái)對(duì)待,可以詢問(wèn)其name,data type,summary(如果有的話),是否有children,有多少children,是否可以詳述每個(gè)child的信息,每個(gè)child的信息其實(shí)也是一個(gè)sbvalue猜煮,所以整個(gè)是一個(gè)遞歸的過(guò)程次员。如果值是一個(gè)比如數(shù)字這樣的標(biāo)量,整數(shù)王带,浮點(diǎn)數(shù)等淑蔚,也可以詢問(wèn)其value。
對(duì)自定義類進(jìn)行summary
def MyClass_Summary(value,unused)://其中value是一個(gè)SBValue
//由于是自己定義的數(shù)據(jù)愕撰,可先獲取其中的成員變量刹衫,成員變量也是SBValue
member1 = value.GetChildMemberWithName("_member1")
member2 = value.GetChildMemeberWithName("_member2")
member1Summary = member1.GetSummary()
member2Summary = member2.GetSummary()
#當(dāng)然也可以做任何你想做的事情,這里僅僅只是簡(jiǎn)單地組合兩個(gè)成員的summary
return member1Summary + " " + member2Summary
完成了這個(gè)python函數(shù)之后搞挣,變量區(qū)域中仍然不能正確顯示MyClass的自定義數(shù)據(jù)類型带迟,因?yàn)槟氵€需要在LLDB中執(zhí)行:
ty su a MyClass -F MyClass_Summary/type summary add MyClass --python-function MyClass_Summary
審視不透明的數(shù)據(jù)
先介紹下用于數(shù)據(jù)分析的expression,可以通過(guò)如下形式定義一個(gè)持久有效的結(jié)構(gòu)體:
?expression struct $NotOpaque{int item1;float item2;char* item3;}
對(duì)于第3方庫(kù)提供的對(duì)象囱桨,你可能連其數(shù)據(jù)類型都不知道仓犬,更不會(huì)知道其中成員變量的定義,可能通過(guò)google之后舍肠,可以發(fā)現(xiàn)其具體的定義搀继,這時(shí)候,就需要使用上述expression再結(jié)合summary翠语,即可以在展示的時(shí)候使用自定義的data formatter了
擴(kuò)展LLDB
自定義LLDB命令
通過(guò)python腳本叽躯,可以為調(diào)試器添加新特性,實(shí)現(xiàn)自定義的操作啡专,自動(dòng)化的操作過(guò)程
比如計(jì)算遞歸的層數(shù)险毁,想想LLDB怎么也算是個(gè)強(qiáng)大的程序制圈,數(shù)數(shù)對(duì)它來(lái)說(shuō)應(yīng)該不是什么難事们童,更加說(shuō)相比于你手工一個(gè)棧幀一個(gè)棧幀地?cái)?shù)了。
LLDB 對(duì)象模型
LLDB的強(qiáng)大在于它所使用的LLDB對(duì)象模型鲸鹦,我們稱其為"SB"(scripting bridge)慧库,這是個(gè)python API,xcode用其來(lái)構(gòu)建debugger的UI馋嗜,這意味著對(duì)你可以完全地通過(guò)LLDB腳本使用SB的所有功能齐板,同時(shí)其也有一套對(duì)調(diào)試session的描述:
對(duì)于上述調(diào)試界面相信大家都比較熟悉,LLDB對(duì)象模型對(duì)其的描述是這樣:SBTarget即是調(diào)試中的target,接著在點(diǎn)擊了xcode中的運(yùn)行按鈕之后甘磨,這個(gè)target成為了一個(gè)活著的實(shí)體橡羞,對(duì)這個(gè)實(shí)體,你可以輸入济舆,點(diǎn)哪卿泽,點(diǎn)哪,點(diǎn)滋觉,這即是在機(jī)器底層上運(yùn)行的進(jìn)程稱為SBProcess签夭,進(jìn)程有著很多用來(lái)完成任務(wù)的thread,即SBThread椎侠,而SBThread會(huì)不停地執(zhí)行function第租,每個(gè)function都會(huì)而每次function調(diào)用都會(huì)對(duì)應(yīng)棧上的一幀,即SBFrame我纪,現(xiàn)在我們已經(jīng)了解到了描述程序運(yùn)行中的所有對(duì)象慎宾,接下來(lái)看看怎樣完成我們想完成的任務(wù)。
首先需要知道的是python命令是如何執(zhí)行的呢浅悉,python函數(shù)是與LLDB中的命令一一對(duì)應(yīng)的璧诵,LLDB看到這個(gè)命令的時(shí)候即會(huì)調(diào)用相應(yīng)的python命令,python命令的原型是這樣
def MyCommand_Impl(debugger,user_input,result,unused):
第一個(gè)參數(shù)debugger是一個(gè)SBDebugger
user_input是用戶輸入的python 字符串
result是SBCommandReturnObject仇冯,是用來(lái)反饋給LLDB的之宿,反饋執(zhí)行成功與否等信息
添加自定義的命令的方式如下:
co sc a foo -f foo/command script add foo --python-function foo
斷點(diǎn)操作
斷點(diǎn)的痛點(diǎn)在于它會(huì)不停在中斷程序的執(zhí)行,條件斷點(diǎn)會(huì)好一點(diǎn)苛坚,有了斷點(diǎn)action比被,我們可以只在自己關(guān)注的場(chǎng)景停下來(lái)
斷點(diǎn)action是將斷點(diǎn)與一個(gè)python函數(shù)聯(lián)系起來(lái),斷點(diǎn)命中的時(shí)候會(huì)調(diào)用此python函數(shù)泼舱,而其可以返回false以勾選斷點(diǎn)編輯界面中的continue選擇框以讓LLDB繼續(xù)運(yùn)行
此python函數(shù)的原型是
def break_on_deep_traversal(frame,bp_loc,unused):
frame 類型為SBFrame
bp_loc類型為SBBreakpointLocation
綁定python函數(shù)的命令是
br co a -s p -F foo 1 /breakpoint command add --script python --python-function foo 1
以遞歸過(guò)深時(shí)需要停止 這個(gè)功能為例
lldbinit
蘋果提供了這么多可以定制的功能等缀,如果一旦重啟xcode所有的自定義都回到解放前,我肯定欲哭無(wú)淚娇昙,不過(guò)別擔(dān)心尺迂,自定義可以固化在一個(gè)LLDB的配置文件中位于~/.lldbinit,在每個(gè)debug session啟動(dòng)時(shí)冒掌,都會(huì)加載此文件噪裕,在里面可以很方便地調(diào)整調(diào)試器設(shè)置,在里面也可以加載常用的腳本股毫。如果只想在用xcode啟動(dòng)lldb的時(shí)候才進(jìn)行這些加載膳音,則你需要的文件是~/.lldbinit-Xcode,在.lldbinit文件中可以進(jìn)行操作比如command script import 添加自定義的python文件铃诬,及命名自定義名字的lldb命令 command script add -f pythonmodule.funcname cmdname
后注:
Debug-only Assertions(NS_BLOCK_ASSERTIONS在開(kāi)發(fā)版中屏蔽assert)
NSAssert(_dictionary != nil,@"_dictionary should be initialized")
其實(shí)在Xcode 7.1中設(shè)置條件斷點(diǎn)時(shí)祭陷,需要顯式地將表達(dá)式轉(zhuǎn)換為BOOL型苍凛,比如添加條件為 [view clipsToBounds],未添加顯式類型轉(zhuǎn)換時(shí)兵志,斷點(diǎn)時(shí)會(huì)報(bào)錯(cuò):
error: no known method '-clipsToBounds:'; cast the message send to the method's return type
解決方案是添加顯式類型轉(zhuǎn)換