三国杀武将|手游三国杀边锋版

首頁 >iOS開發

深入iOS系統底層之程序中的匯編代碼

2019-05-27 14:40 編輯: suiling 分類:iOS開發 來源:歐陽大哥2013

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

對于一個閉源系統來說如果想研究某些邏輯的內部實現就需要對匯編語言進行掌握和了解、對于某些需要高性能實現的邏輯來說用匯編語言實現可能是最好的選擇、對于某些邏輯來說可能只能用匯編來實現。以最后一個能力來說:當我們要實現一個HOOK所有OC方法調用的邏輯時,因為HOOK的方法不能破壞原有函數的參數棧,而且還需要在適當的時候調用原始的函數而不關注原始函數的入參時就只能選擇用匯編語言來實現。

查看程序的匯編代碼

其實更多的時候我們不要求去編寫一段匯編代碼或者機器指令,而是如果能夠讀懂簡單的匯編代碼就能窺探一些系統底層的實現邏輯和原理。當然市面上也有很多的反匯編的工具軟件能夠將匯編代碼轉化為高級語言的偽代碼,缺點就是這些工具大多是靜態分析工具以及反匯編出來的代碼不一定完全正確,有時候我們可能更加希望在運行時去調試或者分析一些問題,這樣能夠閱讀匯編代碼的話效果會更好一些。

查看匯編代碼的三種方法

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

在程序運行時的斷點處可以通過Debug菜單->Debug Workflow->Always Show Disassembly來切換匯編代碼模式和高級語言模式。

通過快捷鍵 alt + command + \ 可以對某個系統函數或者第三方庫函數或者類的方法設置符號斷點,這樣當程序出現相應的函數或者方法調用時就會切換到匯編代碼模式。你可以通過這種方式來閱讀和了解函數或者方法的實現。

如果你想查看某個高級語言文件生成的偽匯編代碼時,你需要在對應的文件處通過Product菜單->Perform Action->Assemble "xxxxx" 來查看這個文件生成的偽匯編代碼。當你在模擬器模式下所看到的就是x64系統下的匯編代碼,當你在設備模式下時所看到的就是arm系統下的匯編代碼。

clang命令的簡單介紹

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

image.png

可以看出無論是源代碼編譯還是程序鏈接都是用clang命令來實現的,不要被命令中大量的編譯鏈接選項所嚇倒,其實這些參數都是我們在可視化的工程的Build Settings里面設置的

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

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

  clang  [-arch ] [-x ] [-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 : 生成的代碼的體系結構,四選一。

 -x

 -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  */
                     ~~~~~~~^~~~~
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 

//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可設置的值就是中那些以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
.quad4倍類型,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, [email protected]
add x0, x0, [email protected]
adrp x1, [email protected]
add x1, x1, [email protected]
ldr x1, [x1]
adrp x2, [email protected]
add x2, x2, [email protected]

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地址

搜索CocoaChina微信公眾號:CocoaChina
微信掃一掃
訂閱每日移動開發及APP推廣熱點資訊
公眾號:
CocoaChina
我要投稿   收藏文章
上一篇:iOS多線程:『pthread、NSThread』詳盡總結

相關資訊

我來說兩句
發表評論
您還沒有登錄!請登錄注冊
所有評論(0

綜合評論

相關帖子

sina weixin mail 回到頂部
三国杀武将