深刻iOS系統底層之程序中的彙編代碼

合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。--(老子·道德經 )html

對於一個閉源系統來講若是想研究某些邏輯的內部實現就須要對彙編語言進行掌握和了解、對於某些須要高性能實現的邏輯來講用匯編語言實現多是最好的選擇、對於某些邏輯來講可能只能用匯編來實現。以最後一個能力來講:當咱們要實現一個HOOK全部OC方法調用的邏輯時,由於HOOK的方法不能破壞原有函數的參數棧,並且還須要在適當的時候調用原始的函數而不關注原始函數的入參時就只能選擇用匯編語言來實現。linux

查看程序的彙編代碼

其實更多的時候咱們不要求去編寫一段彙編代碼或者機器指令,而是若是可以讀懂簡單的彙編代碼就能窺探一些系統底層的實現邏輯和原理。固然市面上也有不少的反彙編的工具軟件可以將彙編代碼轉化爲高級語言的僞代碼,缺點就是這些工具大可能是靜態分析工具以及反彙編出來的代碼不必定徹底正確,有時候咱們可能更加但願在運行時去調試或者分析一些問題,這樣可以閱讀彙編代碼的話效果會更好一些。c++

查看彙編代碼的三種方法

Xcode提供了三種查看程序彙編代碼的方式:git

  1. 在程序運行時的斷點處能夠經過Debug菜單->Debug Workflow->Always Show Disassembly來切換匯編代碼模式和高級語言模式。
  2. 經過快捷鍵 alt + command + \ 能夠對某個系統函數或者第三方庫函數或者類的方法設置符號斷點,這樣當程序出現相應的函數或者方法調用時就會切換到彙編代碼模式。你能夠經過這種方式來閱讀和了解函數或者方法的實現。
  3. 若是你想查看某個高級語言文件生成的僞彙編代碼時,你須要在對應的文件處經過Product菜單->Perform Action->Assemble "xxxxx" 來查看這個文件生成的僞彙編代碼。當你在模擬器模式下所看到的就是x64系統下的彙編代碼,當你在設備模式下時所看到的就是arm系統下的彙編代碼。

clang命令的簡單介紹

經過上述的第三種方式查看生成的彙編代碼的方式實際上是經過clang命令完成的。clang是一個C/C++/Objective-C語言的編譯器,它包含了預處理、語法分析、優化、代碼生成、彙編裝配、連接等功能。咱們經過菜單來進行的構建程序的操做其實內部實現都是藉助clang來完成的。你能夠在命令終端中鍵入man clang來查看這個命令的全部參數和使用介紹,你還能夠在Xcode工程中使用command + 9快捷鍵就能夠看到你每次構建工程的詳細流程,這裏面有對程序使用clang命令的進行編譯和連接的具體實踐。github

程序編譯連接命令流程圖

能夠看出不管是源代碼編譯仍是程序連接都是用clang命令來實現的,不要被命令中大量的編譯連接選項所嚇倒,其實這些參數都是咱們在可視化的工程的Build Settings裏面設置的objective-c

要想了解完整的編譯選項的設置和意義能夠參考:pewpewthespells.com/blog/builds…數組

咱們只介紹clang命令的幾個主要的參數選項:xcode

clang  [-arch <arm|arm64|x86_64>] [-x <objective-c|objective-c++|c|c++|assembler-with-cpp>] [-L<庫路徑>] [-I<頭文件路徑>] [-F<框架頭文件路徑>] [-isysroot 系統SDK路徑] [-fobjc-arc | -fno-objc-arc] [-lxxx] [-framework XXX] [-Xlinker option] [-Xlinker value] [-E 源代碼文件] [-rewrite-objc 源代碼文件] [-c 源代碼文件] [-S 源代碼文件] [-filelist LinkFileList文件] [-o 輸出文件]  

複製代碼

1.常規參數

-arch <arm|arm64|x86_64|i386>: 生成的代碼的體系結構,四選一。sass

-x <objective-c|objective-c++|c|c++|assembler-with-cpp: 指定編譯的文件的語言,五選一,默認爲objective-c。這個選項用在編譯階段。bash

-I<頭文件路徑>: 指定#import或者#include .h文件的搜索路徑。

-L<庫路徑>: 指定連接時的動態庫或者靜態庫文件的搜索路徑。這個選項用在連接階段。

-F<框架頭文件路徑>: 指定#import一個框架庫時的頭文件搜索路徑。

-isysroot 系統SDK路徑: 指定程序使用的系統框架SDK的路徑。好比: -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.1.sdk 代表使用真機版的iOS12.1版本的SDK來編譯或者連接當前程序。

-fobjc-arc | -fno-objc-arc: 代表當前程序是使用arc編譯仍是mrc來編譯。

-lxxx: 只在連接時使用,代表將名字爲libxxx的庫連接到程序中來。

-framework XXX: 只在連接時使用,代表將名字爲XXX的framework庫連接到程序中來。

-Xlinker option -Xlinker value: 設置連接的選項,這裏必需要成對出現,其意義表示: option = value。

2.預處理

-E 源代碼文件 -o 輸出文件: 對源代碼進行預處理。也就是將全部#include和#import的頭文件展開、將全部宏定義展開、將全部枚舉值轉化爲常量值的處理。你能夠藉助**Product菜單->Perform Action->Preprocess "xxxxx"**來查看一個源代碼文件的預處理結果。

3.生成C++代碼

-rewrite-objc 源代碼文件: 將OC代碼轉化爲對應的C++語言實現。並在源代碼文件的當前目錄下生成一個對應的後綴爲.cpp的C++代碼。你能夠經過這種方法來詳細瞭解arc的實現原理、block的實現以及調用原理、各類OC關鍵字的實現邏輯原理、OC類屬性和方法的實現邏輯、類方法的定義以及runtime的機制等等邏輯。所以用這個參數能夠幫助咱們窺探不少iOS系統的祕密。在使用這個命令時可能會遇到一個常見的錯誤:

In file included from xxxx.m:9:
xxxx.h:9:29: fatal error: module 'UIKit' not found
#pragma clang module import UIKit /* clang -E: implicit import for #import <UIKit/UIKit.h> */
                     ~~~~~~~^~~~~
1 warning and 1 error generated.

複製代碼

這個主要是由於找不到系統SDK的路徑文件所致,所以能夠帶上-isysroot參數來同時指定系統SDK路徑。下面就是一個使用的示例:

clang -rewrite-objc -arch arm64  -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.1.sdk xxxx.m

複製代碼

這裏的-isysroot後面的路徑要確保是對應系統SDK的路徑,同時-arch中的值要和路徑中的SDK要是相同的結構體系。

4.生成彙編代碼

-S 源代碼文件 -o 輸出文件: 要將某個源代碼文件生成彙編代碼時須要在 -S 參數後面指定源代碼文件。而-o 後面的輸出文件就是對應的彙編代碼文件,通常這個輸出文件以.s爲擴展名。這裏要注意同時使用-arch參數指定輸出的體系架構。

5.編譯

-c 源代碼文件 -o 輸出文件:要編譯某個源代碼文件時使用這兩個參數選項,其中-c後面跟着的是要編譯的源代碼文件,而-o後面輸出的是.o爲擴展名的目標文件。

6.連接

-filelist LinkFileList文件 -o 輸出文件: 執行連接時要把全部目標.o文件做爲輸入參數,可是爲了管理方即可以將這些.o文件的路徑保存到一個擴展名爲.LinkFileList的文件中,而後再使用-filelist 參數後面跟隨對應的.LinkFileList文件來指定目標文件集合。而-o後面的輸出文件就是對應的可執行程序文件。

工程中引入彙編代碼

你也能夠在xcode工程中直接引入彙編代碼或者使用匯編代碼來編寫程序和函數,添加彙編文件的方法是:File菜單->New->File...->在列表中選擇:Assembly File便可。通常狀況下彙編代碼都是以.s爲擴展名,生成的文件是一個空文件,而後你就能夠在文件裏面編寫對應的彙編代碼了。系統也支持在彙編代碼中設置斷點進行調試。由於iOS系統支持多種體系結構,因此能夠在彙編代碼中使用幾個宏來區分代碼是x86_64的仍是arm或者arm64的, 就好比下面的代碼:

//你能夠像高級語言同樣經過#include引入頭文件。
#include <xxx.h>

//arm體系
#ifdef __arm__

//指令和數據定義

//arm64體系
#elif __arm64__

//指令和數據定義

//x86 32位體系
#elif __i386__

//指令和數據定義

//x86_64位體系
#elif __x86_64__

//指令和數據定義

//其餘體系
#else

#endif

複製代碼

當你在項目中添加了一個彙編文件時,就須要掌握和了解彙編代碼的編寫。關於彙編指令的詳細描述因爲太過龐大這裏就不介紹了,這裏主要介紹一些經常使用的彙編關鍵字,以便幫助你們能更好的閱讀和編寫程序。

常見的彙編語法

在Xcode中不管是AT&T仍是arm彙編語言的關鍵字都以.開頭。編寫彙編代碼主要就是數據的定義以及代碼指令。一個彙編語言文件中還可使用和C語言相似的文件引入以及各類預編譯指令,還能夠引用高級語言中定義的變量和符號以及函數。

1.註釋

彙編指令中註釋和C/C++/OC相同。arm體系下的彙編代碼特有的行註釋是代碼後面的 ;號註釋,而x86_64體系下的彙編代碼的特有的行註釋是##。

2.節

不管是指令仍是數據管理的單位都是節(Section)。由於在iOS系統的mach-o文件格式中的數據和指令的存儲都是以段(Segment)和節爲單位劃分的。任何代碼和數據老是在某個節內被定義。每一個節都歸屬於某個段,每一個節有一個惟一的名字。節定義的關鍵字和語法以下:

.section <段名>,<節名>,<節屬性>

複製代碼

相同的段名和節名能夠出如今多出,數據和代碼都是定義在由.section指定的節下開始,並結束於下一個節的定義開始處。系統最終在生成代碼時會將相同的段名和節名的內容統一彙總到一塊兒存儲。通常狀況下全部的指令代碼都是在__TEXT段下的節中被定義,而數據定義則是在__DATA段下的節中被定義。若是彙編代碼中不指定節名則數據和代碼默認是在__TEXT,__text下。系統還提供了兩個簡化代碼段和數據段的節定義關鍵字。

//代碼段的定義,等價於 .section __TEXT,__text
.text

//數據段的定義,等價於 .section __DATA,__data
.data
複製代碼

在反彙編代碼中的節定義中除了指定名稱外你還會看到一些好比:regular,pure_instructions,no_dead_strip,cstring_literals等等節定義的屬性。這些屬性所表明的意義和mach-o文件格式中的結構體struct section_64中的flags字段所表示的意義一致。flags可設置的值就是<mach-o/loader.h>中那些以S_開頭的宏定義值。

3.標籤和符號

標籤是一個可被理解的地址偏移表示,是一個地址的別名。使用標籤的目標是爲了讓程序代碼更具備可讀性。標籤訂義後能夠在其餘指令中引用,也能夠在數據變量中被引用。標籤的定義規則爲:

標籤名1:
//代碼和數據
標籤名2:
//代碼和數據
複製代碼

標籤能夠當作是一個文件中的局部指針變量,對於數據段中定義的標籤一般用來當作訪問變量的地址,而對於代碼段中定義的標籤一般用來作指令跳轉用。好比下面的代碼:

//x86_64中的代碼
.data
AGE:    //標籤的定義處
.long 13

.text
LAB1:    //標籤的定義處
mov AGE(%rip), %rax     //標籤的使用處
jmp LAB1                         //標籤的使用處

複製代碼

有的時候還能夠定義方向標籤,方向標籤只能是數字,而後能夠在使用這些方向標籤時,在方向標籤後面帶一個b代表跳轉到當前指令前面定義的某個最近的方向標籤,而方向標籤後面帶一個f代表跳轉到當前指令後面定義的某個最近的方向標籤。就好比下面演示的代碼:

//x86_64中的演示代碼,這裏面定義了方向標籤,同時也有如何跳轉到這些方向標籤的使用方法。
.text
mov %rax, %rax
1:                //a
mov %rax, %rax
2:                //b
mov %rax, %rax
2:                //c
mov %rax, %rax
jmp 2b   //跳轉到c處
jmp 1b   //跳轉到a處
jmp 1f   //跳轉到d處

1:                //d
mov %rax, %rax

複製代碼

標籤只是文件內地址偏移的別名,只能在定義的文件內部引用。要想讓這個標籤被外部引用和訪問就須要將標籤聲明爲符號。高級語言文件中定義的能被外部訪問的函數和全局變量其實都是一個符號,不論是函數地址仍是全局變量的內存地址,其實都是一個地址位置,而地址的別名則是能夠用標籤表示,所以要想將一個標籤訂義爲外部可訪問,就須要將標籤名聲明爲符號。就如高級語言中的靜態函數和靜態變量以及全局函數和全局變量同樣,彙編語言中的符號聲明也有兩種:

//對外可見的全局符號,能夠被外部程序引用和訪問。
.global  全局符號名
全局符號名:

//私有外部符號,只在程序內可引用和訪問。
.private_extern  私有外部符號名
私有外部符號名:
複製代碼

符號名要和標籤名匹配。由於C語言的函數名稱以及全局變量等符號在編譯時生成的符號前面添加一個下劃線_。因此在高級語言中的名稱對應的真實符號都是帶一個下劃線前綴的,所以通常狀況下咱們在彙編語言中聲明的符號和標籤名最好帶一個下劃線。而且在其餘高級語言的聲明中不要使用這個下化線,就好比下面的例子:

//xxx.s

//在數據段中定義一個全局變量符號_testSymbol。
.data
.global _testSymbol
_testSymbol:
.int 10

.............................................
//xxx.m

//高級語言中聲明使用這個符號。
extern int testSymbol;

int main(int argc, char *argv[])
{
   printf("testSymbol = %d",testSymbol);
   return 0;
}


複製代碼

同時在彙編代碼中引用高級語言定義的符號時,也要多帶上一個下劃線前綴。

4.對齊

由於內存尋址訪問的一些特性,要求咱們的某些代碼或者數據的存放地址必須是某個數字的倍數,也就是所謂的對齊。設置對齊的關鍵字以下:

//代表此處的地址是(2^3)8的倍數。這裏面p2align貌似和align所表達的意義類似,不知道爲何會有兩個關鍵字。
.align 3
.p2align 3
複製代碼

5.宏定義

彙編語言也能夠和C語言同樣使用宏定義,來作一些代碼複用處理。宏定義的語法以下:

//宏的開始
.macro 宏名稱

//這裏面能夠編寫任何其餘的彙編代碼和關鍵字
// 宏能夠帶參數,宏內使用參數老是從$0開始。
//宏的結束
.endmacro

複製代碼

在使用定義的宏時就直接在相應的地方插入宏的名字便可,若是宏有參數則參數跟在宏名稱後面而且參數之間以逗號分隔。下面就是一個宏定義和使用的例子:

//宏定義
.macro Test

mov x0, $0
mov x1, $1

.endmacro

//宏使用
Test 10,20

複製代碼

6.數據的定義

數據的定義相似C語言中變量的定義,彙編代碼中也支持多種類型的數據定義。定義一個數據的語法以下:

.<數據類型>  值
複製代碼

一共有以下的數據類型:

類型 描述 舉例
.byte 單個字節 .byte 0x10
.long 長整型4字節 .long 0x10
.quad 4倍類型,8字節長度 .quad 0x10
.asciz 以0結尾的字符串 .asciz "Hello world!"
.ascii 不以0結尾的字符串 .ascii "Hello world!"
.space 空字節數,後面跟數量 .space 4
.short 短整型2字節 .short 0x10

數據類型的值能夠是一個常量也但是一個表達式,也能夠是一個標籤符號。若是咱們想給某個數據定義指定一個相似於變量的名稱,則能夠和標籤來結合。好比:

name:
.asciz "歐陽大哥"
age:
.long 13
nickname:
.quad name   //這裏的暱稱變量是一個指針代表和name是相同的。

複製代碼

若是要想在代碼塊中訪問上面定義了標籤名的變量,則能夠採用以下指令:

//x86體系的指令訪問符號變量
leaq name(%rip), %rax
movl age(%rip), %ebx
movq nickname(%rip), %rcx

//arm64體系的指令訪問符號變量
adrp x0, name@PAGE
add x0, x0, name@PAGEOFF
adrp x1, age@PAGE
add x1, x1, age@PAGEOFF
ldr x1, [x1]
adrp x2, nickname@PAGE
add x2, x2, nickname@PAGEOFF

複製代碼

7.函數的定義

彙編語言中並無專門用於函數定義的關鍵字,彙編語言中只有代碼塊的定義,全部可執行的代碼塊都存放在代碼段中。所謂函數調用其實就是調用函數代碼對應的首地址。所以對於文件內的函數調用其實能夠藉助標籤來完成,而其餘文件對函數的調用則能夠藉助符號來完成。對於函數中的參數部分的處理則是按照函數調用參數傳遞的ABI規則來指定,具體詳情能夠參考個人深刻iOS系統底層之CPU寄存器介紹中的介紹。

下面就是一個求兩個參數和的加法函數在x86_64位體系結構下的實現:

//x86_64位下的函數實現
.text
.global _add
.align 3
_add:
movq  %rdi,%rbx
movq  %rsi,%rax
addq  %rbx,%rax
ret
LExit_add:

複製代碼

8.指令的編寫

關於在彙編語言中編寫指令這裏就不贅述了,不然一本書也說不完,你們能夠參考相關的彙編代碼的書籍便可,最好的方法是閱讀CPU體系結構手冊:

9.僞條件語句

彙編語言有相應的進行比較和跳轉的指令,可是咱們仍然能夠藉助僞條件語句來使得咱們的代碼更加具備可讀性。僞條件語句的語法以下:

.if 邏輯表達式
.elseif 邏輯表達式
.else
.endif

複製代碼

10.CFI: 調用框架指令

這部分僞指令以.cfi開頭。主要用來記錄函數的幀棧信息和用於異常處理。具體的指令介紹請參考:blog.csdn.net/permike/art…

引用匯編代碼文件中的符號

由於彙編代碼源文件沒有所謂的.h頭文件聲明。因此當你在其餘文件中要想使用匯編語言中定義的函數或者全局變量時,能夠在你的源代碼文件的頂部進行符號使用的聲明:

//xxxxx.m

//函數聲明
extern void 不帶下劃線的函數符號(參數列表);

//變量使用聲明
extern 類型 不帶下劃線的變量符號;

複製代碼

在高級語言中嵌入彙編代碼

咱們還能夠在高級語言中嵌入彙編代碼,嵌入的主要目的是爲了優化代碼的性能,還有一些高級語言完成不了能力好比獲取當前執行指令的地址以及讀取一些狀態寄存器和特殊寄存器的值,還有一些場景甚至能夠用匯編代碼來解決高級語言須要用鎖來解決的多線程的問題等等。具體的嵌入方法和規則我這裏就偷一下懶,直接訪問這個連接:

blog.csdn.net/pbymw8iwm/a…

就能夠很清楚的知道嵌入的規則了,這篇文章已經介紹得很仔細了。下面我將舉3個具體的例子:

  • 高級語言的變量做爲嵌入彙編代碼的輸入輸出
//計算兩個數相加
long add(long a, long b)
{
    long c = 0;
#if __arm64__
     __asm__(
             "ldr x11, %1\n"
             "ldr x12, %2\n"
             "add %0, x11, x12\n"
             :"=r"(c)
             :"m"(a),"m"(b)
             );
    
#elif __x86_64__
    
    __asm__(
            "movq %1,%%rdi\n"
            "movq %2,%%rsi\n"
            "addq %%rdi,%%rsi\n"
            "movq %%rsi,%0\n"
            :"=r"(c)
            :"m"(a),"m"(b)
            );
    
#else
        c = a + b;
#endif
    
    return c;
}

複製代碼
  • 系統的特殊寄存器的值輸出給高級語言的變量
//打印當前指令的地址以及當前線程ID
void foo()
{
    unsigned long pc = 0;
    unsigned long threadid = 0;
    
#if __arm64__

      //arm64限制了直接讀寫PC寄存器的方式,而是改動相對偏移
      //TPIDRRO_EL0是指內核中的線程ID,用專門的指令mrs來讀取
      __asm__(
              "adr x0, #0\n"
              "stur x0, %0\n"
              "mrs %1,TPIDRRO_EL0\n"
              :"=m"(pc),"=r"(threadid)
              );
    
#elif __x86_64__
    //x86體系的CPU沒有專門的寄存器保存線程ID
    __asm__(
            "leaq (%%rip), %%rdi\n"
            "movq %%rdi, %0\n"
            :"=m"(pc)
            );
#else
    NSAssert(0, @"oops!");
#endif
    
   
    NSLog(@"pc=%ld, threadid=%ld",pc, threadid);
    
}

複製代碼
  • 無鎖多線程變量訪問 假設程序中定義了兩個變量x和y,如今A線程負責讀取這兩個變量的值進行處理,而B線程則負責寫入這兩個變量的最新值,這兩個變量具備關聯繫,必須同時寫入和讀取。若是是用高級語言來實現爲了保證同步則須要在兩個線程的讀寫兩個變量的地方進行加鎖處理。而在arm體系結構下則能夠藉助ldp,stp兩個條指令來實現指令級別上的原子操做,由於無需加鎖從而達到最佳的性能。
//假設x,y變量保存在全局變量critical數組中。
long critical[2];

void read(long *px, long *py)
{
#if __arm64__
    __asm__(
            "ldp x9, x10, %2\n"
            "stur x9,%0\n"
            "stur x10,%1\n"
            :"=m"(*px),"=m"(*py):"m"(critical)
           );  
#else
    //其餘體系結構在讀取時必需要加鎖處理。
    *px = critical[0];
    *py = critical[1];
#endif
}

void write(long x, long y)
{
#if __arm64__
    __asm__(
            "stp %1, %2, %0":"=m"(critical):"r"(x),"r"(y)
           );
#else
    //其餘體系結構在寫入兩個變量時必需要加鎖處理。
    critical[0] = x;
    critical[1] = y;
#endif
}



複製代碼

👉【返回目錄


歡迎你們訪問個人github地址

相關文章
相關標籤/搜索