ARM彙編指令集彙總:https://blog.csdn.net/qq_40531974/article/details/83897559
[翻譯]二進制漏洞利用(二)ARM32位彙編下的TCP Bind shell:https://bbs.pediy.com/thread-253511.htm
史上最全ARM指令集詳解:http://www.elecfans.com/d/857294.html
ARM彙編語言入門
From:ARM彙編語言入門(一):https://zhuanlan.zhihu.com/p/109057983
原文地址:https://azeria-labs.com/writing-arm-assembly-part-1/
1. ARM 彙編介紹
處理器 ARM VS Intel
ARM 與 Intel 有諸多不同,最主要的區別是指令集。Intel 是複雜指令集(CISC:Complex Instruction Set Computing)處理器,擁有功能更多更豐富的指令,允許對內存進行更復雜的操作。因此也擁有更多的指令操作,尋址模式,然而寄存器數量卻比 ARM少。CISC 處理器主要應用在個人電腦,工作站,服務器當中。
ARM 是精簡指令集(RISC:Reduced Instruction set Computing)處理器,擁有更簡單的指令集(少於100個)和更多的通用寄存器。與 Intel 不同,ARM 指令只操作寄存器,且只能使用 Load/Stroe (取/存) 命令來 讀取和寫入內存。也就是說,如果增加某個地址處的32位數據的值,你起碼需要三個指令(取,加,存):首先將該地址處的數據加載到寄存器(取),然後增加寄存器裏的值(加),最後再將寄存器裏的值存儲到原來的地址處(存)。
精簡指令集有優點也有缺點。優點之一是單條指令執行更快,相應地也獲得了更高的處理速度(精簡指令集系統通過減少單條指令的時鐘週期來減少執行時間)。不利的一面是更少的指令意味着更加要求更加註重軟件書寫效率。
還要注意的是 ARM 有兩種工作狀態:
- ARM 模式。
- Thumb 模式。Thumb模式指令可以是2個字節或者4個字節(詳見Part 3:ARM指令集)。
ARM 與 x86 其他區別:
- ARM 中大部分指令都可以用作條件執行。
- x86 和 x86-64 系列處理器使用 小端(little-endian)地址格式。
- ARM 架構在第三版以前是小端模式。之後變爲 大-小 端(BI-endian)格式,允許大端或小端兩種模式進行切換。
不僅 ARM 與 Intel 有不同,而且 ARM 各版本之間也有不同。本教程儘量保留它們之間最通用的部分以便你能理解 ARM 是怎麼工作的。一旦你理解了最基本的部分,當你選擇不同的 ARM 版本時也可以融會貫通。本教程所有的例子是在 32-bit ARMv6 平臺(Raspberry Pi 1)創建,所有的說明都是基於此版本。
不同 ARM 版本命名:
ARM 彙編
在開始 ARM 開發之前我們需要先了解基本的彙編編程。使用一般的編程語言或者腳本語言來開發不行嗎,爲什麼還需要 ARM 彙編?確實不行,如果我們要做逆向工程或者想了解 ARM 二進制程序流,創建自己的 ARM 殼程序( shellcode:利用程序漏洞而執行的代碼 ),手工製作 ROP( Return-Oriented Programming 一種利用特殊返回指令不斷返回多個前一段指令而最終拼成一段有效邏輯代碼,以達到特殊攻擊目的的編程技術 )工具鏈以及調試 ARM 程序就要了解 ARM 彙編。
你不需要了解逆向工程或應用開發方面所有的彙編語言細節,你只需要瞭解一個大概。基礎的知識都會在本教程中講到,如果你想要了解更多可以參考文末的附加鏈接。
那麼究竟什麼是彙編語言?彙編語言你可以看成是包裹在機器碼上的的一層薄薄的語法糖指令,這些指令代表着只有機器(計算機)才能讀懂的二進制碼。那麼爲什麼不直接寫機器碼呢?好吧,如果那樣做的話你絕對會很蛋疼。所以你最好還是寫彙編,人能夠容易讀懂的 ARM 彙編。計算機不能運行彙編代碼,它只能讀懂機器碼。我們要使用工具來將彙編代碼轉換爲機器碼。GNU彙編器 as
爲我們提供了這樣的功能,可以識別 *.s 類型的源代碼文件。
當你編寫完擴展名 *.s 的彙編源文件後,要用 as
編譯然後用 ld
鏈接:
$ as program.s -o program.o
$ ld program.o -o program
圖示:
探祕彙編語言
現在我們從最底層的工作做起。在最底層是電路板上的電信號,電信號是切換兩個不同的電平產生的,0V(off) 或者 5V(on)。因爲很容易地看到電路的電平變化,所以我們可以通過可視化數字 0 和 1 的表示來匹配電壓的開關模式,不僅是因爲 0/1 可以代表電信號的缺失和出現,還因爲 0/1 是二進制系統裏數字。然後用一系列 0/1 組成機器碼指令在計算機處理器中運行。
下面就是一個機器語碼指令。
1110 0001 1010 0000 0010 0000 0000 0001
很好,但是我們難以記得這些 0/1 組合的代表什麼意思。因此我們使用叫做助記符的東西來幫助我們記憶這些二進制組合,每個二進制機器碼給定一個名字。這些助記符通常包含三段字符,但不全是。這種程序被叫做彙編語言程序,它使用一系列助記符代表計算機機器碼。指令中的操作數放在助記符之後。
例如:
MOV R2, R1
現在我們知道了彙編程序是由叫做助記符的文本信息組成的,我們需要把它轉換爲機器碼。前面提到的,GNU Binutils 項目爲我們提供了叫做 as
的彙編工具。使用 as
把 ARM 彙編語言轉換爲 ARM 機器碼的過程就叫做彙編。
綜上,計算機能夠理解(迴應)電信號的缺失和出現,並且我們可以將這一系列電信號表示成一組 0/1 序列(bits)。我們就可以用機器碼(一系列電信號)讓計算機根據一種定義好的行爲做出反應。因爲我們難以記憶這一串 0/1 組成的指令的意義,所以提供了一種助記來代表這些指令。這組助記符是計算機的彙編語言,我們使用名爲 "彙編器" 的程序將代碼從助記符表示形式轉換爲計算機可讀的計算機代碼,就像編譯器對高級語言代碼做的一樣。
擴展閱讀
- Whirlwind Tour of ARM Assembly. https://www.coranac.com/tonc/text/asm.htm
- ARM assembler in Raspberry Pi. http://thinkingeek.com/arm-assembler-raspberry-pi/
- Practical Reverse Engineering: x86, x64, ARM, Windows Kernel, Reversing Tools, and Obfuscation by Bruce Dang, Alexandre Gazet, Elias Bachaalany and Sebastien Josse.
- ARM Reference Manual. http://infocenter.arm.com/help/topic/com.arm.doc.dui0068b/index.html
- Assembler User Guide. http://www.keil.com/support/man/docs/armasm/default.htm
2. ARM 的 數據類型 和 寄存器
From:ARM彙編語言入門(二):https://zhuanlan.zhihu.com/p/109066320
與高級編程語言類似,ARM 也支持操作不同的數據類型。
我們載入(load)或存儲(store)的數據類型可以是有符號或無符號的字、半字或字節。
這些數據類型的擴展符是:
- -h 或 -sh 代表 半字,
- -b 和 -sb 代表 字節,
- 其中 字 沒有擴展符號。
有符號和無符號的區別:
- 有符號數據類型可以存儲正數和負數,因此表示的值範圍更小。
- 無符號數據類型可以存儲大的正數(包含0),不能存儲符數因此可以表示更大的數。
載入 和 存儲 指令使用數據類型:
ldr = Load Word
ldrh = Load unsigned Half Word
ldrsh = Load signed Half Word
ldrb = Load unsigned Byte
ldrsb = Load signed Bytes
str = Store Word
strh = Store unsigned Half Word
strsh = Store signed Half Word
strb = Store unsigned Byte
strsb = Store signed Byte
字節序列
查看內存中的字節有兩種基本方式:小端模式(Little-Endian)和 大端模式(Big-Endian)。它們的不同之處是對象存儲在內存中時每個字節的排列順序 --- 字節順序。在x86這種小端模式的機器上低位字節存儲在低地址(更靠近零地址),而在大端模式的機器上高位字節存儲在低地址。在第三版本之前ARM架構是小端模式,之後是兩種模式都允許,可以進行設置來切換字節序列。例如,在 ARMv6 上,指令是固定的小端,數據訪問可以是小端或大端,由程序狀態寄存器 (CPSR) 的位 9(E 位)控制。
ARM寄存器
寄存器 的 數量 取決於 ARM 的版本。根據 ARM 參考手冊,除了基於 ARMv6-M 和 ARMv7-M 的處理器外,共有 30 個 32 位通用寄存器。前 16 個寄存器可在用戶級模式下訪問,其他寄存器在特權軟件執行中可用(除了 ARMv6-M 和 ARMv7-M )。在本教程中,我們將使用非特權模式下可訪問的寄存器:r0-15。這 16 個寄存器可以分爲兩組:通用寄存器 和 特殊用途寄存器。
下表只是簡要了解 ARM 寄存器與 英特爾處理器中的寄存器 的關係。
說明:
- R0-R12:可用於常見操作期間存儲臨時值、指針(內存位置)等等。例如R0,在算術運算期間可以稱爲累加器,或用於存儲調用的函數時返回的結果。R7在進行系統調用時非常有用,因爲它存儲了系統號,R11可幫助我們跟蹤作爲幀指針的堆棧上的邊界(稍後將介紹)。此外,ARM上的函數調用約定函數的前四個參數存儲在寄存器r0-r3中。
- R14:LR(鏈接寄存器)。進行函數調用時,鏈接寄存器將更新爲當前函數調用指令的下一個指令的地址,也就是函數調用返回後需要繼續執行的指令。這麼做是允許子函數調用完成後,在子函數中利用該寄存器保存的指令地址再返回到父函數中。
- R15:PC(程序計數器)。程序計數器自動按執行的指令大小遞增。此指令大小在ARM模式下始終爲4個字節,在THUMB模式下爲2個字節。執行分支指令時,PC保存目標地址。在執行過程中,在ARM模式下PC將當前指令的地址加上8(兩個ARM指令),在Thumb(v1)狀態下則指令加上4(兩個Thumb指令)。這與x86 中PC始終指向要執行的下一個指令不同。
我們看一下在調試狀態下 PC 的值。我們使用以下程序將 PC 地址存儲到 r0 中,幷包含兩個隨機指令。看看會發生什麼。
.section .text
.global _start
_start:
mov r0, pc
mov r1, #2
add r2, r1, r1
bkpt
使用 GDB 在 _start
處設置斷點並運行:
gef> br _start
Breakpoint 1 at 0x8054
gef> run
輸出:
$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 持有將要執行的下一個指令(mov r0, pc
) 的地址(0x8054)。現在,讓我們執行這條指令,之後 R0 應該持有 PC(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
對嗎?錯!看一下 R0 中的地址。雖然我們期望R0包含以前讀取的PC值(0x8054),但它保留的值比我們之前讀取的 PC 早兩個指令(0x805c)。從這個示例中可以看到,當我們直接讀取PC時,它遵循PC指向下一個指令的定義;但在調試時,PC 會指向當前 PC 值之後的兩個指令(0x8054 + 8 = 0x805C)。這是因爲較舊的 ARM 處理器始終取當前執行的指令之後的兩個指令。ARM 保留此定義的原因是爲了確保與早期處理器兼容。
狀態寄存器
當你用 gdb 調試 ARM 程序時,你會看到一些狀態標誌:
寄存器 $cpsr
顯示當前程序狀態寄存器的值,在它下面你可以看到工作狀態標誌,用戶模式,中斷標誌,溢出標誌,進位標誌,零標誌位,符號標誌。這些標誌代表了CPSR寄存器中特定的位,並根據CPSR的值進行設置,如果標誌位有效則會進行加粗。N、Z、C 和 V 位與x86上的EFLAG寄存器中的SF、ZF、CF和OF位相同。這些位用於支持條件分支中的條件執行,並在彙編層面支持循環語句。我們將在第6部分:條件執行和分支中進行介紹。
上圖顯示了 32 位寄存器(CPSR)的結構,左側是高字節位,右側是低字節位。每個單元(GE和M部分以及空白單元除外)的大小均爲一個 bit 位。這些位定義了程序當前狀態的各種屬性。
假設我們可以使用CMP
指令比較 1 和 2,返回結果應該爲負數(1 - 2 = -1
)。當比較兩個相等的數則會設置 Z(zero)標誌位(例如比較 2 和 2, 2 - 2 = 0
)。記住,CMP 指令中使用的寄存器不會被修改,只有 CPSR 會根據這些寄存器相互比較的結果進行修改。
這是 GDB(安裝了GEF)中的模樣:在此示例中,我們比較寄存器 r1 和 r0,其中 r1 = 4 和 r0 = 2。這是執行 cmp r1,r0 操作後標誌的外觀:
之所以設置 Carry 標誌,是因爲我們使用 cmp r1, r0
將 4 與 2(4 - 2)進行比較。相反,如果我們使用 cmp r0 r1、r1 將較小的數字(2)與較大的數字(4)進行比較,則設置負標誌(N)。
CPSR 包含以下狀態標誌:
- N – 當計算結果爲負時被設置.
- Z – 當計算結果爲零時被設置.
- C – 當計算結果有進位時被設置.
- V – 當計算結果有溢出時被設置.
C:其設置分一下幾種情況:
- 加法運算(包括比較指令cmn):當運算結果產生了進位時(無符號數溢出),C=1,否則C=0.
- 減法運算(包括比較指令cmp):當運算時發生了借位(無符號數下益出),C=0,否則C=1.
- 對於包含移位操作的非加/減運算指令:C爲移位操作中最後移出位的值.
- 對於其他非加減運算指令:C的值通常保持不變.
V:如果加、減或比較的結果大於或等於2^31 或小於-2^31,則會發生溢出。
3. ARM 指令集
From:ARM 彙編語言入門(三):https://zhuanlan.zhihu.com/p/109537645
ARM模式 和 Thumb模式
ARM 處理器主要有兩種工作模式(先不算 Jazelle)
- ARM 狀態 模式
- Thumb 狀態 模式
這些狀態模式與權限級別無關,它們主要區別是指令集,
- 在 ARM 模式下指令集始終是 32-bit,
- 但是在 Thumb模式 下可以是 16-bit 或者 32-bit。
學會怎麼使用 Thumb模式 對於 ARM 開發很重要。編寫 ARM 殼代碼時,我們需要避免 NULL字節,使用16位Thumb指令而不是32位ARM指令可以降低這種風險。ARM各版本的調用規範容易讓人混淆,不是所有的ARM版本都支持相同的Thumb指令集。後來,ARM 引入了增強的 Thumb 指令集(僞名稱:Thumbv2),它允許 32 位 Thumb 指令甚至允許條件執行,而這在之前的版本中就不行。爲了在 Thumb 中支持條件執行,引入了“it
”指令。但是,此指令隨後在更高版本中被刪除,並與更簡單的東西進行了替換。我不知道所有不同 ARM/Thumb 指令集的所有不同變體,實話說,我不關心。你也最好也別關心。您只需要知道的是你的目標設備的 ARM 版本及其特定的 Thumb 支持,然後再調整代碼。ARM 信息中可以幫助您確定ARM 版本的細節(http://infocenter.arm.com/help/index.jsp)。
- Thumb-1(16 位指令):在ARMv6和更早的體系結構中使用。
- Thumb-2(16 位和 32 位指令):在Thumb-1基礎上添加更多指令並允許它們爲 16 位或 32 位寬(ARMv6T2、ARMv7)。
- ThumbEE:更改和添加了一些支持動態生成代碼的功能(在執行之前或執行期間在設備上編譯代碼)。
ARM模式 和 Thumb模式的態區別:
- 條件執行:在ARM模式下所有的指令都支持條件執行。一些版本的ARM處理器可以通過
it
指令在Thumb工作模式下支持條件執行。 - ARM和Thumb模式下的32-bit指令:在Thumb模式下的32-bit指令有
.w
後綴。 - 桶型位移器(barrel shifter)是ARM模式下的另一個特點。它可以將多條指令縮減爲一條。例如,你可以通過向左位移1位的指令後綴將乘法運算直接包含在一條
MOV
指令中(將一個寄存器的值乘以2,再將結果MOV
到另一個寄存器):MOV R1, R0, LSL#1 ;R1 = R0 * 2
,而不需要使用專門的乘法指令來運算。
要切換處理器在其中執行的狀態,必須滿足以下兩個條件之一:- 我們可以使用分支指令 BX(分支和切換狀態)或 BLX(分支、鏈接和切換狀態),並將目標寄存器的最小有效位設置爲 1。可以通過偏移量加1來實現,例如0x5530+1。您可能會認爲這將導致對齊問題,因爲指令是 2 或 4 字節對齊的。這不是問題,因爲處理器將忽略最低有效位。詳見Part 6:條件執行和分支。
- 如果當前程序狀態寄存器的T位被置位,就說明工作在Thumb模式下。
ARM 指令簡介
本節簡單介紹 ARM 指令集以及基本用法。瞭解彙編語言中的最小部分如何操作,它們之間如何銜接,它們之間能組合成什麼樣的功能。
ARM 指令後面通常跟着兩個操作數,像下面這樣的形式:
MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
由於 ARM 指令集的靈活性,並不是所有的指令都用到這些字段。這些字段的解釋如下:
MNEMONIC - 操作指令(機器碼對應的助記符)。
{S} - 可選後綴. 如果指定了該後綴,那麼條件標誌將根據操作結果進行更新。
{condition} - 執行指令所需滿足的條件。
{Rd} - 目標寄存器,存儲操作結果。
Operand1 - 第一操作數(寄存器或者立即數)
Operand2 - 第二操作數. 立即數或者帶有位移操作後綴(可選)的寄存器。
MNEMONIC
, S,
Rd
和 Operand1
字段比較明瞭,condition
和 Operand2
字段需要再解釋一下。condition
字段與 CPSR
寄存器的值有關,準確的說是和 CPSR
某些位有關。Operand2
也叫可變操作數,因爲它可以有多種形式 --- 立即數、寄存器、帶有位移操作的寄存器。例如 Operand2
可以有以下多種形式:
#123 - 立即數。
Rx - 寄存器x (如 R1, R2, R3 ...)。
Rx, ASR n - 寄存器x,算術右移n位 (1 = n = 32)。
Rx, LSL n - 寄存器x,邏輯左移n位 (0 = n = 31)。
Rx, LSR n - 寄存器x,邏輯右移n位 (1 = n = 32)。
Rx, ROR n - 寄存器x,循環右移n位 (1 = n = 31)。
Rx, RRX - 寄存器x,擴展的循環位移,右移1位。
讓我們以一個簡單的例子看一下這些指令的不同:
ADD R0, R1, R2 - 將寄存器R1內的值與寄存器R2內的值相加,結果存儲到R0。
ADD R0, R1, #2 - 將寄存器R1內的值加上立即數2,結果存儲到R0。
MOVLE R0, #5 - 僅當滿足條件LE(小於或等於)時,纔將立即數5移動到R0(編譯器會把它看作MOVLE R0, R0, #5)。
MOV R0, R1, LSL #1 - 將寄存器R1的內容向左移動一位然後移動到R0(Rd)。
因此,如果R1值是2,它將向左移動一位,並變爲4。然後將4移動到R0。
來快速總結一下,看一下後續示例中將涉及的一些常用指令:
4. 內存指令:加載 和 存儲
From:ARM彙編語言入門(四):https://zhuanlan.zhihu.com/p/109540164
ARM 使用 加載(Load)/ 存儲(Stroe)指令來讀寫內存,這意味着你只能使用 LDR 和 STR 指令訪問內存。在 ARM 上數據必須從內存中加載到寄存器之後才能進行其他操作,而在 x86 上大部分指令都可以直接訪問內存中的數據。如前所述,在 ARM 上增加內存裏的一個 32-bit 數據值,需要三個指令( load,increment,store )。爲了解釋 ARM 上的 Load 和 Store 操作的基本原理,我們從一個基本示例開始,然後再使用三個基本偏移形式,每個偏移形式具有三種不同的尋址模式。爲了簡單化,每個示例,我們將在同一段彙編代碼中使用不同 LDR/STR 偏移形式的。遵循這本段教程的最佳方法是在你的測試環境中用調試器(GDB)運行代碼示例。
偏移形式:立即數作爲偏移量
- 尋址模式:立即尋址
- 尋址模式:前變址尋址
- 尋址模式:後變址尋址
偏移形式:寄存器作爲偏移量
- 尋址模式:立即尋址
- 尋址模式:前變址尋址
- 尋址模式:後變址尋址
偏移形式:縮放寄存器作爲偏移量
- 尋址模式:立即尋址
- 尋址模式:前變址尋址
- 尋址模式:後變址尋址
第一個例子:
LDR 用於將內存中的值加載到寄存器中,STR 用於將寄存器內的值存儲到內存地址。
解釋:
LDR R2, [R0] @ [R0] - R0中保存的值是源地址。
STR R2, [R1] @ [R1] - R1中保存的值是目標地址。
LDR : 把 R0 內保存的值作爲地址值,將該地址處的值加載到寄存器 R2 中。
STR : 把 R1 內保存的值作爲地址值,將寄存器 R2 中的值存儲到該地址處。
下面是彙編程序的樣子:
.data /*.data段是動態創建的,無法預測 */
var1: .word 3 /* 內存中的變量var1=3*/
var2: .word 4 /* 內存中的變量var2=4*/
.text /* 代碼段開始位置 */
.global _start
_start:
ldr r0, adr_var1 @ 通過標籤adr_var1獲得變量var1的地址,並加載到R0。
ldr r1, adr_var2 @ 通過標籤adr_var2獲得變量var2的地址,並加載到R1。
ldr r2, [r0] @ 通過R0內的地址獲取到該地址處的值(0x03),加載到R2。
str r2, [r1] @ 將R2內的值(0x03)存儲到R1中的地址處。
bkpt
adr_var1: .word var1 /* 變量var1的地址位置 */
adr_var2: .word var2 /* 變量var2的地址位置 */
在程序底部有我們的 文本池(在代碼段用來存儲常量、字符串或其他可以引用的位置無關的偏移量),使用 adr_var1
和 adr_va2
兩個標籤來存儲 var1
和 var2
的內存地址。第一個 LDR
將 var1
的地址加載到 R0
,然後第二個 LDR
將 var2
的地址加載到·。之後將 R0
中的地址指向的值(0x03
)加載到 R2
,最後將 R2
中的值(0x03
)存儲到 R1
中的地址處。
當加載數據到寄存器中時,使用 []
符號意思時:取寄存器中的值作爲地址值,然後再從該地址處加載數據到目標寄存器中,如果不加 []
那就是將寄存器中保存的值直接加載到目標寄存器。
同樣 STR 命令中也是一個意思。
這聽起來比實際要複雜的多,沒關係,下面是一個更直觀的演示圖:
下面我們看一下調試器中的這段代碼:
gef> disassemble _start
Dump of assembler code for function _start:
0x00008074 <+0>: ldr r0, [pc, #12] ; 0x8088 <adr_var1>
0x00008078 <+4>: ldr r1, [pc, #12] ; 0x808c <adr_var2>
0x0000807c <+8>: ldr r2, [r0]
0x00008080 <+12>: str r2, [r1]
0x00008084 <+16>: bx lr
End of assembler dump.
開頭的兩個 LDR
操作中的第二操作數被替換成了 [pc, #12
]。這被叫做 PC 相對尋址。因爲我們使用了標籤,所以編譯器可以計算出文本池中標籤的地址相對位置(pc+12
)。您可以使用這種精確的方法自行計算位置,也可以像前面一樣使用標籤。唯一的區別是,相較於使用標籤,你需要計算值在文本池中的確切位置。在這種情況下,它距離有效的 PC 位置有3個跳轉(4+4+4=12)。本章稍後將介紹有關PC相對尋址的介紹。
如果你忘了爲什麼有效PC指向當前指位置後兩個指令,在第二部介紹了[...在執行過程中,在ARM模式下,PC將當前指令的地址加上8(兩個ARM指令)作爲最終值存儲起來,在Thumb模式下,將當前指令加上 4(兩個Thumb指令)作爲最終值存儲起來。而x86中PC始終指向要執行的下一個指令...]
1. 偏移模式:立即數作爲偏移量
STR Ra, [Rb, imm]
LDR Ra, [Rc, imm]
這裏,我們使用立即(整數)作爲偏移量。從基寄存器(以下示例中的 R1)中增加或減去此值,在編譯時可以用已知的偏移量訪問數據。
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 通過標籤adr_var1獲得變量var1的地址,並加載到R0。
ldr r1, adr_var2 @ 通過標籤adr_var2獲得變量var2的地址,並加載到R1。
ldr r2, [r0] @ 通過R0內的地址獲取到該地址處的值(0x03),加載到R2。
str r2, [r1, #2] @ 以R1中的值爲基準加上立即數2作爲最終地址,
將R2中的值(0x03)存儲到該地址處,其中R1中的值不會被修改。
str r2, [r1, #4]! @ 前變址尋址:以R1中的值爲基準加上立即數4作爲最終地址,
將R2中的值(0x03)存儲到該地址處,其中R1中的值被修改爲:R1+4。
ldr r3, [r1], #4 @ 後變址尋址:將R1中的值作爲最終地址,獲取該地址處的數據加載到R3,
其中R1中的值被修改爲:R1+4。
bkpt
adr_var1: .word var1
adr_var2: .word var2
假設以上程序文件爲ldr.s
,編譯並用GDB允許,看看會發生什麼。
$ as ldr.s -o ldr.o
$ ld ldr.o -o ldr
$ gdb ldr
GDB
(包含gef
)中,在_start
處設置斷點,運行程序。
gef> break _start
gef> run
...
gef> nexti 3 /* 運行後3條指令 */
系統上的寄存器現在填充了以下值(注意,這些地址在你的系統上可能有所不同):
$r0 : 0x00010098 -> 0x00000003
$r1 : 0x0001x009c -> 0x00000004
$r2 : 0x00000003
$r3 : 0x00000000
$r4 : 0x00000000
$r5 : 0x00000000
$r6 : 0x00000000
$r7 : 0x00000000
$r8 : 0x00000000
$r9 : 0x00000000
$r10 : 0x00000000
$r11 : 0x00000000
$r12 : 0x00000000
$sp : 0xbefff7e0 -> 0x00000001
$lr : 0x00000000
$pc : 0x00010080 -> <_start+12> str r2, [r1]
$cpsr : 0x00000010
下一條指令將在偏移地址模式下執行STR
指令。它將把R2
中的值(0x00000003
)存儲在:R1
(0x0001x009c
)+偏移(#2
)= 0x1009e
地址處,運行完該條指令後用x/w命令查看0x0001x009c
處的值爲0x3
,完全正確。
gef> nexti
gef> x/w 0x1009e
0x1009e <var2+2>: 0x3
再下一條~指令是前變址尋址。可以根據“!
”來識別該模式。唯一區別是,基準寄存器會被更新爲最終訪問地址。這意味着,我們將R2
(0x3
) 中的值存儲到 地址:R1
(0x1009c
)+ 偏移量(#4
) = 0x100A0
,並使用此地址更新 R1
。運行完命令查看0x100A0
地址處的值,然後使用命令info register r1
查看R1
的值。
gef> nexti
gef> x/w 0x100A0
0x100a0: 0x3
gef> info register r1
r1 0x100a0 65696
最後一條LDR
指令是後變址尋址。意思是R1
中的值作爲最終訪問地址,獲取最終訪問地址處的值加載到R3
。然後將R1
(0x100A0
)更新爲R1(0x100A0)+ 偏移(#4)= 0x100a4
。運行完該命令看看寄存器R1
和R3
的值。
gef> info register r1
r1 0x100a4 65700
gef> info register r3
r3 0x3 3
下圖是實際發生的事情:
2. 偏移模式:寄存器作爲偏移量(寄存器基址變址尋址)
STR Ra, [Rb, Rc]
LDR Ra, [Rb, Rc]
這種偏移是使用寄存器作爲偏移量。下面的示例是,代碼在運行時計算要訪問的數組索引。
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 通過標籤adr_var1獲得變量var1的地址,並加載到R0。
ldr r1, adr_var2 @ 通過標籤adr_var2獲得變量var2的地址,並加載到R1。
ldr r2, [r0] @ 通過R0內的地址獲取到該地址處的值(0x03),加載到R2。
str r2, [r1, r2] @ 以R1中的值爲基準地址,R2中的值(0x03)爲偏移量,獲得最終訪問地址,
將R2中的值(0x03)存儲到該地址處,基準寄存器R1中的值保存不變。
str r2, [r1, r2]! @ 前變址尋址:以R1中的值爲基準地址,R2中的值(0x03)爲偏移量,獲得最終訪問
地址,將R2中的值(0x03)存儲到該地址處,基準寄存器R1中的值更新爲R1+R2。
ldr r3, [r1], r2 @ 後變址尋址:以R1中的值爲最終訪問地址,獲取該地址處的數據並加載到R3,
基準寄存器R1中的值更新爲R1+R2。
bx lr
adr_var1: .word var1
adr_var2: .word var2
當執行第一條STR
指令時,R2
中的值(0x00000003
)被存儲到地址:0x0001009c + 0x00000003 = 0x0001009F
。
gef> x/w 0x0001009F
0x1009f <var2+3>: 0x00000003
第二條STR
指令操作是前變址尋址,做了同樣的操作,不同的一點是R1
的值會被更新:R1=R1+R2
。
gef> info register r1
r1 0x1009f 65695
最後一條LDR
指令操作是後變址尋址。以R1
中的值爲訪問地址,獲取該地址處的數據並加載到R3
,然後更新R1
的值:R1 = R1 + R2 = 0x1009f + 0x3 = 0x100a2
。
gef> info register r1
r1 0x100a2 65698
gef> info register r3
r3 0x3 3
圖示:
3. 偏移模式:縮放寄存器作爲偏移量(寄存器基址變址尋址)
LDR Ra, [Rb, Rc, <shifter>]
STR Ra, [Rb, Rc, <shifter>]
第三中偏移形式是縮放寄存器作爲偏移量。這種情況下,Rb是基地址寄存器,Rc
是一個被左移或右移(<shifter>
位移操作)縮放過的立即數(Rc
中保存的值)。意思是桶型位移操作用來縮放偏移量。下面是一個在數組上循環遍歷的例子,可以在GDB
中運行看一下:
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 通過標籤adr_var1獲得變量var1的地址,並加載到R0。
ldr r1, adr_var2 @ 通過標籤adr_var2獲得變量var2的地址,並加載到R1。
ldr r2, [r0] @ 通過R0內的地址獲取到該地址處的值(0x03),加載到R2。
str r2, [r1, r2, LSL#2] @ 以R2中的值左移2位(相當於乘以2)爲偏移量,加上R1中的基準地址獲得
最終訪問地址,將R2中的值(0x03)存儲到該地址,基準寄存器R1中的值不變。
str r2, [r1, r2, LSL#2]! @ 以R2中的值左移2位(相當於乘以2)爲偏移量,加上R1中的基準地址獲得
最終結果地址,將R2中的值(0x03)存儲到該地址,基準寄存器R1中的值被修改: R1 = R1 + R2<<2
ldr r3, [r1], r2, LSL#2 @ 以R1中的值爲訪問地址,加載該地址處的數據到R3,
基準寄存器R1中的值被修改: R1 = R1 + R2<<2
bkpt
adr_var1: .word var1
adr_var2: .word var2
下面是程序運行時的樣子:
第一條不多贅述,第二條STR
指令操作使用了前變址尋址,也就是:R1
的值0x1009c+R2
中的值左移2
位(0x03<<2=0xc
)= 0x100a8
,並更新R1
的值爲0x100a8
:R1 = R1 + 0x03<<2 = 0x100a8 + 0xc = 0x100b4
。
gef> info register r1
r1 0x100a8 65704
最後一條LDR
指令操作使用了後變址尋址。意思是,加載R1
中的值0x100a8
地址處的數據到寄存器R3
,然後將R2
中的值左移兩位(0x03<<2=0xc
)得到值0xC
,再加上R1
中的值0x100a8
得到0x100b4
,最後R1
的值更新爲0x100a8
:R1 = R1 + 0x03<<2 = 0x100a8 + 0xc = 0x100b4
。
gef> info register r1
r1 0x100b4 65716
總結
記住 LDR
和 STR
中有三種偏移形式:
- 立即數作爲偏移量:
ldr r3, [r1, #4]
- 寄存器作爲偏移量:
ldr r3, [r1, r2]
- 帶有位移操作的寄存器作爲偏移量:
ldr r3, [r1, r2, LSL#2]
如何記住 LDR
和 STR
這些尋址模式:
- 如果帶有
!
,就是前變址尋址ldr r3, [r1, #4]!
ldr r3, [r1, r2]!
ldr r3, [r1, r2, LSL#2]!
- 如果基地值寄存器(
R1
)帶中括號,就是後變址尋址ldr r3, [r1], #4
ldr r3, [r1], r2
ldr r3, [r1], r2, LSL#2
- 其他的都是帶偏移量的寄存器間接尋址
ldr r3, [r1, #4]
ldr r3, [r1, r2]
ldr r3, [r1, r2, LSL#2]
LDR 中的 PC 相對尋址
LDR
是唯一用來加載數據到寄存器中的指令。語法如下:
.section .text
.global _start
_start:
ldr r0, =jump /* 加載函數標籤jump的地址到R0 */
ldr r1, =0x68DB00AD /* 加載值0x68DB00AD到R1 */
jump:
ldr r2, =511 /* 加載值511到R2 */
bkpt
這些指令被稱爲僞指令,我們可以使用此語法來引用文本池中的數據。在上面的示例中,我們使用這些僞指令引用一個函數的偏移量,在指令中將一個32位常量加載到寄存器中。我需要使用此語法在一個指令中將 32 位常量移動到寄存器中的原因是,ARM 只能一次加載 8 位值。什麼?要了解原因,您需要了解 ARM 上如何處理立即數的。
ARM 中的 立即數
在ARM上加載一個立即數到寄存器中並不像x86上那麼簡單,ARM對於立即數有很多限制。這些限制是什麼以及如何處理它們並不是ARM彙編所關心的,但請相信我,這只是爲了有助於你理解,並且有一些技巧可以繞過這些限制(提示:LDR
)。
我們知道ARM指令長度是32位,並且所有指令都是可條件執行指令。其中有16種條件碼,就要佔用4位(2^4=16),然後還要2位代指目標寄存器,2位代指操作寄存器,1位作爲狀態標誌,加起其他一些操作碼佔用的位。到這裏分配完指令類型,寄存器以及其他位段,最後只剩下12位用來操作立即數,最多隻能表示4096個數。
這意味着ARM中MOV
指令只能操作一定範圍內的立即數,如果不能直接被調用,就必須被分割成多個部分,用衆多小數字拼起來。
還沒完,這12位還不全是用來表示一個整數,其中8位用來表示0-255範圍的數n
,4位表示旋轉循環右移(其實ARM中只有一種位移,就是旋轉循環右移,左移也是通過旋轉循環右移得到)的次數r
(範圍0-30)。所以一個立即數的表示形式是:v = n ror 2*r
。也就是說,只能以偶數進行旋轉循環右移,一次移動兩位,n組成的有效位圖必須能放到一個字節(8位)中。
下面是一些有效和無效的立即數:
Valid values:
#256 // 1 ror 24 --> 256 循環右移12次,每次兩位(注意數據是32位長度)。
#384 // 6 ror 26 --> 384 循環右移13次,每次兩位。
#484 // 121 ror 30 --> 484
#16384 // 1 ror 18 --> 16384
#2030043136 // 121 ror 8 --> 2030043136
#0x06000000 // 6 ror 8 --> 100663296 (0x06000000 in hex)
Invalid values:
#370 // 185 ror 31 --> 循環右移31位,但超出了(0 – 30)範圍,因此不是有效立即數。
#511 // 1 1111 1111 --> 有效位圖無法放到一個字節(8位)中。
#0x06010000 // 110 0000 0001.. --> 有效位圖無法放到一個字節(8位)中。
譯註:1.以上立即數都是32位長度。2.旋轉循環右移:每位都向右移動,末位不斷放到最前位,類似首尾相連。3.有效位圖要能放到一個字節中:例子中#511
的二進制爲0000 0000 0000 0000 0000 0001 1111 1111
,有效位圖爲1 1111 1111
,超過一個字節。#0x06010000
的二進制位0110 0000 0001 0000 0000 0000 0000
,有效位圖110 0000 0001
超過一個字節。
其結果是無法一次加載完整的 32 位地址。我們可以通過使用以下兩個選項之一來繞過此限制:
- 用較小的值構造較大的值
- 不要使用
MOV r0, #511
- 分成兩部分:
MOV r0, #256
和ADD r0, #255
- 不要使用
- 使用加載方式“
ldr r1, =value
”,編譯器會很樂意將其轉換位MOV
指令,或者是PC
相對尋址來加載。LDR r1, = 511
如果你加載了一個無效的立即數,那麼編譯器會報錯:“Error: invalid constant
”。如果遇到這種問題你應該知道怎麼做。
.section .text
.global _start
_start:
mov r0, #511
bkpt
如果嘗試編譯,編譯器會輸出類似以下錯誤:
azeria@labs:~$ as test.s -o test.o
test.s: Assembler messages:
test.s:5: Error: invalid constant (1ff) after fixup
你應該把511
拆成幾個小數值,或者用前面介紹的LDR
方式。
.section .text
.global _start
_start:
mov r0, #256 /* 1 ror 24 = 256, so it's valid */
add r0, #255 /* 255 ror 0 = 255, valid. r0 = 256 + 255 = 511 */
ldr r1, =511 /* load 511 from the literal pool using LDR */
bkpt
如果你想判斷一個立即數是否是有效的立即數,你可以用我寫的python腳本rotator.py :
azeria@labs:~$ python rotator.py
Enter the value you want to check: 511
Sorry, 511 cannot be used as an immediate number and has to be split.
azeria@labs:~$ python rotator.py
Enter the value you want to check: 256
The number 256 can be used as a valid immediate number.
1 ror 24 --> 256
5. 加載 和 存儲 多個值
From:ARM 彙編語言入門(五):https://zhuanlan.zhihu.com/p/109543429
有時你想要更有效率,一次加載(或存儲)多個值。爲此我們可以使用 LDM(load multiple)和 STM(stroe multiple)指令。這些指令有各種變體,基本上只因訪問初始地址的方式而異。這是我們本節將要使用的代碼,將一步步地認識這些指令。
.data
array_buff:
.word 0x00000000 /* array_buff[0] */
.word 0x00000000 /* array_buff[1] */
.word 0x00000000 /* array_buff[2]. 此處是一個相對地址,等於array_buff+8 */
.word 0x00000000 /* array_buff[3] */
.word 0x00000000 /* array_buff[4] */
.text
.global _start
_start:
adr r0, words+12 /* address of words[3] -> r0 */
ldr r1, array_buff_bridge /* address of array_buff[0] -> r1 */
ldr r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */
ldm r0, {r4,r5} /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
ldmib r0, {r4-r6} /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
bx lr
words:
.word 0x00000000 /* words[0] */
.word 0x00000001 /* words[1] */
.word 0x00000002 /* words[2] */
.word 0x00000003 /* words[3] */
.word 0x00000004 /* words[4] */
.word 0x00000005 /* words[5] */
.word 0x00000006 /* words[6] */
array_buff_bridge:
.word array_buff /* array_buff的地址, 或者說是array_buff[0]的地址 */
.word array_buff+8 /* array_buff[2]的地址 */
開始之前,你一定要記住.word是指內存中的數據是32位,也就是4字節。這對理解地址偏移量很重要。程序中的.data段分配了一個空白的數組,有5個元素。我們將它作爲可寫內存來進行數據存儲。.text段包含我們的代碼,以及包含兩個標籤的只讀數據段。一個標籤是包含7個元素的數組,第二個標籤用來橋接.text段和.date段,以便我們可以訪問保存在.data中的array_buff。
adr r0, words+12 /* address of words[3] -> r0 */
使用ADR
指令(惰性方法)獲取words
的第四個元素(words[3]
)的地址,存儲到R0
。定位到words
數組的中間,以便接下來向前和向後操作。
gef> break _start
gef> run
gef> nexti
現在R0
存有wards[3]的地址0x80B8
,算一下words[0]地址,也就是數組words開始的地址:0x80AC ( 0x80B8 – 0xC)
。看一下內存值。
gef> x/7w 0x00080AC
0x80ac <words>: 0x00000000 0x00000001 0x00000002 0x00000003
0x80bc <words+16>: 0x00000004 0x00000005 0x00000006
在R1
和R2
中分別保存array_buff數組的第一(array_buff[0])和第三(array_buff[2])個元素的地址。
ldr r1, array_buff_bridge /* address of array_buff[0] -> r1 */
ldr r2, array_buff_bridge+4 /* address of array_buff[2] -> r2 */
執行完上面兩條指令,看一下R1
和R2
中的值,分別是array_buff[0]
和array_buff[2]
的地址。
gef> info register r1 r2
r1 0x100d0 65744
r2 0x100d8 65752
下一條指令LDM
從R0
指向的words[3]
位置加載兩個值到R4
和R5
,其中words[3]
給R4
,words[4]
給R5
。
ldm r0, {r4,r5} /* words[3]() -> r4 = 0x03; words[4] -> r5 = 0x04 */
我們一條指令就加載了兩個數據,讓R4=0x00000003
,R5 = 0x00000004
。
gef> info registers r4 r5
r4 0x3 3
r5 0x4 4
很好,現在再用STM
指令一次存儲多條數據值。代碼中STM
從R4
和R5
分別獲取值0x03
和0x04
,然後依次存儲到R1
指定的地址處。前面的指令讓R1
通過array_buff_bridge
指向了數組array_buff
的開始位置,最終運行結果:array_buff[0] = 0x00000003 and array_buff[1] = 0x00000004
。如果沒有特殊說明,LDM
和STM
操作的數據都是32位。
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
現在0x03
和0x04
應該分別被保存到了0x100D0
and 0x100D4
。下面的指令是產看地址0x000100D0
處的兩個字長度的值。
gef> x/2w 0x000100D0
0x100d0 <array_buff>: 0x3 0x4
前面提到,LDM
和STM
有很多變種。其中一種指令後綴。如-IA(increase after)
、-IB(increase before)
、-DA(decrease after)
、-DB(decrease before)
。這些變種依據第一個操作數(保存源地址或目標地址的寄存器)指定的不同的內存訪問方式而不同。在實踐中,LDM
與LDMIA
相同,意思是第一個操作數(寄存器)內的地址隨着元素的加載而不斷增加。通過這種方式我們根據第一個操作數(保存了源地址的寄存器)獲取一連串(正向)的數據。
ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
執行完上面的指令後,寄存器R4-R6
以及地址0x000100D0
, 0x000100D4
和0x000100D8
的值應該是0x3
, 0x4
和0x5
。
gef> info registers r4 r5 r6
r4 0x3 3
r5 0x4 4
r6 0x5 5
gef> x/3w 0x000100D0
0x100d0 <array_buff>: 0x00000003 0x00000004 0x00000005
LDMIB
指令先將源地址加4個字節(一個字)然後再執行加載。這種方式下我們仍然會得到一串加載的數據,但是第一個元素是從源地址偏移4個字節開始的。這就是爲什麼例子中LDMIB
指令操作後R4
中的值是0x00000004
(words[4]
)而不是R0
所指的0x00000003
(words[3]
)的原因。
ldmib r0, {r4-r6} /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
上面兩條指令執行後,寄存器R4-R6
以及地址0x100D4
, 0x100D8
和0x100DC
的值應該是0x4
, 0x5
和0x6
。
gef> x/3w 0x100D4
0x100d4 <array_buff+4>: 0x00000004 0x00000005 0x00000006
gef> info register r4 r5 r6
r4 0x4 4
r5 0x5 5
r6 0x6 6
當使用LDMDA
指令所有的操作都是反向的。R0
當前指向words[3],當執行指令時反方向加載words[3]
,words[2]
,words[1]
到寄存器R6
,R5
,R4
。是的,寄存器也是按照反向順序。執行完指令後R6 = 0x00000003,R5 = 0x00000002,R4 = 0x00000001
。這裏的邏輯是,每次加載後都將源地址遞減一次。加載時寄存器按照反方向是因爲:每次加載時地址在減小,寄存器也跟着反方向,邏輯上保證了高地址上對應的是高寄存器中的值。再看一下LDMIA
(或LDM
)的例子,我們首先加載低寄存器是因爲源地也是低地址,然後加載高寄存器是因爲源地址也增加了。
加載多條值,後遞減:
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
執行後R4、R5和R6的值:
gef> info register r4 r5 r6
r4 0x1 1
r5 0x2 2
r6 0x3 3
加載多條值,前遞減:
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
執行後R4、R5和R6的值:
gef> info register r4 r5 r6
r4 0x0 0
r5 0x1 1
r6 0x2 2
存儲多條值,後遞減:
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
執行後array_buff[2],array_buff[1]和array_buff[0]地址處的值:
gef> x/3w 0x100D0
0x100d0 <array_buff>: 0x00000000 0x00000001 0x00000002
存儲多條值,前遞減:
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
執行後array_buff[2],array_buff[1]和array_buff[0]地址處的值:
gef> x/2w 0x100D0
0x100d0 <array_buff>: 0x00000000 0x00000001
入棧 和 出棧
進程中有一個叫做棧的內存位置。棧指針(SP)寄存器總是指向棧內存中的地址。程序應用中通常使用棧來存儲臨時數據。前面講的ARM中只能使用加載和存儲來訪問內存,就是隻能使用LDR
/STR
指令或者他們的衍生指令(LDM
、STM
、LDMIA
、LDMDA
、STMDA
等等)進行內存操作。在x86
中使用PUSH和POP從棧內取或存,ARM中我們也可以使用這條指令。
當我們將數據 PUSH 入向下生長的棧(詳見Part 7:堆棧與函數)時,會發生以下事情:
- 首先,SP中的地址減少4(譯註:4字節=32位)。
- 然後,數據存儲到SP的新地址值處。
當數據從棧中 POP 出時,發生以下事情:
- 當前SP中地址處的數據加載到指定寄存器中。
- SP中的地址值加4。
下面的例子中使用PUSH/POP
以及LDMIA/STMDB
:
.text
.global _start
_start:
mov r0, #3
mov r1, #4
push {r0, r1}
pop {r2, r3}
stmdb sp!, {r0, r1}
ldmia sp!, {r4, r5}
bkpt
反編譯一下代碼:
azeria@labs:~$ as pushpop.s -o pushpop.o
azeria@labs:~$ ld pushpop.o -o pushpop
azeria@labs:~$ objdump -D pushpop
pushpop: file format elf32-littlearm
Disassembly of section .text:
00008054 <_start>:
8054: e3a00003 mov r0, #3
8058: e3a01004 mov r1, #4
805c: e92d0003 push {r0, r1}
8060: e8bd000c pop {r2, r3}
8064: e92d0003 push {r0, r1}
8068: e8bd0030 pop {r4, r5}
806c: e1200070 bkpt 0x0000
可以看到LDMIA和STMDB被替換成了PUSH和POP。那是因爲PUSH是STMDB的同語義指令,POP是LDMIA的同語義指令。
再GDB中調試運行一下:
gef> break _start
gef> run
gef> nexti 2
[...]
gef> x/w $sp
0xbefff7e0: 0x00000001
運行完頭兩條指令後先查看一下SP指向的地址以及地址處的數值。下一條PUSH指令會將SP減去8,並且將R1
和R0
中的值按順序壓入棧中。
gef> nexti
[...] ----- Stack -----
0xbefff7d8|+0x00: 0x3 <- $sp
0xbefff7dc|+0x04: 0x4
0xbefff7e0|+0x08: 0x1
[...]
gef> x/w $sp
0xbefff7d8: 0x00000003
接下來棧中的值0x03和0x04彈出到寄存器中。
gef> nexti
gef> info register r2 r3
r2 0x3 3
r3 0x4 4
gef> x/w $sp
0xbefff7e0: 0x00000001
6. 條件狀態 和 分支
From:ARM 彙編語言入門(六):https://zhuanlan.zhihu.com/p/109543670
在探討 CPSR 時我們已經接觸了條件狀態。我們通過跳轉(分支)或者一些只有滿足特定條件才執行的指令來控制程序在運行時的執行流。通過CPSR寄存器中的特定bit位來表示條件狀態。這些位根據指令每次執行的結果而不斷變化。例如,比較運算時如果兩個數相等,那麼就置CPSR中的Zero位(Z=1),實際上是因爲:a - b = 0,這種情況下就是相等狀態。如果第一個數大,那麼就是大於狀態。如果第二個數大,就是小於狀態。除此之外,還有小於等於、大於等於等等。
下面的表格列出了可用的條件狀態碼,描述和標誌位:
在下面代碼片段中看一下執行條件加法時的實際用法L:
.global main
main:
mov r0, #2 /* 初始化變量 */
cmp r0, #3 /* 將R0中的值與3比較,負數位置1 */
addlt r0, r0, #1 /* 如果上一條比較結果是小於(查看CPSR),則將R0加1 */
cmp r0, #3 /* 將R0中的值再與3比較, 零位置1,同時負數位重置爲0 */
addlt r0, r0, #1 /* 如果上一條比較結果是小於(查看CPSR),則將R0加1 */
bx lr
第一條cmp
指令結果導致CPSR
中的負數位置1(2- 3 = -1
)意思是R0
小於R3
。因爲滿足小於條件(CPSR
中的溢出位不等於負數位V != N
)所以接下來的ADDLT
指令執行。在執行下一條cmp
指令時,R0 = 3
。所以清除負數位(3 - 3 = 0
,負數位清零),零位置位(Z = 1
)。現在溢出位是0,負數位是0,不滿足小於條件。所以最後一條ADDLT
指令不執行,R0
值保持3不變。
Thumb 模式下的 條件執行
我們在介紹指令集的章節討論了Thumb狀態下的不同。具體而言是Thumb-2版本支持條件執行。某些 ARM 處理器版本支持"IT"指令,允許在 Thumb 狀態下支持多達4個條件執行指令。參考:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0552a/BABIJDIC.html。
語法:IT{x{y{z}}} cond
- cond 指定 IT 塊的第一個指令的條件。
- x 指定 IT 塊中第二個指令的條件開關。
- y 指定 IT 塊中第三個指令的條件開關。
- z 指定 IT 塊中第四個指令的條件開關。
其實IT指令的結構就是“IF-Then-(Else)”,語法都是由字母“T”和“E”構成:
- IT:If-Then(下一條指令是條件的);
- ITT:If-Then-Then(後兩條指令是條件的);
- ITE:If-Then-Else(後兩條指令是條件的);
- ITTE:If-Then-Then-Else(後三條指令是條件的);
- ITTEE:If-Then-Then-Else-Else(後四條指令是條件的);
IT塊中的每條指令必須指定相同或邏輯相反的條件後綴。意思是,如果使用ITE,那麼前兩個指令必須有相同的後綴,而第三個必須是邏輯相反的後綴。下面是 ARM 參考手冊中的一些示例,說明了這些邏輯:
ITTE NE ; 接下來的3條指令都是有條件的。
ANDNE R0, R0, R1 ; ANDNE不更新條件標誌。
ADDSNE R2, R2, #1 ; ADDSNE更新條件標誌。
MOVEQ R2, R3 ; 有條件的移動
ITE GT ; 接下來的2條指令都是有條件的。
ADDGT R1, R0, #55 ; 條件滿足大於時進行相加。
ADDLE R1, R0, #48 ; 條件不滿足大於時進行相加。
ITTEE EQ ; 接下來的4條指令都是有條件的。
MOVEQ R0, R1 ; 有條件的MOV
ADDEQ R2, R2, #10 ; 有條件的ADD
ANDNE R3, R3, #1 ; 有條件的AND
BNE.W dloop ; 分支指令只能在IT塊的最後一個指令中使用。
錯誤示例:
IT NE ; 下一條指令是條件的。
ADD R0, R0, R1 ; 語法錯誤,不是有條件的指令。
下面是條件代碼和相反代碼:
現在使用以下代碼來測試:
.syntax unified @ 非常重要!
.text
.global _start
_start:
.code 32
add r3, pc, #1 @ PC的值加1並存儲到R3。
bx r3 @ 跳轉到R3中的地址處,並切換運行模式 ->切換到Thumb模式,因爲R3最低有效位(LSB) = 1。
.code 16 @ Thumb模式
cmp r0, #10
ite eq @ 如果R0等於10...
addeq r1, #2 @ ... 那麼 R1 = R1 + 2
addne r1, #3 @ ... 否則 R1 = R1 + 3
bkpt
.code 32
示例中的代碼開始在ARM模式下,第一條指令將PC中的地址值加1並存儲到R3
,然後bx指令跳轉到R3
中的地址位置,並且模式切換成Thumb
模式,因爲R3
中的值最低有效位爲1(0不切換)。爲此使用bx
(分支+交換)非常重要。
.code 16
在Thumb
模式下,首先比較R0
和10
,結果將負數位N置位(0 - 10 = -10
)。之後使用If-Then-Else
塊,因爲零位Z(Zero)沒有被置位所以ADDEQ
指令被跳過,然後因爲結果不相等所以執行ADDNE
指令。
在 GDB
中單步執行此代碼會干擾結果,因爲你要在 ITE
塊中執行這兩個指令。 但是,在 GDB
中運行代碼而不設置斷點並單步執行每個指令將生成正確的結果設置 R1
= 3。
分支
分支(跳轉)允許我們跳轉到另一個代碼段。當你需要跳過(或者重複)某塊代碼或者跳轉到指定的函數的時候,分支很有用。此類情形中最佳的示例是IF和循環。先來看看IF案例。
.global main
main:
mov r1, #2 /* 設置初始變量a */
mov r2, #3 /* 設置初始變量b */
cmp r1, r2 /* 比較兩個變量值看哪個更大 */
blt r1_lower /* 因爲R2更大(N==1),跳轉到r1_lower */
mov r0, r1 /* 如果沒有跳轉, 例如R1的值更大(或者相等),則將R1的值存儲到R0 */
b end /* 結束 */
r1_lower:
mov r0, r2 /* R1小於R2時跳轉到此處, 將R2的值存儲到R0 */
b end /* 結束 */
end:
bx lr /* THE END */
上面代碼是比較兩個初始值並返回最大值,C語言僞代碼:
int main() {
int max = 0;
int a = 2;
int b = 3;
if(a < b) {
max = b;
}
else {
max = a;
}
return max;
}
現在再看一下怎麼使用條件分支實現循環:
.global main
main:
mov r0, #0 /* 設置初始變量a */
loop:
cmp r0, #4 /* 比較a==4 */
beq end /* 如果a==4,結束 */
add r0, r0, #1 /* 否則將R0中的值遞增1 */
b loop /* 跳轉到loop開始位置 */
end:
bx lr /* THE END */
C語言僞代碼:
int main() {
int a = 0;
while(a < 4) {
a= a+1;
}
return a;
}
B、BX、BLX 指令
有三種類型的分支指令:
- 普通分支(B)
- 簡單的跳轉到一個函數。
- 帶鏈接的跳轉(BL)
- 將PC+4的值保存到LR寄存器,然後跳轉。
- 帶狀態切換的跳轉(BX)和帶狀態切換及鏈接的跳轉(BLX)
- 與 B 和 BL 一致,只是添加了工作狀態的切換( ARM模式 - Thumb模式 )。
- 需要寄存器作爲第一個操作數。
BX、BLX 用來切換 ARM 模式到 Thumb 模式。
.text
.global _start
_start:
.code 32 @ ARM mode
add r2, pc, #1 @ put PC+1 into R2
bx r2 @ branch + exchange to R2
.code 16 @ Thumb mode
mov r0, #1
這裏的技巧是獲得當前PC的值,加1然後保存到一個寄存器,然後跳轉(並且切換狀態模式)到這個寄存器內的地址。可以看到加指令(add r2, pc, #1
)獲取到有效的PC地址值(當前PC內的值+8=0x805C
)然後加1(0x805C + 1 = 0x805D
)。接下來,我們跳轉的地址( 0x805D = 10000000 01011101
)最低有效位爲1,那麼意味着地址不是4字節(32bit
)對齊的。跳轉到這樣的地址不會導致非對齊問題。在GDB
中運行的樣子(含GEF
):
注意上面的 gif
圖片是在低版本的 GEF
下創建的,所以你的顯示界面可能不一樣,但是邏輯是一樣的。
條件分支
分支也可以有條件地執行,用於在滿足特定條件時跳轉到函數。我們看一個使用BEQ
應用條件分支的例子,這是一段沒太有用的彙編代碼,只不過是在寄存器等於特定值時將一個值移動到寄存器並跳轉到另一個函數的過程。
示例代碼:
.text
.global _start
_start:
mov r0, #2
mov r1, #2
add r0, r0, r1
cmp r0, #4
beq func1
add r1, #5
b func2
func1:
mov r1, r0
bx lr
func2:
mov r0, r1
bx lr
7. 棧 和 函數
ARM彙編語言入門(七):https://zhuanlan.zhihu.com/p/109544390
在這一部分我們來看一下進程中叫做棧的內存區域。本章涵蓋了棧的用途和相關操作。此外我們將介紹 ARM 中函數的實現、類型和差異。
棧
一般而言,棧就是進程中的一段內存。這段內存是在進程創建時分配的。我們使用棧來保存一些臨時數據,如函數中的局部變量,函數之間轉換的環境變量等。使用PUSH和POP指令與棧進行交互。在Part 4:內存指令:加載與存儲中我們講到PUSH和POP是一些其他內存操作指令的別名,這裏爲簡單起見我們使用PUSH和POP指令。
在看實例之前,我們先要明白棧有多種實現方式。首先,當我們說棧增長了,意思是一個數據(32位)被放入了棧中。棧可以向上增長(當棧是按照降序方式實現)或者向下增長(當棧是按照升序方式實現)。下一條信息將被放置的實際位置是由棧指針定義的。準確的說是保存在寄存器SP中的地址指定的。地址可以是棧中的當前(最後入棧)項或者下一個可用的內存位置。如果SP指向的是棧中的最後一個項(完整棧實現方式),那麼是先增加(向上增加棧)或減小(向下增長棧)SP再放入數據;如果SP指向的是棧內下一個有效的空位置,那麼是數據先入棧後再增加SP(向上增加棧)或減少SP(向下增長棧)。
總結了棧的不同實現,我們可以用以下表格列出了不同情況下使用不同的多數據存儲或多數據加載指令。
我們的例子中使用了完整降序棧(Full descending)。下面是一個簡單例子,看一下這種棧是如何處理棧指針的。
/* azeria@labs:~$ as stack.s -o stack.o && gcc stack.o -o stack && gdb stack */
.global main
main:
mov r0, #2 /* 設置R0的初始值*/
push {r0} /* 將R0的值保存到棧*/
mov r0, #3 /* 覆蓋R0的值 */
pop {r0} /* 恢復R0的初始值 */
bx lr /* 結束程序 */
在一開始,棧指針指向地址0xbefff6f8 (你的環境中可能不同)代表棧中的最後一項值。這時我們看一下這個地址處的值(同樣,你的環境中可能不同):
gef> x/1x $sp
0xbefff6f8: 0xb6fc7000
當執行完第一條MOV指令後,棧內數據沒有變化。當執行PUSH指令時,將發生以下事情:首先SP的值減4(4 bytes = 32 bits);然後R0中的值保存到SP指定的地址處。現在再看一下SP中指定的地址處的值:
gef> x/x $sp
0xbefff6f4: 0x00000002
例子中的指令mov r0, #3用來模擬R0中的數據被覆蓋的情形。然後使用POP再將之前的數據恢復。所以,當執行POP指令時,實際發生了以下事情:首先從當前SP指向的內存地址(0xbefff6f4)處讀取一個32位的數據(前面PUSH時保存的2),然後SP寄存器的值減4(變成0xbefff6f8 ),最後將從棧中讀取的數值2保存到R0。
gef> info registers r0
r0 0x2 2
(注意,下面的gif展示的棧的低地址在上面,高地址在下面。不是前面展示不同堆棧實現時的圖片的那種方式,這樣是爲了讓棧看起來跟GDB中展示一樣):
我們看一下函數如何利用Stack來保存本地變量、保留寄存器狀態。爲了讓一切變得井然有序,函數使用棧幀(專門用於函數中使用的局部內存區域)。棧幀是在函數開始調用時創建的(下一節將詳細介紹)。棧幀指針(FP)被置爲棧幀的底部,然後分配棧幀的緩衝區。棧幀中通常(從底部)保存了返回地址(前面的LR寄存器值)、棧幀指針、其他一些需要保存的寄存器、函數參數(如果超過4個參數)、局部變量等等。雖然棧幀的實際內容可能有所不同,但基本就這些。最後棧幀在函數結束時被銷燬。
下面是棧中棧幀的示意圖:
爲了直觀點,再看一段代碼:
/* azeria@labs:~$ gcc func.c -o func && gdb func */
int main()
{
int res = 0;
int a = 1;
int b = 2;
res = max(a, b);
return res;
}
int max(int a,int b)
{
do_nothing();
if(a<b)
{
return b;
}
else
{
return a;
}
}
int do_nothing()
{
return 0;
}
下面的GDB截圖中我們可以看一下棧幀的樣子:
從上圖中我們可以看到,當前我們即將離開函數max(反彙編代碼底部的箭頭)時,這時,FP
(R11
寄存器)指向棧幀最底部的0xbefff254
。看棧中的綠色地址保存了返回地址0x00010418
(前面的LR寄存器)。再往上4字節的地址處(0xbefff250
)保存值0xbefff26c
,這是前一個棧幀指針(FP
)。地址0xbefff24c
和0xbefff248
處的0x1
和0x2
是函數max運行時的局部變量。所以剛纔分析的這個棧幀只包含了LR
,FP
和兩個局部變量。
函數
要理解 ARM 中的函數,首先要熟悉函數體的結構:開始、執行體和收尾。
開始時需要保存程序前面的狀態(LR和R11分別入棧)然後爲函數的局部變量設置堆棧。雖然開始部分的實現可能因編譯器而異,但通常是用PUSH/ADD/SUB指令來完成的。大體看起來是下面這樣:
push {r11, lr} /* 將lr和r11入棧 */
add r11, sp, #0 /* 設置棧幀的底部位置 */
sub sp, sp, #16 /* 棧指針減去16爲局部變量分配緩存區 */
函數體部分就是你程序的實際邏輯區,包含了你代碼邏輯的各種指令:
mov r0, #1 /* 設置局部變量(a=1). 同時也爲函數max的第一個參數 */
mov r1, #2 /* 設置局部變量(b=2). 同時也爲函數max的第二個參數 */
bl max /* 調用函數max */
上面的代碼展示了爲函數設置局部變量並跳轉到另一個函數的過程。同時還展示了通過寄存器爲另一個函數(max)傳遞參數的過程。在某些情況下,當要傳遞的參數超過4個時,我們需要另外使用棧來存儲剩餘的參數。還要說明一下,函數通過寄存器R0返回結果。所以不論max函數結果是什麼,最後都要在函數結束返回後從R0中取返回值。在某些情況下,結果可能是 64 位的長度(超過 32 位寄存器的大小),這時候就需要結合R0和R1來存儲返回值。
函數的最後部分用於將程序的狀態還原到它初始的狀態(函數調用前),這樣就可以從函數被調用的地方繼續執行。所以我們需要重新調整棧指針(SP)。這是通過加減幀指針寄存器(R11)來實現的。重新調整棧指針後,將之前(函數開始處)保存的寄存器值從堆棧彈出到相應的寄存器來還原這些寄存器值。根據函數類型,一般POP指令是函數最後結束的指令。但是,在還原寄存器值後,我們需要使用 BX 指令來離開函數。示例如下:
sub sp, r11, #0 /* 重新調整棧指針 */
pop {r11, pc} /* 恢復棧幀指針, 通過加載之前保存的LR到PC,程序跳轉到之前LR保存位置。函數的棧幀被銷燬 */
所以我們現在知道:
- 函數在開始時設置相應的環境。
- 函數體中執行相關邏輯,然後通過R0保存返回值。
- 函數收尾時恢復所有的狀態,以便程序可以在函數調用前的位置繼續執行。
另一個重要的知識點時函數類型:葉子函數和非葉子函數。葉子函數在函數內不會調用/跳轉到另一個函數。非葉子函數則會在自己的函數邏輯中調用另一個函數。這兩種函數的實現方式類似。不過,也有一些不同。我們用下面的代碼分析一下:
/* azeria@labs:~$ as func.s -o func.o && gcc func.o -o func && gdb func */
.global main
main:
push {r11, lr} /* 開始,棧幀指針和LR分別入棧 */
add r11, sp, #0 /* 設置棧幀的底部(譯註:其實是將sp的值給R11,棧指針指向初始的棧幀指針位置(棧幀底部)) */
sub sp, sp, #16 /* 在棧上分配一些內存作爲接下來局部變量要用的緩存區(譯註:棧指針減16,相當於將棧幀指針往下移動了16字節)) */
mov r0, #1 /* 設置局部變量 (a=1). 同時也爲函數max準備參數a */
mov r1, #2 /* 設置局部變量 (b=2). 同時也爲函數max準備參數b */
bl max /* 跳轉到函數max */
sub sp, r11, #0 /* 重新調整棧指針 */
pop {r11, pc} /* 恢復棧幀指針, 通過加載之前保存的LR到PC,程序跳轉到之前LR保存位置 */
max:
push {r11} /* 開始,棧幀指針入棧 */
add r11, sp, #0 /* 設置棧幀底部 */
sub sp, sp, #12 /* 棧指針減12,分配棧內存 */
cmp r0, r1 /* 比較R0和R1(a和b) */
movlt r0, r1 /* 如果R0<R1, 將R1存儲到R0 */
add sp, r11, #0 /* 收尾,調整棧指針 */
pop {r11} /* 恢復棧幀指針 */
bx lr /* 通過寄存器LR跳轉到main函數 */
上面的例子包含兩個函數:main函數是一個非葉子函數,max函數是葉子函數。之前說了非葉子函數有跳轉到其他函數的邏輯(bl , max),而max中沒有(最後一條是跳轉到LR指定的地址,不是函數分支)這類代碼,所以是葉子函數。
另一個不同點是函數的開始與收尾的實現有差異。來看一段代碼,這是葉子函數與非葉子函數在開始部分的差異:
/* 非葉子函數 */
push {r11, lr} /* 分別保存棧幀指針和LR */
add r11, sp, #0 /* 設置棧幀底部 */
sub sp, sp, #16 /* 在棧上分配緩存區*/
/* 葉子函數 */
push {r11} /* 保存棧幀指針 */
add r11, sp, #0 /* 設置棧幀底部 */
sub sp, sp, #12 /* 在棧上分配緩存區 */
不同之處是非葉子函數保存了更多的寄存器。原因也很自然,因爲非葉子函數中執行時LR會被修改,因此要先保存LR以便最後恢復。當然如果有必要也可以在函數開始時保存更多的寄存器。
下面這段代碼可以看到,葉函數與非葉函數在收尾時的差異主要是在於,葉子函數在結尾直接通過LR中的值跳轉回去,而非葉子函數需要先通過POP恢復LR寄存器,再進行分支跳轉。
/* A prologue of a non-leaf function */
push {r11, lr} /* Start of the prologue. Saving Frame Pointer and LR onto the stack */
add r11, sp, #0 /* Setting up the bottom of the stack frame */
sub sp, sp, #16 /* End of the prologue. Allocating some buffer on the stack */
/* A prologue of a leaf function */
push {r11} /* Start of the prologue. Saving Frame Pointer onto the stack */
add r11, sp, #0 /* Setting up the bottom of the stack frame */
sub sp, sp, #12 /* End of the prologue. Allocating some buffer on the stack */
最後,我們要再次強調一下在函數中BL和BX指令的使用。在我們的示例中,通過使用BL指令跳轉到葉子函數中。在彙編代碼中我們使用了標籤,在編譯過程中,標籤被轉換爲相對應的內存地址。在跳轉到對應位置之前,BL會將下一條指令的地址存儲到LR寄存器中這樣我們就能在函數max結束的時候返回了。
BX指令在被用在我們離開一個葉函數時,使用LR作爲寄存器參數。剛剛說了LR存放着函數調用返回後下一條指令的地址。由於葉函數不會在執行時修改LR寄存器,所以就可以通過LR寄存器跳轉返回到main函數了。同樣可以使用BX指令幫助我們切換ARM模式和Thumb模式。可以通過LR寄存器的最低比特位來完成,0代表ARM模式,1代表Thumb模式。
換一種方式看一下函數及其內部,下面的動畫說明了非葉子函數和葉子函數的內部工作過程。
Assembly Basics Cheatsheet
From:https://azeria-labs.com/assembly-basics-cheatsheet/