iOS逆向之ARM64匯編基礎

ARM處理器

我們知道,目前為止Apple的所有iOS設備都采用的是ARM處理器。ARM處理器的特點是體積小、低功耗、低成本、高性能,所以很多手機處理器都基于ARM,ARM在嵌入式系統中也具有廣泛的應用。
ARM處理器的指令集對應的就是ARM指令集。armv6|armv7|armv7s|arm64都是ARM處理器的指令集,這些指令集都是向下兼容的,例如arm64指令集兼容armv7,只是使用armv7的時候無法發揮出其性能,無法使用arm64的新特性,從而會導致程序執行效率沒那么高。在iPhone5s及其之后的iOS設備指令集都是ARM64。
還有兩個我們也很熟悉的指令集:i386和x86_64是Mac處理器的指令集,i386是針對intel通用微處理器32架構的。x86_64是針對x86架構的64位處理器。所以當使用iOS模擬器的時候會遇到i386|x86_64,因為iOS模擬器沒有ARM指令集。

不同的處理器架構使用不同的指令集。或者說,每一個處理器架構都有其特定的指令集。指令集決定了處理器的架構,處理器架構和指令集是強綁定的。因為處理器架構就是用硬件電路實現指令集。至此,我們知道了處理器架構和指令集的強依賴關系。

匯編的誕生

通過上面,我們了解到,處理器和指令集的強相關性。要設計處理器,首先就需要有指令集,規定處理器相應操作,通過指令集去控制處理器實現相應功能。但處理器是一堆硬件電路,只能識別二進制數據,所以指令集也是由一堆二進制數據組成。

而二進制數據對人類來說讀起來很麻煩。為了方便人類操作指令集,發明了匯編語言來描述指令集。匯編語言是用類似人類的語言描述指令集,讀起來相對容易。

雖然匯編語言讀起來方便了,但也有缺陷。首先匯編語言操作起來還是挺麻煩的,一個簡單的數學運算需要執行多條指令行,我們需要清楚每一步的操作,完全“面向過程”。其次因為匯編語言是對指令集的描述,匯編語言包括一條條指令,所以當指令集改變時,就得修改相應匯編語言,導致其可移植性很差。不能跨平臺使用,比如ARM的匯編語言與Intel X86的就格格不入。因為這種描述指令集的匯編語言移植性差,在跨平臺上表現出來了力不從心,于是前輩們就進一步進行了抽象,發明了若干種超越指令集的高級語言,比如C、C++、Java。
但處理器只能識別二進制碼,那怎么能識別高級語言呢?于是人們開發了編譯器,借助于編譯器,我們可以把高級語言進行編譯轉換為匯編語言,匯編語言進一步解釋為二進制機器碼。
另外除了編譯器之外,還有解釋器,對于編譯型語言(比如C、C++)通常是由編譯器進行編譯&優化成低級語言或中間語言,然后就可以在目標機器上運行編譯后的產物。對于解釋性語言(比如PHP、JS)通常是不需要進行編譯處理,在運行時直接由解釋器逐行的解釋執行的。所以通常認為解釋型語言比編譯型語言執行效率低、性能差。關于編譯器&解釋器、編譯型語言&解釋型語言的關系和區別此處不展開討論,讀者可自行查閱搜索資料,這里有一篇文章編譯器與解釋器的區別和工作原理

匯編的核心

匯編語言本身并不難,復雜的是匯編語言的操作。匯編的核心就是對寄存器、指令、堆棧的操作
CPU從邏輯上可以劃分成3個模塊,分別是控制單元、運算單元和存儲單元,又叫控制器、運算器、寄存器。
寄存器用于存放指令、數據、地址等信息供運算器計算。一個CPU內部有多個寄存器。
控制器負責把內存上的指令、數據等讀入寄存器,并根據指令的執行結果來控制整個計算機。
堆棧就是指令執行時臨時存放函數參數、局部變量的內存空間。棧的入口和出口是同一個,所以棧的特性是先進后出(FILO)后進先出(LIFO)。先入棧的數據在棧底,后入棧的在棧頂,棧頂的數據先出棧,棧底的數據最后出棧。通常棧的增長方向是從高內存地址到低內存地址,所以棧底是高地址,棧頂是低地址。iOS是小端模式,小端模式的特點是從高內存地址到低內存地址存儲數據的高字節到低字節。大端模式正好相反。

大小端模式

什么是大小端模式?
我們知道數據在內存中是按字節存儲的,有些數據可能占用多個字節。大小端模式就是字節序問題。字節序,顧名思義就是字節的順序。就是一個數據的多個字節在內存中的存放順序。如果一個數據就占一個字節也就談不上字節序問題,畢竟無論如何排列都是相同的。

小端模式(little-endian):從低地址到高地址的順序存放數據的低位字節到高位字節。即小端模式下低位字節存放在低地址端,高位字節存放在高地址端。
大端模式(big-endian):從低地址到高地址的順序存放數據的高位字節到低位字節。即大端模式下高位存放在低地址,低位字節存放在高地址端。

小端模式

1.有些文章說棧的增長方向和大小端模式有關,其實這種說法是錯誤的。無論大小端模式棧的增長方向通常都是從高地址到低地址。大小端模式取決于處理器的架構。
2.關于大小端模式又稱字節順序、字節序。小端模式又稱為小端序、小尾序、小端法、低位優先。大端模式又稱為大端序、大尾序、大端法、高位優先。

寄存器

ARM處理器共有37個寄存器,被分為若干個組(BANK),這些寄存器包括:
1 31個通用寄存器,包括程序計數器(PC指針)。
2 6個狀態寄存器,用以標識CPU的工作狀態及程序的運行狀態,均為32位,只使用了其中的一部分。

另一個角度,寄存器通常可分為通用寄存器、浮點寄存器、狀態寄存器。
ARM64有31個通用寄存器,每個寄存器可以讀取一個64位的數據。當使用X0~X30的時候,他就是一個64位的數;當使用W0~W30的時候,他就是一個32位的數;32位的數實際上訪問的是寄存器的低32位,寫入時會將高32位清零(早期32位通用寄存器是R0~R28)。X代表64位寄存器。W代表32位寄存器,W即為word(一個字),一個字代表32位。

X0~X7:這8個寄存器是用來傳遞函數的參數的,如果有更多的函數參數則使用棧來傳遞,實際運算時再從棧上讀取函數參數。X0寄存器也用來存放函數的返回值。

FP:全稱Frame Pointer,幀指針寄存器,即X29,是指向棧底的寄存器。用于標識棧的底部,以免讀取棧數據時“越界”。

SP:全稱Stack Pointer,棧指針寄存器,指向棧的頂部。其32位版為WSP

LR:全稱Link Register,鏈接寄存器,即X30。x30無法像普通寄存器那樣拆分出低32位來單獨使用。LR寄存器用于存放函數跳轉前的下一條指令的地址,方便函數執行完后返回到函數的下一條指令繼續執行。或者說,LR存儲著函數調用完成時的返回地址,用于做函數調用棧跟蹤。程序在崩潰時能夠將函數調用棧打印出來就是借助LR寄存器來實現的。

PC:全稱program counter,程序計數器。用于存儲將要執行的下一條指令的內存地址。通常在調試狀態下看到的PC值都是當前斷點處的地址。所以很多人認為PC用于存儲CPU當前執行的指令的地址,記錄CPU當前執行的是哪一條指令,實際上這種理解是錯誤的。

LR和PC的完美配合:通常我們調用BL無條件跳轉指令時會先將函數跳轉前的下一條指令的地址存放到(X30)LR寄存器中(即把函數調用完成時的返回地址存入X30)。 BL跳轉到標記處執行函數代碼, 代碼執行完之后,會跳回LR存儲的指令地址處繼續執行。那是如何從LR中取出的指令的呢?BL指令中的ret會把LR(X30)寄存器的地址賦值給PC寄存器,這樣CPU取PC寄存器中的指令地址就可以取到執行BL指令跳轉前的下一條指令的地址。 程序得以跳回原來的地方繼續有序執行。
這里面程序可以跳回繼續執行離不開BL、LR、PC的精誠合作。

零寄存器:
wzr 32位的零寄存器。可對標 32位的普通寄存器w0~w28
xzr 64位的零寄存器。可對標 32位的普通寄存器w0~w28
專門用來存儲數字0。比如null、nil、false、NO
因為匯編不支持將立即數存儲到一個地址中,所以需要先將立即數存儲到寄存器中,然后將寄存器中的數字存儲到存儲器。如下:

mov xzr, 0 ; 先把立即數存儲到64位的零寄存器zxr中
str xzr [x1, #0x8] ; 再把zxr中的數據存儲到x1+0x8的存儲空間中

指令

1.關于匯編的大小寫問題?
匯編不區分大小寫,只有字符型數據或者字符串區分大小寫。所以匯編中的指令和寄存器可以是大寫也可以是小寫。例如:如下兩行指令是等價的。

add x0, x1, x2
ADD X0, X1, X2

2.關于匯編如何添加注釋?
匯編語言的注釋是以分號";"開頭的,分號之后的內容都屬于注釋。一般而言,匯編語言的注釋出現在以下3個地方:

  • 1>. 程序的最前面,注釋內容一般是該程序的說明,解釋程序的主要功能,程序的版本號,程序的修改日志,程序的編制人等等
  • 2>. 子程序的前面,一般說明該子程序或函數完成的功能,輸入參數,輸出參數,影響的標志位等等。
  • 3>. 指令行的后面,解釋該行指令語句的功能。

常見的指令集通常包括以下幾種:

  • 算術指令
  • 跳轉指令
  • 邏輯指令
  • 數據傳輸指令
  • 地址偏移指令
  • 移位運算指令
  • 加載/存儲指令

算數指令

ADD:加法運算指令。把一個寄存器中的數據或立即數與另一個寄存器中的數據或立即數進行相加。例如:

ADD X0, X1, X2 ; 把寄存器X1、X2的值相加后賦值給寄存器X0。即X0 = X1+X2

SUB:減法運算指令。把一個寄存器中的數據或立即數與另一個寄存器中的數據或立即數進行相減。例如:

SUB X0, X1, X2 ; 把寄存器x1、x2的值相減后賦值給寄存器x0。即x0 = X1-X2

MUL:乘法運算指令。把一個寄存器中的數據或立即數與另一個寄存器中的數據或立即數進行相乘。例如:

MUL X0, X0, X8 ; 把寄存器x0、x8的值相乘后賦值給寄存器x0。即x0 = X0*X8

SDIV:有符號除法運算指令。
UDIV:無符號除法運算指令。

SDIV X0, X0, X1 ; 即X0 = X0 / X1 
UDIV X0, X0, X1 ; 即X0 = X0 / X1

CMP:比較(compare)指令。把兩個寄存器的數據進行相減,不存儲結果,只更新CPSR中的標志位。例如:

CMP X28, X0 ; X28與X0相減,不存儲結果只更新CPSR中的標志位。 (CPSR即為current program status register)

跳轉指令

跳轉指令分為條件跳轉和無條件跳轉。條件跳轉即若條件為真則跳轉。無條件跳轉是直接跳轉到某個位置執行指令。無條件跳轉可以類比函數調用。

無條件跳轉

B:無條件跳轉指令。例如:

B label ; 跳轉到label標簽處開始執行

BL:無條件跳轉指令。與B相比,執行BL前返回地址會被保存到X30(LR)寄存器。BL執行完后會把LR保存的地址賦值給PC寄存器。這樣就可以回到BL跳轉前的位置繼續向下執行。。BL實例:

BL label ; 

RET:子程序返回指令。返回地址默認保存在X30(LR)寄存器

為什么B指令跳轉的代碼中有ret也回不到跳轉前的下一條指令呢?
原因是B指令在跳轉前沒有像BL指令那樣把下一條指令的地址存儲到LR(x30)寄存器中,所以B的ret就不能從LR寄存器中讀出正確的地址賦值個PC寄存器。

邏輯指令

AND:邏輯與運算指令。例如:

AND X0, X1, X2 ; X1和X2寄存器的數據進行邏輯與運算結果保存到X0中。即:X0 = X1 & X2

ORR:邏輯或運算指令。例如:

ORR X0, X1, X2 ; X1和X2寄存器的數據進行邏輯或運算結果保存到X0中。即:X0 = X1 | X2

EOR:邏輯抑或運算指令(else or)。例如:

EOR X0, X1, X2 ; X1和X2寄存器的數據進行邏輯抑或運算結果保存到X0中。即:X0 = X1 ^ X2

數據傳輸指令

MOV:把寄存器中的數據存儲到另外一個寄存器中。例如:

MOV X19, X1 ; 把寄存器X1 中的數據存儲到寄存器X19中。即 X1 = X19

其他常用數據傳輸指令還有:MOVZ、MOVN、MOVK。

地址偏移指令

ADR:小范圍的地址讀取指令。ADR 指令將基于PC 相對偏移的地址值讀取到寄存器中。例如:

ADR Xn, label ; 即Xn = PC + label

ADRP:以頁為單位的大范圍的地址讀取指令,這里的P就是page的意思。

加載/存儲指令

1.加載(load)、存儲(store)指令都是成對出現的
2.無論是加載還是存儲相關的指令,指令后面都只能寫寄存器,不能把寄存器放到指令行的最后面。

LDR、LDUR、LDP都是和加載相關的指令。即把內存中的數據讀取到寄存器中。LD即為load的縮寫,R即為register的縮寫,P即為pair的縮寫,即同時操作兩個寄存器。

LDR & LDUR :從內存中讀取8/4字節數據到一個64/32位寄存器中,即從源寄存器中讀取數據寫入目的寄存器。LDUR中的“U”是unscaled的縮寫,代表不需要按字節對齊。LDUR和LDR指令的區別:可以簡單理解為地址偏移量為負數則使用LDUR,偏移量為正數則使用LDR。例如:

LDR X1, [sp, #0x28] ; 從SP+ 0x28處開始讀取8字節數據到X1寄存器中。
LDUR X30, [X29, #-0x10] ; 從X29 - 0x10出開始讀取8字節數據到X30(LR)寄存器中。

LDP:從內存中讀取數據放到兩個寄存器中。例如:

LDP  w0, w1, [x0, #0x10] ; 讀取x0+0x10內存的字數據,然后四個字節賦值給w0, 另外四個字節賦值給w1

STR、STUR、STP都是和存儲相關的指令。即把寄存器中的數據寫入內存中。ST即為store的縮寫,R即為register的縮寫,P即為pair的縮寫,即同時操作兩個寄存器。

STR & STUR:將寄存器中的數據寫入內存中,即把源寄存器的內容寫入目的寄存器。STR&STUR的區別和LDR&LDUR的區別類似。如下:

STR X0, [sp, #0x28] ; 將X0寄存器中的數據寫入SP + 0x28的位置。
STUR X0, [sp, #-0x10] ; 將X0寄存器中的數據寫入SP - 0x10的位置。

STP:將兩個寄存器的數據寫入內存中。例如:

STP X1, X2, [X0, #0x28]; 將X1和X2的數據 寫入 X0+#0x28的位置

堆棧

基于前面的講解,我們知道了程序運行時會內存上申請一個稱為棧的數據空間。數據在存儲時是從內存的下層(大的地址編號)逐漸往上層(小的地址編號)累積,讀出時則是按照從上往下的順序進行的。

32位的x86系列的CPU中,進行1次棧的push或pop操作,即可處理32位(4字節)的數據。
64位的x86_64系列的CPU中,進行1次棧的push或pop操作,即可處理64位(8字節)的數據。
在數據入棧和出棧的時候,不需要指定“哪一個地址編號的內存進行push或pop”。這是因為對棧讀寫的內存地址是由SP寄存器(棧指針寄存器)進行管理的。數據入棧或出棧后,SP寄存器的值會自動更新(32位CPU下,push指令會使SP存儲的地址-4字節,pop指令+4字節)。

寄存器數量有限,在實際的操作中,需要借助棧來完成一些計算任務。比如我們知道X0~X7這8個寄存器可以用來存儲函數參數,如果函數參數多余8個就需要借助于棧來存儲額外的參數。而對于函數參數是浮點數的情況,傳參時則不再使用X0~X7寄存器,而是使用D0~D7寄存器。超出8個的參數仍然使用棧來傳遞。
在操作棧的時候,通常包括棧空間的開辟和回收。開辟和回收棧空間通常使用的是SUB和ADD指令和FP、SP寄存器。

通過上面敘述,我們已經知道如下結論:
1.棧是一塊連續的內存空間
2.棧的增長方向是從高地址到低地址
3.棧底為高地址,棧頂為低地址,先入棧的數據地址高
4.FP(X29)寄存器為幀指針寄存器,用于指向棧底
5.SP寄存器為棧指針寄存器,用于指向棧頂
6.每次數據入棧或出棧都會更新SP寄存器的值

如下2張圖所示,可以看出兩個變量在棧內存中的布局。
圖1中先后聲明兩個變量num1、num2。先聲明的變量先入棧,后聲明的變量后入棧。假設棧中只有num1和num2兩個變量,那么0x222在棧底,0x333在棧頂。
通過圖2可以看出從做左到右、從上到下內存地址是增加的。所以0x333的內存地址高于0x222的內存地址。即可證明棧的增長方向確實是高地址到低地址

由圖2也可以看出,小端模式下高位字節存儲在高地址,低位字節存儲在低地址。以0x333的內存布局(紅色部分33 03 00 00)舉例 ,0x333在內存中占4個字節,低位的33在第一字節,其內存地址是0x7ffee66a8238(十進制為140732764160568)。較高位的03在第二字節,其內存地址是0x7ffee66a8239(十進制為140732764160569)。其余兩個高位字節為0,用0占位補齊。不難看出小端模式下確實是高字節位在高地址,低字節位在低地址。
圖3是num1(0x222)和num2(0x333)兩個變量在棧中的布局和對應的內存地址。可見棧的增長方向確實是由高地址向低地址方向
圖4描述了小端模式下,4字節整型變量num1(0x222)和num2(0x333)在內存中的布局。可見小端模式下確實是低位字節存放在低地址端,高位字節存放在高地址端.

圖1

圖2
圖3
圖4

參考文章

字節序
arm官網
ARM百度百科
《iOS應用逆向與安全之道》
《程序是怎樣跑起來的》第10章
堆、棧的地址高低? 棧的增長方向?


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容