ARM汇编基础

title: ARM汇编
date: 2019-03-11 09:03:27
tags: ARM
categories: categories
  • Part 1:ARM汇编介绍

  • Part 2:数据类型寄存器

  • Part 3: ARM指令集

  • Part 4: 内存相关指令:加载以及存储

  • Part 5:重复性加载及存储

  • Part 6: 分支和条件执行

  • Part 7:栈以及函数


ARM&X86cpu的一些区别:

Intel是一个CISC(Complex Instruction Set Computing,复杂指令集)处理器。因此它具有更庞大,功能更丰富的指令集,并且允许指令进行一些复杂的访存操作。它也因此具有支持更多的复杂操作和寻址方式,并且寄存器的数量比ARM要少的多。CISC处理器一般用在通用PC,工作站和服务器中。

ARM是一个RISC(Reduced Instruction Set Computing,精简指令集)处理器。因此它拥有一套精简的指令集(100个左右,甚至更少的指令)以及比CISC处理器更多的通用寄存器。与Intel处理器不同,ARM指令只处理寄存器中的数据,并使用了load/store结构访问存储器,也就是说只有load/store指令可以访问存储器。所以如果我们要增加某个内存地址中保存的值,至少需要三种类型的指令(load指令、加法指令和store指令),首先我们需要使用load指令将指定地址内存中的值加载到寄存器中,然后使用加法指令增加寄存器中的值,然后用store指令将寄存器中的值写回内存。

#简单的说 ARM执行一个操作,需要的步骤更多。而x86的直接就能进行访存

ARM优势

一个重要的优势是指令可以被更快的执行(RISC处理器通过引入流水线机制,减少每个指令的占用的CPU的时钟周期来缩短执行时间)。它的劣势也很明显,较少的指令增加了软件(事实上是编译器)的复杂性。另一个重要的事实是,ARM具有两种运行模式(可以类比x86的实模式和保护模式),ARM模式和Thumb模式。

ARM也更加节省能耗

简介剪贴自:https://azeria-labs.com/writing-arm-assembly-part-1/

ARM和X86的差异:https://www.cnblogs.com/bitter/p/4023176.html

ARM汇编数据类型基础

与高级语言类似,ARM也支持操作不同的数据类型。

被加载或者存储的数据类型可以是无符号(有符号)的字(words,四字节),半字(halfwords,两字节),或者字节(bytes)。这些数据类型在汇编语言中的扩展后缀为-h或者-sh对应着半字,-b或者-sb对应着字节,但是对于字并没有对应的扩展。无符号类型与有符号类型的差别是:

  • 符号数据类型可以包含正负数所以数值范围上更低些

  • 无符号数据类型可以放得下很大的正数但是放不了负数

这有一些要求使用对应数据类型做存取操作的汇编指令示例:

ldr = 加载字,宽度四字节
ldrh = 加载无符号的半字,宽度两字节
ldrsh = 加载有符号的半字,宽度两字节
ldrb = 加载无符号的字节
ldrsb = 加载有符号的字节

str = 存储字,宽度四字节
strh = 存储无符号的半字,宽度两字节
strsh = 存储有符号的半字,宽度两字节
strb = 存储无符号的字节
strsb = 存储有符号的字节

字节序

在内存中有两种字节排布顺序,大端序(BE)或者小端序(LE)。两者的主要不同是对象中的每个字节在内存中的存储顺序存在差异。一般X86中是小端序,最低的字节存储在最低的地址上。在大端机中最高的字节存储在最低的地址上。

ARM汇编基础

在版本3之前,ARM使用的是小端序,但在这之后就都是使用大端序了,但也允许切换回小端序。在我们样例代码所在的ARMv6中,指令代码是以[小端序排列对齐]。但是数据访问时采取大端序还是小端序使用程序状态寄存器(CPSR)的第9比特位来决定的。

ARM汇编基础

image

ARM寄存器

寄存器的数量由ARM版本决定。根据ARM参考手册,在ARMv6-M与ARMv7-M的处理器中有30个32bit位宽度的通用寄存器。前16个寄存器是用户层可访问控制的,其他的寄存器在高权限进程中可以访问(但ARMv6-M与ARMv7-M除外)。我们仅介绍可以在任何权限模式下访问的16个寄存器。这16个寄存器分为两组:通用寄存器与有特殊含义的寄存器。

# 别名 用途
R0 - 通用寄存器
R1 - 通用寄存器
R2 - 通用寄存器
R3 - 通用寄存器
R4 - 通用寄存器
R5 - 通用寄存器
R6 - 通用寄存器
R7 - 一般放系统调用号
R8 - 通用寄存器
R9 - 通用寄存器
R10 - 通用寄存器
R11 FP 栈帧指针
R12 IP 内部程序调用
R13 SP 栈指针
R14 LR 链接寄存器(一般存放函数返回地址)
R15 PC 程序计数寄存器
CPSR - 当前程序状态寄存器

下面这张表是ARM架构与寄存器与Intel架构寄存器的关系:

ARM 描述 X86
R0 通用寄存器 EAX
R1-R5 通用寄存器 EBX,ECX,EDX,ESI,EDI
R6-R10 通用寄存器 -
R11(FP) 栈帧指针 EBP
R12 内部程序调用 -
R13(SP) 栈指针 ESP
R14(LR) 链接寄存器 -
R14(LR) <-程序计数器/机器码指针-> EIP
CPSR 程序状态寄存器 EFLAGS

R0-R12:用来在通用操作中存储临时的值,指针等。R0被用来存储函数调用的返回值。R7经常被用作存储系统调用号,R11存放着帮助我们找到栈帧边界的指针(之后会讲)。以及,在ARM的函数调用约定中,前四个参数按顺序存放在R0-R3中。

R13:SP(栈指针)。栈指针寄存器用来指向当前的栈顶。栈是一片来存储函数调用中相关数据的内存,在函数返回时会被修改为对应的栈指针。栈指针用来帮助在栈上申请数据空间。比如说你要申请一个字的大小,就会将栈指针减4,再将数据放入之前所指向的位置。

R14:LR(链接寄存器)。当一个函数调用发生,链接寄存器就被用来记录函数调用发生所在位置的下一条指令的地址。这么做允许我们快速的从子函数返回到父函数。

R15:PC(程序计数器)。程序计数器是一个在程序指令执行时自增的计数器。它的大小在ARM模式下总是4字节对齐,在Thumb模式下总是两字节对齐。当执行一个分支指令时,PC存储目的地址。在程序执行中,ARM模式下的PC存储着当前指令加8(两条ARM指令后)的位置,Thumb(v1)模式下的PC存储着当前指令加4(两条Thumb指令后)的位置。这也是X86与ARM在PC上的主要不同之处。

我们可以通过调试来观察PC的行为。我们的程序中将PC的值存到R0中同时包含了两条其他指令,来看看会发生什么。

.section .text
.global _start

_start:
mov r0, pc
mov r1, #2
add r2, r1, r1
bkpt

在GDB中,我们开始调试这段汇编代码:

gef> br _start
Breakpoint 1 at 0x8054
gef> run

在开始执行触发断点后,首先会在GDB中看到:

$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指向0x8054这个位置即第一条要被执行的指令,那么此时我们使用GDB命令si,执行下一条机器码。下一条指令是把PC的值放到R0寄存器中,所以应该是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

当然不是,在执行0x8054这条位置的机器码时,PC已经读到了两条指令后的位置也就是0x805c(见R0寄存器)。所以我们以为直接读取PC寄存器的值时,它指向的是下一条指令的位置。但是调试器告诉我们,PC指向当前指令向后两条机器码的位置。这是因为早期的ARM处理器总是会先获取当前位置后两条的机器码。这么做的原因也是确保与早期处理器的兼容性。

当前程序状态寄存器(CPSR)

当你用GDB调试ARM程序的的时候你能会可以看见Flags这一栏(GDB配置插件GEF后就可以看见了,或者直接在GDB里面输入flags也可以)。

ARM汇编基础

image

图中寄存器$CSPR显示了当前状态寄存器的值,Flags里面出现的thumb,fast,interrupt,overflow,carry,zero,negative就是来源于CSPR寄存器中对应比特位的值。ARM架构的N,Z,C,V与X86架构EFLAG中的SF,ZF,CF,OF相对应。这些比特位在汇编级别的条件执行或者循环的跳出时,被用作判断的依据。

CF是进位标志;PF是奇偶标志;AF是辅助进位标志;ZF是零标志;SF是符号标志;OF是溢出标志。

ARM汇编基础

上图展示了32位的CPSR寄存器的比特位含义,左边是最大比特位,右边是最小比特位。每个单元代表一个比特。这一个个比特的含义都很丰富:

标记 含义
N(Negative) 指令结果为负值时置1
Z(Zero) 指令结果为零值时置1
C(Carry) 对于加法有进位则置1,对于减法有借位则置0
V(Overflow) 指令结果不能用32位的二进制补码存储,即发生了溢出时置1
E(Endian) 小端序置0,大端序置1
T(Thumb) 当为Thumb模式时置1,ARM模式置0
M(Mode) 当前的权限模式(用户态,内核态)
J(Jazelle) 允许ARM处理器去以硬件执行java字节码的状态标示

假设我们用CMP指令去比较1和2,结果会是一个负数因为1-2=-1。然而当我们反过来用2和1比较,C位将被设定,因为在一个较大的数上减了较小的数,没有发生借位。当我们比较两个相同的数比如2和2时,由于结果是0,Z标志位将被置一。注意CMP指令中被使用的寄存器的值并不会被修改,其计算结果仅仅影响到CPSR寄存器中的状态位。

在开了GEF插件的GDB中,计算结果如下图:在这里我们比较的两个寄存器是R1和R0,所以执行后的flag状态如下图。

ARM汇编基础

Carry位Flag被设置的原因是CMP R1,R0会去拿4和2做比较。因为我们用以个较大的数字去减一个较少的数字,没有发生借位。Carry位便被置1。相反的,如果是CMP R0,R1那么Negative位会被置一。

原作链接:https://www.jianshu.com/p/a28638589d55

ARM模式与THUMB模式

ARM处理器有两个主要的操作状态,ARM模式以及Thumb模式(Jazelle模式先不考虑)。这些模式与特权模式并不冲突。SVC模式既可以在ARM下调用也可以在Thumb下调用。只不过两种状态的主要不同是指令集的不同,ARM模式的指令集宽度是32位而Thumb是16位宽度(但也可以是32位)。知道何时以及如何使用Thumb模式对于ARM漏洞利用的开发尤其重要。当我们写ARM的shellcode时候,我们需要尽可能的少用NULL以及使用16位宽度的Thumb指令以精简代码。

不同版本ARM,其调用约定不完全相同,而且支持的Thumb指令集也是不完全相同。在某些版本,ARM提出了扩展型Thumb指令集(也叫Thumbv2),允许执行32位宽的Thumb指令以及之前版本不支持的条件执行。为了在Thumb模式下使用条件执行指令,Thumb提出了"IT"分支指令。然而,这条指令在之后的版本又被更改移除了,说是为了让一些事情变得更加简单方便。并不清楚各个版本的ARM架构所支持的具体的ARM/Thumb指令集,而且我也的确不想知道。我觉得你也应该不用深究这个问题。因为你只需要知道你设备上的关键ARM版本所支持的Thumb指令集就可以了。以及ARM信息中心可以帮你弄清楚你的ARM版本到底是多少。

就像之前说到的,Thumb也有很多不同的版本。不过不同的名字仅仅是为了区分不同版本的Thumb指令集而已(也就是对于处理器来说,这些指令永远都是Thumb指令)。

  • Thumb-1(16位宽指令集):在ARMv6以及更早期的版本上使用。

  • Thumb-2(16位/32位宽指令集):在Thumb-1基础上扩展的更多的指令集(在ARMv6T2以及ARMv7即很多32位Android手机所支持的架构上使用)

  • Thumb-EE:包括一些改变以及对于动态生成代码的补充(即那些在设备上执行前或者运行时编译的代码)

ARM与Thumb的不同之处在于:

  • 对于条件执行指令(不是条件跳转指令):所有的ARM状态指令都支持条件执行。一些版本的ARM处理器上允许在Thumb模式下通过IT汇编指令进行条件执行。条件执行减少了要被执行的指令数量,以及用来做分支跳转的语句,所以具有更高的代码密度。

  • ARM模式与Thumb模式的32位指令:Thumb的32位汇编指令都有类似于a.w的扩展后缀。

  • 桶型移位是另一种独特的ARM模式特性。它可以被用来减少指令数量。比如说,为了减少使用乘法所需的两条指令(乘法操作需要先乘2然后再把结果用MOV存储到另一个寄存器中),就可以使用在MOV中自带移位乘法操作的左移指令(Mov R1, R0, LSL #1)。

在ARM模式与Thumb模式间切换的话,以下两个条件之一必须满足:

  • 我们可以在使用分支跳转指令BX(branch and exchange)或者分支链接跳转指令BLX(branch,link and exchange)时,将目的寄存器的最低位置为1。之后的代码执行就会在Thumb模式下进行。你也许会好奇这样做目标跳转地址不就有对齐问题了么,因为代码都是2字节或者4字节对齐的?但事实上这并不会造成问题,因为处理器会直接忽略最低比特位的标识。更多的细节我们会在第6篇中解释。

  • 我们之前有说过,在CPSR当前程序状态寄存器中,T标志位用来代表当前程序是不是在Thumb模式下运行的。

ARM指令集规律含义

这一节的目的是简要的介绍ARM的通用指令集。知道每一句汇编指令是怎么操作使用,相互关联,最终组成程序是很重要的。之前说过,汇编语言是由构建机器码块的指令组成。所以ARM指令通常由助记符外加一到两个跟在后面的操作符组成,如下面的模板所示:

MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
助记符{是否使用CPSR}{是否条件执行以及条件} {目的寄存器}, 操作符1, 操作符2

由于ARM指令的灵活性,不是全部的指令都满足这个模板,不过大部分都满足了。下面来说说模板中的含义:

MNEMONIC     - 指令的助记符如ADD
{S}         - 可选的扩展位,如果指令后加了S,则需要依据计算结果更新CPSR寄存器中的条件跳转相关的FLAG
{condition} - 如果机器码要被条件执行,那它需要满足的条件标示
{Rd}         - 存储结果的目的寄存器
Operand1     - 第一个操作数,寄存器或者是一个立即数
Operand2     - 第二个(可变的)操作数,可以是一个立即数或者寄存器或者有偏移量的寄存器

当助记符,S,目的寄存器以及第一个操作数都被声明的时候,条件执行以及第二操作数需要一些声明。因为条件执行是依赖于CPSR寄存器的值的,更精确的说是寄存器中的一些比特位。第二操作数是一个可变操作数,因为我们可以以各种形式来使用它,立即数,寄存器,或者有偏移量的寄存器。举例来说,第二操作数还有如下操作:

#123                    - 立即数
Rx                     - 寄存器比如R1
Rx, ASR n               - 对寄存器中的值进行算术右移n位后的值
Rx, LSL n               - 对寄存器中的值进行逻辑左移n位后的值
Rx, LSR n               - 对寄存器中的值进行逻辑右移n位后的值
Rx, ROR n               - 对寄存器中的值进行循环右移n位后的值
Rx, RRX                 - 对寄存器中的值进行带扩展的循环右移1位后的值

在知道了这个机器码模板后,然我们试着去理解这些指令:

ADD   R0, R1, R2         - 将第一操作数R1的内容与第二操作数R2的内容相加,将结果存储到R0中。
ADD   R0, R1, #2         - 将第一操作数R1的内容与第二操作数一个立即数相加,将结果存到R0中
MOVLE R0, #5             - 当满足条件LE(Less and Equal,小于等于0)将第二操作数立即数5移动到R0中,注意这条指令与MOVLE R0, R0, #5相同
MOV   R0, R1, LSL #1     - 将第一操作数R1寄存器中的值逻辑左移1位后存入R0

最后我们总结一下,满足这个模板的一些通用ARM指令集以及其含义:

指令 含义 指令 含义
MOV 移动数据 EOR 比特位异或
MVN 取反码移动数据 LDR 加载数据
ADD 数据相加 STR 存储数据
SUB 数据相减 LDM 多次加载
MUL 数据相乘 STM 多次存储
LSL 逻辑左移 PUSH 压栈
LSR 逻辑右移 POP 出栈
ASR 算术右移 B 分支跳转
ROR 循环右移 BL 链接分支跳转
CMP 比较操作 BX 分支跳转切换
AND 比特位与 BLX 链接分支跳转切换
ORR 比特位或 SWI/SVC 系统调用

参考:https://www.jianshu.com/p/ba4f055e99de

内存访问相关指令

上一篇:STM32F4xx那点事(四)——定时器的使用及配置


下一篇:基于openEuler的OpenSSL编译安装和编程实践