ARM汇编

1.概述

1.相关知识和辨析

  • ARM是一个32位的RISC处理器架构(关于RISC架构我自己的笔记:MIPS汇编 – 碇シンジ (giraffexiu.love)
  • 采用的与x86 相比更为精简的指令集,相应的寄存器的个数会增加,运行速度更快,从而需要更加注意指令之间的关系和约束
  • ARM分为两种模式:Arm和Thumb
  • x86和x64 采用小端格式
  • ARM在v3之前采用小端模式后面采用大端模式,但是兼容小端模式

2.简述一下Arm和Thumb模式

1.概念以及规范区分

Arm模式(32位)和Thumb模式(16位也可以是32位)是Arm处理器的两个主要操作状态

  • Thumb-1(16位宽指令集):ARMv6以及更早期的版本中使用
  • Thumb-2(16位\32位宽指令集):在Thumb-1的基础上拓展了更多的指令集(ARMv6T2、ARMv7以及很多32位Android手机所支持的架构上使用)
  • Thumb-EE:包括一些改变以及对于动态生成代码的补充

2.简单辨析区分

  • Arm的常见指令是32位的,Thumb指令时16 位的模式
  • 程序可以在两种模式之间切换
  • Thumb指令集是基于Arm指令集的一个提高代码密度从而提高运行效率的指令集(类似于Arm指令集的压缩指令集,但仅支持通用的功能,必要时要基于Arm指令集才能运行)

2.Thumb和Arm模式

1.状态切换方式

lable为符号地址(字节对齐后最后一位为0)

1.ARM切换到Thumb状态

LDR R0, =label + 1

BX R0

2.从Thumb状态切换到ARM状态

LDR R0, = label

BX R0

2.批量寄存器加载和存储指令

  1. LDM和STM指令可以将R0-R7中的任何寄存器子集加载或者存储
  2. 除了R0-R7之外,push指令可以存储R14,pop可以加载pc寄存器
  • Arm的常见指令是32位的,Thumb指令时16 位的模式
  • 程序可以在两种模式之间切换
  • Thumb指令集是基于Arm指令集的一个提高代码密度从而提高运行效率的指令集(类似于Arm指令集的压缩指令集,但仅支持通用的功能,必要时要基于Arm指令集才能运行)

3.常见的具体的指令集差异

1.缺少协处理指令和信号量指令

  • 协处理指令:在ARM指令集中,有一些指令用于与协处理器(如浮点单元)进行数据交换和处理MCR(Move to Coprocessor from ARM Register)和MRC(Move to ARM Register from Coprocessor)
  • 信号量指令:信号量指令用于多线程编程中对共享资源的访问和同步LDREX(Load-Exclusive)和STREX(Store-Exclusive)

2.无法直接访问 CPSR、SPSR 寄存

在ARM指令集中,可以使用特定的指令来读取和修改当前程序状态寄存器(CPSR)和保存程序状态寄存器(SPSR)

MRS(Move to Register from Status)和MSR(Move to Status from Register)

3.缺少乘加指令和64位乘法指令

  • 乘加指令:在ARM指令集中,有一些指令可以同时进行乘法和累加操作MLA(Multiply and Accumulate)和MLS(Multiply and Subtract)
  • 64位乘法指令:在ARM指令集中,有一些指令专门用于执行64位整数的乘法操作UMULL(Unsigned Multiply Long)和SMULL(Signed Multiply Long)

4.指令第二操作数受限

  • 在Thumb指令集中,指令的第二个操作数可能会受到一些限制只能使用立即数、只能使用寄存器等

5.大多数指令为无条件执行

  • 在Thumb指令集中,大多数指令都是无条件执行的,这意味着它们会在遇到指令时立即执行,而不考虑条件码或状态位的设置ADD指令用于执行加法操作,无论条件是否满足,都会立即执行

6.Thumb 数据处理指令采用地址格式

  • Thumb指令集中的数据处理指令通常采用地址格式,这使得对内存或寄存器中的数据进行处理时更加简洁和高效MOV指令用于将数据从一个寄存器移动到另一个寄存器,其格式为MOV Rd, Rm,其中Rd表示目标寄存器,Rm表示源寄存器

3.数据类型

1.ARM汇编数据类型

数据类型:

arm汇编中扩展后缀-sh和-h对应半字、-sb和-b对应字节

无符号(有符号)的字(word)

半字(halfword)

字节(bytes)

相关指令:

ldr     @加载字
ldrh    @加载无符号半字
ldrsh   @加载半字
ldrb    @加载无符号字节
ldrsb   @加载字节
str     @存储字
strh    @存储无符号半字
strsh   @存储半字
strb    @存储无符号字节
strsb   @存储字节

2.字节序

1.简述序端原理

大端字节序(Big Endian):
  • 在大端字节序中,数据的高位字节存储在低地址处,低位字节存储在高地址处
  • 举例来说,对于一个32位整数0x12345678,其内存表示为:0x12 0x34 0x56 0x78
小端字节序(Little Endian):
  • 在小端字节序中,数据的低位字节存储在低地址处,高位字节存储在高地址处
  • 举例来说,对于同样的32位整数0x12345678,在小端字节序中其内存表示为:0x78 0x56 0x34 0x12

1.字节序在arm中的调用

ARM在v3之前采用小端模式后面采用大端模式,但是兼容小端模式

2.访问数据时采用大端序还是小端序由程序状态寄存器(CPSR)的第九个比特位来决定

  1. ARM架构中的程序状态寄存器(CPSR)的第九个比特位被称为E位。
  2. E位用于指示处理器采用的字节序是大端序还是小端序
  3. 当E位为0时,表示处理器采用大端序(Big Endian)
  4. 当E位为1时,表示处理器采用小端序(Little Endian)
  5. 字节序决定了数据在内存中的存储顺序,即高位字节和低位字节的存储顺序

3.寄存器

1.ARM寄存器简述

37个寄存器

1.重要的CPSR寄存器
  • 用于存储当前程序的执行状态和控制处理器的行为
  • 包含了一些关键的状态标志和控制位,如执行状态、中断使能、当前模式等
  • 通常不直接操作,除非进行异常处理或任务切换
31个通用寄存器:(均为32位的寄存器)
  • 未分组寄存器R0-R7
  • 分组寄存器R8-R14
  • 程序计数器( PC 指针)
6个状态寄存器:(均为32位寄存器)

用以标识 CPU 的工作状态及程序的运行状态

程序状态寄存器 CPSR

5个物理状态寄存器 SPSR (用以异常发生时保存 CPSR 的值,异常退出时恢复 CPSR )

2.37个寄存器具体

寄存器用户模式 (usr)系统模式 (sys)特权模式 (svc)中止模式 (abt)未定义指令模式 (und)外部中断模式 (irq)快速中断模式 (fiq)
R0R0R0R0R0R0R0R0
R1R1R1R1R1R1R1R1
R2R2R2R2R2R2R2R2
R3R3R3R3R3R3R3R3
R4R4R4R4R4R4R4R4
R5R5R5R5R5R5R5R5
R6R6R6R6R6R6R6R6
R7
R8R8R8R8R8R8 (fiq)
R9R9R9R9R9R9 (fiq)
R10R10R10R10R10R10 (fiq)
R11R11R11R11R11R11 (fiq)
R12R12R12R12R12R12 (fiq)
R13 (SP)R13R13R13_svcR13_abtR13_undR13_irqR13_fiq
R14 (LR)R14R14R14_svcR14_abtR14_undR14_irqR14_fiq
PC (R15)PCPCPCPCPCPCPC
CPSRCPSRCPSRCPSRCPSRCPSRCPSRCPSR
SPSRSPSR_svcSPSR_abtSPSR_undSPSR_irqSPSR_fiq

1.未分组的寄存器(R0-R7)

没有被系统用于特别的用途,因此任何可采用通用寄存器的应用场合都可以使用未分组寄存器

1.R0-R3
  • 用于传参数,更多的参数须通过栈来传递,调用函数的时候,参数先从R0依次传递
  • R0-R1也作为结果寄存器,保存函数返回结果,被调用的子程序在返回前无须恢复这些寄存器的内容
  • 2.R4-R6
  • 无特殊约束,即是普通寄存器
  • 为被调保存(callee-save)寄存器,一般保存函数的局部变量(local variables)
  • 被调保存寄存器(callee-save register)是指,如果这个寄存器被调用/使用之前,需要被保存

2.分组寄存器(R8-R14)

1.概述
每一次所访问的物理寄存器和处理器当前的运行模式有关

例如:

在快速中断模式 fiq下R8-R12访问寄存器 R8_fiq-R12_fiq ,而在其他模式下又访问 R8_usr-R12_usr

对于R13(SP)和R14(LR)这两个寄存器分析

1.构成

  • 每个寄存器对应着6个不同的物理寄存器
  • 物理寄存器根据不同的运行模式进行命名
  • 一个是用户模式(usr)与系统模式(sys)共用的
  • 另外的5个物理寄存器则分别对应于其他5种不同的运行模式

2.区分格式

R13_<mode>

代表R13(SP)在不同模式下的物理寄存器

<mode>为以下几种模式之一:usr、fiq、irq、svc、abt、und

R14_<mode>

代表R14(LR)在不同模式下的物理寄存器

<mode>为以下几种模式之一:usr、fiq、irq、svc、abt、und

1.R8,R10-R11

没有特殊的规定,就是普通寄存器

2.R9

是操作系统保留

3.R11 :帧指针(FP)

Arm模式下R11作为帧指针

Thumb模式下R7则作为帧指针

4.R13:栈指针寄存器(SP)

存放栈顶指针

简述ARM架构的函数调用和返回
  1. 寄存器使用:
    • R0-R3:这些寄存器用于传递函数调用的前四个参数
    • R4-R11:被称为局部寄存器,用于在函数内部保存局部变量
    • R12:通常用作Intra-Procedure-call scratch register
    • R13(SP, Stack Pointer):堆栈指针,用来管理函数的调用栈
    • R14(LR, Link Register):链接寄存器,用来保存函数返回地址
    • R15(PC, Program Counter):程序计数器,指向当前执行的指令地址
  2. 函数调用:
    • 当一个函数调用另一个函数时,调用者会将前四个参数放置在R0到R3这四个寄存器中。如果有更多的参数,这些参数将通过堆栈传递
    • 调用者(caller)还需要在调用之前将返回地址(即下一条指令的地址)存储到链接寄存器(LR,R14)中
    • 使用BL(Branch with Link)指令来实现函数调用。BL指令将当前PC值加上指令中指定的偏移量存入链接寄存器LR中,并将PC设置为目标函数的起始地址
  3. 函数执行:
    • 在函数开始时,通常会有入栈操作,保存所有需要保存的寄存器(如被调用者寄存器R4-R11)和其他必需的状态
    • 执行函数体中的代码
    • 函数可以利用R0寄存器来返回一个值
  4. 函数返回:
    • 函数完成后,需要恢复在函数入口保存的寄存器
    • 函数通过BX LR指令(Branch and Exchange to Link Register)来返回,这条指令会使处理器的程序计数器跳转到链接寄存器LR中存储的返回地址
2.R13 的作用

由于处理器的每种运行模式均有属于自己的物理寄存器R13,使其指向该运行模式下的栈空间

这样,当程序的运行进入异常模式时,可以将需要保护的寄存器放入R13所指向的堆栈

而当程序从异常模式返回时,则从对应的堆栈恢复,采用这种方式可以保证异常发生后程序的正常执行

5.R14:链接寄存器(LR)
1.概述
  • 当一个子程序被调用时,LR 会被填入程序计数器(PC)
  • 当一个子程序执行完毕后,PC从 LR 的值恢复,从而返回(到主函数中)
2.实例
0x00008d68 <+44>:    bl  0x8cd4 <func>
0x00008d6c <+48>:   ...
0x00008d70 <+52>:   ...123
  • 在执行 bl 指令时,r14 存储了返回到调用者的地址(在这个案例中是指令 0x00008d6c 的地址)
  • 被调用的函数 <func> 在执行结束后,通常会使用 BX LR 或者类似的指令
  • 这条指令会使得程序计数器(PC)跳转到 LR(即 r14)中存储的地址,从而实现了函数的而返回

3.R15:程序计数器(pc寄存器指向取值阶段的指令,即是当前执行阶段指令的后两条指令)

1.作用概述
  1. 指令地址跟踪:
    • PC 存储着当前执行指令的地址或下一条要执行指令的地址。这使得处理器知道从哪里读取指令,并且在执行完一条指令后能够自动加载下一条指令
  2. 控制流管理:
    • 通过修改 PC 中的值,程序可以实现跳转、循环、条件执行等控制流操作。在执行如 branch 指令时,目标地址直接写入 PC,从而改变程序的执行流

这里涉及到一个重要的arm架构下的概念-流水线机制

2.流水线机制
  • ARM处理器采用了流水线(Pipeline)技术,用以提高处理器的指令执行速率和整体效率
  • 流水线技术允许在一个时钟周期内同时执行多个指令的不同阶段,从而在每个时钟周期完成一条指令的执行
3.基本步骤:

分为两种情况

1.v9:五级
  1. 取指阶段(IF,Instruction Fetch)
    • CPU从内存中读取下一条要执行的指令
  2. 译码阶段(ID,Instruction Decode)
    • 译码器解析取出的指令,确定指令类型、所需操作数以及执行所需的操作
  3. 执行阶段(EX,Execute)
    • 在这个阶段,执行实际的计算或逻辑操作
  4. 访存阶段(MEM,Memory Access)
    • 如果指令需要读取或写入数据到内存,这一阶段将完成这个工作。例如,加载(load)和存储(store)指令
  5. 写回阶段(WB,Write Back)
    • 最后,将执行结果写回到寄存器或者更新程序状态
2.v7:三级
  1. 取指阶段(IF,Instruction Fetch)CPU从内存中读取下一条要执行的指令
  2. 译码阶段(ID,Instruction Decode)译码器解析取出的指令,确定指令类型、所需操作数以及执行所需的操作
  3. 执行阶段(EX,Execute)完成指令要求的操作,并根据需要将结果写回寄存器。指令占用数据路径,寄存器堆被读取,操作数在桶行移位器中被移位运算器产生运算结果并回写到目的寄存器中,运算器根据指令需求和运输结果更改状态寄存器的条件位
总结:
这里就是说将一条指令的执行分为3个部分,在程序进程加载后,三条连续的指令会同时进行这三个部分的一个(错位进行)
Screenshot_2024-05-12-19-12-30-99_c9ddfdcc587d441
4.r14 和r15 调用的详细分析
  • 在程序进行函数的调用和跳转指令的过程中程序计数器将会被更新为新的地址
  • 函数的调用和程序的进程的跳转是通过r14和r15 实现
1.调用和跳转分为两个分支(B分支和BL分支)
  1. B 分支(Branch)无条件的跳转到另一个进程中,他将目标地址的计算结果直接写入程序计数器R15中例如:B label会跳转到标签 label 指示的地址处执行
  2. BL分支(Branch with Link)这种指令在填的过程中会无条件的跳转到新的执行进程中,在将当前的函数目标地址的计算结果写入到R15 的前提下并且将返回的地址保存到r14中(它允许函数在调用结束后,返回调用点)例如:BL function会跳转到 function 开始处执行,并且将跳转指令后面的第一条指令的地址保存在 LR(R14)
2.概念解析(函数的目标地址的计算结果)

这里学过pwn的应该很清楚

程序运行的过程中函数的地址存在偏移的情况

简单举个例子进行说明:

B label//假设指令位于地址 `0x1000`,并且指令中的偏移量是 `0x4

当前的PC值是0x100+(0x4*4)=0x1010

解析:

  • 在thumb下,ARM架构下的pc通常会指向当前地址的下两个指令位置(即加4个字节),但是这里的+4个字节主要用于内部操作并不会会影响当前真实地址的计算,但实际会存储到pc寄存器(因为这里的thumb是2个字节一条指令)
  • 在arm下,ARM架构下的pc通常会指向当前地址的下两个指令位置(即加8个字节),但是这里的+8个字节主要用于内部操作并不会会影响当前真实地址的计算,但实际会存储到pc寄存器中(因为这里的arm是4个字节一条指令)
  • 这里的偏移值是0x4,但是ARM的内存是按照4个字节对齐的所以将0x4 左移 2位(即是*4)这里差点被摆了一道,这个地方的偏移量是指令数偏移量,所以这里乘4 对齐就不难理解了,ARM架构下的一个指令是4个字节,所以4个指令数就是16个字节)

4.SP,SF(这里建议先学习arm的栈帧,再来看)

1.栈指针(SP)

  • 栈指针是指向当前栈顶的指针,它随着栈的操作而动态变化
  • 在函数调用时,栈指针通常会向下移动,分配新的栈帧空间;在函数返回时,栈指针会向上移动,释放栈帧空间

2.栈帧(Stack Frame):

  • 帧是函数调用期间在栈上分配的一块内存区域,用于存储函数的局部变量、参数、返回地址以及临时数据
  • 栈帧的边界由栈指针(SP)和帧指针(Frame Pointer,FP)来限定。FP 指向栈帧的底部,SP 指向栈顶

3.在函数调用时:

  • 当一个函数被调用时,当前函数的执行上下文(如 PC、LR、SP、FP)会被压入栈中,其中包括当前栈指针的值
  • 栈指针(SP)会向下移动,分配新的栈帧空间,以便存储被调用函数的局部变量和参数

4.在函数返回时:

  • 当函数执行完毕后,会将当前函数的执行上下文从栈中弹出,包括栈指针(SP)的值
  • 栈指针(SP)会向上移动,释放当前函数的栈帧空间,恢复到上一个函数的栈帧

5.状态寄存器R16

4.ARM的栈帧(基本概述)

1.基本概述

这里栈的基本结构和规范跟x86类似:

  1. 后进先出的结构(后入栈的先去除,先入栈的后取出)
  2. 栈底是第一个数据进栈的地址,栈顶是最后一个数据进入的位置
  3. 数据结构(和x86基本相同):来年表,堆栈,树,哈希表等

2.sp的指针(满栈指针和空栈指针)

1.满栈

堆栈指针总是指向栈顶(即是sp指向堆栈最后一个数据项位置

满堆栈的关键是最后一个已使用的地址

2.空栈

堆栈指针总是指向下一个将要放入数据的空位置(即是p指向堆栈最后一个数据项的下一个位置

空堆栈的关键是第一个没有使用的地址

3.结构图辨析

根据arm的相关的书籍的数据:直观的辨析

image-20240515224448357

ARM是满栈结构

3.栈的升降

ARM的栈的底端是高地址,顶端是低地址

1.升栈

数据入栈,sp指针从低地址想高地址移动

image-20240516005633466

2.降栈

数据入栈,sp指针从高地址向低地址移动

image-20240516005642668

5.ARM的栈帧(详细结构)

大致结构如下

image-20240516011601515

1.概念解析

  • main stack frame为调用函数的栈帧
  • func1 stack frame为当前函数(被调用者)的栈帧
  • 栈的底端在高地址(即这个示意图是倒着的)
  • FP就是栈基址,指向函数的栈帧起始地址
  • SP则是函数的栈指针,它指向栈顶的位置
  • 调用另外一个函数前,临时变量区要保存另一个函数的参数
  • ARM的压栈顺序:
    1. 当前函数指针PC
    2. 返回指针LR
    3. 栈指针SP
    4. 栈基址FP
    5. 传入参数个数及指针
    6. 本地变量和临时变量

2.栈帧的规范

1.栈帧定义

  • 栈帧指一个函数所使用的那部分栈
  • 所有函数的栈帧串起来就组成了一个完整的栈

2.栈帧的边界

栈帧的两个边界分别由fp(r11)和sp(r13)来限定

fp(r11)栈帧指针,栈帧上边界由fp指针界定

fp(r13)栈帧指针,栈帧下边界由sp指针界定

main函数的上边界和下边界保存在被它调用的栈帧里面

3.栈的作用(这里跟x86类似,简析一些不同即可)

1.保存局部变量

 mov ip,sp        //保存sp到ip
stmdb sp!,{fp,ip,lr,pc} /*先对sp-4,再对fp,ip,lr,pc压栈*/
//sp=sp-4;push {pc};sp=pc; /*先压pc*/
//sp=sp-4;push {lr};sp=lr; /*压lr*/
//sp=sp-4;push {ip};sp=ip; /*压ip*/
//sp=sp-4;push {fp};sp=fp; /*压fp*/
sub fp,ip,#4 //fp指向ip-4
sub sp,sp,#4 //开辟一块空间

ldr r3,[fp,#-16] //临时存放在[fp-16]
add r3,r3,#1
str r3,[fp,#-16]

2.参数传递

参数大于4个的时候,多出来的参数用栈传递

#include <stdio.h>

void func(int a,int b,int c,int d,int e,int f)
{
int k;
int l;
k=e+f;
l=a+b;
}

int main()
{
func(1,2,3,4,5,6);
return 0;
}

3.保存寄存器的值

#include <stdio.h>

void func2(int a,int b)
{
int k;
k=a+b;
}

void func1(int a,int b)
{
int c;
func2(3,4);
}

int main()
{
func1(1,2);
return 0;
}

如果栈不适用,r0和r1的值会被覆盖掉

6.ARM指令集模板

1.模板

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

2.简析

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

3.满足上述模板的指令集

指令含义指令含义
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系统调用

7.内存访问指令(寻址指令)

1.概述

1.概念

arm使用加载-存储模式对内存进行访问(即是只有加载和存储指令能够访问内存)

LDR 将内存中的值加载到寄存器中,STR 将寄存器内的值存储到内存地址

实现了指令集的简化、指令执行效率的提高和流水线设计的优化

2.示例:

LDR R0, [R1]    ; 将内存地址R1处的数据加载到寄存器R0中
ADD R0, R0, #1 ; 将寄存器R0中的数据加1
STR R0, [R1] ; 将寄存器R0中的数据存回内存地址R1处

网上找到一个生动的gif演示

动图

拓展:指令分类

1.算数和逻辑指令

只在寄存器之间操作:

寄存器加法,乘法等

2.加载和存储指令

这些指令用于在内存和寄存器之间传递数据

将内存中的数据加载到寄存器中,或者将寄存器的数据存储到内存中

2.三种偏移形式

1.立即数偏移

1.概念
1.立即数

一个指定的偏移量数据

(一个8位或12位的二进制数,有时也会在指令中直接指定)

2.基址寄存器

存放基地址的通用寄存器

3.偏移地址

立即数+基地址,实际要访问的而地址

2.示例

立即数偏移通常用于指令的第二个操作数

LDR R0, [R1, #4]  ; 将R1寄存器中的值加上4,得到的地址作为内存地址,然后将对应内存中的值加载到R0中

[R1, #4]就是立即数偏移,R1是基址寄存器,#4是立即数偏移量

动图
3.深入理解ARM中的立即数规范
1.概述

ARM对立即数存在很多限制

ARM指令的构造:(ARM指令长度是32位,所有指令都是可条件执行指令)

  • 16种条件码(占2*4=16位)
  • 2位代指目标寄存器
  • 2位代指操作寄存器
  • 1位作为状态标志
  • 其他操作码占用的位

就只剩下12位用来执行操作立即数,最多只能表示4096

所以MOV指令只能操作一定范围内的立即数,如果不能直接被调用,就必须被分割成多个部分,用众多小数字拼起来

12位的分区:

  • 8位用来表示0-255范围的数n
  • 4位表示旋转循环右移(其实ARM中只有一种位移,就是旋转循环右移,左移也是通过旋转循环右移得到)的次数r(范围0-30)立即数的表示形式是:v = n ror 2*r只能以偶数进行旋转循环右移,一次移动两位,n组成的有效位图必须能放到一个字节(8位)中
2.有效和无效的立即数
Valid values:
256 // 1 ror 24 --> 256 循环右移12次,每次两位(注意数据是32位长度)
384 // 6 ror 26 --> 384 循环右移13次,每次两位
484 // 121 ror 30 --> 484
16384 // 1 ror 18 --> 16384
2030043136 // 121 ror 8 --> 2030043136
0x06000000 // 6 ror 8 --> 100663296 (0x06000000 in hex)

Invalid values:
370 // 185 ror 31 --> 循环右移31位,但超出了(0 – 30)范围,因此不是有效立即数。
511 // 1 1111 1111 --> 有效位图无法放到一个字节(8位)中。
0x06010000 // 110 0000 0001.. --> 有效位图无法放到一个字节(8位)中
  • 以上立即数都是32位长度
  • 旋转循环右移:每位都向右移动,末位不断放到最前位,类似首尾相连
  • 有效位图要能放到一个字节中:例子:
511的二进制为0000 0000 0000 0000 0000 0001 1111 1111,有效位图为1 1111 1111,超过一个字节

0x06010000的二进制位‭0110 0000 0001 0000 0000 0000 0000‬,有效位图110 0000 0001超过一个字节
3.无法一次加载完整的 32 位地址,两种方法进行绕过:
  1. 用较小的值构造较大的值不要使用 MOV r0, #511分成两部分: MOV r0, #256ADD r0, #255
  2. 使用加载方式“ldr r1, =value”,编译器将其转换位MOV指令,或者是PC相对寻址来加载`LDR r1, = 511
4.加载了无效立即数的处理方式
.section .text
.global _start

_start:
mov r0, #511
bkpt
  • 示例:.section .text
    .global _start

    _start:
    mov r0, #511
    bkpt
  • 处理:把511拆成几个小数值,然后利用LDR
.section .text
.global _start

_start:
mov r0, #256 /* 1 ror 24 = 256, so it's valid */
add r0, #255 /* 255 ror 0 = 255, valid. r0 = 256 + 255 = 511 */
ldr r1, =511 /* load 511 from the literal pool using LDR */
bkpt
5.总结简述
  1. ARM架构下,立即数的使用受到限制,直接加载32位立即数到寄存器可能会受限
  2. 有效的立即数值需要满足条件:
    • 可通过循环右移操作得到,右移的位数要在0到30之间
    • 可通过加载0到255之间的任意值得到
  3. 对于超出范围的立即数值,可以采取以下方法加载:
    • 将其拆分成小部分,然后分别加载
    • 使用加载指令(如LDR)从数据段中加载立即数值

2.寄存器偏移

1.概念
1.基址寄存器

存放基地址的通用寄存器

2.偏移寄存器

存放偏移量的通用寄存器

3.偏移的地址

基址寄存器+偏移寄存器,实际要访问的而地址

2.示例
LDR R0, [R1, R2]  ; 将R1寄存器中的值和R2寄存器中的值相加,得到的地址作为内存地址,然后将对应内存中的值加载到R0中

[R1, R2]就是寄存器偏移,R1是基址寄存器,R2是偏移寄存器

3.辨析

这里可以注意到和前面的立即数偏移很像,但是这里的偏移是放在寄存器当中的与立即数不同

这里的偏移是可已更改的,从而可以处理动态变化的计算情况

3.缩放寄存器作为偏移

1.概述

在arm中出了可以将寄存器中的值作为偏移量,话可以将寄存器中的值及进行缩放后作为偏移量

通常用于处理数组或结构体等数据结构的访问,其中偏移量需要乘以某个固定的倍数

2.示例
1.概述

当处理数组或结构体等数据结构时,常常需要根据元素的大小和排列方式来计算偏移量,以便正确访问每个元素

一个数组,每个元素的大小为4个字节(32位)

使用缩放寄存器作为偏移量来访问数组的不同元素

2.模拟示例

假一个32位整数数组,我们想要访问其中的第三个元素

我们知道每个元素占据4个字节

因此我们需要将偏移量设置为第三个元素的偏移量(数组的索索引值是从0开始计算的,所以这里的第三个元素的索引值是2)

即2乘以每个元素的大小

以下是一个示例代码:

.data
my_array:
.word 10 ; 第一个元素
.word 20 ; 第二个元素
.word 30 ; 第三个元素
.word 40 ; 第四个元素

.text
.global _start
_start:
LDR R0, =my_array ; 加载数组的基址
MOV R1, #2 ; 设置要访问的元素的索引,这里是第三个元素
LDR R2, [R0, R1, LSL #2] ; 使用寄存器R1作为偏移寄存器,左移两位(相当于乘以4),然后将对应内存中的值加载到R2中

; 此时R2中应该包含数组中的第三个元素的值,即30

使用LSL #2将寄存器R1`中的值左移了两位,这样就相当于将其乘以4(因为4个字节的大小)

将得到的结果作为偏移量(索引值乘以元素的大小),从数组的基址处开始访问,得到第三个元素的值

4.总结

1.三种偏移形式
  • 立即数作为偏移量ldr r3, [r1, #4]
  • 寄存器作为偏移量ldr r3, [r1, r2]
  • 带有位移操作的寄存器作为偏移量ldr r3, [r1, r2, LSL#2]
2.LDR和STR的寻址模式
1.存在”!”,前变址寻址
ldr r3, [r1, #4]!
ldr r3, [r1, r2]!
ldr r3, [r1, r2, LSL#2]!
2.基地址包裹在括号中(R1),前变址寻址
ldr r3, [r1], #4
ldr r3, [r1], r2
ldr r3, [r1], r2, LSL#2
3.其余基本存在偏移的寄存器的间接寻址
ldr r3, [r1, #4]
ldr r3, [r1, r2]
ldr r3, [r1, r2, LSL#2]
3.LDR中的PC相对寻址
1.概述

PC 相对寻址就是相对于当前指令的地址来计算内存地址

=符号表示将要加载的值的地址,而不是值本身

当使用LDR指令加载一个常量时,汇编器会计算该常量在代码段(text section)中的偏移量,并将其作为一个立即数编码到指令中

2.示例代码:
.section .text
.global _start

_start:
ldr r0, =jump /* 将标签jump的地址加载到R0 */
ldr r1, =0x68DB00AD /* 将值0x68DB00AD加载到R1 */
jump:
ldr r2, =511 /* 将值511加载到R2 */
bkpt
  1. ldr r0, =jump:这条指令将标签jump的地址加载到寄存器r0中。汇编器会将jump标签在代码段中的偏移量编码为一个立即数,并将其加载到r0
  2. ldr r1, =0x68DB00AD:这条指令将值0x68DB00AD加载到寄存器r1中。由于这是一个立即数,汇编器会直接将其编码到指令中
  3. ldr r2, =511:这条指令将值511加载到寄存器r2中,汇编器会将立即数编码到指令中
3.伪指令

ldr r0, =jumpldr r1, =0x68DB00ADldr r2, =511 这些指令被称为伪指令

它们不是直接从内存中加载数据到寄存器,而是在汇编阶段由汇编器处理的

根据上面具体来说:

  • ldr r0, =jump:指令告诉汇编器将标签 jump 的地址加载到寄存器 r0 中在汇编时,汇编器会将 jump 标签在代码段中的地址计算出来,并将地址作为立即数编码到指令中
  • ldr r1, =0x68DB00AD:指令直接将立即数 0x68DB00AD 加载到寄存器 r1 中因为这是一个立即数,所以汇编器会直接将这个数编码到指令中
  • ldr r2, =511:指令将立即数 511 加载到寄存器 r2

3.跳转指令BL/BLX偏移值计算规则

1.ARM4字节对齐

1.规范:
偏移=( 跳转地址-(指令地址+8) )/4
2.解析
  • 指令地址 + 8:因为ARM的流水线使得指令执行到当前指令处时,PC实际的值是A+8
  • 跳转指令 – 上一步得到地址:得到跳转指令与当前PC处的差值
  • ÷4:因为ARM的指令是4对齐的,即最低两位为00,于是将这个值右移两位
3.执行流程
  1. 取出偏移
  2. 左移两位
  3. 加入PC

(这时PC的值刚好为目标处的地址值,即目标地址指令进入取值,流水线前两级被清空)

2.thumb2指令

1.向后跳转
1.示例
0012 00F001F8 bl .Lhelo

.Lhelo:

0018 05F0D1F7 pld [r1, r5]
2.解析
  • 取高位 f000, 取后11位 => 000
  • 取低位 f801, 取后11位 => 001
  • 计算: (000 << 12) | (001 << 1) = 2

(由于这个最高位符号位为0. 代表向后跳转, 只需要保留该值2即可)

计算得到的目标地址为 : 0x0012 + 4 + 2 = 0x0018

2.向前跳转
1.示例
00001164 FF F7 BE FF BL _Z4testv

_Z4testv

000010E4 07 B5 PUSH {R0-R2,LR}
2.解析
  • 取高位 f7ff, 取后11位 => 7ff
  • 取低位 ffbe, 取后11位 => 7be
  • 计算: (7ff << 12) | (7be << 1) = 7fff7c

(由于这个最高位符号位为1 代表向前跳转, 需要-1然后取反 得到值为 ff800084,取84即可)

然后计算得到的目标地址为 : 0x1164 + 4 - 0x84 = 0x10e4

3.BL机器码的逆向实现

offset = dstAddr - srcAddr;


offset = (offset -4) & 0x007fffff;


high = offset >> 12;

low = (offset & 0x00000fff) >> 1;


machineCode = ((0xFF00 | low) << 16) | (0xF000 | high);

4.BLX机器码的逆向实现

offset = dstAddr - srcAddr;

offset = (offset -4) & 0x007fffff;


high = offset >> 12;

low = (offset & 0x00000fff) >> 1;


if(low%2 != 0) {

low++;

}

machineCode = ((0xEF00 | low) << 16) | (0xF000 | high);

8.连续存取

1.示例

.data
array_buff:
.word 0x00000000 /* array_buff[0] */
.word 0x00000000 /* array_buff[1] */
.word 0x00000000 /* array_buff[2]. 这一项存的是指向array_buff+8的指针 */
.word 0x00000000 /* array_buff[3] */
.word 0x00000000 /* array_buff[4] */
.text
.global main
main:
adr r0, words+12 /* words[3]的地址 -> r0 ,adr指令用于将基于PC相对偏移的地址值读取到寄存器中,ldr与adr功能一致,区别在于ldr主要用于远端的地址*/
ldr r1, array_buff_bridge /* array_buff[0]的地址 -> r1 */
ldr r2, array_buff_bridge+4 /* array_buff[2]的地址 -> r2 */
ldm r0, {r4,r5} /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
ldmib r0, {r4-r6} /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
bx lr
words:
.word 0x00000000 /* words[0] */
.word 0x00000001 /* words[1] */
.word 0x00000002 /* words[2] */
.word 0x00000003 /* words[3] */
.word 0x00000004 /* words[4] */
.word 0x00000005 /* words[5] */
.word 0x00000006 /* words[6] */
array_buff_bridge:
.word array_buff /* array_buff的地址*/
.word array_buff+8 /* array_buff[2]的地址 */
  • .word标识用于对内存中长度为32位的数据块做引用:程序中由.data段组成的数据,在内存中会申请一个长度为5的4字节数组array_buff
  • ldm用于加载,stm用于存储:将从某个地址开始连续读取n个字节的数据或向某个地址连续写入n个字节的数据

多种形式的idm和stm

IA(increase after)
IB(increase before)
DA(decrease after)
DB(decrease before)

2.LDMIA/STMDB

1.概述:

在ARM汇编中,一些指令(如LDMIASTMDB)允许在连续的内存地址范围内进行多个数据的读取或存储操作,这些操作通常用于处理数组、结构体等数据结构

2.LDMIA指令(与pop类似)

1.功能

LDMIA指令用于从内存中连续读取多个数据,并存储到一组寄存器中。在这个过程中,地址会递增

2.原理

LDMIA指令会从指定的内存地址开始读取数据,然后将这些数据存储到一组寄存器中

每次读取一个数据后,地址会递增,因此在连续读取的过程中,可以按照递增的顺序依次将数据加载到寄存器中

3.辨析

LDMIA是递增地址的,而POP是从栈顶弹出数据

3.STMDB指令(与push类似)

1.功能

STMDB指令用于将一组寄存器中的数据连续存储到内存中,并在存储完成后递减地址

2.原理

STMDB指令会将一组寄存器中的数据依次存储到指定的内存地址中

每次存储一个数据后,地址会递减,因此在连续存储的过程中,可以按照递减的顺序依次将数据存储到内存中

3.辨析

STMDB是递减地址的,而PUSH是向栈中推入数据

9.条件执行

1.示例

.global main
main:
mov r0, #2 /* 初始化值 */
cmp r0, #3 /* 将R0和3相比做差,负数产生则N位置1 */
addlt r0, r0, #1 /* 如果小于等于3,则R0加一 */
cmp r0, #3 /* 将R0和3相比做差,零结果产生则Z位置一,N位置恢复为0 */
addlt r0, r0, #1 /* 如果小于等于3,则R0加一*/
bx lr

2.Thumb模式下的条件执行

1.基本指令

1.格式
Syntax: IT{x{y{z}}} cond
2.参数
  • cond:代表IT指令后第一条执行指令需要满足的条件
  • x:代表第二条条件执行的指令要满足的条件逻辑相同还是相反
  • y:代表第三条条件执行的指令要满足的条件逻辑相同还是相反
  • z:代表第四条条件执行的指令要满足的条件逻辑相同还是相反

2.IT指令

1.概述

含义是if-then-(else)

  • IT:if-then,接下来的一条指令条件执行
  • ITT:if-then-then,接下来的两条指令条件执行
  • ITTE:if-then-then-else,接下来的三条指令条件执行
  • ITTEE:if-then-then-else-else,接下来的四条指令条件执行
2.执行规范
1.概述

在IT块中的每一条条件指令必须是相同的逻辑条件或相反的逻辑条件

2.示例

ITE指令,第一条和第二条指令必须使用相同的逻辑条件,而第三条必须是和前两条逻辑上相反的条件

ITTE   NE           ; 后三条指令条件执行
ANDNE R0, R0, R1 ; ANDNE不更新条件执行相关flags
ADDSNE R2, R2, #1 ; ADDSNE更新条件执行相关flags
MOVEQ R2, R3 ; 条件执行的move
ITE GT ; 后两条指令条件执行
ADDGT R1, R0, #55 ; GT条件满足时执行加
ADDLE R1, R0, #48 ; GT条件不满足时执行加
ITTEE EQ ; 后两条指令条件执行
MOVEQ R0, R1 ; 条件执行MOV
ADDEQ R2, R2, #10 ; 条件执行ADD
ANDNE R3, R3, #1 ; 条件执行AND
BNE.W dloop ; 分支指令只能在IT块的最后一条指令中使用
3.错误格式
IT     NE           ; 下一条指令条件执行
ADD R0, R0, R1 ; 格式错误:没有条件指令

3.逻辑关系

指令逻辑相反
EQNE
HS(or CS)LO(or CC)
MIPL
VSVC
HILS
GELT
GTLE

4.在arm和Thunb模式切换的条件执行

1.示例

.syntax unified    @ 这很重要!
.text
.global _start
_start:
.code 32
add r3, pc, #1 @ R3=pc+1
bx r3 @ 分支跳转到R3并且切换到Thumb模式下(因为R3此时最低比特位为1)
.code 16 @ Thumb模式
cmp r0, #10
ite eq @ if R0 == 10
addeq r1, #2 @ then R1 = R1 + 2
addne r1, #3 @ else R1 = R1 + 3
bkpt

2.解析

1.ARM和Thumb模式切换:
  • .code 32指令告诉汇编器接下来的指令将使用ARM指令集,然后使用add r3, pc, #1指令将当前程序计数器(PC)的值加上1,并存储到寄存器r3中,以便切换到Thumb模式执行
  • bx r3指令用于跳转到寄存器r3所指示的地址,并切换到Thumb模式执行
2.Thumb模式:(条件执行)
  • .code 16指令指定接下来的指令将使用Thumb指令集,然后使用cmp r0, #10指令将寄存器r0的值与立即数10进行比较
  • ite eq条件执行指令表示如果前面的比较结果等于,则执行下一条指令,否则执行跳过下一条指令
  • 如果比较结果等于,则执行addeq r1, #2指令将立即数2加到寄存器r1中;如果不等于,则执行addne r1, #3指令将立即数3加到寄存器r1中
  • bkpt指令用于产生断点异常,用于调试或中断程序的执行

10.分支指令

1.概述

分支指令(分支跳转)允许在代码中跳转到别的段,比如要跳到某个函数执行或者跳过一段代码块。常用于条件跳转和循环语句

2.示例

1.if-else

汇编实现

@ 条件分支
.global main
main:
mov r1, #2 @ 初始化 a
mov r2, #3 @ 初始化 b
cmp r1, r2 @ 比较谁更大些
blt r1_lower @ 如果R2更大跳转到r1_lower
mov r0, r1 @ 如果分支跳转没有发生,将R1的值放到到R0
b end @ 跳转到结束
r1_lower:
mov r0, r2 @ 将R2的值放到R0
b end @ 跳转到结束
end:
bx lr @ lr是链接寄存器,此处相当于main函数执行完后返回

c代码实现

int a(2), b(3);
if(a>b)
return a;
else
return b;

2.whiile循环

汇编实现

.global main
main:
mov r0, #0 @ 初始化 a
loop:
cmp r0, #4 @ 检查 a==4
beq end @ 如果是则结束
add r0, r0, #1 @ 如果不是则加1
b loop @ 重复循环
end:
bx lr

c代码实现

int a(0);
while(a < 4)
a += 1;

3.分支跳转相关指令

  • B:Branch,简单的跳转到一个函数
  • BL:Branch link,将下一条指令的入口PC-4保存到LR,然后跳转到函数
  • BX、BLX:同B/BL,只是外加了执行模式的切换,且此处需要寄存器作为第一操作数
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇