ARM匯編基礎
title: ARM匯編 date: 2019-03-11 09:03:27 tags: ARM categories: categories
-
Part 1:ARM匯編介紹
-
Part 2:數據類型寄存器
-
Part 3: ARM指令集
-
-
Part 5:重復性加載及存儲
-
Part 6: 分支和條件執行
-
Part 7:棧以及函數
ARM&X86cpu的一些區別:
Intel是一個CISC(Complex Instruction Set Computing,復雜指令集)處理器。因此它具有更龐大,功能更豐富的指令集,並且允許指令進行一些復雜的訪存操作。
ARM是一個RISC(Reduced Instruction Set Computing,精簡指令集)處理器。因此它擁有一套精簡的指令集(100個左右,甚至更少的指令)以及比CISC處理器更多的通用寄存器。與Intel處理器不同,ARM指令只處理寄存器中的數據,並使用了load/store結構訪問存儲器,也就是說只有load/store指令可以訪問存儲器。所以如果我們要增加某個內存地址中保存的值,至少需要三種類型的指令(load指令、加法指令和store指令),首先我們需要使用load指令將指定地址內存中的值加載到寄存器中,然後使用加法指令增加寄存器中的值,然後用store指令將寄存器中的值寫回內存。
#簡單的說 ARM執行一個操作,需要的步驟更多。而x86的直接就能進行訪存
ARM優勢
一個重要的優勢是指令可以被更快的執行(RISC處理器通過引入流水線機制,減少每個指令的占用的CPU的時鐘周期來縮短執行時間)。它的劣勢也很明顯,較少的指令增加了軟件(事實上是編譯器)的復雜性。另一個重要的事實是,ARM具有兩種運行模式(可以類比x86的實模式和保護模式),ARM模式和Thumb模式。
ARM也更加節省能耗
簡介剪貼自:https://azeria-labs.com/writing-arm-assembly-part-1/
ARM和X86的差異:https://www.cnblogs.com/bitter/p/4023176.html
ARM匯編數據類型基礎
與高級語言類似,ARM也支持操作不同的數據類型。
被加載或者存儲的數據類型可以是無符號(有符號)的字(words,四字節),半字(halfwords,兩字節),或者字節(bytes)。這些數據類型在匯編語言中的擴展後綴為-h
或者-sh
對應著半字,-b
或者-sb
對應著字節,但是對於字並沒有對應的擴展。無符號類型與有符號類型的差別是:
-
符號數據類型可以包含正負數所以數值範圍上更低些
-
無符號數據類型可以放得下很大的正數但是放不了負數
這有一些要求使用對應數據類型做存取操作的匯編指令示例:
ldr = 加載字,寬度四字節
ldrh = 加載無符號的半字,寬度兩字節
ldrsh = 加載有符號的半字,寬度兩字節
ldrb = 加載無符號的字節
ldrsb = 加載有符號的字節
?
str = 存儲字,寬度四字節
strh = 存儲無符號的半字,寬度兩字節
strsh = 存儲有符號的半字,寬度兩字節
strb = 存儲無符號的字節
strsb = 存儲有符號的字節
字節序
在內存中有兩種字節排布順序,大端序(BE)或者小端序(LE)。兩者的主要不同是對象中的每個字節在內存中的存儲順序存在差異。一般X86中是小端序,最低的字節存儲在最低的地址上。在大端機中最高的字節存儲在最低的地址上。
在版本3之前,ARM使用的是小端序,但在這之後就都是使用大端序了,但也允許切換回小端序。在我們樣例代碼所在的ARMv6中,指令代碼是以[小端序排列對齊]。但是數據訪問時采取大端序還是小端序使用程序狀態寄存器(CPSR)的第9比特位來決定的。
image
ARM寄存器
寄存器的數量由ARM版本決定。根據ARM參考手冊,在ARMv6-M與ARMv7-M的處理器中有30個32bit位寬度的通用寄存器。前16個寄存器是用戶層可訪問控制的,其他的寄存器在高權限進程中可以訪問(但ARMv6-M與ARMv7-M除外)。我們僅介紹可以在任何權限模式下訪問的16個寄存器。這16個寄存器分為兩組:通用寄存器與有特殊含義的寄存器。
# | 別名 | 用途 |
---|---|---|
R0 | - | 通用寄存器 |
R1 | - | 通用寄存器 |
R2 | - | 通用寄存器 |
R3 | - | 通用寄存器 |
R4 | - | 通用寄存器 |
R5 | - | 通用寄存器 |
R6 | - | 通用寄存器 |
R7 | - | 一般放系統調用號 |
R8 | - | 通用寄存器 |
R9 | - | 通用寄存器 |
R10 | - | 通用寄存器 |
R11 | FP | 棧幀指針 |
R12 | IP | 內部程序調用 |
R13 | SP | 棧指針 |
R14 | LR | 鏈接寄存器(一般存放函數返回地址) |
R15 | PC | 程序計數寄存器 |
CPSR | - | 當前程序狀態寄存器 |
下面這張表是ARM架構與寄存器與Intel架構寄存器的關系:
ARM | 描述 | X86 |
---|---|---|
R0 | 通用寄存器 | EAX |
R1-R5 | 通用寄存器 | EBX,ECX,EDX,ESI,EDI |
R6-R10 | 通用寄存器 | - |
R11(FP) | 棧幀指針 | EBP |
R12 | 內部程序調用 | - |
R13(SP) | 棧指針 | ESP |
R14(LR) | 鏈接寄存器 | - |
R14(LR) | <-程序計數器/機器碼指針-> | EIP |
CPSR | 程序狀態寄存器 | EFLAGS |
R0-R12:用來在通用操作中存儲臨時的值,指針等。R0被用來存儲函數調用的返回值。R7經常被用作存儲系統調用號,R11存放著幫助我們找到棧幀邊界的指針(之後會講)。以及,在ARM的函數調用約定中,前四個參數按順序存放在R0-R3中。
R13:SP(棧指針)。棧指針寄存器用來指向當前的棧頂。棧是一片來存儲函數調用中相關數據的內存,在函數返回時會被修改為對應的棧指針。棧指針用來幫助在棧上申請數據空間。比如說你要申請一個字的大小,就會將棧指針減4,再將數據放入之前所指向的位置。
R14:LR(鏈接寄存器)。當一個函數調用發生,鏈接寄存器就被用來記錄函數調用發生所在位置的下一條指令的地址。這麽做允許我們快速的從子函數返回到父函數。
R15:PC(程序計數器)。程序計數器是一個在程序指令執行時自增的計數器。它的大小在ARM模式下總是4字節對齊,在Thumb模式下總是兩字節對齊。當執行一個分支指令時,PC存儲目的地址。在程序執行中,ARM模式下的PC存儲著當前指令加8(兩條ARM指令後)的位置,Thumb(v1)模式下的PC存儲著當前指令加4(兩條Thumb指令後)的位置。這也是X86與ARM在PC上的主要不同之處。
我們可以通過調試來觀察PC的行為。我們的程序中將PC的值存到R0中同時包含了兩條其他指令,來看看會發生什麽。
.section .text
.global _start
?
_start:
mov r0, pc
mov r1, #2
add r2, r1, r1
bkpt
在GDB中,我們開始調試這段匯編代碼:
gef> br _start
Breakpoint 1 at 0x8054
gef> run
在開始執行觸發斷點後,首先會在GDB中看到:
$r0 0x00000000 $r1 0x00000000 $r2 0x00000000 $r3 0x00000000
$r4 0x00000000 $r5 0x00000000 $r6 0x00000000 $r7 0x00000000
$r8 0x00000000 $r9 0x00000000 $r10 0x00000000 $r11 0x00000000
$r12 0x00000000 $sp 0xbefff7e0 $lr 0x00000000 $pc 0x00008054
$cpsr 0x00000010
?
0x8054 <_start> mov r0, pc <- $pc
0x8058 <_start+4> mov r0, #2
0x805c <_start+8> add r1, r0, r0
0x8060 <_start+12> bkpt 0x0000
0x8064 andeq r1, r0, r1, asr #10
0x8068 cmnvs r5, r0, lsl #2
0x806c tsteq r0, r2, ror #18
0x8070 andeq r0, r0, r11
0x8074 tsteq r8, r6, lsl #6
可以看到在程序的開始PC指向0x8054這個位置即第一條要被執行的指令,那麽此時我們使用GDB命令si,執行下一條機器碼。下一條指令是把PC的值放到R0寄存器中,所以應該是0x8054麽?來看看調試器的結果。
$r0 0x0000805c $r1 0x00000000 $r2 0x00000000 $r3 0x00000000
$r4 0x00000000 $r5 0x00000000 $r6 0x00000000 $r7 0x00000000
$r8 0x00000000 $r9 0x00000000 $r10 0x00000000 $r11 0x00000000
$r12 0x00000000 $sp 0xbefff7e0 $lr 0x00000000 $pc 0x00008058
$cpsr 0x00000010
?
0x8058 <_start+4> mov r0, #2 <- $pc
0x805c <_start+8> add r1, r0, r0
0x8060 <_start+12> bkpt 0x0000
0x8064 andeq r1, r0, r1, asr #10
0x8068 cmnvs r5, r0, lsl #2
0x806c tsteq r0, r2, ror #18
0x8070 andeq r0, r0, r11
0x8074 tsteq r8, r6, lsl #6
0x8078 adfcssp f0, f0, #4.0
當然不是,在執行0x8054這條位置的機器碼時,PC已經讀到了兩條指令後的位置也就是0x805c(見R0寄存器)。所以我們以為直接讀取PC寄存器的值時,它指向的是下一條指令的位置。但是調試器告訴我們,PC指向當前指令向後兩條機器碼的位置。這是因為早期的ARM處理器總是會先獲取當前位置後兩條的機器碼。這麽做的原因也是確保與早期處理器的兼容性。
當前程序狀態寄存器(CPSR)
當你用GDB調試ARM程序的的時候你能會可以看見Flags這一欄(GDB配置插件GEF後就可以看見了,或者直接在GDB裏面輸入flags也可以)。
image
圖中寄存器$CSPR
顯示了當前狀態寄存器的值,Flags裏面出現的thumb,fast,interrupt,overflow,carry,zero,negative就是來源於CSPR寄存器中對應比特位的值。ARM架構的N,Z,C,V與X86架構EFLAG中的SF,ZF,CF,OF相對應。這些比特位在匯編級別的條件執行或者循環的跳出時,被用作判斷的依據。
CF是進位標誌;PF是奇偶標誌;AF是輔助進位標誌;ZF是零標誌;SF是符號標誌;OF是溢出標誌。
上圖展示了32位的CPSR寄存器的比特位含義,左邊是最大比特位,右邊是最小比特位。每個單元代表一個比特。這一個個比特的含義都很豐富:
標記 | 含義 |
---|---|
N(Negative) | 指令結果為負值時置1 |
Z(Zero) | 指令結果為零值時置1 |
C(Carry) | 對於加法有進位則置1,對於減法有借位則置0 |
V(Overflow) | 指令結果不能用32位的二進制補碼存儲,即發生了溢出時置1 |
E(Endian) | 小端序置0,大端序置1 |
T(Thumb) | 當為Thumb模式時置1,ARM模式置0 |
M(Mode) | 當前的權限模式(用戶態,內核態) |
J(Jazelle) | 允許ARM處理器去以硬件執行java字節碼的狀態標示 |
假設我們用CMP指令去比較1和2,結果會是一個負數因為1-2=-1。然而當我們反過來用2和1比較,C位將被設定,因為在一個較大的數上減了較小的數,沒有發生借位。當我們比較兩個相同的數比如2和2時,由於結果是0,Z標誌位將被置一。註意CMP指令中被使用的寄存器的值並不會被修改,其計算結果僅僅影響到CPSR寄存器中的狀態位。
在開了GEF插件的GDB中,計算結果如下圖:在這裏我們比較的兩個寄存器是R1和R0,所以執行後的flag狀態如下圖。
Carry位Flag被設置的原因是CMP R1,R0會去拿4和2做比較。因為我們用以個較大的數字去減一個較少的數字,沒有發生借位。Carry位便被置1。相反的,如果是CMP R0,R1那麽Negative位會被置一。
原作鏈接:https://www.jianshu.com/p/a28638589d55
ARM模式與THUMB模式
ARM處理器有兩個主要的操作狀態,ARM模式以及Thumb模式(Jazelle模式先不考慮)。這些模式與特權模式並不沖突。SVC模式既可以在ARM下調用也可以在Thumb下調用。只不過兩種狀態的主要不同是指令集的不同,ARM模式的指令集寬度是32位而Thumb是16位寬度(但也可以是32位)。知道何時以及如何使用Thumb模式對於ARM漏洞利用的開發尤其重要。當我們寫ARM的shellcode時候,我們需要盡可能的少用NULL以及使用16位寬度的Thumb指令以精簡代碼。
不同版本ARM,其調用約定不完全相同,而且支持的Thumb指令集也是不完全相同。在某些版本,ARM提出了擴展型Thumb指令集(也叫Thumbv2),允許執行32位寬的Thumb指令以及之前版本不支持的條件執行。為了在Thumb模式下使用條件執行指令,Thumb提出了"IT"分支指令。然而,這條指令在之後的版本又被更改移除了,說是為了讓一些事情變得更加簡單方便。並不清楚各個版本的ARM架構所支持的具體的ARM/Thumb指令集,而且我也的確不想知道。我覺得你也應該不用深究這個問題。因為你只需要知道你設備上的關鍵ARM版本所支持的Thumb指令集就可以了。以及ARM信息中心可以幫你弄清楚你的ARM版本到底是多少。
就像之前說到的,Thumb也有很多不同的版本。不過不同的名字僅僅是為了區分不同版本的Thumb指令集而已(也就是對於處理器來說,這些指令永遠都是Thumb指令)。
-
Thumb-1(16位寬指令集):在ARMv6以及更早期的版本上使用。
-
Thumb-2(16位/32位寬指令集):在Thumb-1基礎上擴展的更多的指令集(在ARMv6T2以及ARMv7即很多32位Android手機所支持的架構上使用)
-
Thumb-EE:包括一些改變以及對於動態生成代碼的補充(即那些在設備上執行前或者運行時編譯的代碼)
ARM與Thumb的不同之處在於:
-
對於條件執行指令(不是條件跳轉指令):所有的ARM狀態指令都支持條件執行。一些版本的ARM處理器上允許在Thumb模式下通過IT匯編指令進行條件執行。條件執行減少了要被執行的指令數量,以及用來做分支跳轉的語句,所以具有更高的代碼密度。
-
ARM模式與Thumb模式的32位指令:Thumb的32位匯編指令都有類似於
a.w
的擴展後綴。 -
桶型移位是另一種獨特的ARM模式特性。它可以被用來減少指令數量。比如說,為了減少使用乘法所需的兩條指令(乘法操作需要先乘2然後再把結果用MOV存儲到另一個寄存器中),就可以使用在MOV中自帶移位乘法操作的左移指令(
Mov R1, R0, LSL #1
)。
在ARM模式與Thumb模式間切換的話,以下兩個條件之一必須滿足:
-
我們可以在使用分支跳轉指令BX(branch and exchange)或者分支鏈接跳轉指令BLX(branch,link and exchange)時,將目的寄存器的最低位置為1。之後的代碼執行就會在Thumb模式下進行。你也許會好奇這樣做目標跳轉地址不就有對齊問題了麽,因為代碼都是2字節或者4字節對齊的?但事實上這並不會造成問題,因為處理器會直接忽略最低比特位的標識。更多的細節我們會在第6篇中解釋。
-
我們之前有說過,在CPSR當前程序狀態寄存器中,T標誌位用來代表當前程序是不是在Thumb模式下運行的。
ARM指令集規律含義
這一節的目的是簡要的介紹ARM的通用指令集。知道每一句匯編指令是怎麽操作使用,相互關聯,最終組成程序是很重要的。之前說過,匯編語言是由構建機器碼塊的指令組成。所以ARM指令通常由助記符外加一到兩個跟在後面的操作符組成,如下面的模板所示:
MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
助記符{是否使用CPSR}{是否條件執行以及條件} {目的寄存器}, 操作符1, 操作符2
由於ARM指令的靈活性,不是全部的指令都滿足這個模板,不過大部分都滿足了。下面來說說模板中的含義:
MNEMONIC - 指令的助記符如ADD
{S} - 可選的擴展位,如果指令後加了S,則需要依據計算結果更新CPSR寄存器中的條件跳轉相關的FLAG
{condition} - 如果機器碼要被條件執行,那它需要滿足的條件標示
{Rd} - 存儲結果的目的寄存器
Operand1 - 第一個操作數,寄存器或者是一個立即數
Operand2 - 第二個(可變的)操作數,可以是一個立即數或者寄存器或者有偏移量的寄存器
當助記符,S,目的寄存器以及第一個操作數都被聲明的時候,條件執行以及第二操作數需要一些聲明。因為條件執行是依賴於CPSR寄存器的值的,更精確的說是寄存器中的一些比特位。第二操作數是一個可變操作數,因為我們可以以各種形式來使用它,立即數,寄存器,或者有偏移量的寄存器。舉例來說,第二操作數還有如下操作:
#123 - 立即數
Rx - 寄存器比如R1
Rx, ASR n - 對寄存器中的值進行算術右移n位後的值
Rx, LSL n - 對寄存器中的值進行邏輯左移n位後的值
Rx, LSR n - 對寄存器中的值進行邏輯右移n位後的值
Rx, ROR n - 對寄存器中的值進行循環右移n位後的值
Rx, RRX - 對寄存器中的值進行帶擴展的循環右移1位後的值
在知道了這個機器碼模板後,然我們試著去理解這些指令:
ADD R0, R1, R2 - 將第一操作數R1的內容與第二操作數R2的內容相加,將結果存儲到R0中。 ADD R0, R1, #2 - 將第一操作數R1的內容與第二操作數一個立即數相加,將結果存到R0中 MOVLE R0, #5 - 當滿足條件LE(Less and Equal,小於等於0)將第二操作數立即數5移動到R0中,註意這條指令與MOVLE R0, R0, #5相同 MOV R0, R1, LSL #1 - 將第一操作數R1寄存器中的值邏輯左移1位後存入R0
最後我們總結一下,滿足這個模板的一些通用ARM指令集以及其含義:
指令 | 含義 | 指令 | 含義 |
---|---|---|---|
MOV | 移動數據 | EOR | 比特位異或 |
MVN | 取反碼移動數據 | LDR | 加載數據 |
ADD | 數據相加 | STR | 存儲數據 |
SUB | 數據相減 | LDM | 多次加載 |
MUL | 數據相乘 | STM | 多次存儲 |
LSL | 邏輯左移 | PUSH | 壓棧 |
LSR | 邏輯右移 | POP | 出棧 |
ASR | 算術右移 | B | 分支跳轉 |
ROR | 循環右移 | BL | 鏈接分支跳轉 |
CMP | 比較操作 | BX | 分支跳轉切換 |
AND | 比特位與 | BLX | 鏈接分支跳轉切換 |
ORR | 比特位或 | SWI/SVC | 系統調用 |
參考:https://www.jianshu.com/p/ba4f055e99de
內存訪問相關指令
ARM匯編基礎