ARM架构是一种基于RISC(精简指令集计算机)原则的计算机处理器架构,由英国的ARM公司开发。ARM架构在移动设备、嵌入式系统和低功耗应用等领域得到广泛应用。
相比于传统的复杂指令集计算机(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语言调用的结果如下:
读/写特殊寄存器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
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
执行结果如下:
练习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
移位指令
按位与操作
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
按位或/异或
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
运行结果如下:
零计数指令
clz,计算最高位1前的0个数
mov x1, 0xf clz x2, x1 //x2为60
4.3比较和跳转指令
pstate的nzcv标志位
比较指令
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
运行结果如下:
条件选择指令
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)
基本跳转指令
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
运行结果如下图:
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来查看各个段的地址
程序启动:看到PC值:
重新运行测试程序,结果如下:
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
3.异常处理指令
svc:系统调用指令
hvc:虚拟化系统调用指令
smc:安全监控系统调用指令
4. 系统寄存器访问指令
5.内存屏障指令
五、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)数据定义伪指令
函数相关伪操作
与段相关的伪指令
(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