ARM汇编

PSR寄存器

N位

Z位 zero 标志位

C位 carrcyr condition标志位,用于比较无符号数的大小

V位 overflow位,溢出标志位

没有隐式内存操作指令(push)

条件和标志位响应

截图.png

Arm汇编条件跳转表,指令是否执行判断取决于指令的第一字节前四位,分为下面的16中情况

截图.png

这里对比x86的jcc,分析arm的条件跳转,关于ZF位的使用,和x86一样,都是结果为零则置zf为1,但是CF位的处理和x86有所不同

先看加法指令adds(注意,add指令不影响标志位,只有在指令后加s,才代表指令影响标志位,对应的指令的第9位要置1,比如sub不影响标志位,subs则影响)

adds r0,r0,r1 将r0和r1的值加到r0中,这里会产生溢出,并且结果为0,所以zf和cf都会被置1,这和x86没有什么不同

截图.png

截图.png

截图.png

再来看减法

subs r0,r0,r1

这里发生了借位,所以cf为应该被置1

截图.png

截图.png

可以看到结果正确但是,cf并没有被置一

截图.png

我们再来看一个普通减法

subs r0,r0,r1

执行2-1

截图.png

可以看到cf为被置1了

截图.png

这是因为在arm汇编中,当执行到sub指令时,会先将cf置1,如果发生了溢出,就会将cf置0,这里和x86里相反

读PC寄存器

ARM中的PC寄存器功能和x86中的类似

直接写PC寄存器就可以修改下条指令的地址

但是读PC寄存器存在一些问题

当读取PC寄存器时,如果当前CPU模式为ARM模式,会写入PC+8的值

如果当前CPU模式为thumb模式,会写入PC+4的值

这里有一种情况,就是如果我的下一条指令是跳转,那么我存储的还是下下条指令的地址吗?

答案是肯定的。

截图.png

同时,也可以使用PC+偏移的方式读取

比如,这里我们的指令为

LDR r0,[pc,#-4]

这里读取的就是下两条指令的前一条指令,就是下一条指令的硬编码

但是这里实际读取时,读取出来的是一个固定的值0xE7F001F0

我们知道,调试的本质是通过下断点,触发异常,

那么当我们执行到下一条语句时,这条语句应该被修改了为一个能够触发断点异常的值,

在x86中,这个值是0xCC,在arm汇编中,这个值是0xE7F001F0

截图.png

常用汇编指令

MOV指令

MOV指令分为三种

1.mov register,immediate

截图.png

2.mov register,register

截图.png

3.mov register,register_SHFITED

截图.png

第一种

mov register,immediate

把立即数存到寄存器里

分为两种格式A1和A2,先来看A2

截图.png

把16位的立即数分为两部分,中间四位为寄存器比俺还,其余位数固定

E3 01 22 34

截图.png

可以看到,立即数的位数最多为16位,也就是最多表示2^16次方的数字,这可能远远不够

所以我们引出第二种格式A1格式

截图.png

这里立即数只有12位,但是我们却能表示出0x8000000

E3 A0 03 02

截图.png

这里的操作是把立即数的低八位循环移位 立即数的高四位乘2

即0010循环右移3 * 2,即6位,就可以得到0x8000000

但是这种只适用于部分数字,因为有些数字只靠8位和循环移位是表示不出来的

所以如果要表示类似于0x12345678这样的数,可以使用两个mov指令完成

截图.png

这里可能看不出来,因为这里ida把两条指令默认合成了一条指令

movw r1,0x5678 写入了r1寄存器的低两字节

movt r1,0x1234 写入了r1寄存器的高两字节

注意,要先写低字节,再写高字节,因为写低字节会自动清零高字节

截图.png

2.mov register,register

截图.png

mov r4,r3

先来看最简单的版本,12~15位存储目标寄存器,低四位存放源寄存器

截图.png

然后再看带移位的

mov r4,r3,LSL 4

把R3寄存器的值逻辑右移四位,然后存放到r4寄存器中

7~11位为移位的位数,stype有四种,逻辑右移 逻辑左移 算术右移和循环移位

截图.png

3.mov register,register_SHFITED

mov r4,r3,LSL r1

和上面的区别就是将立即数换为了寄存器

截图.png

基本运算指令

截图.png

先看ADD指令

1.ADD register,register,immedidate

截图.png

这里的立即数和之前的A1格式是一样的,也是采用循环右移的格式,这种格式叫做A32ExpandImm

截图.png

截图.png

2.ADD register,register,register

截图.png

只是把立即数换为了寄存器,并且附带移位

截图.png

截图.png

3.ADD register,register,register-shifted

截图.png

只是把移位的立即数换为了寄存器

截图.png

截图.png

4.ADD register,sp,inmmdiate

截图.png

寄存器寻址立即数相加

相当于,ADD r0,r0,[sp+0x12]

当然,上面这条指令可能不存在

截图.png

截图.png

5.ADD r0,pc+immediate

截图.png

PC寻址

add r1,pc,4

截图.png

截图.png

访存指令

截图.png

1.ldr Rn,[Rt,<imm>]

截图.png

偏移为立即数,U为代表偏移的正负,1为正,0为负,P位为1代表外偏移,0代表内偏移,W代表有无附加行为

ldr r0,[r1],读取寄存器R1地址处的值到R0中,这是偏移为0的情况,且没有附加条件

截图.png

ldr r0,[r1,4],读取寄存器R1地址偏移+4处的值到R0中,无附加条件

截图.png

ldr r0,[r1,#-4],读取寄存器R1地址偏移-4处的值到R0中,无附加条件

截图.png

ldr r0,[r1,4]! 读取寄存器R1地址偏移+4处的值到R0中,R1寄存器加4

截图.png

ldr r0,[r1],4,读取寄存器R1地址偏移处的值到R0中,R1寄存器加4

截图.png

注意,上面两条指令都对r1寄存器+4,但是区别是内偏移读取的是加完后的地址处的值,外偏移是加之前的值

2.ldr Rn,[Rt,Rx]

截图.png

将立即数换为了寄存器,没有什么很大的改变

截图.png

Push和Pop的首先

由于ldr和str指令可以对任意寄存器进行读写,所以可以用ldr和str实现push和pop

push r0

将r0寄存器的值压入栈,栈顶减4

str r0,[sp,#-4]!

截图.png

pop

读取栈顶的值,栈顶加4

ldr r0,[sp],#4

截图.png

全局变量与重定位

截图.png

这里定义了一个全局变量,并把它打印了出来

截图.png

块访存指令

块访存指令主要分为LDM和STM,后缀为附加条件

先来看LDM,作为访存指令,数据的流向是从内存到内存

默认的LDM指令,指令后缀为IA

截图.png

介绍一下后缀的含义

IA是从当前的指针开始读取,读取次,每次读一个数据,指针向高地址移动,读取的次数取决于参数2的寄存器的个数,如果加上!,代表读取完之后指针也跟着移动

IB是从当前指针偏移-4的位置开始读取

DA代表从高地址向低地址读取或者写入

DA代表从高地址偏移+4开始读取或者写入

截图.png

截图.png

上面讲了用LDR和STR实现push和pop,同样的,作为访存指令,LDM和STM也可以实现

push指令,先将栈指针-4,然后写入数据,要写入SP寄存器指向的值

对应后缀DB,从参数2写入参数1是STM指令,

整合一下

STMDB SP!,{R1-R4}

相当于push R1,R2,R3,R4

截图.png

IDA里直接翻译成了PUSH指令

pop指令,先将数据读出,然后SP+4

LDMIA SP!,{R1-R4}

相当于POP R1,R2,R3,R4

IDA直接翻译为了pop指令

截图.png

如果是其他版本的IDA

上面的指令则会被翻译为LDMFD和STMFD

因为这两条指令默认第一个操作数为SP!

分支跳转语句

B,BL,BX,BLX

截图.png

关于分支跳转指令,主要关注三个点

1.跳转的目标地址

2.跳转时是否发生模式切换

3.跳转时是否写入LR寄存器,写入的值是什么

1.B imm

无条件跳转

截图.png

输入一个地址,会计算出距离当前PC的偏移,如果是arm模式,会将偏移除4,然后写入硬编码

比如当前要跳转到0x001360DC,减去当前PC除4

(0x001360DC - 0x001360C4) / 4 = 4

截图.png

2.BL imm

无条件跳转,并且将下一条指令的地址写入LR寄存器

截图.png

BL 0x1360E4,计算方法和B指令一样

截图.png

这里我们看对LR寄存器的写操作

截图.png

截图.png

这里把下一条指令的地址写入了LR寄存器,这里写入的值和当前的模式有关,如果当前模式为arm模式,就直接写入,如果是thumb模式,则会把这个地址 or 1,这里到后面还会提到

截图.png

3.BX reg

BX r0

截图.png

直接跳转到寄存器存储的地址处

BX是可以修改模式的,这里跳转的地址为0x10465,实际是0x10464 | 1,表示跳转的同时切换到thumb模式,相当于把1写入了T标志位

截图.png

截图.png

截图.png

4.BLX imm

BLX 0x10474

截图.png

和BL指令不同的是BL会根据地址的值判断是否进行模式切换,BLX则是直接切换,如果当前为arm,则切换为thumb,如果为thumb,就切换为arm,并且写入LR寄存器

截图.png

截图.png

BLX reg和BX指令类似,根据寄存器内的值(最后一位)判断是否进行模式切换,但是BLX会写入LR寄存器,BX不会

当调用函数返回时,如何判断返回处是thumb模式还是arm模式呢?

通过LR寄存器,LR寄存器存储的是先前的指令下一条指令地址,根据存放的地址最后一位,判断是否进行模式切换

上面是一种修改PC的方法

还有两种修改PC的方法,一种是直接写PC,另一种是使用访存指令

直接写PC

MOV PC,r0

这样是无法判断是否切换模式的,一般不用

访存指令

LDR PC,[r0]

和BX指令,类似,可以通过寄存器的值判断是否切换模式,通常使用这条指令来调用函数

thumb模式

thumb模式和arm模式最大的区别就是变长指令,指令分为双字节和四字节

编码格式比较复杂

thumb模式一般没有条件码(EQ...)和标志响应位

//运算指令优先对于第一,第二操作数相同情况会生成短指令编码

在thumb模式下,双字节的指令比四字节指令多,四字节指令大部分都会在指令后加上W表示四字节,但是B这种无条件跳转指令就不会

截图.png

thumb模式中,如果指令影响标志位,一般会被编为短指令

截图.png

如果第一操作数和第二操作数相同,则会合并前两个操作数,并且编为短指令

下面第一条指令是把R1和R2的合放到R0里,编为长指令

第二条指令把R2和R0的合放到R0里,由于前两个操作数相同,所以合并为一个,并且编为短指令

第三条指令和第一条指令的不同点是影响标志位,影响标志位会被编为短指令

截图.png

在thumb模式中,一般使用R0~R7寄存器,不使用R8~R12寄存器

这和指令编码有关

比如ADD指令

截图.png

可以看到,上面的指令中,及时ADDS指令影响标志位,但是因为使用了R8寄存器,还是编为了长指令

下面是ADD指令的格式

截图.png

截图.png

可以看到,短指令编码时,留给寄存器的位数是3位,最多有8种可能,所以只能是R0~R7,如果是R8~R12,只能使用长指令编码

IT指令

截图.png

这条指令是thumb模式中特有的指令,执行到IT语句时,根据IT后面的条件判断下面的指令是否执行,比如EQ判断Z标志位是否为1,如果为1,则执行MOV指令,如果为0,则不执行

截图.png

现在Z标志位为0,R0为0,执行IT语句,MOV指令应该不会执行

截图.png

这里直接跳到了MOV的下一条指令

截图.png

IT指令可以控制多条指令的执行,在IT后面加T或者E来实现

T表示与附加判断相同,E表示相反

截图.png

这里Z位为1,EQ条件成立,所以条件为T的指令都会执行,所以除了R1,其他寄存器都会被写入值(这里有一个问题,其实IT只控制了三条指令,MOVS r4,#4这条指令并不受IT控制,因为I后面跟的是TET,这里是我写的有问题)

截图.png

注意,无论后面的条件怎样设置,IT指令的下一条指令判断条件一定是T,和IT指令后的判断条件相同

还有,如果IT下面的指令为B指令,那么IT块影响的指令可能就会发生变化,可能就会变为跳转后地址的后续指令

IT指令的长度可以通过硬编码来判断

IT 1000

ITE 1100

ITTE 0110

ITEEE 1111

指令的长度根据最后一个1来判断,如果最后一个1位于最低位,就代表长度为4,位于第一位,就代表长度为1

指令后面为T或者E由最后一个1前面的值来决定

比如ITEEE -> 1111

最后一个1前面为111,代表后三个判断都为E,指令为IT EEE

比如ITTE 0110

最后一个1前面为01,代表TE ,指令为IT TE

调用约定

posted @ 2025-03-18 20:49  写在风中的信  阅读(42)  评论(0)    收藏  举报