ARM彙編基礎教程——數據類型和寄存器

數據類型

這是ARM彙編基礎教程的第二篇,包含了數據類型和寄存器的相關知識。

和高級語言一樣,ARM彙編語言支持對不同數據類型的操作。我們可以load(或store)的數據類型包括signed/unsigned words,halfwords或者bytes。我們用「-h」或「-sh」後綴表示half words,用「-b」或「-sb」表示bytes,無後綴默認表示words。有符號和無符號數據類型之間的區別有:

§ 有符號數可以表示正直和負值,所以範圍較小;

§ 無符號數只能表示正值,所以範圍更大。

下面是使用load和store指令操作不同類型數據的示例:

ldr = Load Wordldrh = Load unsigned Half Wordldrsh = Load signed Half Wordldrb = Load unsigned Byteldrsb = Load signed Bytes str = Store Wordstrh = Store unsigned Half Wordstrsh = Store signed Half Wordstrb = Store unsigned Bytestrsb = Store signed Byte

位元組序

在內存中有兩種存儲多位元組數據的方式,大端序和小端序。這兩種方式的差異是數據存儲時的位元組順序不同。在以小端序存儲數據的設備中(如x86),位權低(個位的位權比十位低)的位元組存儲在低地址(地址值小的地址)。在以大端序存儲數據的設備上,位權高的位元組存儲在低地址。上一篇我們提到過,ARM架構在ARMv3之前是小端序的,在那之後,ARM處理器可以通過硬體配置在大小端之間切換。以ARMv6為例,指令是固定的以小端序存儲的,而內存數據的讀取方式可以通過控制程序狀態寄存器CPSR的第9位實現在大端和小端之間切換。

ARM寄存器

ARM處理器的寄存器個數與ARM指令集版本有關。根據ARM手冊,除了基於ARMv6-M和ARMv7-M的處理器,其它的ARM處理器都有30個32 bit的通用寄存器。前17個(原文是16個,我覺的可能是作者犯的off-by-one錯誤,如果理解有誤,請高手指正)寄存器是在用戶模式下可訪問的,其它的寄存器只有在特定的運行模式下才可以訪問(ARMv6-M和ARMv7-M除外,它們的架構有一些差異,有興趣的話可以單獨去學習)。在這篇教程中,我們將關注那些可以在任何運行模式下被訪問的寄存器:r0~r15還有CPSR。這16個寄存器可以被分為兩組:通用寄存器和專用寄存器。

寄存器

下面這張表將ARM的寄存器和x86寄存器做了一個簡單類比:

R0~R12(R12的使用要慎重):R0~R12是通用寄存器(R12已經不完全是了),它們可以在常規操作中使用,來存儲臨時變數或地址。習慣上,R0常在算數運算中作為累加器,或者存儲函數的返回地址。R7常用於存儲系統調用號。R11常作為棧幀指針來標記函數棧幀的邊界。此外,ARM的函數調用約定規定,函數的前四個參數存儲在寄存器r0~r3中。

R13:R13是堆棧指針(SP,Stack Point)。它指向堆棧的頂部。堆棧是用來存儲函數局部存儲的一段內存,在函數返回時回收。堆棧指針通過減去我們要分配的空間大小,來分配堆棧上的空間。比如,我們要分配一個32 bit的空間,那麼就令R13減4。

R14:R14是鏈接寄存器(LR,Link Register)。當進行函數調用時,鏈接寄存器被更新為調用函數指令的下一條指令的地址。這樣做可以使程序在執行完子函數之後得以返回父函數。

R15:R15是程序計數器(PC,Program Counter)。在執行指令時,PC總是自動的增加,增加的大小等於正在執行指令的長度。這個長度在ARM架構下是固定的,ARM模式是4位元組,Thumb模式是2位元組。當執行分支指令時,PC被更新為目的地址。需要注意的是,由於RISC CPU流水線優化的原因,在執行期間,ARM模式下PC等於當前指令地址加8,Thumb模式下等於當前指令地址加4,也就是後移兩條指令。這不同於x86的EIP寄存器,總是指向當前指令的下一條指令。

下面我們通過調試器來看看PC的行為。我們使用下面的程序,先將PC保存在寄存器r0中,然後隨意執行兩條指令。讓我們看看會發生什麼:

.section .text.global _start.global _main_start:b _main_main:mov r0, pcmov r1, #2add r2, r1, r1bkpt

我們使用gdb遠程調試(作者在這裡使用了gef增強腳本,可以在github.com/hugsy/gef找到,關於gdb遠程調試環境搭建以及gef的配置使用,後面會單獨寫文章介紹)在_main標號處設置斷點,然後執行:

gef? b _mainBreakpoint 1 at 0x10058: file src/0x00pc/pc.s, line 9.gef? cContinuing.

你應該會看到類似下面的輸出:

$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 : 0xbefff740 → 0x00000001$lr : 0x00000000$pc : 0x00010058 → <_main+0> mov r0, pc$cpsr: [thumb fast interrupt overflow carry zero negative]──────────────[ source:src/0x00pc/pc.s+9 ]────────────5 _start:6 b _main7 8 _main:-> 9 mov r0, pc10 mov r1, #211 add r2, r1, r112 bkpt0x8070 andeq r0, r0, r11

我們可以看到,PC中存儲的地址為(0x10058),也就是下一條即將執行的指令地址。我們使用si命令單步執行,下一條指令中PC將被存儲到寄存器r0中,屆時寄存器r0的值將會是0x10058,是這樣嗎?

$r0 : 0x00010060 → <_main+8> add r2, r1, r1$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 : 0xbefff740 → 0x00000001$lr : 0x00000000$pc : 0x0001005c → <_main+4> mov r1, #2$cpsr: [thumb fast interrupt overflow carry zero negative]─────────────[ source:src/0x00pc/pc.s+10 ]────────────6 b _main7 8 _main:9 mov r0, pc-> 10 mov r1, #211 add r2, r1, r112 bkpt

然而,事實並非如此。我們來看寄存器r0,我們期待的結果是調試器顯示的PC的值0x10058,然而指令執行的結果表明,在指令執行時PC指向的是0x10060,相當於向後偏移兩條指令的位置。產生這種差異的原因其實很簡單,調試器顯示的PC寄存器的值是經過處理的。下面我簡單解釋一下,當0x10058處的指令被執行時,PC寄存器已經指向了0x10058+0x8處的指令,這是由於CPU的流水線機制導致的。CPU取指令,解碼指令和執行指令時使用的是不同的硬體部件,因此,這幾個操作(實際的CPU可能更複雜,有更多的操作步驟)是可以並行執行的。因為RISC CPU的指令長度一定,所以CPU可以在解碼指令之前就知道下一條指令的長度,從而在解碼指令時取下一條指令,在執行指令時,對下一條指令進行解碼,並取下下一條指令,這稱為三級流水線。所以ARM當我們在執行0x10058處的指令時,PC已經指向0x10060處進行取指令操作了。這是硬體中真實發生的情況,而調試器為了令展示更有邏輯性,所以PC寄存器顯示了當前執行指令的地址,當我們真實調試時不要受此影響。

當前程序狀態寄存器

當你調試一個ARM二進位文件時,你會關心Flags。

如果你查看gef顯示的寄存器信息,會發現一個特殊的寄存器$cpsr(Current Program Status Register,當前程序狀態寄存器)。你可以看到其中存儲了thumb、fast、interrupt、overflow、carry、zero和negative這些Flags標誌位。

$sp : 0xbefff740 → 0x00000001

$lr : 0x00000000

$pc : 0x0001005c → <_main+4> mov r1, #2

$cpsr: [thumb fast interrupt overflow carry zero negative] thumb、fast、interrupt、overflow、carry、zero、negative這些標誌位,由寄存器$cpsr中特定的比特位表示。當$cpsr寄存器的某個比特位被置位時,gef中的標誌位會顯示為粗體。N,Z,C和V位與x86上EFLAG寄存器中的SF,ZF,CF和OF位相同。這些標誌位被用於實現彙編語言層變的條件分支和循環,我們將在第六篇:條件分支中對它們進行詳細介紹。

上圖顯示了32 bit寄存器$cpsr的布局,左邊是高位,右邊是低位。每個單元格(除了GE、MM和空白單元格)都是1 bit。這些1 bit的標誌位定義了程序當前狀態的各種屬性。

假設我們用cmp指令來比較1和2,結果將為負,Negative標誌位被置1。因為cmp指令執行一次隱式的減法操作,1-2=-1。然而,如果我們比較2和1(和剛才相反),減法操作不借位,Carry標誌位被置1。如果我們比較兩個相同的數,比如2和2,那麼2-2=0,在Carry標誌位置1的同時,Zero標誌位也被置1。可以使用如下示常式序檢驗上述分析。

.section .text.global _start.global _main_start:b _main_main:mov r1, #1mov r2, #2cmp r1, r2cmp r2, r1cmp r2, r2 bkpt

本文由看雪翻譯小組 ljcnaix 編譯,來源Azeria Labs

轉載請註明來自看雪社區


推薦閱讀:

把51的7段數碼管顯示程序移植到ZYNQ上(三)——SDK下的程序調試和結果
MCU: ARM的調試架構
Nvidia Tegra K1的色彩壓縮和PowerVR Series/ARM Mali-Txxx的幀緩存壓縮是什麼?
如何通俗地講清 I2C、SPI、USB、UART、RS232 到底是什麼東西?
如何看待 macOS Sierra 代碼中發現新增的 ARM 處理器,以及要求提交中間代碼一事?

TAG:ARM | 汇编语言 | 寄存器 |