工欲善其事必先利其器 --《論語·衛(wèi)靈公》
一個好的IDE不僅要提供舒適簡潔和方便的源代碼編輯環(huán)境,還要提供功能強大的調(diào)試環(huán)境性昭。XCODE是目前來說對iOS應用開發(fā)支持的最好的IDE(雖然Visual Studio2017也開始支持iOS應用的開發(fā)了)拦止,畢竟XCODE和iOS都是蘋果公司的親生兒子。唯一要吐槽的就是系統(tǒng)和編譯環(huán)境綁的太死了糜颠,每當手機操作系統(tǒng)的一個小升級汹族,都需要去升級一個好幾G的新版本程序,這確實是有點坑爹其兴!
目前市面上有很多反編譯的工具顶瞒,比如IDA、Hopper Disassembler等還有操作系統(tǒng)自帶的工具諸如otool元旬、lldb榴徐。這些工具里面有的擅長靜態(tài)分析有的擅長調(diào)試的,這里就不展開分析了匀归。如果在程序運行時去窺探一些系統(tǒng)內(nèi)部實現(xiàn)以及做實時調(diào)試分析我覺得XCODE本身也非常的棒坑资,既然深入系統(tǒng)我們必須要了解和學習一些關(guān)于匯編的東西,那么就必須要了解和掌握一些工具穆端,而XCODE其實就是你手頭上最方便的工具之一袱贮。
XCODE的匯編模式切換
你是否在聯(lián)機運行時因為系統(tǒng)崩潰而出現(xiàn)過如下的畫面:
不要慌!它其實就是XCODE的匯編模式的界面体啰。我們不僅在程序崩潰時可以看到它攒巍,我們也可以人為的進入到這個界面模式里面。這篇文章更像是一個XCODE工具使用上的一些介紹狡赐,您可以經(jīng)常在使用它們,也可能還從來沒有接觸和了解過它們钦幔。對于匯編代碼和源代碼之間的切換可以通過菜單:Debug -> Debug Workflow -> Always Show Disassembly 來完成枕屉。
記得要設(shè)置有斷點并運行到斷點處時切換才能看到匯編指令啊鲤氢!
上一篇文章深入iOS系統(tǒng)底層之指令集介紹中我們有說過模擬器上運行的是Intel指令搀擂,而真機上運行的是arm指令西潘,在這里我們分別看模擬器和真機下的匯編指令的差異性:
通過上面三張圖你會發(fā)現(xiàn)其中的源代碼和匯編代碼之間有很大的差異,以及不同指令集下的匯編代碼之間也有很大的差異哨颂!匯編代碼的差異其實就是不同CPU上運行的指令的差異喷市。還記得前一篇文章所說的指令集嗎?前者是在模擬器上運行的所以展示的是x64的指令威恼,而后者是在真機上運行的因此展示的是arm64指令品姓。通過圖片對比你能否發(fā)現(xiàn)他們之間的相同點和差異嗎?
- 系統(tǒng)所有的代碼都是由一個個的函數(shù)或者說方法組成棺禾,即使是類中定義的方法以及Block里面的方法也是如此惦界。在編譯時系統(tǒng)將所有定義的函數(shù)方法依次編譯鏈接為機器指令并保存到文件的代碼段中谭贪,一個函數(shù)內(nèi)的機器指令是連續(xù)存儲的,但是函數(shù)之間卻不一定是連續(xù)存儲的植酥。
- 上面的圖片中每條匯編指令都和一條機器指令唯一對應,這里要注意的是雖然顯示的是匯編代碼弦牡,但是真實存儲和運行的還是機器代碼友驮,只不過我們通過匯編代碼來展示能夠容易閱讀和理解而已。
- 每條指令前面的地址表示的是這條指令在運行時所處在的內(nèi)存地址驾锰。也許你會問指令不是在CPU上嗎卸留?沒有錯,指令雖然是在CPU上執(zhí)行稻据,但是存儲還是要在內(nèi)存或者磁盤上艾猜。CPU上有一個叫ip(Intel)或者pc(arm)的寄存器保存著下一條將要執(zhí)行的指令的內(nèi)存地址,這樣每執(zhí)行一條指令時都是從ip/pc中所指定的內(nèi)存地址讀取出指令并執(zhí)行捻悯,并同時將當前指令的下一條繼續(xù)保存在ip/pc上匆赃,就這樣不停重復的方式來完成指令的執(zhí)行(實際上CPU為了加快處理速度會將一部分內(nèi)存中的指令緩存到CPU的內(nèi)部緩存中去,而不是每條指令都從內(nèi)存中讀取)今缚。
- 每個函數(shù)方法的第一個地址算柳,就是這個函數(shù)的入口地址,也就是說我們進行函數(shù)調(diào)用時姓言,實際上是讓CPU跳轉(zhuǎn)到這個地址并執(zhí)行瞬项,更加具體的就是將ip/pc寄存器的值設(shè)置為這個函數(shù)的入口地址。 對于OC類中的方法來說方法入口地址其實就是這個方法的IMP何荚。
- 在模擬器下你會發(fā)現(xiàn)每條指令的長度是不一樣的囱淋,有1個字節(jié)到7個字節(jié)不等,所以你看到的每條指令的偏移量都不一樣餐塘,而真機時你會發(fā)現(xiàn)每條指令的長度總是固定為4個字節(jié)妥衣。這其實就是CISC和RISC指令集中的一個非常顯著的差別:CISC指令長度不固定而RISC指令則長度固定。你還會發(fā)現(xiàn)模擬器下的匯編代碼數(shù)量要比真機下的匯編代碼數(shù)量要少,這也是CISC指令和RISC指令的差別:CISC指令復雜而且眾多税手,一條指令完成的功能要比RISC多蜂筹;而RISC則指令簡單,因此某些功能需要多條指令來完成芦倒。
- 在匯編模式下的注釋都是由;號開頭的艺挪。大家在通過匯編語言研究內(nèi)部實現(xiàn)時建議看模擬器下的AT&T匯編,原因其實就是模擬器下運行的匯編注釋要比真機模式下的匯編指令要詳細一些兵扬。
- 每條匯編指令的格式總是由: 操作碼麻裳, 操作數(shù)1,操作數(shù)2周霉,操作數(shù)3組成掂器。 操作數(shù)要么就是常數(shù),要么就是寄存儲器俱箱,要么就是內(nèi)存地址国瓮。你所看到的操作數(shù)中的RAX,RSI,RDI,R0,R1... 這些都是CPU中的寄存器(關(guān)于寄存器部分我將在下一篇文章中具體介紹)。而且在XCODE的左下角部分我們可以查看當前CPU中的所有寄存器的值狞谱,你可以打印并修改他們乃摹。
斷點
可能有的同學會說為什么我打開了匯編模式我還是看不到匯編代碼?那是因為你沒有給你的代碼設(shè)置斷點跟衅!什么是斷點孵睬?為什么設(shè)置了斷點程序就會暫停運行? 一般情況下CPU總是按照順序依次執(zhí)行指令并完成任務(wù)伶跷,當正在執(zhí)行某個任務(wù)時如果遇到了特殊事件或者更高優(yōu)先級的任務(wù)時就需要打斷現(xiàn)有執(zhí)行的代碼并去執(zhí)行優(yōu)先級更高的代碼掰读,這種機制就是中斷。中斷有因為外部硬件設(shè)備事件而產(chǎn)生的硬中斷, 同時CPU也提供一個軟中斷指令叭莫。當在代碼里面執(zhí)行一條軟中斷指令時蹈集,程序就會暫停運行,同時CPU把操作權(quán)限提交給操作系統(tǒng)來執(zhí)行中斷處理程序雇初。當我們在程序某處設(shè)置了斷點或者某個指令處設(shè)置斷點時拢肆,系統(tǒng)會將斷點處的指令保存到一個臨時的斷點列表中,同時將斷點處的指令替換為軟中斷指令靖诗,這樣當程序運行到斷點處時因為執(zhí)行的其實是軟中斷指令郭怪,而導致系統(tǒng)調(diào)用的發(fā)生,并執(zhí)行軟中斷處理程序刊橘,軟中斷處理程序等待用戶處理斷點處的操作鄙才,比如當用戶按下的是鍵盤上的Ctrl + F7時,軟中斷處理程序就會把保存在臨時斷點列表中真實斷點處的指令恢復到指定的內(nèi)存促绵,同時把下次要執(zhí)行的指令改為真實的指令攒庵,然后再次執(zhí)行真實的指令据途,這樣就完成了斷點處指令的繼續(xù)執(zhí)行。(要想了解斷點的具體實現(xiàn)叙甸,需要具有一些匯編的知識,這里就不展開了位衩,后面我會在專門的章節(jié)里面詳解介紹斷點的實現(xiàn)原理)裆蒸。
符號斷點
當我們在程序代碼某處設(shè)置了斷點或者指令某處設(shè)置了斷點后,程序執(zhí)行到斷點處時就會暫停下來糖驴。這時候如果我們是在匯編模式下僚祷,您看到的就是匯編程序斷點,而當你在源代碼模式下時贮缕,你看到的將是源代碼斷點辙谜。 除了在代碼處設(shè)置斷點外我們還可以設(shè)置符號斷點。我們先來考察下面3個應用場景:
我們程序的某個視圖的frame值在運行時不知道什么原因總是被莫名其妙的改變了感昼,但是你就是不知道在哪里執(zhí)行了視圖frame的更改設(shè)置装哆。這時候一個解決方法就是重載setFrame方法并設(shè)置斷點來調(diào)試查看frame被何時調(diào)用。
我們的上線程序出現(xiàn)了在某個系統(tǒng)方法被調(diào)用時的crash問題定嗓,但是因為是系統(tǒng)的方法我們無法看到其中的源代碼蜕琴,從而無法進行crash問題分析(比如我們遇到的很多沒有上下文的crash).
假如我懂匯編語言,我想研究一下系統(tǒng)框架的某個方法是如何實現(xiàn)的宵溅。
上面的三個問題我不知道大家會如何去解決凌简? 其實這三種場景我們都可以借助于符號斷點來完成。一般情況下我們可以在源代碼某處設(shè)置斷點來調(diào)試程序恃逻,對于沒有源代碼的情況下我們則可以通過設(shè)置符號斷點來實現(xiàn)程序的調(diào)試和運行雏搂。要設(shè)置符號斷點很簡單。你只需要在XCODE的菜單:Debug -> Breakpoints -> Create Symbolic Breakpoint 或者快捷鍵:option + command + \ 來建立符號斷點:
建立符號斷點后寇损,當某個與符號名相同某個函數(shù)或者方法在執(zhí)行開始前就會產(chǎn)生斷點凸郑,從而可以窺探某個方法的內(nèi)部實現(xiàn)。還可以幫助我們對那些沒有上下文以及非源代碼處產(chǎn)生的崩潰進行分析和重現(xiàn)润绵,從而幫助我們定位問題线椰。下面是運行符號斷點后的我們看到的兩處符號斷點的匯編語言內(nèi)容:
VCTest1`-[ViewController setA:]:
-> 0x1029855e0 <+0>: sub sp, sp, #0x20 ; =0x20
0x1029855e4 <+4>: adrp x8, 4
0x1029855e8 <+8>: add x8, x8, #0x70 ; =0x70
0x1029855ec <+12>: str x0, [sp, #0x18]
0x1029855f0 <+16>: str x1, [sp, #0x10]
0x1029855f4 <+20>: str w2, [sp, #0xc]
0x1029855f8 <+24>: ldr w2, [sp, #0xc]
0x1029855fc <+28>: ldr x0, [sp, #0x18]
0x102985600 <+32>: ldrsw x8, [x8]
0x102985604 <+36>: add x8, x0, x8
0x102985608 <+40>: str w2, [x8]
0x10298560c <+44>: add sp, sp, #0x20 ; =0x20
0x102985610 <+48>: ret
-----------------
libsystem_c.dylib`abs:
-> 0x1813dd984 <+0>: cmp w0, #0x0 ; =0x0
0x1813dd988 <+4>: cneg w0, w0, mi
0x1813dd98c <+8>: ret
你是否看到了屬性setA的內(nèi)部實現(xiàn)以及函數(shù)abs的內(nèi)部實現(xiàn)了?
調(diào)試
調(diào)試程序是一個程序員應該掌握的最基本的工夫尘盼,這里就不介紹其他的詳細的調(diào)試命令以及方法憨愉,其他很多文章里面都有介紹了。主要介紹一下調(diào)試代碼時單步運行的幾個菜單和快捷鍵:
- 源代碼模式下
F7 : 代碼單步執(zhí)行卿捎,當遇到函數(shù)調(diào)用時會跳入函數(shù)內(nèi)部配紫。
F6: 代碼單獨執(zhí)行,當遇到函數(shù)調(diào)用時不會跳入函數(shù)內(nèi)部午阵。
F8: 跳出函數(shù)執(zhí)行躺孝,返回到調(diào)用此函數(shù)的下一句代碼享扔。
- 匯編模式下
control + F7 : 指令單步執(zhí)行,當遇到函數(shù)調(diào)用時會跳入函數(shù)內(nèi)部植袍。
control + F6: 指令單獨執(zhí)行惧眠,當遇到函數(shù)調(diào)用時不會跳入函數(shù)內(nèi)部。
- 多線程之間的切換:
control + shift + F7: 切換到當前線程于个,并執(zhí)行單步指令氛魁。
control + shift + F6: 切換到當前線程,并跳轉(zhuǎn)到函數(shù)調(diào)用的者的下一條指令厅篓。
在調(diào)試運行時當出現(xiàn)斷點時我們可以在lldb命令行中輸入各種調(diào)試命令秀存,其他的不介紹,就單獨介紹一下expr命令羽氮。expr命令其實是p或者po的完整版本或链,通過expr命令除了能夠用來顯示外,還可以用來進行數(shù)據(jù)的修改档押、方法的調(diào)用等強大能力澳盐。下面展示一下一些常用的expr方法:
expr 變量|表達式 //顯示變量或者表達式的值。
expr -f h -- 變量|表達式 //以16進制格式顯示變量或表達式的內(nèi)容
expr -f b -- 變量|表達式 //以二進制格式顯示變量或者表達式的內(nèi)容令宿。
expr -o -- oc對象 //等價于po oc對象
expr -P 3 -- oc對象 //上面命令的加強版本洞就,他還會顯示出對象內(nèi)數(shù)據(jù)成員的結(jié)構(gòu),具體的P后面的數(shù)字就是你要想顯示的層次掀淘。
expr my_struct->a = my_array[3] //給my_struct的a成員賦值旬蟋。
expr (char*)_cmd //顯示某個oc方法的方法名。
expr (IMP)[self methodForSelector:_cmd] //執(zhí)行某個方法調(diào)用.
查看內(nèi)存地址
程序運行時革娄,操作系統(tǒng)為其構(gòu)建出一個進程倾贰,同時構(gòu)建出一個虛擬的內(nèi)存空間。操作系統(tǒng)將進程中的虛擬內(nèi)存空間劃分為代碼存儲區(qū)域拦惋、全局數(shù)據(jù)存儲區(qū)域匆浙、堆存儲區(qū)域、棧存儲區(qū)域等區(qū)域厕妖。每種區(qū)域都有特殊的用途:代碼存儲區(qū)域保存的是程序中的代碼部分(這部分也可稱為映像image)首尼;全局數(shù)據(jù)存儲區(qū)域保存的是一些全局數(shù)據(jù)、常量以及一些描述信息(比如runtime里面的所有OC類的定義描述信息也是存儲在這個區(qū)域中)言秸;堆存儲區(qū)域則用來進行堆內(nèi)存的動態(tài)分配软能;棧存儲區(qū)域則保存著函數(shù)中的局部變量。因此可以看出無論是代碼和數(shù)據(jù)在運行時都保存在內(nèi)存中举畸。每個進程能訪問的內(nèi)存空間的尺寸大小由操作系統(tǒng)決定查排,一般來說32位的操作系統(tǒng)中每個進程的內(nèi)存空間為2^32 = 4GB;而64位的操作系統(tǒng)中每個進程的內(nèi)存空間為2^64 = 4TB抄沮。需要注意的是這個空間是虛擬的可訪問空間并不是真實的物理內(nèi)存可訪問的空間跋核,操作系統(tǒng)內(nèi)部通過分頁映射的方式將虛擬空間轉(zhuǎn)化為真實的物理空間岖瑰。
進程的虛擬內(nèi)存空間是一個可以連續(xù)存儲和訪問的線性空間,為了能夠訪問這些內(nèi)存空間砂代,操作系統(tǒng)為其進行了編碼蹋订,這個編碼就是內(nèi)存的地址。地址也被稱為指針刻伊,因此我們所說的某個變量的指針其實就是這個變量在內(nèi)存中的地址辅辩。為了更好的理解內(nèi)存和地址的概念,你可以將內(nèi)存理解為一個數(shù)組娃圆,而地址則是訪問這個數(shù)組元素時所用到的索引。我們對數(shù)組中元素的讀寫操作總是通過索引進行蛾茉,同樣CPU對內(nèi)存中的數(shù)據(jù)訪問時也是通過內(nèi)存地址進行的讼呢。進程中的內(nèi)存地址總是從0開始編碼,并以字節(jié)為單位進行遞增谦炬,直到虛擬內(nèi)存空間的上限悦屏。
上面說過進程中的代碼和數(shù)據(jù)都保存在內(nèi)存中,當我們要想一覽整個進程內(nèi)存中的代碼和數(shù)據(jù)時键思,你可以在程序運行時通過菜單:Debug -> Debug Workflow -> View Memory 或者通過快捷鍵:shift+command + m 來調(diào)用內(nèi)存查看界面:
上面的圖片剛好展示的是一個類的所有方法名稱在內(nèi)存中的位置和布局础爬。可以看出我們可以很方便的借助查看內(nèi)存地址菜單的功能來了解以及分析代碼以及數(shù)據(jù)在內(nèi)存中的結(jié)構(gòu)吼鳞。你可以在地址輸入欄中輸入你想查看的任意內(nèi)存地址看蚜。比如你想查看某個函數(shù)代碼的機器指令,那么你只需要在匯編模式下將函數(shù)最開始的地址輸入到內(nèi)存查看界面的地址欄中赔桌,那么就會展示出這個函數(shù)代碼的所有機器指令字節(jié)碼供炎。這里還要注意一點的是因為內(nèi)存地址是從低位按字節(jié)依次排列而來,所以對于比如int類型的值的讀取我們就要從高位到低位開始讀取疾党。
計算器 應用
程序調(diào)試時代碼和地址以及一些數(shù)據(jù)都經(jīng)常以16進制的形式顯示音诫。數(shù)據(jù)處理時,尤其是計算地址偏移都以16進制的形式進行展示雪位。你可以在lldb中通過expr或者p命令來計算竭钝。如果你喜歡界面形式的工具,則可以啟動mac OS操作系統(tǒng)中的應用:計算器 來處理各種計算雹洗,你要做的就是在顯示菜單中選擇編程型即可香罐,編程型界面的效果如下(別告訴我作為一個程序員的你不會操作這些功能):
bc 命令
如果你喜歡命令行的方式來做計算,那么還可以介紹給你一個系統(tǒng)提供的命令式計算工具:bc时肿。這個工具的官方定義是:一個任意精度計算器語言(An arbitrary precision calculator language)穴吹。我們可以以交互的方式進入bc:
bc -i
使用bc時你可以通過ibase = [2|8|10|16]的值來指定輸入數(shù)字的進制,可以通過指定obase=[2|8|10|16]的值來指定輸出數(shù)字的顯示格式嗜侮。你還可以通過scale=n來指定輸出的小數(shù)位數(shù)港令,你可以在里面用表達式啥容、函數(shù)、運算符顷霹、甚至可以定義變量和函數(shù)咪惠。可以看出bc可不是只有計算的功能這么簡單淋淀,你可以用bc來編寫程序RC痢!具體bc的使用你可以在終端下執(zhí)行 man bc
查看bc的使用手冊朵纷。下面是一段用bc語言寫的代碼(請在執(zhí)行了bc -i 命令后編寫如下代碼):
sum = 0
for (i = 0; i < 100; i++)
{
sum += i
}
sum
??【返回目錄】