Arm64体系结构-- 指令集

adtxl
2022-09-01 / 0 评论 / 608 阅读 / 正在检测是否收录...

在kernel开发以及debug过程中,难免接触到汇编命令,这里仅记录一些常用的,基于arm64的汇编命令。

1. A64常用寄存器

x0-x30 64bit 通用寄存器,有只用低 32bit的w0-w30

FP(x29) 64bit 栈底指针

LR(x30) 64bit 通常称x30为程序链接寄存器,保存跳转返回信息地址

XZR 64bit Zero寄存器,写入此寄存器的数据被忽略,读出的数据全为0

WZR 32bit Zero寄存器的32bit形式

ELR_ELx 64bit 异常链接寄存器,保存异常进入ELx的异常地址(x={1,2,3})

SP_ELx 64bit 栈指针, 保存进入ELx的栈地址(x={0,1,2,3})

PC 64bit 当前程序运行的指针

SPSR_ELx 32bit 保存的处理状态寄存器(x={1,2,3}),用于存放程序运行中一些状态标识。

状态寄存器又分为 The Current Program Status Register (CPSR) 和The Saved Program Status Registers (SPSRs)。 一般都是使用CPSR,当发生异常时, CPSR会存入SPSR。当异常恢复,再拷贝回CPSR。

这里的ELx (x={0,1,2,3}) 是异常等级(exception level),每个程序能跑到不同级别,最低的是EL0最高的是EL3,这里不具体展开说明。

x0-x7: 用于子程序调用时的参数传递,x0还用于返回值传递。

2. A64指令集--加载与存储指令

常见的内存加载指令是LDR指令,存储指令是STR指令。

LDR 目标寄存器, <存储器地址>        // 把存储器地址中的数据加载到目标寄存器中
STR 源寄存器, <存储器地址>        // 把源寄存器的数据存储到存储器中

在A64指令集中,加载和存储指令有多种寻址模式

1.基地址模式

基地址模式首先使用寄存器的值来表示一个地址,然后把这个内存地址的内容加载到通用寄存器中。基地址加偏移量模式是指在这个基地址的基础上再加上偏移量,从而计算内存地址,并且把这个内存地址的值加载到通用寄存器中。偏移量可以是正数,也可以是负数。

LDR Xt, [Xn]    // 以Xn寄存器中的内容作为内存地址,加载此内存地址的内容到Xt寄存器中。
STR Xt, [Xn]    // 把Xt寄存器中的内容存储到Xn寄存器上的内存地址中
LDR Xt, [Xn, #offset]    // 把Xn寄存器中内容加一个偏移量(offset必须是8的倍数),以相加的结果作为内存地址,加载此内存地址的内容到Xt寄存器
STR Xt, [Xn, #offfset]    // 把Xt寄存器的值存储到以Xn寄存器的值加一个偏移量表示的内存地址中

2.变基模式

变基模式主要有如下两种。

前变基模式:先更新偏移量地址,后访问内存地址
后变基模式:先访问内存地址,后更新偏移量地址

前变基模式的指令格式如下:
首先,更新Xn/SP寄存器的值为Xn/SP寄存器的值加simm。然后,以新的Xn/SP寄存器的值为内存地址,加载该内存地址的值到Xt寄存器,
simm:表示偏移量,带符号的立即数,取值范围为-256~255.

LDR <Xt>, [<Xn|SP>, #simm]!

后变基模式的指令格式如下:
首先以Xn/SP寄存器的值为内存地址,加载该内存地址的值到Xt寄存器,然后更新Xn寄存器的值为Xn/SP寄存器的值加simm.

LDR <Xt>, [<Xn|SP>], #<simm>

3.PC相对地址模式

汇编代码里常常会使用标签(label)来标记代码片段。LDR指令还提供一种访问标签的地址模式,指令格式如下。

LDR <Xt>, <label>

这条指令读取label所在内存地址的内容到Xt寄存器中。但是这个label必须在当前PC地址前后1MB的范围内,若超出这个范围,汇编器会报错。

4.LDR伪指令

伪指令是对汇编器发出的命令,它在源程序汇编期间由汇编器处理。伪指令可以完成选择处理器、定义程序模式、定义数据、分配存储区、指示程序结束等功能。总之,伪指令可以分解成几条指令的集合。

LDR指令既可以是在大范围内加载地址的伪指令,也可以是普通的内存访问指令。当它的第二个参数前面有“=”时,表示伪指令;否则,表示普通的内存访问指令。注意,GNU汇编器没有对应的STR伪指令。

LDR伪指令的格式如下:

LDR Xt, =<label>    // 把label标记的地址加载到Xt寄存器

一个简单总结:

// 前变基模式。先更新X1寄存器的值为X1寄存器的值加8,然后以新的值为内存地址,加载该内存地址的值到X0寄存器
LDR X0, [X1, #8]!

方括号([])表示从该内存地址中读取或者存储数据,而指令中的感叹号(!)表示是否更新存放内存地址的寄存器,即写回和更新寄存器。

3. A64指令集--加载与存储指令的变种

1.不同位宽的加载与存储指令

指令说明
LDR数据加载指令
LDRSW有符号的数据加载指令,单位为字
LDRB数据加载指令,单位为字节
LDRSB有符号的加载指令,单位为字节
LDRH数据加载指令,单位为半字
LDRSH有符号的数据加载指令,单位为半字
STRB数据存储指令,单位为字节
STRH数据存储指令,单位为半字

2.不可扩展的加载和存储指令--LDUR

3.多字节内存加载和存储指令--LDP和STP

4.独占内存访问指令--LDXR,STXR,LDXP,STXP

5.隐含加载-获取/存储-释放内存屏障原语--LDAR,STLR

4. 入栈与出栈

在函数调用过程中,如果传递的参数少于或等于8个,那么使用X0-X7通过寄存器来传递。当参数多于8个时,则需要使用栈来传递.
A32指令集提供了PUSH和POP指令来实现入栈和出栈操作,不过,A64指令集已经去掉了PUSH和POP指令。在A64指令集上,入栈和出栈操作可以使用加载和存储指令来实现

5. MOV指令

MOV指令常常用于寄存器之间的搬移和立即数搬移。

用于寄存器之间搬移的MOV指令格式如下。

MOV <Xd|SP>, <Xn|SP>

用于立即数搬移的MOV指令格式如下:

MOV <Xd>, #<imm>

这里能搬移的立即数只有两种:

  • 16位立即数
  • 16位立即数左移16位、32位或者48位后的立即数

因此用于立即数搬移的MOV指令等同于如下的MOVZ指令

MOVZ <Xd>, #<imm16>, LSL #<shift>

MOV指令还能搬移一些用于位图的立即数,此时它等同于ORR指令。

ORR <Xd|SP>, XZR, #<imm>

6. A64指令集--算术与移位指令

6.1 条件操作码

A64指令集沿用了A32指令集中的条件操作,在PSTATE寄存器中有4个条件标志诶,即N、Z、C、V。

条件标志位描述
N负数标志(上一次运算结果为负值)
Z零结果标志(上一次运算结果为零)
C进位标志(上一次运算结果发生了无符号数溢出)
V溢出标志(上一次运算结果发生了有符号数溢出)

6.2 加法与减法指令

普通的加法指令有下面几种用法。

  • 使用立即数的加法

格式如下:

  • 使用寄存器的加法
  • 使用移位操作的加法

常用汇编命令

MOV x1,x0; 将寄存器x0的值传送到寄存器x1

ADD x0,x1,x2; 寄存器x1和x2的值相加后传送到x0

SUB x0,x1,x2; 寄存器x1和x2的值相减后传送到x0

AND x0,x0,#0xF; x0的值与0xF相位与后的值传送到x0

ORR x0,x0,#9; x0的值与9相位或后的值传送到x0

EOR x0,x0,#0xF; x0的值与0xF相异或后的值传送到x0

LDR x5,[x6,#0x8]; x6寄存器加0x8的和值传送到x5

STR x0, [SP, #0x8]; x0寄存器的数据传送到SP+0x8地址值指向的存储空间

STP x29, x30, [sp, #0x10]; 入栈指令

LDP x29, x30, [sp, #0x10]; 出栈指令

CBZ x19, 0x10; 比较,如果结果为零(Zero)就转移(只能跳到后面的指令)

CBNZ x19, 0x10; 比较,如果结果非零(Non Zero)就转移(只能跳到后面的指令)

B/BL ; 绝对跳转,返回地址保存到LR(x30)

b ffff000008283b80

bl ffff000008dc566c

其中 MOV 指令只能用于寄存器之间传值,寄存器和内存之间传值通过 LDR 和 STR.

A64指令又一个重要特点就不是所有指令都是带有条件的,就是说汇编中部分指令(跳转指令为主)需要根据状态寄存器中的一些状态来控制分支的执行。例如:

`b.cc ffff000008283dac <show_slab_objects+0x94>

ARM手册里面各个状态指令和代表的含义:

image.png

ARM手册里面各个状态指令和代表的含义

3、举例说明

通常出问题的时候是需要将ko或者整个内核反汇编出来的,下面以ko为例,如何反汇编。这里的-S 如果有符号信息,会把代码与汇编放一起,如果没有符号信息,就只有反汇编内容。

aarch64-linux-gnu-objdump -S reg_hal.ko > test.txt

以一段非常简单的代码为例

int example_test(int a, u32 *b)
{
    if(a > 0)
       *b = 1;
    else
       *b = 16;
    return 0;
}

对应的反汇编如下,我们一句一句翻译一下,大概就能看明白了。

000000000003a150 <example_test>:
 3a150: a9be7bfd  stp x29, x30, [sp,#-32]! //把x29 x30 的值存到sp-32的地址后,sp=sp-32
 3a154: 910003fd  mov x29, sp              //把 sp的值放在x29中
 3a158: a90153f3  stp x19, x20, [sp,#16]   //把x19 x20 的值存到sp+16的地址中后sp=sp+16
 3a15c: 2a0003f3  mov w19, w0              //把w0 的值存到w19 中
 3a160: aa1e03e0  mov x0, x30              //把x30 的值放在x0
 3a164: aa0103f4  mov x20, x1              //把x1的值放在x20
 3a168: 94000000  bl 0 <_mcount>           //跳转到地址0,这里还不太清楚,我理解是需要加载ko后,跳转到某些符号对应的地址上
 3a16c: 6b1f027f  cmp w19, wzr             //比较w19 和 0
 3a170: 540000ed  b.le 3a18c <example_test+0x3c> //如果小于 就跳转到<example_test+0x3c>这个地址
 3a174: 52800020  mov w0, #0x1             // 写w0 为1
 3a178: b9000280  str w0, [x20]            //w0的值写到x20内
 3a17c: 52800000  mov w0, #0x0             // w0 清零
 3a180: a94153f3  ldp x19, x20, [sp,#16]   // sp+16 地址的值分别放回x19 x20
 3a184: a8c27bfd  ldp x29, x30, [sp],#32   //sp地址取的值分别放回x29 x30后sp=sp+32
 3a188: d65f03c0  ret                      //返回
 3a18c: 52800200  mov w0, #0x10            //w0 的值写成0x10
 3a190: b9000280  str w0, [x20]            //w0 的值存到 x20
 3a194: 52800000  mov w0, #0x0             // w0 清零
 3a198: a94153f3  ldp x19, x20, [sp,#16]   // sp+16 地址的值分别放回x19 x20
 3a19c: a8c27bfd  ldp x29, x30, [sp],#32   //sp地址取的值分别放回x29 x30后sp=sp+32
 3a1a0: d65f03c0  ret                      //返回
 3a1a4: d503201f  nop                      //空操作

参考文献:

  1. 基于ARM64 的常见汇编命令记录
0

评论 (0)

取消