与高级编程语言类似,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中。
R13:SP(栈指针)。始终指向当前栈顶。
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,则会发生溢出。