Arm汇编指令初探

转载arm语法,来自看雪学院

原文链接

数据类型

这是ARM汇编基础教程的第二篇,包含了数据类型和寄存器的相关知识。

和高级语言一样,ARM汇编语言支持对不同数据类型的操作。我们可以load(或store)的数据类型包括signed/unsigned words,halfwords或者bytes。我们用“-h”或“-sh”后缀表示half words,用“-b”或“-sb”表示bytes,无后缀默认表示words。有符号和无符号数据类型之间的区别有:
§ 有符号数可以表示正直和负值,所以范围较小;
§ 无符号数只能表示正值,所以范围更大。
下面是使用load和store指令操作不同类型数据的示例:

1
2
3
4
5
6
7
8
9
10
11
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

字节序


在内存中有两种存储多字节数据的方式,大端序和小端序。这两种方式的差异是数据存储时的字节顺序不同。在以小端序存储数据的设备中(如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寄存器,总是指向当前指令的下一条指令。

示例

1
2
3
4
5
6
7
8
.section .text
.global _start
_start:
mov r0, pc
mov r1, #2
add r2, r1, r1
bkpt

将上述代码保存为test.s文件,使用编译汇编代码[ubuntu16需要安装qemu]:

1
2
3
arm-linux-gnueabi-as test.s -o test.o
arm-linux-gnueabi-objdump -d test.o //查看汇编
arm-linux-gnueabi-ld test.o -o test

我们使用gdb远程调试(这里我用一个gdb的插件pwndbg)

1
2
3
4
gdb ./test
pwndbg> b _start
Breakpoint 1 at 0x10054
pwndbg> c

汇编指令

1.ldr 和mov的区别
mov只能传递寄存器之间的值
例如 mov r0,r1
ldr 传递内存地址里的值到寄存器
例如 ldr r1,[r3]
例如 ldr r0,[r1,#4]
这在0x86里都是mov负责的工作,现在被分工了
ldr伪指令可以当mov一样使用
例如 ldr r0,=ffff 操作数前加一个等号
操作数大小在mov指令范围内的话会自动编译成mov指令

2.str 和 ldr
str是传递寄存器里的值到内存地址中
例如 str r0,[r1] r0寄存器的值写入到r1所表示的内存地址里
这跟我们以往见到的汇编指令是相反的,第一个操作数是源操作数,第二个操作数是目标操作数,这里应该注意一下.
理解一str 是ldr反操作
对比 ldr r0,[r1] 和 str r0,[r1] 是相反的执行过程
理解二可以理解str为写内存 ldr 为读内存
这些在0x86里都是MOV 负责的工作,现在被分工了

3.stm和ldm 批量读写 后面往往会跟着ia ib da db等等条件
i表示 加4 d表示减4 a表示赋值后 b表示赋值前 都是对应的英文单词的缩写
例子:
stmia r0!,{r1,R3,R5}
ia表示赋值后加4,也就是先存后增
产生的结果是:
r1存至r0
R3存至r0+4
R5存至r0+8
r0最后指向的地址是r0(初)+0xC
因为是先存后增,所以寄存器组存数据也是递增的,从r1递增取至R5
成对使用的取数据操作是:
ldmdb r0!,{r1,R3,R5}
这个时候r0指向的地址已经是r0(初)+0xC,先减后取,所以产生的结果是:
r0+8取至R5
r0+4取至R3
r0取至r1
这两句命令相当于保存和还原的操作 类似PUSHAD POPAD的功能

4.跳转
B 无条件跳转指令 对应JMP
mov pc ,xxxx 相当于B(PC 之前我们说了 相当于EIP)
ldr pc,_xxxx往往都是异常处理
例如 ldr pc, _data_abort 数据异常
BEQ相等跳转指令 对应JE
BL保存PC到LR再跳转 相当于CALL
也就是 mov lr,pc 然后B
mov pc,lr 可以返回 相当于retn
BLX和BX 中的X表示跳转切换指令集 arm和thumb

5.状态寄存器
我们看到很多指令后面跟个S
例如 adds 带s 表示影响状态寄存器

6.立即数 前面要加#

7.SWP 后面跟B交换字节否则交换32位
SWP 指令举例如下
SWP R1,R1,[R0] 将R1 的内容与R0 指向的内存地址里的值交换
SWPB R1,R2,[R0] 将R0指向的内存地址里的值读取一字节数据到R1中(高 24位清零) 并将R2的内容写入到该内存单元中
实际上这个过程可以拆分成3句代码 来赋值,相当于C++里的变量交换

8.异或 E0R 对应 XOR
lsl,lsr 左右移
例如 mov r0,r1,lsl #2 结果 r0=r1*4

9.Ball对应Call 省略成 BL Ball=B+All(条件)+保存LR

10.mvn 取反然后mov
例如 mvn r0,#0 结果 r0=ffffffff

汇编指令很多不是我们几天能够完全掌握的 可以在逆向的学习过程中慢慢吸收
这里面只是给大家列举比较常用的一些指令而已

Donate
-------------本文结束感谢您的阅读-------------