iOS匯編教程:理解ARM

lmii1361 8年前發布 | 11K 次閱讀 ARM匯編 iOS開發 移動開發

iOS匯編教程:理解ARM

當你寫Objective-C代碼時,它們最終轉換成機器碼---ARM處理器能理解的原始的0和1指令。在Objective-C和機器碼之間,還有一種可直接理解的匯編語言。

理解匯編會讓你在調試和優化時更加深入了解你的代碼,破譯Objective-C運行時,而且能滿足如呆子般癡迷于內核的好奇心。

在這篇iOS匯編教程中,你將學到:

  • 匯編是什么及為什么你要關心它;
  • 看懂匯編。特別是Objective-C方法生成的匯編;
  • 在調試時如何使用匯編。對于看清代碼運行狀態及為什么bug或者崩潰發生非常有用;

為了更好的理解這篇教程,你應該熟悉Objective-C編程。你也要理解一些簡單的計算機術語,比如棧,處理器(CPU)及他們是如何工作的。如果你不熟悉CPU,你可能需要預先了解 這篇文章 下再繼續。

打開你的XCode4(作者的文章寫于2013年,當時還是4),讓我們準備探索ARM的內部世界吧~

開始:什么是匯編

眾所周知,Objective-C是一個高級語言。Objective-C語言通過編譯器編譯成匯編語言,它雖然是低級語言,但也不是最低級的。

匯編語言通過匯編器轉譯成CPU可識別的01機器碼。幸運的是,你不需要關心機器碼,但理解匯編細節有時候非常有用。

每個匯編指令都是告訴CPU去執行一項任務,比如將兩個數相加,從內存中加載數據。

除了主要的內存外---iPhone5是1GB,Mac上可能是8GB,CPU也有一些能夠快速訪問的小塊工作內存。這小塊工作內存被分割成寄存器,就像能持有值的變量一樣。

所有的iOS設備(事實上,目前幾乎所有的手機設備)使用基于 ARM架構 的CPU。幸運的是,這個指令集很容易讀懂,不僅僅因為它是 精簡指令集 (RISC)---意味著更少的指令。總之,它比 x86 更易讀。

一個匯編指令看起來像這樣:

mov r0, #42

匯編語言有許多指令或操作符。其中之一的mov操作符,用于移動數據。在ARM匯編里,第一個參數是移動數據的目的地,因此上面的指令就是把值42賦值給了r0寄存器。看看下面這個例子:

ldr r2, [r0]
ldr r3, [r1]
add r4, r2, r3

別擔心,我并沒有期望你能理解上面代碼。但是你可能已經猜到了代碼大致意思。指令的意思是從內存中加載數據并把他們存儲到寄存器r2,r3中,然后將他們倆相加存儲到寄存器r4中。

現在你已經發現其實并沒有那么恐怖,讓我們來更深入詳細些吧~

調用約定

理解匯編首先且最重要的事就是明白匯編代碼與代碼之間是如何交互的。這里我的意思是函數調用其他函數。包括參數是如何傳遞給函數,及函數返回值是如何返回的。

這些事情執行的過程與實現被稱為調用約定。編譯器必須遵循它預定義的標準,這樣才能讓編譯后的代碼能和其他不同編譯器編譯出的代碼能夠交互。沒有標準,編譯器能編譯出不相配的代碼。

如上討論,寄存器是和CPU聯系非常緊密的一小塊內存,經常用于存儲一些正在使用的數據。ARM處理器有16個寄存器,從r0到r15,每一個都是32位比特。調用約定指定他們其中的一些寄存器有特殊的用途,例如:

  • r0-r3:用于存放傳遞給函數的參數;
  • r4-r11:用于存放函數的本地參數;
  • r12:是內部程序調用暫時寄存器。這個寄存器很特別是因為可以通過函數調用來改變它;
  • r13:棧指針sp(stack pointer)。在計算機科學內棧是非常重要的術語。寄存器存放了一個指向棧頂的指針。看 這里 了解更多關于棧的信息;
  • r14:是鏈接寄存器lr(link register)。它保存了當目前函數返回時下一個函數的地址;
  • r15:是程序計數器pc(program counter)。它存放了當前執行指令的地址。在每個指令執行完成后會自動增加;

你可以在 ARM文檔 里了解更多關于ARM調用約定的信息。蘋果也在文檔 【iOS開發調用約定】 內有做過詳細描述。

OK,前戲已足,是時候開始真正的碼代碼了~

創建工程

在這個匯編教程里,你不會創建一個應用,但還是得使用Xcode工程來說明其中含義。啟動Xcode,再新建工程,選擇iOS\應用\單個view的工程( iOS\Application\Single View Application)點擊下一步,工程最終如下:

  • Product name: ARMAssembly;
  • Company Identifier: 你的通常的反向域名標識;
  • Class Prefix:置空;
  • Devices:iPhone;
  • Use StoryBoards:否
  • 使用ARC:是
  • Include Unit Tests:否

最后點擊下一步,選擇一個保存你工程的地址。

1 + 1

你要做的第一件事就是寫一個兩個數相加后并返回的函數。沒有比這更簡單的事了。

事實上,你可以先寫一個C語言函數,因為Objective-C會是我們的復雜度增加。在Supporting Files里打開main.m,并把下面的代碼貼到文件內:

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

現在保證你選擇的Scheme是iOS設備(或者你的設備名),比如,如果你連上了設備就是xx的iPhone。選擇Scheme是設備的原因是這樣才會用ARM匯編,而選擇模擬器的話就會用x86的方式匯編。Scheme選擇后的類似于這樣:

現在去Product\Generate Output\Assembly File生成匯編碼(其實現在Xcode7是Product\Perform Action\Assemble "main.m")。Xcode會展示一個奇怪的文件。在文件最頂部,你可以看到許多以.section開頭的行。如果有那說明你生成成功了!

注意:你默認運行的Scheme是Debug配置。在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:和Ltmp0:,被稱為標簽。他們是這部分的匯編代碼名字。稱為_addFunction:標簽實際上是這個函數的入口。

這個標簽是必須的,這樣其他代碼可以通過使用addFunction標簽來路由函數而不需要知道函數的位置。當最終應用二進制生成的時候,將這些標簽轉換成實際內存地址是鏈接器的工作。

注意到編譯器通常在函數名的前面添加一個下劃線,這也是一個約定。其他的所有以L.開頭的叫本地標簽,這些標簽只能用于函數內部。在這個簡單的例子里,沒有任何一個本地標簽真正被使用,但編譯器仍然生成了,因為這個代碼并沒有做任何編譯優化。

注意是以@字符開頭的。在匯編代碼后邊注釋上對應的main.c文件行數對我們看懂匯編代碼非常有用。

因此,忽略掉注釋和標簽,重要的代碼如下:

_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. 首先,分配棧所需的所有臨時存儲空間。棧是一大塊函數隨時想使用的內存。ARM中的棧內存是高地址向低地址分布的,意味著你必須從棧指針開始減。在這里,分配了12個字節。
  2. r0和r1存放傳給函數的參數。如果入參有四個參數,那么r2和r3就會分別存放第三和第四個參數。如果函數超過四個參數,或者一些例如結構體的參數超過了32位比特,那么參數將會通過棧來傳遞。 這里,兩個參數被存入棧中。這是通過存儲寄存指令(str)實現的。第一個參數是要存儲的寄存器,第二個是存儲的位置。方括號代表里面值是內存地址。 這個方括號指令允許你為一個值指定偏移量,因此 [sp, #8] 的意思『在棧指針的地址上加上8字節偏移量』。同樣地, str r0, [sp, #8] 意味著『存儲r0寄存器的值到棧指針地址加上8字節內存的位置』。
  3. 剛被保存到棧的值又被讀取到相同的寄存器內。和 str 指令相反的, ldr 指令是從一個內存中加載內容到寄存器。兩者語法非常相似。因此, ldr r0, [sp, #8] 意思是『讀取出在棧指針地址加上8字節內存的位置的內容,并將內容賦值給寄存器r0』。 如果你好奇為何r0和r1剛被存儲又被加載出來,對,它是很奇怪,這兩行明明就是多余的嘛!如果編譯器允許基本的編譯優化,那么這多余的就會被消除。
  4. 這是該函數最重要的指令了:做加法。意思是將r0和r1中的內容相加,并將相加的值賦值給r0。 add 指令入參可以是兩個或者三個,如果是三個,那第一個參數就是存儲后兩個參數相加的值的寄存器。所以,這行指令也可以寫成: add r0, r0, r1 。
  5. 再一次,編譯器生成了一些多余的代碼:將相加的結果存儲起來,又讀取到相同的位置。
  6. 函數即將終止,因此棧指針放回原來的地方。函數開始時從sp(棧指針)上減去了12個字節而得到12個字節內存使用。現在它把12個字節還回去。函數必須保證棧指針操作平衡,否則棧指針可能漂移,最終可能超出了已分配的內存。你應該不希望那樣...
  7. 最后,間接分支調度指令 bx 被執行,用于返回到調用函數(調用本函數的函數)。 lr (link register)存放了調用函數執行完當前函數的下一條指令。注意到,在 addFunction 執行返回后,r0保存了相加的值。這是調用約定的另一部分。函數的返回值總是r0,除非一個寄存器不夠存儲,這種情況下才會使用r1-r3。

其實并沒有那么難,對不?想知道更多關于指令的信息,可以看看 這個文檔 ,或者 看其他的中文 .

你發現了這個函數很多匯編代碼是多余的。因為一開始,我們的編譯器就是Debug(調試)模式,沒有任何編譯器優化的。如果你把編譯優化打開,你會得到一個非常精簡的代碼。

改變 Show Assembly Output For 到選擇器到 存檔(Xcode7不一樣,區別是如果你想獲取精簡的匯編,需要Edit Scheme成Release模式,另外切換后記得clean下工程)。搜索 _addFunction: ,你可以看到如下代碼:

_addFunction:
    add    r0, r1
    bx    lr

這非常簡潔! 可以看到僅僅兩個指令就寫完了這個函數。你可能沒想到僅用兩個指令就完成了~ 當然,你平時寫的函數一般更長也更有趣點~

現在你已經有一個以返回到調用者分支為結束的函數。那么作為一個相互關系的另一個函數,調用該函數的調用者呢?

調用函數

首先,你需要給 addFunction 函數添加一個讓編譯器不做優化的屬性。你已經發現如果我們開啟優化,那么代碼會移除掉不必要的指令,甚至連函數調用都會被移除,或者可能直接將函數作為內聯函數使用。

例如,編譯器可能直接用 add 指令代替函數調用。實際上,編譯器是非常強大智能的,它可能直接幫你計算好了相加后的值,連 add 指令都不需要生成。

這個教程,我們不希望編譯器做優化或者將函數內聯。回到main.m文件,修改函數成如下:

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

繼續在下方添加另一個函數如下:

void fooFunction() {
    int add = addFunction(12, 34);
    printf("add = %i", add);
}

fooFunction 函數簡單地用 addFunction 讓12和34相加并且打印出值。這里使用的是C語言的printf而不是Objective-C的NSLog函數的原因是后者的匯編結果更加復雜。

再一次生成匯編代碼,搜索 _fooFunction ,你可以看到如下代碼:

_fooFunction:
@ 1:
    push    {r7, lr}
@ 2:
    movs    r0, #12
    movs    r1, #34
@ 3:
    mov    r7, sp
@ 4:
    bl    _addFunction
@ 5:
    mov    r1, r0
@ 6:
    movw    r0, :lower16:(L_.str-(LPC1_0+4))
    movt    r0, :upper16:(L_.str-(LPC1_0+4))
LPC1_0:
    add    r0, pc
@ 7:
    blx    _printf
@ 8:
    pop    {r7, pc}

這里引入了一些教程之前沒有介紹過的指令,但不用擔心,他們并不復雜,我們來看:

  1. 這個指令跟我們之前的 add sp, #12 指令做的事情差不多。這里,r7和lr被推入到棧,意味著sp(棧指針)減掉了8字節(棧指針始終指向棧頂,所以在push的時候會變小),因為r7和lr都是4字節。注意到棧指針變小了而且通過一個指令存儲了兩個值。r7的值需要存儲起來的原始是之后函數執行時它會被使用到并重寫。lr被存儲的原因在函數最后將會知曉;
  2. 這兩個指令(mov)是Move組的成員之一。有時候你會看到 movs ,或者 movw ,或者其他,但他們的作用都是用一個值來填充寄存器。你可以將一個寄存器的值移動到另一個寄存器,因此 mov r0, r1 會將r1寄存器內容填充到r0,r1的值不變。在這兩行代碼中,r0和r1是用函數中定義的兩個常量賦值的。注意到他們是被加載到r0和r1中,剛好是 addFunction 的入參。
  3. 在函數調用邊界時,棧指針應該被保存起來,因此作為可存儲本地變量的寄存器之一r7被使用了。你會發現剩余的函數代碼中并沒有使用棧指針或者r7,因此這是個小小的多余處理。有時候開啟了優化也優化不掉。
  4. 指令 bl 意味著函數調用。記得函數的入參已經放入相關的寄存器r0及r1了吧。這個指令執行的代碼我們稱之為分支。因為是指令 bl 而不是指令 b ,指令 bl 全稱是『branch with link』,意味著在執行分支代碼之前,需要將lr(鏈接寄存器)置為當前函數的下一個指令。回想下,當 addFunction 方法返回時,lr就是指向下一個要執行的指令。
  5. 這是將兩個數相加的 addFunction 分支返回的節點。記得之前說明過函數的返回值是存放在r0的吧~ 這個值會作為printf函數的第二個參數,因此 mov 指令用于將r0的內容賦值給r1。
  6. printf函數的第一個參數是一個字符串。這三條指令加載指向所需的字符串的開始地址的指針到r0寄存器。字符串存儲在我們稱之為二機制『數據段』的位置。但只有最終二進制被鏈接時才能知道該數據的具體位置。 字符串可以在main.m生成的目標文件例找到。如果你在生成的匯編代碼內搜索『L.str』,便可找到它。前兩個指令加載常量的地址,并減去標簽的地址(LPC1_0加上4字節)。看到第三個指令這么做的目的就很明顯了。r0加上pc(程序計數器),這樣無論L.str在二進制文件的什么位置都能夠準確的存放字符串的位置。 下面的圖展示了內存分布。 L_.str - (LPC1_0 + 4) 差值可以隨便變動,但是加載r0的代碼卻不用變化。

  7. 這條指令是調用printf函數。這里的 blx 跟 bl 指令有點不同,x代表交換,當有需要時,處理器可以改變當前運行模式。處理器運行模式有點超越了本教程的范圍,ARM處理器有兩種運行模式:ARM和Thumb。Thumb指令集是16位寬,而ARM是32位。Thumb指令比ARM少,使用Thumb意味著更少的代碼大小及更好的CPU緩存。通常使用有限的Thumb指令集可以讓你從更小的包大小中獲益。想了解更多Thumb的知識 請戳這里
  8. 最后一條指令是推出第一條指令推入的值。這次列表中的寄存器的值是用棧中的值填充的,且棧指針增加了。回想下,r7和lr之前是被推入到棧中,那么此時為何是推出來的值存入到了r7和pc中,而不是r7和lr呢? 好的,記得lr是存儲當前函數執行完成后的下一個指令地址吧。當你把lr推出棧賦值給pc后,執行將會從本函數調用的地方繼續執行。這通常是一個函數返回的實現方式,而不是像 addFunction 那樣切分支的方式。

以上是對ARM指令集大致的介紹。還有很多其他指令集,但這些是開始理解指令集最重要的的指令。讓我們來用偽代碼快速回憶一下代碼做的事情:

mov r0, r1 => r0 = r1
mov r0, #10 => r0 = 10
ldr r0, [sp] => r0 = *sp
str r0, [sp] => *sp = r0
add r0, r1, r2 => r0 = r1 + r2
add r0, r1 => r0 = r0 + r1
push {r0, r1, r2} => r0, r1 r2 入棧
pop {r0, r1, r2} => 棧頂出三個, 并賦值給r0, r1 and r2.
b _label => pc = _label
bl _label => lr = pc + 4; pc = _label

哇哦~ 現在你可以讀懂一些ARM匯編代碼了~

Objective-C 匯編

至此,你看到的函數都是C語言的。Objective-C代碼要復雜點,不過讓我們來檢驗一下。在ViewController.m代碼中添加以下代碼實現:

  • (int)addValue:(int)a toValue:(int)b { int c = a + b; return c; }</pre>

    讓我們再次重復之前精簡的匯編方式,搜索 addValue:toValue: 函數,可以看到:

    "-[ViewController addValue:toValue:]":
      adds    r0, r3, r2
      bx    lr

    首先你會注意到標簽名字。這次便簽名字包含了類名及全部的方法名。

    如果你和之前的 addFunction 匯編代碼相比較,你會發現兩個入參存儲在了r2及r3而不是r0和r1。為什么呢?

    OK,因為Objective-C函數在C函數的基礎上多傳了兩個隱式的參數。 addValue:toValue: 方法語法上和以下方法相同:

    int ViewController_addValue_toValue(id self, SEL _cmd, int a, int b) {
      int c = a + b;
      return c;
    }

    這就是為什么a和b變量分別存儲在r2和r3內了。現在你大概知道了前兩個隱式參數的含義了,你總是可以使用self這個變量。

    但是, _cmd 可能之前你沒有見過。像self變量一樣,在Objective-C代碼中它是可獲取的,而且代表著當前函數的selector。你一般從不會用到它,這就是你為何沒聽過的原因了。

    為了看清Objective-C函數是如何被調用的,我們在ViewController中添加如下代碼:

  • (void)foo { int add = [self addValue:12 toValue:34]; NSLog(@"add = %i", add); }</pre>

    生成代碼并找到該方法,你可以看到類似下面的代碼(Xcode7生成的有點不一樣了):

    "-[ViewController foo]":
    @ 1:
      push    {r7, lr}
    @ 2:
      movw    r1, :lower16:(L_OBJC_SELECTOR_REFERENCES_-(LPC1_0+4))
      movt    r1, :upper16:(L_OBJC_SELECTOR_REFERENCES_-(LPC1_0+4))
    LPC1_0:
      add    r1, pc
    @ 3:
      ldr    r1, [r1]
    @ 4:
      movs    r2, #12
      movs    r3, #34
    @ 5:
      mov    r7, sp
    @ 6:
      blx    _objc_msgSend
    @ 7:
      mov    r1, r0
    @ 8:
      movw    r0, :lower16:(L__unnamed_cfstring_-(LPC1_1+4))
      movt    r0, :upper16:(L__unnamed_cfstring_-(LPC1_1+4))
    LPC1_1:
      add    r0, pc
    @ 9:
      blx    _NSLog
    @ 10:
      pop    {r7, pc}

    再次,和之前我們看到的C語言函數差不多。分解它:

    1. 將r7及lr入棧;
    2. 使用同樣加載字符串的方式,加載在名為 L_OBJC_SELECTOR_REFERENCES_ 的標簽處的字符串指針到r1。像便簽名字一樣,它是一個selector的引用。其實selector就是存儲在數據段的字符串。
    3. 如果你在匯編代碼里查找 L_OBJC_SELECTOR_REFERENCES_ ,你會發現: L_OBJC_SELECTOR_REFERENCES_:.long L_OBJC_METH_VAR_NAME_ ,這說明r1指向的是 L_OBJC_METH_VAR_NAME_ 標簽的地址。如果你繼續查看該便簽,你將找到 addValue:toValue: 字符串。 指令 ldr r1, [r1] 表示加載存儲在r1指針內的內容并賦值給r1。用偽代碼是這么表述的 r1 = *r1 。再仔細想想,r1其實已經指向 addValue:toValue: 字符串地址。
    4. 加載常量到r2和r3。
    5. 保存棧指針。
    6. 以保存lr指針且可更換模式的方式切分支到 objc_msgSend 。這個方式是Objective-C語言的核心。它調用它的入參selector的實現。參數最終和傳給這個方法的參數一樣,r0是self,r1是_cmd,r2和r3是剩下的參數。這就是為何selector要賦值給r1,剩余參數賦值給r2和r3,r0是隱式加載的,因為self變量已經存在了。
    7. addValue:toValue: 方法的返回值還是r0。這個指令將r0的值賦值給r1,因為r0之后要作為C函數NSLog的參數。
    8. 加載NSLog需要的字符串給r0,像printf函數一樣。
    9. 以保存lr指針且可更換模式的方式切分支到 NSLog 。
    10. 兩個值被推出來,一個賦值給r7一個給pc。這個指令將使函數返回。

    如你所見,當生成匯編代碼時,C函數和Objective-C沒有多大差別。兩者的主要差別在于,Objective-C隱式的傳遞了兩個參數,且selector是在保存在數據段內的。

    Objective-C函數執行過程

    你已經大致看到了 objc_msgSend 函數,你可能也在Crash日志內見過它。這個函數是Objective-C運行時的核心。運行時是膠合Objective-C應用的代碼,包括所有的內存管理方法及類處理。

    每一次Objective-C函數調用,都需要 objc_msgSend C函數來派發消息。它會去對應的對象方法列表內搜索方法的實現。 objc_msgSend 函數簽名如下:

    id objc_msgSend(id self, SEL _cmd, ...)

    在函數執行當中的第一個參數是self對象。因此當我們寫諸如 self.someProperty 代碼時,self就是這么來的。

    第二個參數是少有人知的隱藏參數。你可以試試,在Objective-C方法里寫這樣一句代碼: NSLog(@"%@", NSStringFromSelector(_cmd)); ,你可以看到當前的方法被打印出來。明白了不?

    剩下的參數就是方法所需的參數了。像 addValue:toValue: 方法有兩個參數的方法,初次外,還有另外兩個參數。因此,不調用Objective-C函數,你可以直接這樣寫也可達到同樣的效果:

  • (void)foo { int add = (int)objc_msgSend(self, NSSelectorFromString(@"addValue:toValue:"), 12, 34); NSLog(@"add = %i", add); }</pre>

    注: objc_msgSend 函數的返回值是id類型,但被強轉成int類型。這沒問題是因為他們的大小都是一樣的。如果方法返回不同大小的返回值,實際上是另一個方法被調用了。你可以 在這里 了解更多信息。如果返回值是浮點型,那么另一個 objc_msgSend 方法的 變種被調用 .

    回想下上面Objective-C方法生成的等量C函數的簽名如下:

    int ViewController_addValue_toValue(id self, SEL _cmd, int a, int b)

    現在對于這個寫法應該沒什么驚訝的。可以看出它跟我們 objc_msgSend 的簽名非常匹配!意味著當 objc_msgSend 方法找到了對應方法實現時,調用的各個參數都正確了。你可以 在這里 閱讀更多關于 objc_msgSend 方法的信息。

    現在,你可以逆向工程

    獲得了ARM匯編的一些知識,你應該對程序中一些中斷,崩潰或者運行不正確有種感覺了。為何你會想去看匯編代碼?因為你找到更多信息看清到底是哪一步導致bug發生。

    有時候,你并沒有源碼,例如,你崩潰發生在第三方庫或者系統框架內。若能通過匯編調查將會讓你快速找到問題所在。iOS SDK所有的框架都裝在這個目錄下:

    <Path_to_Xcode>/Contents/Developer/Platforms/iPhoneOS.platform/Developer/ SDKs/iPhoneOS6.1.sdk/System/Library/Frameworks

    為調查這些庫,我建議你買這個軟件 HopperApp ,該軟件可以反匯編既而你可以查看。例如,打開UIKit庫,你可以看到每個方法做了啥,看清來像這樣:

    這是 http://www.raywenderlich.com/wp-content/uploads/2013/04/05-HopperApp-1.png 方法的匯編代碼。運用你新得到的ARM匯編知識,你應該可以知道方法做了什么。

    第一個selector指針加載到r1,為 objc_msgSend 方法做準備。注意到并沒有動過其他寄存器,那么r0中的self就和 shouldAutorotateToInterfaceOrientation 方法一樣。

    同樣地,你也發現被調用的函數也是只有一個參數,因為他的名字里只有一個冒號。因為只剩下r2未處理了,那么傳給 shouldAutorotateToInterfaceOrientation 的第一個參數就是我們需要傳給調用函數的參數。

    最后,調用函數后,r0沒有動過。那么調用函數的返回值,就是當前函數的返回值。

    因此你可以推論出這個函數是這么實現的:

  • (BOOL)shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)interfaceOrientation { return [self _doesTopViewControllerSupportInterfaceOrientation:interfaceOrientation]; }</pre>

    哇哦!太簡單了!通常一個函數的邏輯比這個復雜一些,但通常你都可以把他們組織起來,并快速的想明白一些代碼做了什么。

    何去何從?

    iOS匯編教程給了你一些洞察ARM內核的方法。你也學會了一些關于C和Objective-C的一些調用約定。

    裝備了這些知識,當你的應用崩潰在系統庫深部時,你就有了理解這些隨機代碼的方法。或者你只想通過看匯編代碼了解你的代碼到底是如何運行的。

    如果你對深入研究ARM感興趣,我建議你買一個 Raspberry Pi . 這些曉得設備擁有一個和iOS設備非常類似的ARM處理器,而且有許多文檔教你如何為它編寫代碼。

    另一個值得關注的是NEON。這是在所有除了iPhone 3GS之前的處理器上的額外的指令集。它提供單指令多數據流(SIMD)能力讓你非常高效地處理數據。運用這些指令的應用一般是圖像處理的。如果你需要高效地處理事情,學習如何寫NEON指令使用內聯匯編將會對你有益。這真的非常高端~

    這個事情應該夠你忙一會了,讓我們在論壇上了解下你的ARM探索之旅吧~

     

    來自:http://www.jianshu.com/p/544464a5e630

     

 本文由用戶 lmii1361 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!