日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

ARM 汇编语言入门

發布時間:2024/7/23 编程问答 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ARM 汇编语言入门 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.


?

[翻譯]二進制漏洞利用(二)ARM32位匯編下的TCP Bind shell:https://bbs.pediy.com/thread-253511.htm

?

?

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, pcmov r1, #2add r2, r1, r1bkpt

    使用 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 0x0001009F0x1009f <var2+3>: 0x00000003

    第二條STR指令操作是前變址尋址,做了同樣的操作,不同的一點是R1的值會被更新:R1=R1+R2。

    gef> info register r1r1 0x1009f 65695

    最后一條LDR指令操作是后變址尋址。以R1中的值為訪問地址,獲取該地址處的數據并加載到R3,然后更新R1的值:R1 = R1 + R2 = 0x1009f + 0x3 = 0x100a2。

    gef> info register r1r1 0x100a2 65698 gef> info register r3r3 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<<2ldr r3, [r1], r2, LSL#2 @ 以R1中的值為訪問地址,加載該地址處的數據到R3,基準寄存器R1中的值被修改: R1 = R1 + R2<<2bkpt ? 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, #511bkpt

    如果嘗試編譯,編譯器會輸出類似以下錯誤:

    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, #3mov r1, #4push {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, #38058: e3a01004 mov r1, #4805c: 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 32add 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 + 2addne r1, #3 @ ... 否則 R1 = R1 + 3bkpt

    .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 modeadd r2, pc, #1 @ put PC+1 into R2bx r2 @ branch + exchange to R2 ?.code 16 @ Thumb modemov 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, #2mov r1, #2add r0, r0, r1cmp r0, #4beq func1add r1, #5b func2 func1:mov r1, r0bx lr func2:mov r0, r1bx 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/

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    ?

    總結

    以上是生活随笔為你收集整理的ARM 汇编语言入门的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。