面向未来:理解ARM处理器的新一代技术(上)

简介: 面向未来:理解ARM处理器的新一代技术

ARM架构是一种基于RISC(精简指令集计算机)原则的计算机处理器架构,由英国的ARM公司开发。ARM架构在移动设备、嵌入式系统和低功耗应用等领域得到广泛应用。

image.png

相比于传统的复杂指令集计算机(CISC),ARM架构采用了更加精简的指令集,具有较高的执行效率和低功耗特性。这使得它成为了智能手机、平板电脑和其他移动设备的主要处理器选择。


此外,ARM还提供了一系列不同级别的处理器核心设计,包括Cortex-A系列(面向高性能)、Cortex-R系列(面向实时应用)和Cortex-M系列(面向嵌入式系统)。这些核心可以根据需求进行选择和定制,适应各种不同类型的应用场景。


一、ARM64架构简介

1.1ARMv8特色

  • (1) 超大的物理地址空间(Large Physical Address),提供超过4GB物理内存的访问;
  • (2) 64位宽的虚拟地址空间(64-bit Virtual Addresing);
  • (3) 提供31个64位宽的通用寄存器,可以减少对栈的访问,从而提高性能;
  • (4) 提供16KB和64KB的页面,有助于降低TLB的未命中率(miss rate);
  • (5) 全新的异常处理模型,有助于降低操作系统和虚拟化的实现复杂度;
  • (6)全新的加载-获取,存储-释放指令(Load-Acquire, Store-Release Instructions)。专门为C++11,C11以及Java内存模型设计;


1.2执行状态

AArch64: 64位的执行状态:

  • (1) 提供31个64位通用寄存器;
  • (2) 提供64位的程序计数器寄存器PC、栈指针寄存器SP以及异常链接寄存器;
  • (3) 提供A64指令集;
  • (4)定义ARMv8异常模型,支持4个异常等级,EL0~EL3;
  • (5)提供64位的内存模型;
  • (6)定义一组处理器状态(PSTATE)用来保存PE的状态;

AArch32: 32位的执行状态:

  • (1) 提供13个632位通用寄存器, 32位的程序计数寄存器PC、栈指针寄存器SP、链接寄存器;
  • (2) 提供A32,T32指令集;
  • (3)定义ARMv7异常模型,基于PE模式并映射到ARMv8的异常模型中;
  • (4)提供32位虚拟内存访问机制;
  • (5)定义一组处理器状态(PSTATE)用来保存PE的状态;


1.3ARMv8包含的寄存器


1)ARMv8包含31个通用寄存器

AArch64运行状态支持31个通用寄存器X0~X30,AArch32状态支持16个32位通用寄存器;

X0~X30:通用寄存器;

SP: 栈指针寄存器;

PC:程序计数寄存器;


2)系统寄存器

系统寄存器提供控制和状态,在AArch64状态下,很多系统寄存器根据不同的异常等级提供不同的变种寄存器:

_ELx, x is 0,1,2, or 3

比如SP_EL0表示在EL0下的栈指针寄存器;


3)SIMD/FP寄存器

支持128bit寄存器


4)特殊寄存器

XZE

5)数据类型

Byte: 8bit
Halfword: 16bit
Word: 32bit
Doubleworld: 64bit
Quadword: 128bit


6)异常模型

Exception Levels确定了处理器当前运行的特权级别,类似ARMv7架构中的特权等级

  • EL0: 用户特权,用于运行普通应用程序;
  • EL1: 系统特权,通常用于运行操作系统;
  • EL2: 运行虚拟化扩展的虚拟监控程序(Hypervisor);
  • EL3: 运行安全世界中的安全监控器(Secure Monitor);


7)A64指令分类

以下是一些常见的指令和操作,涉及内存加载和存储、算术和移位、位操作、条件操作、跳转、独占访存、内存屏障、异常处理和系统寄存器访问等方面。请注意,具体的指令和操作可能因不同的架构和编程语言而异。


内存加载和存储指令:

  • Load: 从内存中读取数据到寄存器。
  • Store: 将寄存器中的数据写入到内存。


多字节内存加载和存储:

  • Load Multiple: 一次性从内存中读取多个字节或字,并将它们加载到连续的寄存器。
  • Store Multiple: 一次性将多个字节或字从连续的寄存器写入到内存。


算术和移位指令:

  • Add: 执行加法运算。
  • Subtract: 执行减法运算。
  • Multiply: 执行乘法运算。
  • Divide: 执行除法运算。
  • Shift Left/Right: 对二进制数进行左移或右移操作。


位操作指令:

  • AND: 对两个二进制数执行按位与操作。
  • OR: 对两个二进制数执行按位或操作。
  • XOR: 对两个二进制数执行按位异或操作。
  • NOT: 对一个二进制数执行按位取反操作。

条件操作:

  • Compare: 比较两个值,并设置标志位以便后续的条件判断。
  • Conditional Branch: 根据条件跳转到不同的代码位置。


跳转指令:

  • Jump/Call: 无条件跳转或调用子程序。
  • Return: 返回调用者。


独占访存指令:

  • Load-Linked: 加载数据并标记该内存区域为独占访问状态。
  • Store-Conditional: 将修改后的数据写入内存,并检查是否被其他线程修改过。


内存屏障指令:

  • Memory Barrier/Fence: 在特定位置插入屏障,确保内存访问顺序和可见性的一致性。


异常处理指令:

  • Trap/Exception Handling: 触发异常或中断,并执行相应的异常处理程序。


系统寄存器访问指令:

  • Move to/from System Register: 读取或写入系统寄存器,用于控制处理器或监视其状态。
【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!


二、ARM64基础

2.1用ARM汇编实现,查找给定数组的最大数

.section .data  //定义数据段段
.align 3    //以2^3对齐
my_data:    //定义一个数据
  .quad  1
  .quad  2
  .quad  5
  .quad  8
  .quad  10
  .quad  12
my_data_count:  //数组的个数
  .quad  6
.align  3
print_data:     //定义一个字符串
  .string "big data: %d\n"
.section .text //定义代码段
.globl main    //定义入口函数main
main:
  stp     x29, x30, [sp, -16]! //x29是FP栈帧寄存器,x30是连接寄存器,入栈
  ldr x0, =my_data //读取my_data标签的地址
  ldr x1, my_data_count //读取my_data_count标签内容(6)
  add x4, x0, #40  //数组最后一个数地址,存入x4
  mov x3, xzr      //x3临时寄存器,初始值0
1:
  ldr x2, [x0], #8 //x0地址处的值(数组my_data[0]),加载到x2寄存器,然后更新x0=x0+8
  cmp x2, x3
  csel x3, x2, x3, hi //csel 条件选择,当条件满足(hi,x2>x3),返回x2,存入x3中,否则返回x3; x3保存最大值
  cmp x0, x4  //判断x0是否为my_data最后一个数
  b.ls 1b   //有条件(无符号小于或等于)跳转1b,
  ldr x0, =print_data //加载print_data标签地址
  //adrp x0, print_data
  //add x0, x0, :lo12:print_data
  mov x1, x3    //通过x0,x1传递参数给printf函数
  bl printf
  ldp  x29, x30, [sp], 16 //从start函数栈恢复x29,x30寄存器
  ret   //函数返回退出

交叉编译

aarch64-linux-gnu-gcc test.S -o test -g   --static

如果qemu-linux安装了gcc,也可在qemu-linux中编译:

gcc test.S -o test -g   --static

可以在qemu中用gdb调试

gdb --tui ./test

main函数设置断点:(gdb) b main

运行test程序:(gdb) r

单步运行:(gdb) s

查看寄存器值:(gdb) info reg x3

程序运行结束,最后结果

也可以用qemu直接运行test

qemu-aarch64-static test

2.2在C语言调用ARM64汇编函数

1.创建两个文件:compare.S和main.c

compare.S

.section .text
.globl compare_data
compare_data:
        cmp x0, x1
        csel x0, x0, x1, hi //若x0>1则返回x0, 否则返回x1
        ret

main.c文件

#include <stdio.h>
extern int compare_data(int a, int b); //extern导入compare_data函数声明
int main()
{
        int val;
        val = compare_data(5, 6); //调用汇编函数
        printf("big data: %d\n", val);
}

2.编译

aarch64-linux-gnu-gcc -o main main.c compare.S --static -g

3.运行

# ./main
big data: 6

2.3在ARM64汇编中调用C语言函数

1.创建两个文件

compare.c

int compare_data(int a, int b)
{
        return (a >= b) ? a : b;
}

main.S文件

.section .data
.align 3
print_data:
        .string "big data: %d\n"
.section .text
.globl main
main:
        stp     x29, x30, [sp, -16]!  //保存栈帧寄存器,返回寄存器
        mov x0, #6
        mov x1, #5
        bl compare_data    //调用C函数
        mov x1, x0
        ldr x0, =print_data
        bl printf
        ldp  x29, x30, [sp], 16  //恢复栈帧寄存器,返回寄存器
        ret

2.编译

aarch64-linux-gnu-gcc  -o main main.S compare.c --static -g

3.运行

# ./main
big data: 6

2.4在C语言中嵌入ARM64汇编代码

如题,有时为了提高部分代码运行性能,可以将部分C代码实现,用汇编改写(自己编写常比编译器优化效果更好)

1.先看案例

#include <stdio.h>                                                                                          
static int compare_data(int a, int b)
{
  int val;
  __asm__ __volatile__ (
      "cmp %1, %2\n"
      "csel %0, %1, %2, hi\n"
      : "+r" (val)
      : "r" (a), "r" (b) 
      : "memory");
  return val;
}
int main()
{
  int val;
  val = compare_data(5, 6); 
  printf("big data: %d\n", val);
  val = compare_data(6, 4); 
  printf("big data: %d\n", val);
  return 0;
}

编译运行:

aarch64-linux-gnu-gcc -o main main.c --static -g
# ./main
big data: 6
big data: 6

2.语法解析

C/C++中嵌入汇编,常用格式如下:

__asm__ [__volatile__] ( assembler template 
           : [output operand list]             /* optional */
           : [input operand list]              /* optional */
           : [clobbered register list]         /* optional */
           );

关键字__asm__:其实也可以写成“asm”。但是“asm”并不是所有版本的GCC编译器都支持的,而且可能有命名冲突问题,所以用“asm”的话,兼容性更好。

关键字__volatile__:也可以写“volatile”,理由同上;__volatile__是可选的,作用是禁止编译器对后面汇编指令再进行优化。一般自己写的汇编,考虑性能,已经做过优化,编译器再优化的话,可能效果反而更差,所以通常还是带上这个关键字;

括号里:是真正的汇编代码,主要有四部分组成,第一部分是具体的汇编代码,是必须的;其他三个为辅助参数,可选;各部分之间用冒号“:”分割,即使参数为空,也要加冒号;

1)assembler template

所有汇编代码必须用双引号括起来,如果多行汇编代码,每一条语句都要用双引号括起来,代码后面加上换行符("\n"或"\n\t"),具体形式如下:

__asm__ __volatile__ ( "instruction 1\n\t" 
           "instruction 2\n\t"
           ......
           "last instruction"
           );

注:即使一行汇编代码也没有,也要传入空字符串"",否则会报错;

1)[output/input operand list] :输出/入参数列表;

分别对应汇编与C语言交互的C表达式,若有多个,用逗号隔开;

在前面汇编中,还可以用"%n",直接引用后面的操作数;

__asm__("mov %0, %1, ror #1" 
            : "=r"(result) 
            : "r"(value)
            );

这里%0代表第一个操作数,result变量;

%1代表第二个操作数,即value变量;

操作数格式:每一个操作数,最多由四部分组成

[name]"modifier+constraint"(C expression)

name:别名,可选;

modifier:修改符,要用双引号括起来;

constraint:限定符,要用双引号括起来;

c表达式:用小括号括起来;

(1)如果指定了别名的话,那在汇编模板中,引用该变量,就可以使用别名,增加可读性,例如:

int x=10, y;
__asm__ ("mov %[in],%[out]"
   : [out]"=r"(y)
   : [in]"r"(x)
   :
);

(2) 说说限定符,操作数在这里的作用是将C语言定义的变量与汇编中使用的变量进行一 一对应,但并不是所有的汇编指令都可以接受任何类型变量,因此汇编器需要知道这些变量到底用在什么地方,传递前做一些转换。常用限定符如下表

(3)修改符号,在限定符之前,可选,为空的话,表明该操作数是只读的。

GCC定义了三个修改符,分别是:

如果想让一个C变量既作为输入操作数,也作为输出操作数的话,可以使用“+”限定符,并且这个操作数只需要在输出操作数列表中列出就行了。例如:

__asm__("mov %0, %0, ror #1" 
        : "=r"(y)
        : "0"(y)
        );

&”:为了避免编译器优化输出和输入使用同一个寄存器,可在输出操作数中使用“&”修改符,明确告诉编译器,代表输出操作数的寄存器一定不能使用输入操作数已经使用过的寄存器。

3)修改寄存器列表

为保持寄存器,内存数据一致性,提供三个类型

添加“memory”之后,编译器会在执行汇编代码之前,保证将保存在寄存器中,没有更新到内存中的值全部都写入到内存中。此列表中的每一项都要用双引号("")括起来,每项之间要用逗号(“,”)分割。

三、ARM64存储和加载指令

ldr和str指令

ARMv8也是基于指令加载和存储的架构,即不能直接操作内存;

LDR <reg_dst>,<addr>  //把存储器地址的数据加载到目的寄存器中;
STC <reg_src>,<addr> //把原寄存器的值,存储到内存中;

ldr指令寻址1:地址偏移模式

ldr Xd,[Xn,$offset]


.global ldr_test //申明全局函数
ldr_test:
  // 1. ldr地址偏移模式
  mov x1, 0x80000
  mov x3, 16
  /* 读取0x80000地址的值, 到x0寄存器*/
  ldr x0, [x1]
  /* 读取0x80000+8地址的值*/
  ldr x2, [x1, #8]
  /* 读取x1+x3地址=0x80000+16的值*/
  ldr x4, [x1, x3]
  /* 读取(x1+ x3<<3) =0x80080地址的值*/
  ldr x5, [x1, x3, lsl #3]
  ret

用qemu+Eclipse单步调试,查看寄存器值如下:

ldr指令寻址2:变基模式

LDR X0,[X1, #8]! //前变基模式,先更新x1=x1+8,再加载x1地址的值到x0
LDR X0,[X1],#8   //后变基模式,先读取x1的地址处的值到x0, 再更新x1=x1+8
STR X2,[X1, #8]! //前变基模式,先更新x1=x1+8,再存储x2的值到x1地址处
LDR X2,[X1],#8   //后变基模式,先存储x2的值到x1地址处, 再更新x1=x1+8

测试代码:

mov x2,0x400000
ldr x6, =0x1234abcd
str x6,[x2,#8]!

观察内存值如下图:

mov x2,0x500000
ldr x6, =0x1234abcd
str x6,x2,#8

观察内存值如下图:

ldr指令寻址3:加载标签模式

LDR LABEL //读取(PC指针+label offset)地址的值

#define MY_label 0x20
ldr x6,MY_label  //此时PC=0x802e4,读取内存值如下图
ldr x7,=MY_label //立即数, 伪指令

加载指令mov

mov:

(1) 16位立即数;

(2)或者16为立即数,左移16,32,48位;

多字节加载和存储指令ldp和stp

ldp和stp可以一条指令价值或存储16个字节;

LDP X1,X2,[X0]  //加载x0地址的值到x1,加载x0+8地址的值到x2
STP X1,X2,[X0]  //存储x1的值到x0地址处,存储x2到x0+8地址处

汇编实现: 假设16bytes对齐

.global my_memset_16bytes
my_memset_16bytes:
  mov x4, #0
1:
  stp x1, x1, [x0], #16
  add x4, x4, #16
  cmp x4, x2
  bne 1b
  ret

C语言调用的结果如下:

640.jpg

读/写特殊寄存器mrs,msr

mrs x0,TTBR0_EL1   //把TTBR0_EL1寄存器的值读到x0中
msr TTBR0_EL1,X0   //把X0寄存器的值,写入TTBR0_EL1寄存器

访问标签

/*lab07: test for label acess*/
#define my_label 0x30
.globl string1
string1:
  .string  "Hello a64"
.align 3
.globl my_data
my_data:
  .word 0xaa
.align 3
.global  access_label_test
access_label_test:
  //1.load a big num
  //mov,x0,0xffff0000ffff0000 //err,mov, 16bit
  ldr x1,=0xffff0000ffff0000
  //2,init reg for bit
  ldr x2,=(1<<1)|(1<<15)|(48)
  //3.acess a macro
  ldr x1,=my_label  //0x30
  ldr x2,my_label   //pc+0x30
  //4.access a string
  ldr x3,string1    //ascii of string
  ldr x4,=string1   //addr of string1
  //5, access a data
  ldr x5,my_data   //value of my_data
  ldr x6,=my_data  //addr of my_data
  ret

四、ARM64算术和移位指令

4.1条件操作码

在pstate处理器(对应a32之前是CPSR)状态中有4个条件操作吗NCZV

640.jpg

4.2普通加法指令add

1.使用寄存器的加法;

2.使用立即数的加法;

3.使用移位操作的加法

adds指令,影响条件标志位c

subs指令,影响条件标志位c

adc指令

带进位的加法指令,

ADC Xd, Xn, Xm //Xd = Xn + Xm + c

SBC指令

SBC Xd, Xn, Xm //Xd = Xn-Xm-1+C

CMP指令

内部用subs指令实现的,影响C标志位

cmp x1,x2 //x1+not(x2)+1

当x1>=x2,C=1

x1

练习1:测试C条件标志位

.global my_add_test
  my_add_test:
    mov x0,#0
    ldr x1,=0xffffffffffffffff
    mov x2,#3
      //test adds, influence C flag
    adds x0,x1,x1  //update C
    adc x3, xzr, xzr 
      //test for cmp, if x1>x2 ,C=1 ,or C=0
    cmp x1, x2
    adc x4, xzr, xzr 
    ret

执行结果如下:

640.jpg

练习2:cmp和sbc搭配使用

/*
 * when arg1 >= arg2, return 0
 * when arg1 < arg2, return 0xffffffffffffffff
 * SBC Rd,Rn,Rm -->Rd = Rn-RM-1+c
 */
.global my_compare_test
my_compare_test:   //param x0,x1, return x0
  cmp x0, x1
  sbc x0, xzr, xzr  //0-0-1+C
  ret

移位指令

640.jpg

按位与操作

AND Xd,Xn  -->Xd = Xd & Xn
ANDS影响Z标志位

ands影响z比特位

/*
 * ands influence Z flag in pstate
 */
.global my_ands_test
my_ands_test:
  mov x1, #0x3
  mov x2, #0
  ands x3,x1,x2
  mrs x0, nzcv  //z at 30bit
  ret

640.jpg

按位或/异或

ORR Xd, Xn  -->Xd = Xd | Xn
EOR Xd, Xn  -->Xd = Xd ^ Xn

异或的三个特点:

(1)0异或任何数=任何数;

(2)1异或任何数=任何数相反数;

(3)任何数异或自己=0;

异或的几个技巧运用

(1) 使某些位翻转:

0xffff,ffff ^ 0x1<<5  //把第5位翻转

(2) 交换两个数

a = a^b
b = b^a
a = a^b

(3) 在汇编里设置0

eor x0,x0

(4) 判断两个数是否相等

return ((a^b)==0)

按位清除

bic:位清零指令

mov x0 ,0xabcd

bic x0,x0,#0xf

位段操作指令

填充比特位bfi

提取比特位bfx

BFI Xd,Xn, #LSB, #WIDTH //get [0,width) from Xn,  to  [lsb,lsb+width) of Xd
BFX Xd, Xn,#LSB, #WIDTH // get [lab,lsb+width) from Xn, to [x,width)
 //ubfx:fill '0' in other bits
 //sbuf:fill '1' when bit[lsb+width-1] is 1, or fill '0'

测试程序:

.global my_bitfiled_test
my_bitfiled_test:
  mov x0,0xabcd
  bic x0,x0,0xf
  ldr x6, =0x346
  mov x1, 0
  bfi x1, x6, #8,#4
  ldr x2, =0x5678abcd
  ubfx x3, x2, #4, #8
  sbfx x4, x2, #4, #8
//read bitfiled from register
  mrs x7,ID_AA64ISAR0_EL1
  //support LSE?
  ubfx x8,x7, #20, #4
  //support AES?
  ubfx x9, x7, #4, #4
  ret

运行结果如下:

640.jpg

零计数指令

clz,计算最高位1前的0个数

mov x1, 0xf
clz x2, x1 //x2为60

4.3比较和跳转指令

pstate的nzcv标志位

640.jpg

640.jpg

比较指令

cmp: 比较两个数

cmn:负向比较(一个数与另一个数的二进制补码相比较)

cmp x1, x2 -->x1 - x2
cmn x1, x2 -->x1 + x2

测试代码:

/*
 * compare lab01:test cmp and cmn
 */
.global my_cmp_cmn_test
my_cmp_cmn_test:
  mov x1, 1
  mov x2, -3
1:
  cmn x1,x2
  add x2, x2, 1
  mrs x0,nzcv
  b.mi 1b  //MI:负数
2:
  cmp x2,x1
  add x1,x1,1
  mrs x0, nzcv
  b.cs 2b  //cs:无符号大于等于
  ret

运行结果如下:

640.jpg

条件选择指令

CSEL: 条件选择指令

CSET:条件置位指令

CSINC:条件选择并增加指令

/* 
 * cond = true, Xd = Xn
 * cond = flase, Xd = Xm
 */
CSEL Xd, Xn, Xm, cond  
CSEL Xd, cond  //cond=true, return 1; or return 0


/*
 * compare lab2:
 */
.global my_csel_test
my_csel_test:
  cmp x0,0
  sub x2, x1, 1
  add x3, x1, 2
  csel x4, x3, x2, eq  //eq, ==
  mov x0, x4
  ret

运行结果如下:

my_csel_test(3,6)

640.jpg

基本跳转指令

b: 无条件跳转,不返回;范围PC+/-128MB
b.cnd: 有条件跳转,不返回; 范围pc+/-1MB
bx: 跳转到寄存器指定的地址处,不返回

带返回地址的跳转指令

bl: 带返回地址(PC+4–>x30), 适用于调用子函数
返回地址:保存在X30中,父函数的PC+4
跳转范围:PC +/- 128MB
blx:跳转到寄存器指定地址处,返回地址(父函数PC+4)保存在X30
RET: 从子函数返回
ERET:从当前异常模式返回,通常用于模式切换

从当前的一场模式返回,它会从SPSR中恢复PSTATE, 从ELR中获取跳转地址,并返回该地址;

测试代码:

/*
 * compare lab2:
 */
.global my_csel_test
my_csel_test:
  cmp x0,0
  sub x2, x1, 1
  add x3, x1, 2
  csel x4, x3, x2, eq  //eq, ==
  mov x0, x4
  ret
/*
 * bl
 */
 .global my_bl_test
 my_bl_test:
  mov x8, x30
  mov x0, 1  // 函数嵌套调用,会冲掉x30里保存的返回地址,这里先做备份,在推出本函数时,恢复x30
  mov x1, 3
  bl my_csel_test
  mov x30,x8
  ret

比较并跳转指令

cbz: xt寄存器是否为0,为0则跳转到label处,跳转范围+/1 1MB
cbnz: xt寄存器是否为0,不为0则跳转到label处,跳转范围+/1 1MB
tbz: 测试寄存器中某位是否为0,为0则跳转到label处,跳转范围+/1 32KB
tbnz: 测试寄存器中某位是否为0,为0则跳转到label处,跳转范围+/1 32KB

4.4其他常用指令

1.加载指令(PC相对地址)

ADR: 加载PC相对地址的label地址,范围+/- 1MB;

ADRP: 加载PC相对地址,label地址,并且4KB对齐,范围+/- 4GB;

/* others inst */
.align 3
.global my_test_data
my_test_data:
  .dword 0x12345678abcdeeff
.global adrp_test
adrp_test:
  adr x0, my_test_data
  adrp x1, my_test_data   //4KB align
  add x2, x1,#:lo12:my_test_data
  ldr x3, [x2] //get val
  ldr x4,=my_test_data  //get addr
  ldr x5, my_test_data  //get value
  adrp x6, init_pg_dir  //get addr
  ldr x7, =init_pg_dir  //get addr
  ret

运行结果如下图:

640.jpg

LDR和ADRP区别

LDR伪指令:加载的是链接地址(虚拟地址)

ADRP指令:加载的是当前运行地址的PC值+label的offset,即label的当前运行地址(物理地址)

在link脚本,将链接地址改成0xffff000000080000,重新执行上面程序;

注:当程序链接地址不等于运行地址时,用如下命令加载符号表,否则无法单步调试;

add-symbol-file xxx.elf 0x80030 -s .text.boot 0x80000 -s .rodata 0x80758
0x80030:text段的起始地址;
0x80000:text.boot段的起始地址;
0x80758:只读数据段的起始地址;

可通过aarch64-linux-gnu-readelf -S xxx.elf来查看各个段的地址640.jpg


程序启动:看到PC值:

640.jpg

重新运行测试程序,结果如下:

640.jpg

2. 内存独占加载和访问指令

ldxr xd, [xn|sp] //独占的读取xn或sp地址的内容到xd寄存器
stxr wd, xt, [xn|sp] //独占的把xt内容写入到xn或sp指向的地址
//wd为0, 表示成功;wd = 1,表示不成功1

Linux内核常用来实现atomic访问;

spinlock机制可以简单易用ldxr和lstxr指令实现;

.align 3
.global my_data2
my_data2:
  .dword 0x0
.global my_atomic_write
my_atomic_write:
  adr x6, my_data2;
1:
  ldxr x2, [x6];
  orr x2,x2,x0;
  stxr w3, x2, [x6]
  cbnz w3, 1b
  mov x0,x2
  ret

640.jpg

3.异常处理指令

svc:系统调用指令

hvc:虚拟化系统调用指令

smc:安全监控系统调用指令

4. 系统寄存器访问指令

640.png

5.内存屏障指令

640.jpg

五、GNU AS汇编器介绍

5.1ARM64的汇编器

  • (1)ARM公司官方的汇编器;
  • (2)GNU AS汇编器:aarch64-linux-gnu-as;
  • (3)gcc采用as作为汇编器,所以汇编码是AT&T格式;
  • (4)AT&T格式:源于贝尔实验室;
  • (5)ARM格式:ARM官方汇编语法;

汇编语法

(1)label:任何以冒号结尾的标识符,都被认为是一个标号;

(2)注释:

“//”,注释

“#”,在一行的开始,注释整行;

(3)指令、伪指令、寄存器,可以全部大写或者小写,GNU默认风格是小写;

(4)Symbol:代表它所在的地址,也可以当作变量或者函数来使用;

全局symbol, 可以用.global声明;

局部symbol, 局部范围使用,开头以0-99为标号,通常和b指令结合使用

f:告诉编译器向前搜索;

b:告诉编译器向后搜索;

伪指令

(1).glign对齐,填充数据实现对齐。ARM64系统中,第一个参数表示2^n大小;

(2)数据定义伪指令

640.jpg

函数相关伪操作

640.jpg

与段相关的伪指令

(1).section:表示接下来的汇编会链接到哪个段里,如代码段、数据段等

每一个段都以段名开始,以下一个段名或文件结尾结束

.section name, "flags"

“flags”,表示段的属性,可写,可执行等;

.section ".idmap.txt","awx"
//表示下面代码是在".idmap.txt"段里,具有可分配,可写和可执行的属性;

(2).pushsection:把下面代码push到指定section中;

.popsection:技术push,成对使用;

仅仅是把pushsection/.popsection之间的代码,加载到指定section中,其他不变;

实例1:使用伪指令,实现一个类似linux内核中表的定义;

/*
 * as lab1:pesudo code, realize a table
 */
 .align 3
 .global func_addr
 func_addr:
  .quad 0x800800
  .quad 0x800860
  .quad 0x800880
.align 3
.global func_string
func_string:
  .asciz "func_a"
  .asciz "func_b"
  .asciz "func_c"
.align 3
.global func_num_syms
func_num_syms:
  .quad 3


extern unsigned long func_addr[];
extern unsigned long func_num_syms;
extern char func_string[];
static int print_func_name(unsigned long addr)
{
  int i;
  char *p, *string;
  for (i = 0; i < func_num_syms; i++) {
    if (addr == func_addr[i])
      goto found;
  }
return 0;
found:
  p = &func_string;
  while (1) {
    p++;
    if (*p == '\0')
      i--;
    if (i == 0) {
      p++;
      string = p;
      uart_send_string(string);
      break;
    }
  }
  return 0;
}

(1).macro和.endm组成一个宏;

(2).macro后面跟着的依次是宏名称,宏参数;

(3)在宏中使用参数,需要添加前缀"";

.macro add a,b   //宏名称add,参数a,b

(4)红参数定义时,可以设置初始值

.macro test p1=0 p2//可以用test a,b或者test b来使用

(5)宏可以使用空格

.macro label 1
\1 :
.endm

(6)使用"()"表示字符串结束

.macro kernel_ventry, el, label
b el\()\el\()_\label
//在arm/arm64/kernel/entry.S文件,表示el1_irq

练习2:

/*
 * lab02:macro test
 */
  .align 3
 .macro add_func add,a,b
  mov x0, \a
  mov x1, \b
  bl add_\()\add
.endm
.align 3
.global add_1
add_1:
  add x0,x0,x1
  add x0,x0,1
  ret
.global add_2
add_2:
  add x0,x0,x1
  ret
.global macro_test1
macro_test1:
  mov x9,x30
  add_func 1,x0,x1
  mov x30, x9
  ret
.global macro_test2
macro_test2:
  mov x9,x30
  add_func 2,x0,x1
  mov x30, x9
  ret


print_func_name(0x800880);
  unsigned long val1 = 0,val2=0;
  val1 = macro_test1(3,5);
  val2 = macro_test2(3,5);

ARM64编译选项

  • -EB:用于大端模式的CPU, -EL表示小端模式;
  • -mabi:指定ABI模式,ilp32表示elf32, lp6表示ELF64,默认lp64;
  • -mcpu=processor+extension: 指定CPU型号,比如cortex-a72;
  • -march=,用于指定架构,比如armv-8.2-a;
yu@sys:~$ aarch64-linux-gnu-as --help
 AArch64-specific assembler options:
  -mbig-endian            assemble for big-endian
  -mlittle-endian         assemble for little-endian
  -mverbose-error         output verbose error messages
  -mno-verbose-error      do not output verbose error messages
  -mabi=<abi name>        specify for ABI <abi name>
  -mcpu=<cpu name>        assemble for CPU <cpu name>
  -march=<arch name>      assemble for architecture <arch name>
  -EB                     assemble code for a big-endian cpu
  -EL                     assemble code for a little-endian cpu

特殊字符

“//”注释

“#”: 在一行开头,注释一行;还可以表示立即数;

“#:lo12”:表示地12位;

adrp x0,foo
ldx x0,[x0,#:lo12:foo]

“ldr”:伪操作;

ARM64特有的伪操作;

.bss:切换到bss段;

.dword/.xword:64位数据;

name .reg register_name:为寄存器创建别名;

foo .req x0
code


相关实践学习
阿里云图数据库GDB入门与应用
图数据库(Graph Database,简称GDB)是一种支持Property Graph图模型、用于处理高度连接数据查询与存储的实时、可靠的在线数据库服务。它支持Apache TinkerPop Gremlin查询语言,可以帮您快速构建基于高度连接的数据集的应用程序。GDB非常适合社交网络、欺诈检测、推荐引擎、实时图谱、网络/IT运营这类高度互连数据集的场景。 GDB由阿里云自主研发,具备如下优势: 标准图查询语言:支持属性图,高度兼容Gremlin图查询语言。 高度优化的自研引擎:高度优化的自研图计算层和存储层,云盘多副本保障数据超高可靠,支持ACID事务。 服务高可用:支持高可用实例,节点故障迅速转移,保障业务连续性。 易运维:提供备份恢复、自动升级、监控告警、故障切换等丰富的运维功能,大幅降低运维成本。 产品主页:https://www.aliyun.com/product/gdb
相关文章
|
2月前
ARM64技术 —— 系统调用指令SVC、HVC和SMC的使用规则
ARM64技术 —— 系统调用指令SVC、HVC和SMC的使用规则
|
2月前
ARM64技术 —— MMU处于关闭状态时,内存访问是怎样的?
ARM64技术 —— MMU处于关闭状态时,内存访问是怎样的?
|
2月前
ARM技术 —— 条件执行
ARM技术 —— 条件执行
|
2月前
ARM64技术 —— Device Memory Type
ARM64技术 —— Device Memory Type
|
2月前
ARM处理器函数调用时的参数传递
ARM处理器函数调用时的参数传递
|
6月前
|
弹性计算 编解码 运维
飞天技术沙龙回顾:业务创新新选择,倚天Arm架构深入探讨
阿里云、平头哥与Arm联合举办的飞天技术沙龙在上海举行,聚焦Arm Neoverse核心优势和倚天710计算实例在大数据、视频领域的应用。活动中,专家解读了倚天710的性能提升和成本效益,强调了CIPU云原生基础设施处理器的角色,以及如何通过软件优化实现资源池化和稳定性平衡。实例展示在视频编码和大数据处理上的性能提升分别达到80%和70%的性价比优化。沙龙吸引众多企业代表参与,促进技术交流与实践解决方案的探讨。
飞天技术沙龙回顾:业务创新新选择,倚天Arm架构深入探讨
|
6月前
|
Cloud Native 安全 数据中心
|
6月前
|
弹性计算 编解码 运维
飞天技术沙龙回顾:业务创新新选择,倚天Arm架构深入探讨
基于「倚天710自研芯片+CIPU云原生基础设施处理器」组合的倚天ECS实例为解决算力挑战提供新思路。
|
6月前
|
存储 机器学习/深度学习 人工智能
嵌入式中一文搞懂ARM处理器架构
嵌入式中一文搞懂ARM处理器架构
239 1
|
6月前
|
人工智能 安全 数据安全/隐私保护
叫好不叫座?Arm、英特尔、AMD 等 5 位技术大咖畅聊机密计算技术
机密计算作为一项新兴技术,为我们如何安全处理和计算敏感数据提供了新的视角和解决方案。