深入iOS系統底層之XCODE對匯編的支持

工欲善其事必先利其器 --《論語·衛靈公》

一個好的IDE不僅要提供舒適簡潔和方便的源代碼編輯環境,還要提供功能強大的調試環境。XCODE是目前來說對iOS應用開發支持的最好的IDE(雖然Visual Studio2017也開始支持iOS應用的開發了),畢竟XCODE和iOS都是蘋果公司的親生兒子。唯一要吐槽的就是系統和編譯環境綁的太死了,每當手機操作系統的一個小升級,都需要去升級一個好幾G的新版本程序,這確實是有點坑爹!
目前市面上有很多反編譯的工具,比如IDAHopper Disassembler等還有操作系統自帶的工具諸如otool、lldb。這些工具里面有的擅長靜態分析有的擅長調試的,這里就不展開分析了。如果在程序運行時去窺探一些系統內部實現以及做實時調試分析我覺得XCODE本身也非常的棒,既然深入系統我們必須要了解和學習一些關于匯編的東西,那么就必須要了解和掌握一些工具,而XCODE其實就是你手頭上最方便的工具之一。

XCODE的匯編模式切換

你是否在聯機運行時因為系統崩潰而出現過如下的畫面:

程序運行的崩潰界面或者切換函數調用棧時的界面

不要慌!它其實就是XCODE的匯編模式的界面。我們不僅在程序崩潰時可以看到它,我們也可以人為的進入到這個界面模式里面。這篇文章更像是一個XCODE工具使用上的一些介紹,您可以經常在使用它們,也可能還從來沒有接觸和了解過它們。對于匯編代碼和源代碼之間的切換可以通過菜單:Debug -> Debug Workflow -> Always Show Disassembly 來完成。

匯編代碼切換的操作菜單

記得要設置有斷點并運行到斷點處時切換才能看到匯編指令啊!

上一篇文章深入iOS系統底層之指令集介紹中我們有說過模擬器上運行的是Intel指令,而真機上運行的是arm指令,在這里我們分別看模擬器和真機下的匯編指令的差異性:

源代碼

模擬器下的指令代碼
真機下的指令代碼

通過上面三張圖你會發現其中的源代碼和匯編代碼之間有很大的差異,以及不同指令集下的匯編代碼之間也有很大的差異!匯編代碼的差異其實就是不同CPU上運行的指令的差異。還記得前一篇文章所說的指令集嗎?前者是在模擬器上運行的所以展示的是x64的指令,而后者是在真機上運行的因此展示的是arm64指令。通過圖片對比你能否發現他們之間的相同點和差異嗎?

  • 系統所有的代碼都是由一個個的函數或者說方法組成,即使是類中定義的方法以及Block里面的方法也是如此。在編譯時系統將所有定義的函數方法依次編譯鏈接為機器指令并保存到文件的代碼段中,一個函數內的機器指令是連續存儲的,但是函數之間卻不一定是連續存儲的。
  • 上面的圖片中每條匯編指令都和一條機器指令唯一對應,這里要注意的是雖然顯示的是匯編代碼,但是真實存儲和運行的還是機器代碼,只不過我們通過匯編代碼來展示能夠容易閱讀和理解而已。
  • 每條指令前面的地址表示的是這條指令在運行時所處在的內存地址。也許你會問指令不是在CPU上嗎?沒有錯,指令雖然是在CPU上執行,但是存儲還是要在內存或者磁盤上。CPU上有一個叫ip(Intel)或者pc(arm)的寄存器保存著下一條將要執行的指令的內存地址,這樣每執行一條指令時都是從ip/pc中所指定的內存地址讀取出指令并執行,并同時將當前指令的下一條繼續保存在ip/pc上,就這樣不停重復的方式來完成指令的執行(實際上CPU為了加快處理速度會將一部分內存中的指令緩存到CPU的內部緩存中去,而不是每條指令都從內存中讀取)。
  • 每個函數方法的第一個地址,就是這個函數的入口地址,也就是說我們進行函數調用時,實際上是讓CPU跳轉到這個地址并執行,更加具體的就是將ip/pc寄存器的值設置為這個函數的入口地址。 對于OC類中的方法來說方法入口地址其實就是這個方法的IMP。
  • 在模擬器下你會發現每條指令的長度是不一樣的,有1個字節到7個字節不等,所以你看到的每條指令的偏移量都不一樣,而真機時你會發現每條指令的長度總是固定為4個字節。這其實就是CISC和RISC指令集中的一個非常顯著的差別:CISC指令長度不固定而RISC指令則長度固定。你還會發現模擬器下的匯編代碼數量要比真機下的匯編代碼數量要少,這也是CISC指令和RISC指令的差別:CISC指令復雜而且眾多,一條指令完成的功能要比RISC多;而RISC則指令簡單,因此某些功能需要多條指令來完成。
  • 在匯編模式下的注釋都是由;號開頭的。大家在通過匯編語言研究內部實現時建議看模擬器下的AT&T匯編,原因其實就是模擬器下運行的匯編注釋要比真機模式下的匯編指令要詳細一些。
  • 每條匯編指令的格式總是由: 操作碼, 操作數1,操作數2,操作數3組成。 操作數要么就是常數,要么就是寄存儲器,要么就是內存地址。你所看到的操作數中的RAX,RSI,RDI,R0,R1... 這些都是CPU中的寄存器(關于寄存器部分我將在下一篇文章中具體介紹)。而且在XCODE的左下角部分我們可以查看當前CPU中的所有寄存器的值,你可以打印并修改他們。

斷點

可能有的同學會說為什么我打開了匯編模式我還是看不到匯編代碼?那是因為你沒有給你的代碼設置斷點!什么是斷點?為什么設置了斷點程序就會暫停運行? 一般情況下CPU總是按照順序依次執行指令并完成任務,當正在執行某個任務時如果遇到了特殊事件或者更高優先級的任務時就需要打斷現有執行的代碼并去執行優先級更高的代碼,這種機制就是中斷。中斷有因為外部硬件設備事件而產生的硬中斷, 同時CPU也提供一個軟中斷指令。當在代碼里面執行一條軟中斷指令時,程序就會暫停運行,同時CPU把操作權限提交給操作系統來執行中斷處理程序。當我們在程序某處設置了斷點或者某個指令處設置斷點時,系統會將斷點處的指令保存到一個臨時的斷點列表中,同時將斷點處的指令替換為軟中斷指令,這樣當程序運行到斷點處時因為執行的其實是軟中斷指令,而導致系統調用的發生,并執行軟中斷處理程序,軟中斷處理程序等待用戶處理斷點處的操作,比如當用戶按下的是鍵盤上的Ctrl + F7時,軟中斷處理程序就會把保存在臨時斷點列表中真實斷點處的指令恢復到指定的內存,同時把下次要執行的指令改為真實的指令,然后再次執行真實的指令,這樣就完成了斷點處指令的繼續執行。(要想了解斷點的具體實現,需要具有一些匯編的知識,這里就不展開了,后面我會在專門的章節里面詳解介紹斷點的實現原理)。

符號斷點

當我們在程序代碼某處設置了斷點或者指令某處設置了斷點后,程序執行到斷點處時就會暫停下來。這時候如果我們是在匯編模式下,您看到的就是匯編程序斷點,而當你在源代碼模式下時,你看到的將是源代碼斷點。 除了在代碼處設置斷點外我們還可以設置符號斷點。我們先來考察下面3個應用場景:

  1. 我們程序的某個視圖的frame值在運行時不知道什么原因總是被莫名其妙的改變了,但是你就是不知道在哪里執行了視圖frame的更改設置。這時候一個解決方法就是重載setFrame方法并設置斷點來調試查看frame被何時調用。

  2. 我們的上線程序出現了在某個系統方法被調用時的crash問題,但是因為是系統的方法我們無法看到其中的源代碼,從而無法進行crash問題分析(比如我們遇到的很多沒有上下文的crash).

  3. 假如我懂匯編語言,我想研究一下系統框架的某個方法是如何實現的。

上面的三個問題我不知道大家會如何去解決? 其實這三種場景我們都可以借助于符號斷點來完成。一般情況下我們可以在源代碼某處設置斷點來調試程序,對于沒有源代碼的情況下我們則可以通過設置符號斷點來實現程序的調試和運行。要設置符號斷點很簡單。你只需要在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 命令

如果你喜歡命令行的方式來做計算,那么還可以介紹給你一個系統提供的命令式計算工具:bc。這個工具的官方定義是:一個任意精度計算器語言(An arbitrary precision calculator language)。我們可以以交互的方式進入bc:
bc -i

bc命令

使用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地址簡書地址

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容