iOS彙編

注:本文由破船譯自:raywenderlich。感謝唐巧抽出時間對本文進行double-check。 

我們寫的Objective-C代碼,最終會被轉換爲機器代碼 —— 由ARM處理器能識別的1和0組成。實際上,在機器代碼之間,還有一門人類可以閱讀的語言 —— 彙編語言。
 
瞭解彙編,可以深入到你的代碼裏面進行調試和優化的探索,並有助於你對Objective-C運行時(runtime)的理解,同時也能滿足你內心的好奇!
 
在這篇iOS彙編教程中,你能學到:
什麼是彙編 —— 以及爲什麼需要關注它。
如何閱讀彙編 —— 特別是由Objective -C生成的彙編。
在調試的時候如何使用assembly view —— 遇到一個bug或者crash,看看到底是怎麼回事,這非常有用。
 
爲了有效吸收本文內容,建議本文的讀者對象爲已經熟悉Objective-C編程了。當然,你也應該要知道一些簡單的計算機科學相關概念,例如棧、CPU以及它們是如何運行的。如果你對CPU不太熟悉,建議在閱讀本文之前,先看看這裏的內容:微處理器的工作原理。
 
——————————————————————-
iOS彙編教程:ARM(1)
開始:什麼是彙編
函數調用約定
創建工程
加法(addFunction)
 
開始:什麼是彙編
Objective-C是一門高級語言。編譯器會將你的Objective-C代碼編譯爲彙編語言代碼:一門低級語言,不過還不是最低級的語言。
 
這些彙編會被彙編器(assembler)組裝爲機器代碼——CPU可以識別的0和1。好在一般開發者並沒有必要考慮機器代碼,不過有時候詳細的瞭解彙編,會非常有用。
 
每一個彙編指令都會告訴CPU執行一個相關任務,例如“對兩個數字執行加(add)操作”,或“從某個內存地址加載數據”。
 
除了主存外 ——如 iPhone 5有1GB的主存、Mac電腦可能會有8GB —— CPU還有少許的存儲部件,稱之爲寄存器,寄存器的訪問速度非常快,一個寄存器就像一個變量一樣,可以存儲單個值。
 
所有的iOS設備(實際上,現如今,幾乎所有的移動設備)使用的CPU都是基於ARM架構。 ARM芯片使用的指令集是RISC(精簡指令集),該指令集非常的精簡,並且易讀(比x86的指令集精簡多了)。
 
一個彙編指令(或者語句)看起來如下所示:
  1. mov r0, #42 
上面的這行彙編指令,涉及到好多命令(或操作)。mov的作用是對數據進行移動。在ARM彙編指令中,目標是第一個,所以,上面的指令是將值42移動到寄存器r0中。再來看看下面的代碼:
  1. ldr r2, [r0] 
  2. ldr r3, [r1] 
  3. add r4, r2, r3 
 
上面彙編指令的作用是首先將寄存器r0和r1中的值裝載到寄存器r2和r3中,然後對寄存器r2和r3中的值進行加(add)操作,加的結果存放到r4中。
 
函數調用約定
要想理解彙編代碼,首先重要的事情就是理解代碼之間的交互——意思是一個函數調用另一個函數的方式。這包括了參數如何傳遞以及如何從函數返回結果——稱之爲調用的約定。編譯器必須嚴格的遵守相關標準進行代碼編譯,這樣生成的代碼,才能夠相互兼容。
 
上面討論過,寄存器是的存儲空間非常少,並且靠近CPU——用來存儲當前使用的一些值。ARM CPU有16個寄存器:r0到r15。每個寄存器爲32bit。調用約定規定了這些寄存器的特定用途。如下:
 r0 – r3:存儲傳遞給函數的參數值。
 r4 – r11:存儲函數的局部變量。
r12:是內部過程調用暫時寄存器(intra-procedure-call scratch register)。
r13:存儲棧指針(sp)。在計算機中,棧非常重要。這個寄存器保存着棧頂的指針。這裏可以看到更多關於棧的信息:Wikipedia
r14:鏈接寄存器(link register)。存儲着當被調用函數返回時,將要執行的下一條指令的地址。
r15:用作程序計數器(program counter)。存儲着當前執行指令的地址。每條執行被執行後,該計數器會進行自增(+1)。
 
這裏可以看到更多相關ARM 調用約定的內容:this document from ARM。蘋果公司也給出了一份文檔詳細介紹了在iOS開發中的調用約定: calling convention used for iOS development
下面我們就從代碼上開始真正的認識彙編。
 
創建工程
打開Xcode,File\New\New Project,選擇iOS\Application\Single View Application,然後點擊Next,工程的配置如下:
Product nameARMAssembly
Company Identifier: 一般爲反向的DNS標示
Class Prefix: 空白
Devices: iPhone
Use Storyboards: No
Use Automatic Reference Counting: Yes
Include Unit Tests: No
 
點擊 Next 選擇工程存儲的位置——完成工程的創建。
 
加法(addFunction)
下面我們寫一個加法函數:對兩個數進行相加,然後返回結果。這裏我們先用C語法寫,後面再介紹用OC來寫(OC稍微複雜一點)。在工程的Supporting Files目錄中打開main.m文件,然後將下面的函數拷貝並粘貼到文件的頂部。
  1. int addFunction(int a, int b) { 
  2.     int c = a + b; 
  3.     return c; 
 現在將Xcode中的scheme設置爲爲設備構建:選中iOS Device作爲scheme target(如果你將設備連接到電腦中,會現實<你的設備名稱>,如“Matt Galloway的iPhone 5”)——這樣選擇之後,生成的彙編就是針對ARM的,而不是針對x86(模擬器使用)。Xcode的選擇效果如下圖所示:
 
然後選擇:Product\Generate Output\Assembly File。過一會之後,Xcode會生成一個文件,這個文件裏面有很多行都有下劃線__。在文件的頂部,好多行都是以.section開頭。接着選中Show Assembly Output For中的Running
 
 注意:默認情況下,使用的是debug scheme中的設置信息,所以默認選中的就是Running。在debug模式下,編譯器對代碼沒有做優化處理——首先觀察沒有進過優化處理的彙編,更利於理解代碼具體都發生了什麼。
 
在生成的文件中搜索_addFunction,會看到類似如下的代碼:
  1.     .globl  _addFunction 
  2.     .align  2 
  3.     .code   16                      @ @addFunction 
  4.     .thumb_func _addFunction 
  5. _addFunction: 
  6.     .cfi_startproc 
  7. Lfunc_begin0: 
  8.     .loc    1 13 0                  @ main.m:13:0 
  9. @ BB#0: 
  10.     sub sp, #12 
  11.     str r0, [sp, #8] 
  12.     str r1, [sp, #4] 
  13.     .loc    1 14 18 prologue_end    @ main.m:14:18 
  14. Ltmp0: 
  15.     ldr r0, [sp, #8] 
  16.     ldr r1, [sp, #4] 
  17.     add r0, r1 
  18.     str r0, [sp] 
  19.     .loc    1 15 5                  @ main.m:15:5 
  20.     ldr r0, [sp] 
  21.     add sp, #12 
  22.     bx  lr 
  23. Ltmp1: 
  24. Lfunc_end0: 
  25.     .cfi_endproc 
 
上面的代碼看起來有點凌亂,實際上也不難以讀懂。我們來看看,首先,所有以”.”開頭的代碼行都不是彙編指令,我們可以忽略所有這些以”.”開頭的代碼行。
 
代碼中以冒號結尾的的代碼行(例如_addFunction:Ltim0: ),我們稱之爲標籤(label)。這些標籤的作用是給彙編代碼片段指定相關的名字.名爲_addFunction:的標籤,實際上是一個函數的入口點.
 
這個標籤(_addFunction: )是必須有的:別的代碼調用addFunction函數時,並不需要知道該函數具體在什麼地方,通過簡單的一個符號或標籤就可以進行調用.在最終生成程序二進制文件時,鏈接器會把這個標籤轉換到實際的地址.
 
我們需要注意的時,編譯器總是會在函數名前面添加一個下劃線——這僅僅是一個約定。另外,其他所有的標籤都是以L開頭——這些通常稱爲局部標籤(local label),只會在函數內部使用。在上面的代碼中,雖然沒有實際用到局部標籤,不過編譯器還是爲我們生成了一些——之所以會生成這些沒有被使用到的局部標籤,是由於代碼還沒有做任何的優化處理。
 
註釋是以@字符開頭。通過上面的分析,這樣一來,忽略掉註釋和標籤,代碼看起來如下所示:
  1. _addFunction: 
  2. @ 1: 
  3.     sub sp, #12 
  4. @ 2: 
  5.     str r0, [sp, #8] 
  6.     str r1, [sp, #4] 
  7. @ 3: 
  8.     ldr r0, [sp, #8] 
  9.     ldr r1, [sp, #4] 
  10. @ 4: 
  11.     add r0, r1 
  12. @ 5: 
  13.     str r0, [sp] 
  14.     ldr r0, [sp] 
  15. @ 6: 
  16.     add sp, #12 
  17. @ 7: 
  18.     bx  lr 
 
下面我們來看看代碼中每部分彙編都做了什麼:
 
1、首先,在棧(stack)創建臨時存儲所需要的空間。棧提供了許多內存供函數使用。ARM中的棧是向下延伸的,也就是說,在棧上創建一些空間,需要從棧指針開始減去(subtract)一些空間。在這裏,預留了12個字節。
 
2、r0和r1用來存儲傳遞給調用函數的參數值。如果函數有4個參數,那麼會把r2和r3當做第三個和第四個參數。如果函數的參數超過了4個,或者攜帶的參數不適合使用32位的寄存器(例如很大的數據結構),那麼可以通過棧來傳遞這些參數。
 
在這裏,兩個參數被保存到棧中。這是由存儲寄存器(str)指令完成的。
 
上面的指令可以指定一個偏移量,用來應用在某個值上面。所以[sp, #8]的意思是存儲至“棧指針寄存器+8的地方”,因此,str r0, [sp, #8]的作用是:將寄存器r0中的內容存儲到棧指針(加8)指向的內存地址.
 
3、將剛剛保存到棧中的值讀取至相同的寄存器中(r0和r1)。這裏,的ldr指令與str指令剛好相反,ldr(load register)會把指定內存位置中的的內容加載到寄存器中。ldr和str的語法非常相似:ldr r0, [sp, #8]的作用是“將棧指針加8後指向的地址內容加載到r0寄存器中”。
 
這裏你可能會感覺到奇怪,爲什麼ro和r1寄存器中的值剛剛保存,馬上又將其加載回來,答案是:這兩行代碼是冗餘的,可以去掉!如果編譯器做了優化處理,那麼這些冗餘的代碼會被忽略掉.
 
4、這是該函數中最終的要一個指令:執行加操作。該執行的意思是:將r0和r1中的內容進行相加,然後把結果放到r0中。
 
add指令可以是兩個參數,也可以是三個參數.如果指定三個參數,那麼第一個參數就被當做目標寄存器,剩下的兩個則爲源寄存器.因此,這裏的指令可以寫成這樣:add r0, r0, r1。
 
5、同樣,編譯器生成了一些冗餘代碼:將加的結果存儲到棧中,接着立即從棧中讀取回來。
 
6、終止函數的地方:將棧指針指向調用addFunction函數時的最初地方。addFunction開始於:sp減去12的地方:預留了12個字節。現在將12加回去即可。這裏必須確保棧指針的正確操作,否則棧指針會指向錯誤的地方。
 
最後,執行bx指令會回到調用函數的地方.這裏的寄存器lr是鏈接寄存器(link register),該存儲器存儲着將要執行的下一條指令。注意,addFunction返回之後,r0寄存器會存儲着該函數相加的結果值——這也是調用約定中的一部分:函數的返回值永遠都被存儲在r0寄存器中。除非一個寄存器不夠存儲,這是可以使用r1-r3。
 
上面就是所有相關addFunction的介紹,並不複雜吧?預知關於這些指令的更多內容,請看這裏: ARM website.
 
重申一下,上面的方法有好多冗餘的地方:這是由於編譯器處於debug模式,不會對代碼做優化處理.如果對代碼進行了優化處理,會看到生成的彙編代碼非常的少。
 
選中Show Assembly Output For中的Archiving。然後搜索_addFunction:,會看到如下指令(只有這些):
  1. _addFunction: 
  2.     add r0, r1 
  3.     bx  lr 
 
這看起來非常簡潔:只需要兩條指令就完成了addFunction函數的功能。當然,在實際開發中,一個函數一般都會有好多指令。
現在,這個addFunction已經返回到調用的函數那裏了.下面我們就來看看關於調用的函數的相關信息.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章