iOS彙編教程:ARM(1)

感謝唐巧抽出時間對本文進行double-check。

SpeakAssemblySmallSpeakAssemblySmall

你說的是彙編嗎?

我們寫的Objective-C代碼,最終會被轉換爲機器代碼 —— 由ARM處理器能識別的1和0組成。實際上,在機器代碼之間,還有一門人類可以閱讀的語言 —— 彙編語言。

瞭解彙編,可以深入到你的代碼裏面進行調試和優化的探索,並有助於你對Objective-C運行時(runtime)的理解,同時也能滿足你內心的好奇!

在這篇iOS彙編教程中,你能學到:

  • 什麼是彙編 —— 以及爲什麼需要關注它。
  • 如何閱讀彙編 —— 特別是由Objective -C生成的彙編。
  • 在調試的時候如何使用assembly view —— 遇到一個bug或者crash,看看到底是怎麼回事,這非常有用。

爲了有效吸收本文內容,建議本文的讀者對象爲已經熟悉Objective-C編程了。當然,你也應該要知道一些簡單的計算機科學相關概念,例如棧、CPU以及它們是如何運行的。如果你對CPU不太熟悉,建議在閱讀本文之前,先看看這裏的內容:微處理器的工作原理

目錄[分兩篇文章翻譯]:

iOS彙編教程:ARM(1)

  • 開始:什麼是彙編
  • 函數調用約定
  • 創建工程
  • 加法(addFunction)

iOS彙編教程:ARM(2)

  • 函數的調用
  • Objective -C 彙編
  • Obj-C 消息發給了誰
  • 你現在可以進行逆向工程了
  • 何去何從

——————————————————————–

iOS彙編教程:ARM(1)

開始:什麼是彙編

Objective-C是一門高級語言。編譯器會將你的Objective-C代碼編譯爲彙編語言代碼:一門低級語言,不過還不是最低級的語言。

這些彙編會被彙編器(assembler)組裝爲機器代碼——CPU可以識別的0和1。好在一般開發者並沒有必要考慮機器代碼,不過有時候詳細的瞭解彙編,會非常有用。

SpeakAssemblySpeakAssembly

每一個彙編指令都會告訴CPU執行一個相關任務,例如“對兩個數字執行加(add)操作”,或“從某個內存地址加載數據”。

除了主存外 ——如 iPhone 5有1GB的主存、Mac電腦可能會有8GB —— CPU還有少許的存儲部件,稱之爲寄存器,寄存器的訪問速度非常快,一個寄存器就像一個變量一樣,可以存儲單個值。

所有的iOS設備(實際上,現如今,幾乎所有的移動設備)使用的CPU都是基於ARM架構。 ARM芯片使用的指令集是RISC(精簡指令集),該指令集非常的精簡,並且易讀(比x86的指令集精簡多了)。

一個彙編指令(或者語句)看起來如下所示:

mov r0, #42

上面的這行彙編指令,涉及到好多命令(或操作)。mov的作用是對數據進行移動。在ARM彙編指令中,目標是第一個,所以,上面的指令是將值42移動到寄存器r0中。再來看看下面的代碼:

ldr   r2, [r0]
ldr r3, [r1]
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,工程的配置如下:

01-Create-the-project01-Create-the-project

  • Product name: ARMAssembly
  • 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文件,然後將下面的函數拷貝並粘貼到文件的頂部。

int addFunction(int a, int b) {
    int c = a + b;
    return c;
}

現在將Xcode中的scheme設置爲爲設備構建:選中iOS Device作爲scheme target(如果你將設備連接到電腦中,會現實<你的設備名稱>,如“Matt Galloway的iPhone 5”)——這樣選擇之後,生成的彙編就是針對ARM的,而不是針對x86(模擬器使用)。Xcode的選擇效果如下圖所示:

02-Select-iOS-Device-scheme02-Select-iOS-Device-scheme

 

然後選擇:Product\Generate Output\Assembly File。過一會之後,Xcode會生成一個文件,這個文件裏面有很多行都有下劃線__。在文件的頂部,好多行都是以.section開頭。接着選中Show Assembly Output For中的Running

  注意:默認情況下,使用的是debug scheme中的設置信息,所以默認選中的就是Running。在debug模式下,編譯器對代碼沒有做優化處理——首先觀察沒有進過優化處理的彙編,更利於理解代碼具體都發生了什麼。

在生成的文件中搜索_addFunction,會看到類似如下的代碼:

.globl    _addFunction
    .align  2
    .code   16                      @ @addFunction
    .thumb_func _addFunction
_addFunction:
    .cfi_startproc
Lfunc_begin0:
    .loc    1 13 0                  @ main.m:13:0
@ BB#0:
    sub sp, #12
    str r0, [sp, #8]
    str r1, [sp, #4]
    .loc    1 14 18 prologue_end    @ main.m:14:18
Ltmp0:
    ldr r0, [sp, #8]
    ldr r1, [sp, #4]
    add r0, r1
    str r0, [sp]
    .loc    1 15 5                  @ main.m:15:5
    ldr r0, [sp]
    add sp, #12
    bx  lr
Ltmp1:
Lfunc_end0:
    .cfi_endproc

上面的代碼看起來有點凌亂,實際上也不難以讀懂。我們來看看,首先,所有以”.”開頭的代碼行都不是彙編指令,我們可以忽略所有這些以”.”開頭的代碼行。

代碼中以冒號結尾的的代碼行(例如_addFunction:和Ltim0: ),我們稱之爲標籤(label)。這些標籤的作用是給彙編代碼片段指定相關的名字.名爲_addFunction:的標籤,實際上是一個函數的入口點.

這個標籤(_addFunction: )是必須有的:別的代碼調用addFunction函數時,並不需要知道該函數具體在什麼地方,通過簡單的一個符號或標籤就可以進行調用.在最終生成程序二進制文件時,鏈接器會把這個標籤轉換到實際的地址.

我們需要注意的時,編譯器總是會在函數名前面添加一個下劃線——這僅僅是一個約定。另外,其他所有的標籤都是以L開頭——這些通常稱爲局部標籤(local label),只會在函數內部使用。在上面的代碼中,雖然沒有實際用到局部標籤,不過編譯器還是爲我們生成了一些——之所以會生成這些沒有被使用到的局部標籤,是由於代碼還沒有做任何的優化處理。

註釋是以@字符開頭。通過上面的分析,這樣一來,忽略掉註釋和標籤,代碼看起來如下所示:

_addFunction:
@ 1:
    sub sp, #12
@ 2:
    str r0, [sp, #8]
    str r1, [sp, #4]
@ 3:
    ldr r0, [sp, #8]
    ldr r1, [sp, #4]
@ 4:
    add r0, r1
@ 5:
    str r0, [sp]
    ldr r0, [sp]
@ 6:
    add sp, #12
@ 7:
    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:,會看到如下指令(只有這些):

_addFunction:
    add r0, r1
    bx  lr

這看起來非常簡潔:只需要兩條指令就完成了addFunction函數的功能。當然,在實際開發中,一個函數一般都會有好多指令。

現在,這個addFunction已經返回到調用的函數那裏了.下面我們就來看看關於調用的函數的相關信息.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章