这是 ARM 汇编基础知识系列教程的第二部分,涉及数据类型和寄存器。
与高级语言类似,ARM支持对不同数据类型的操作。我们可以加载(或存储)的数据类型可以是有符号和无符号字、半字或字节。这些数据类型的扩展是。-h或-sh用于半字,-b或-sb用于字节,而字则没有扩展。有符号和无符号数据类型之间的区别是。
有符号的数据类型可以容纳正值和负值,因此范围较小。
无符号数据类型可以保存大的正值(包括 "零"),但不能保存负值,因此范围更广。
下面是一些例子,说明这些数据类型如何与指令Load和Store一起使用。
在内存中,有两种查看字节的基本方法。小端(LE)或大端(BE)。区别在于一个对象的每个字节在内存中的存储顺序。在像英特尔x86这样的小端机器上,最不重要的字节被存储在最低地址(最接近零的地址)。在big-endian机器上,最重要的字节被存储在最低地址。ARM架构在第3版之前是小-endian,从那时起,它是双-endian,这意味着它有一个允许可切换endianness的设置。例如,在ARMv6中,指令是固定的小字节,数据访问可以是小字节或大字节,由程序状态寄存器(CPSR)的第9位(E位)控制。
寄存器的数量取决于ARM的版本。根据ARM参考手册,除了基于ARMv6-M和ARMv7-M的处理器外,有30个通用的32位寄存器。前16个寄存器可在用户级模式下访问,其他寄存器可在特权软件执行中使用(ARMv6-M和ARMv7-M例外)。在本系列教程中,我们将处理在任何特权模式下都可以访问的寄存器:r0-15。这16个寄存器可以分成两组:通用寄存器和特殊用途寄存器。
下表展示了ARM寄存器与Intel处理器中的寄存器之间的关系。
R0-R12:在普通操作中可用于存储临时值、指针(存储器的位置)等。例如,R0在进行算术运算时可作为累加器,或用于存储先前调用的函数的结果。R7在处理系统调用时变得非常有用,因为它存储了系统调用的编号,R11帮助我们跟踪堆栈上的边界,作为框架指针(将在后面介绍)。此外,ARM的函数调用惯例规定,函数的前四个参数存储在寄存器r0-r3中。
R13:SP(堆栈指针)。堆栈指针指向堆栈的顶部。堆栈是一个用于特定函数存储的内存区域,在函数返回时被回收。因此,堆栈指针用于分配堆栈的空间,方法是用堆栈指针减去我们要分配的值(以字节为单位)。换句话说,如果我们想分配一个32位的值,我们从堆栈指针中减去4。
R14:LR(链接寄存器)。当一个函数被调用时,链接寄存器被更新为内存地址,引用函数启动的下一条指令。这样做允许程序在 "子 "函数完成后返回到启动 "子 "函数的 "父 "函数。
R15:PC(程序计数器)。程序计数器根据所执行的指令的大小自动递增。这个大小在ARM状态下总是4字节,在THUMB模式下是2字节。当一个分支指令被执行时,PC保存目标地址。在执行过程中,PC在ARM状态下存储当前指令的地址加8(两条ARM指令),在Thumb(v1)状态下存储当前指令加4(两条Thumb指令)。这与x86不同,x86的PC总是指向要执行的下一条指令。
让我们看看PC在调试器中是如何表现的。我们用下面的程序将PC的地址存入r0,并包括两条随机指令。让我们看看会发生什么。
在gdb中我们在_start处设定一个断点
如下是运行的结果:
我们可以看到,PC持有将被执行的下一条指令(mov r0, pc)的地址(0x8054)。现在让我们执行下一条指令,之后R0应该持有PC的地址(0x8054),对吗?
...对吗?错了。看看R0中的地址。当我们期望R0包含先前读取的PC值(0x8054)时,它却包含了比我们先前读取的PC值(0x805c)提前两条指令的值。从这个例子中你可以看到,当我们直接读取PC时,它遵循PC指向下一条指令的定义;但在调试时,PC指向当前PC值前面的两条指令(0x8054 + 8 = 0x805C)。这是因为较早的ARM处理器总是在当前执行的指令之前获取两条指令。ARM保留这一定义的原因是为了确保与早期处理器的兼容性。
当你用gdb调试一个ARM二进制文件时,你会看到一个叫做Flags的东西。
寄存器$cpsr显示了当前程序状态寄存器(CPSR)的值,在它下面可以看到Flagsthumb, fast, interrupt, overflow, carry, zero, and negative。这些标志代表了CPSR寄存器中的某些位,并根据CPSR的值来设置,激活时变成粗体。N、Z、C和V位与x86上EFLAG寄存器中的SF、ZF、CF和OF位相同。这些位被用来支持汇编级的条件和循环的条件执行。我们将在第6部分 "条件执行和分支 "中介绍使用的条件代码。
上图显示了一个32位寄存器(CPSR)的布局,左边()是最小的位。每一个单元(除了GE和M部分以及空白部分)都是一个比特的大小。这些一比特的部分定义了程序当前状态的各种属性。
让我们假设我们使用CMP指令来比较数字1和2。结果是 "负",因为1-2=-1。当我们比较两个相等的数字时,比如2对2,Z(零)标志被设置,因为2-2=0。请记住,CMP指令使用的寄存器不会被修改,只有CPSR会根据这些寄存器相互比较的结果被修改。
这是GDB中的情况(安装了GEF)。在这个例子中,我们比较寄存器r1和r0,其中r1=4,r0=2。这是执行了cmp r1, r0操作后的标志的情况。
进位标志被设置,因为我们用cmp r1, r0来比较4和2(4-2)。相反,如果我们使用cmp r0, r1来比较一个较小的数字(2)和一个较大的数字(4),则负标志(N)被设置。
下面是ARM信息中心的一段摘录:
APSR包含以下ALU状态标志。
N - 当操作的结果为负数时设置。
Z - 当操作的结果为零时设置。
C - 当操作的结果是Carry时设置。
V--当操作引起溢出时设置。
carry在以下情况被设置:
如果加法的结果大于或等于2^32
如果减法的结果是正数或零
作为移动或逻辑指令中的内联移位操作的结果。
如果加法、减法或比较的结果大于或等于2^31,或小于2^31,则发生溢出。
本文是ARM汇编系列教程的第2部分, 介绍arm的基本数据类型, 和基本寄存器相关知识