工欲善其事必先利其器 --《論語·衛靈公》git
一個好的IDE不只要提供溫馨簡潔和方便的源代碼編輯環境,還要提供功能強大的調試環境。XCODE是目前來講對iOS應用開發支持的最好的IDE(雖然Visual Studio2017也開始支持iOS應用的開發了),畢竟XCODE和iOS都是蘋果公司的親生兒子。惟一要吐槽的就是系統和編譯環境綁的太死了,每當手機操做系統的一個小升級,都須要去升級一個好幾G的新版本程序,這確實是有點坑爹! 目前市面上有不少反編譯的工具,好比IDA、Hopper Disassembler等還有操做系統自帶的工具諸如otool、lldb。這些工具裏面有的擅長靜態分析有的擅長調試的,這裏就不展開分析了。若是在程序運行時去窺探一些系統內部實現以及作實時調試分析我以爲XCODE自己也很是的棒,既然深刻系統咱們必需要了解和學習一些關於彙編的東西,那麼就必需要了解和掌握一些工具,而XCODE其實就是你手頭上最方便的工具之一。程序員
你是否在聯機運行時由於系統崩潰而出現過以下的畫面:github
不要慌!它其實就是XCODE的彙編模式的界面。咱們不只在程序崩潰時能夠看到它,咱們也能夠人爲的進入到這個界面模式裏面。這篇文章更像是一個XCODE工具使用上的一些介紹,您能夠常常在使用它們,也可能還歷來沒有接觸和了解過它們。對於彙編代碼和源代碼之間的切換能夠經過菜單:Debug -> Debug Workflow -> Always Show Disassembly 來完成。編程
記得要設置有斷點並運行到斷點處時切換才能看到彙編指令啊!數組
上一篇文章深刻iOS系統底層之指令集介紹中咱們有說過模擬器上運行的是Intel指令,而真機上運行的是arm指令,在這裏咱們分別看模擬器和真機下的彙編指令的差別性: 緩存
經過上面三張圖你會發現其中的源代碼和彙編代碼之間有很大的差別,以及不一樣指令集下的彙編代碼之間也有很大的差別!彙編代碼的差別其實就是不一樣CPU上運行的指令的差別。還記得前一篇文章所說的指令集嗎?前者是在模擬器上運行的因此展現的是x64的指令,然後者是在真機上運行的所以展現的是arm64指令。經過圖片對比你可否發現他們之間的相同點和差別嗎?sass
可能有的同窗會說爲何我打開了彙編模式我仍是看不到彙編代碼?那是由於你沒有給你的代碼設置斷點!什麼是斷點?爲何設置了斷點程序就會暫停運行? 通常狀況下CPU老是按照順序依次執行指令並完成任務,當正在執行某個任務時若是遇到了特殊事件或者更高優先級的任務時就須要打斷現有執行的代碼並去執行優先級更高的代碼,這種機制就是中斷。中斷有由於外部硬件設備事件而產生的硬中斷, 同時CPU也提供一個軟中斷指令。當在代碼裏面執行一條軟中斷指令時,程序就會暫停運行,同時CPU把操做權限提交給操做系統來執行中斷處理程序。當咱們在程序某處設置了斷點或者某個指令處設置斷點時,系統會將斷點處的指令保存到一個臨時的斷點列表中,同時將斷點處的指令替換爲軟中斷指令,這樣當程序運行到斷點處時由於執行的實際上是軟中斷指令,而致使系統調用的發生,並執行軟中斷處理程序,軟中斷處理程序等待用戶處理斷點處的操做,好比當用戶按下的是鍵盤上的Ctrl + F7時,軟中斷處理程序就會把保存在臨時斷點列表中真實斷點處的指令恢復到指定的內存,同時把下次要執行的指令改成真實的指令,而後再次執行真實的指令,這樣就完成了斷點處指令的繼續執行。(要想了解斷點的具體實現,須要具備一些彙編的知識,這裏就不展開了,後面我會在專門的章節裏面詳解介紹斷點的實現原理)。bash
當咱們在程序代碼某處設置了斷點或者指令某處設置了斷點後,程序執行到斷點處時就會暫停下來。這時候若是咱們是在彙編模式下,您看到的就是彙編程序斷點,而當你在源代碼模式下時,你看到的將是源代碼斷點。 除了在代碼處設置斷點外咱們還能夠設置符號斷點。咱們先來考察下面3個應用場景:多線程
咱們程序的某個視圖的frame值在運行時不知道什麼緣由老是被莫名其妙的改變了,可是你就是不知道在哪裏執行了視圖frame的更改設置。這時候一個解決方法就是重載setFrame方法並設置斷點來調試查看frame被什麼時候調用。app
咱們的上線程序出現了在某個系統方法被調用時的crash問題,可是由於是系統的方法咱們沒法看到其中的源代碼,從而沒法進行crash問題分析(好比咱們遇到的不少沒有上下文的crash).
假如我懂彙編語言,我想研究一下系統框架的某個方法是如何實現的。
上面的三個問題我不知道你們會如何去解決? 其實這三種場景咱們均可以藉助於符號斷點來完成。通常狀況下咱們能夠在源代碼某處設置斷點來調試程序,對於沒有源代碼的狀況下咱們則能夠經過設置符號斷點來實現程序的調試和運行。要設置符號斷點很簡單。你只須要在XCODE的菜單:Debug -> Breakpoints -> Create Symbolic Breakpoint 或者快捷鍵:option + command + \ 來創建符號斷點:
創建符號斷點後,當某個與符號名相同某個函數或者方法在執行開始前就會產生斷點,從而能夠窺探某個方法的內部實現。還能夠幫助咱們對那些沒有上下文以及非源代碼處產生的崩潰進行分析和重現,從而幫助咱們定位問題。下面是運行符號斷點後的咱們看到的兩處符號斷點的彙編語言內容:
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的內部實現以及函數abs的內部實現了?
調試程序是一個程序員應該掌握的最基本的工夫,這裏就不介紹其餘的詳細的調試命令以及方法,其餘不少文章裏面都有介紹了。主要介紹一下調試代碼時單步運行的幾個菜單和快捷鍵:
F7 : 代碼單步執行,當遇到函數調用時會跳入函數內部。
F6: 代碼單獨執行,當遇到函數調用時不會跳入函數內部。
F8: 跳出函數執行,返回到調用此函數的下一句代碼。
複製代碼
control + F7 : 指令單步執行,當遇到函數調用時會跳入函數內部。
control + F6: 指令單獨執行,當遇到函數調用時不會跳入函數內部。
複製代碼
control + shift + F7: 切換到當前線程,並執行單步指令。
control + shift + F6: 切換到當前線程,並跳轉到函數調用的者的下一條指令。
複製代碼
在調試運行時當出現斷點時咱們能夠在lldb命令行中輸入各類調試命令,其餘的不介紹,就單獨介紹一下expr命令。expr命令實際上是p或者po的完整版本,經過expr命令除了可以用來顯示外,還能夠用來進行數據的修改、方法的調用等強大能力。下面展現一下一些經常使用的expr方法:
expr 變量|表達式 //顯示變量或者表達式的值。
expr -f h -- 變量|表達式 //以16進制格式顯示變量或表達式的內容
expr -f b -- 變量|表達式 //以二進制格式顯示變量或者表達式的內容。
expr -o -- oc對象 //等價於po oc對象
expr -P 3 -- oc對象 //上面命令的增強版本,他還會顯示出對象內數據成員的結構,具體的P後面的數字就是你要想顯示的層次。
expr my_struct->a = my_array[3] //給my_struct的a成員賦值。
expr (char*)_cmd //顯示某個oc方法的方法名。
expr (IMP)[self methodForSelector:_cmd] //執行某個方法調用.
複製代碼
程序運行時,操做系統爲其構建出一個進程,同時構建出一個虛擬的內存空間。操做系統將進程中的虛擬內存空間劃分爲代碼存儲區域、全局數據存儲區域、堆存儲區域、棧存儲區域等區域。每種區域都有特殊的用途:代碼存儲區域保存的是程序中的代碼部分(這部分也可稱爲映像image);全局數據存儲區域保存的是一些全局數據、常量以及一些描述信息(好比runtime裏面的全部OC類的定義描述信息也是存儲在這個區域中);堆存儲區域則用來進行堆內存的動態分配;棧存儲區域則保存着函數中的局部變量。所以能夠看出不管是代碼和數據在運行時都保存在內存中。每一個進程能訪問的內存空間的尺寸大小由操做系統決定,通常來講32位的操做系統中每一個進程的內存空間爲2^32 = 4GB;而64位的操做系統中每一個進程的內存空間爲2^64 = 4TB。須要注意的是這個空間是虛擬的可訪問空間並非真實的物理內存可訪問的空間,操做系統內部經過分頁映射的方式將虛擬空間轉化爲真實的物理空間。 進程的虛擬內存空間是一個能夠連續存儲和訪問的線性空間,爲了可以訪問這些內存空間,操做系統爲其進行了編碼,這個編碼就是內存的地址。地址也被稱爲指針,所以咱們所說的某個變量的指針其實就是這個變量在內存中的地址。爲了更好的理解內存和地址的概念,你能夠將內存理解爲一個數組,而地址則是訪問這個數組元素時所用到的索引。咱們對數組中元素的讀寫操做老是經過索引進行,一樣CPU對內存中的數據訪問時也是經過內存地址進行的。進程中的內存地址老是從0開始編碼,並以字節爲單位進行遞增,直到虛擬內存空間的上限。 上面說過進程中的代碼和數據都保存在內存中,當咱們要想一覽整個進程內存中的代碼和數據時,你能夠在程序運行時經過菜單:Debug -> Debug Workflow -> View Memory 或者經過快捷鍵:shift+command + m 來調用內存查看界面:
上面的圖片恰好展現的是一個類的全部方法名稱在內存中的位置和佈局。能夠看出咱們能夠很方便的藉助查看內存地址菜單的功能來了解以及分析代碼以及數據在內存中的結構。你能夠在地址輸入欄中輸入你想查看的任意內存地址。好比你想查看某個函數代碼的機器指令,那麼你只須要在彙編模式下將函數最開始的地址輸入到內存查看界面的地址欄中,那麼就會展現出這個函數代碼的全部機器指令字節碼。這裏還要注意一點的是由於內存地址是從低位按字節依次排列而來,因此對於好比int類型的值的讀取咱們就要從高位到低位開始讀取。
程序調試時代碼和地址以及一些數據都常常以16進制的形式顯示。數據處理時,尤爲是計算地址偏移都以16進制的形式進行展現。你能夠在lldb中經過expr或者p命令來計算。若是你喜歡界面形式的工具,則能夠啓動mac OS操做系統中的應用:計算器 來處理各類計算,你要作的就是在顯示菜單中選擇編程型便可,編程型界面的效果以下(別告訴我做爲一個程序員的你不會操做這些功能):
若是你喜歡命令行的方式來作計算,那麼還能夠介紹給你一個系統提供的命令式計算工具:bc。這個工具的官方定義是:一個任意精度計算器語言(An arbitrary precision calculator language)。咱們能夠以交互的方式進入bc: bc -i
使用bc時你能夠經過ibase = [2|8|10|16]的值來指定輸入數字的進制,能夠經過指定obase=[2|8|10|16]的值來指定輸出數字的顯示格式。你還能夠經過scale=n來指定輸出的小數位數,你能夠在裏面用表達式、函數、運算符、甚至能夠定義變量和函數。能夠看出bc可不是隻有計算的功能這麼簡單,你能夠用bc來編寫程序!!具體bc的使用你能夠在終端下執行 man bc
查看bc的使用手冊。下面是一段用bc語言寫的代碼(請在執行了bc -i 命令後編寫以下代碼):
sum = 0
for (i = 0; i < 100; i++)
{
sum += i
}
sum
複製代碼
👉【返回目錄】
歡迎你們訪問個人github地址