ARMv8汇编基础介绍
在嵌入式Linux开发过程中会经常遇到空指针等内核的Oops报错,会打印一些寄存器的值及调用栈。
当遇到如上情况,那大概率是要上汇编了。涉及到objdump
命令反汇编vmlinux或者对应的.o文件用来分析定位问题点。此文章围绕ARMv8上的汇编基础知识展开介绍。
反汇编指得到汇编代码,反编译是指得到对应语言的源代码。
1 寄存器
[ 9.795823] PC is at rs485_do_tasklet+0x8c/0x11c
[ 9.795827] LR is at rs485_do_tasklet+0x64/0x11c
[ 9.795829] pc : [<ffffff800861c4ec>] lr : [<ffffff800861c4c4>] pstate: 60400145
[ 9.795831] sp : ffffffc07c95bc60
[ 9.795835] x29: ffffffc07c95bc60 x28: 0000000000000006
[ 9.795838] x27: ffffff8009064018 x26: 000000000000000a
[ 9.795842] x25: 0000000000000100 x24: 0000000000000000
[ 9.795845] x23: ffffffc07b5ee170 x22: 0000000000000000
[ 9.795849] x21: ffffff80096bf730 x20: ffffff800944d000
[ 9.795852] x19: 0000000000000060 x18: 0000000000000004
[ 9.795855] x17: 0000007f9e5111c8 x16: ffffff8008215018
[ 9.795859] x15: 0000000000000001 x14: ffffff8008e355ab
[ 9.795862] x13: 0000000000000003 x12: 0000000000000000
[ 9.795865] x11: ffffffc07f305c90 x10: 0000000000000aa0
[ 9.795869] x9 : 0000200000000000 x8 : 0000000000000001
[ 9.795872] x7 : 0000000008000000 x6 : 0000200000000000
[ 9.795876] x5 : 0000000000000000 x4 : 000000000001c200
[ 9.795879] x3 : 0000000000000000 x2 : 0000000000000000
[ 9.795882] x1 : ffffffc07c95bca8 x0 : 0000000000000130
1.1 通用寄存器
ARMv8存在31个64位通用寄存器。
31 个通用寄存器r0 ~ r30,每个寄存器可以存取一个 64位大小的数。 当使用 x0 - x30访问时,是一个 64位的数;当使用 w0 - w30访问时,是一个 32 位的数,访问的是寄存器的 低 32
位,如图:
特殊寄存器
- sp: (Stack Pointer),栈顶寄存器,用于保存栈顶地址;
-
fp(x29): (Frame Pointer),为栈基址寄存,用于保存栈底地址;
- lr(x30): (Link Register) ,保存调用跳转指令
bl
指令的下一条指令的内存地址; - pc:保存将要执行的指令的地址。
2 指令
指令读取
在 arm64
架构中,每个指令读取都是 64 位,即 8
字节 空间。
arm64 约定(一般来说)
x0 ~ x7
分别会存放方法的前 8 个参数;如果参数个数超过了8个,多余的参数会存在栈上,新方法会通过栈来读取。- 方法的返回值一般都在 x0 上;如果方法返回值是一个较大的数据结构时,结果会存在 x8 执行的地址上。
2.1 指令介绍
-
mov: 将某一寄存器的值复制到另一寄存器(只能用于寄存器与寄存器或者寄存器与常量之间传值,不能用于内存地址),如:
mov x1, x0 ; 将寄存器 x0 的值复制到寄存器 x1 中
-
add: 将某一寄存器的值和另一寄存器的值 相加 并将结果保存在另一寄存器中,如:
add x0, x0, #1 ; 将寄存器 x0 的值和常量 1 相加后保存在寄存器 x0 中 add x0, x1, x2 ; 将寄存器 x1 和 x2 的值相加后保存到寄存器 x0 中 add x0, x1, [x2] ; 将寄存器 x1 的值加上寄存器 x2 的值作为地址,再取该内存地址的内容放入寄存器 x0 中
-
sub: 将某一寄存器的值和另一寄存器的值 相减 并将结果保存在另一寄存器中,如:
sub x0, x1, x2 ; 将寄存器 x1 和 x2 的值相减后保存到寄存器 x0 中
-
mul:将某一寄存器的值和另一个寄存器的值 相乘 并将结果保存在另一寄存器中,如:
mul x0, x1, x2 ; 将寄存器 x1 和 x2 的值相乘后结果保存到寄存器 x0 中
-
sdiv:(有符号数,对应 udiv: 无符号数)将某一寄存器的值和另一个寄存器的值 相除 并将结果保存在另一寄存器中,如:
sdiv x0, x1, x2 ; 将寄存器 x1 和 x2 的值相除后结果保存到寄存器 x0 中
-
and: 将某一寄存器的值和另一寄存器的值 按位与 并将结果保存到另一寄存器中,如:
and x0, x0, #0xf ; 将寄存器 x0 的值和常量 0xf 按位与后保存到寄存器 x0 中
-
orr: 将某一寄存器的值和另一寄存器的值 按位或 并将结果保存到另一寄存器中,如:
orr x0, x0, #9 ; 将寄存器 x0 的值和常量 9 按位或后保存到寄存器 x0 中
-
eor: 将某一寄存器的值和另一寄存器的值 按位异或 并将结果保存到另一寄存器中,如:
eor x0, x0, #0xf ; 将寄存器 x0 的值和常量 0xf 按位异或后保存到寄存器 x0 中
-
str: (store register) 将寄存器中的值写入到内存中,如:
str w9, [sp, #0x8] ; 将寄存器 w9 中的值保存到栈内存 [sp + 0x8] 处
-
strb: (store register byte) 将寄存器中的值写入到内存中(只存储一个字节),如:
strb w8, [sp, #7] ; 将寄存器 w8 中的低 1 字节的值保存到栈内存 [sp + 7] 处
-
ldr: (load register) 将内存中的值读取到寄存器中,如:
ldr x0, [x1] ; 将寄存器 x1 的值作为地址,取该内存地址的值放入寄存器 x0 中 ldr w8, [sp, #0x8] ; 将栈内存 [sp + 0x8] 处的值读取到 w8 寄存器中 ldr x0, [x1, #4]! ; 将寄存器 x1 的值加上 4 作为内存地址, 取该内存地址的值放入寄存器 x0 中, 然后将寄存器 x1 的值加上 4 放入寄存器 x1 中 ldr x0, [x1], #4 ; 将寄存器 x1 的值作为内存地址,取内该存地址的值放入寄存器 x0 中, 再将寄存器 x1 的值加上 4 放入寄存器 x1 中 ldr x0, [x1, x2] ; 将寄存器 x1 和寄存器 x2 的值相加作为地址,取该内存地址的值放入寄存器 x0 中
-
ldrsb: (load register byte) 将内存中的值(只读取一个字节)读取到寄存器中,如:
ldrsb w8, [sp, #7] ; 将栈内存 [sp + 7] 出的 低 1 字节的值读取到寄存器 w8 中
-
stur:同
str
将寄存器中的值写入到内存中(一般用于负
地址运算中),如:stur w10, [x29, #-0x4] ; 将寄存器 w10 中的值保存到栈内存 [x29 - 0x04] 处
-
ldur: 同
ldr
将内存中的值读取到寄存器中(一般用于负
地址运算中),如:ldur w8, [x29, #-0x4] ; 将栈内存 [x29 - 0x04] 处的值读取到 w8 寄存器中
-
stp: 入栈指令(
str
的变种指令,可以同时操作两个寄存器),如:stp x29, x30, [sp, #0x10] ; 将 x29, x30 的值存入 sp 偏移 16 个字节的位置
-
ldp: 出栈指令(
ldr
的变种指令,可以同时操作两个寄存器),如:ldp x29, x30, [sp, #0x10] ; 将 sp 偏移 16 个字节的值取出来,存入寄存器 x29 和寄存器 x30
-
scvtf: (Signed Convert To Float)带符号 定点数 转换为 浮点数,如:
scvtf d1, w0 ; 将寄存器 w0 的值(顶点数,转化成 浮点数) 保存到 向量寄存器/浮点寄存器 d1 中
-
fcvtzs:(Float Convert To Zero Signed)浮点数 转化为 定点数 (舍入为0),如:
fcvtzs w0, s0 ; 将向量寄存器 s0 的值(浮点数,转换成 定点数)保存到寄存器 w0 中
-
cbz: 和 0 比较(Compare),如果结果为零(Zero)就转移(只能跳到后面的指令),如:
cbz x8, loc_1800b4530 ; 将寄存器 x8 的值和 0 比较,如果结果为 “0” 则跳转到 ‘loc_1800b4530’ 标签处开始指令
-
cbnz: 和非 0 比较(Compare),如果结果非零(Non Zero)就转移(只能跳到后面的指令),如:
cbnz x9, loc_1800b4530 ; 将寄存器 x9 的值和 0 比较,如果结果为 “非 0” 则跳转到 ‘loc_1800b4530’ 标签处开始指令
-
cmp: 比较指令,相当于
subs
,影响程序状态寄存器 CPSR ; -
cset:比较指令,满足条件,则并置
1
,否则置0
,如:cmp w8, #2 ; 将寄存器 w8 的值和常量 2 进行比较 cset w8, gt ; 如果是大于(grater than),则将寄存器 w8 的值设置为 1,否则设置为 0
-
LSL: 逻辑左移
-
LSR: 逻辑右移
-
ASR: 算术右移
-
ROR: 循环右移
-
adrp: 用来定位数据段中的数据用, 因为 aslr 会导致代码及数据的地址随机化, 用 adrp 来根据 pc指针做辅助定位。指令计算出指定标签的地址的页表基地址,并将这个基地址的高20位加载到目标寄存器中。由于 ARMv8 架构采用了 4KB 对齐的页表(Page),因此
adrp
指令的目的是为了加载目标标签所在页面的起始地址,而不是标签本身的地址。 -
b: (branch)跳转到某地址(无返回), 不会改变 lr (x30) 寄存器的值;一般是本方法内的跳转,如
while
循环,if else
等 ,如:b LBB0_1 ; 直接跳转到标签 ‘LLB0_1’ 处开始执行
-
bl: 跳转到某地址(有返回),先将下一指令地址(即函数返回地址)保存到寄存器 lr (x30)中,再进行跳转 ;一般用于不同方法直接的调用 ,如:
bl 0x100cfa754 ; 先将下一指令地址(‘0x100cfa754’ 函数调用后的返回地址)保存到寄存器 ‘lr’ 中,然后再调用 ‘0x100cfa754’ 函数
-
blr: 跳转到
某寄存器
(的值)指向的地址(有返回),先将下一指令地址(即函数返回地址)保存到寄存器 lr (x30)中,再进行跳转 ;如:blr x20 ; 先将下一指令地址(‘x20’指向的函数调用后的返回地址)保存到寄存器 ‘lr’ 中,然后再调用 ‘x20’ 指向的函数
-
br: 跳转到某寄存器(的值)指向的地址(无返回), 不会改变 lr (x30) 寄存器的值。
-
brk: 可以理解为跳转指令特殊的一种。
-
ret: 子程序(函数调用)返回指令,返回地址已默认保存在寄存器 lr (x30) 中
3 实际案例分析
每个函数调用,都会有 入栈 和 出栈 操作。
编写一个Demo程序,实现打印字符串,如下:
//test.c
#include <stdio.h>
void TestPushAndPop()
{
printf("Push an Pop !");
}
使用gcc编译成汇编文件:aarch64-linux-gnu-gcc -S -o test.s test.c
; test.s
.arch armv8-a ;架构
.file "test.c" ;文件名称
.text
.section .rodata ;数据放到只读数据段
.align 3 ;2^3对齐,8字节对齐
.LC0:
.string "Push an Pop !" ;存储了一个字符串常量
.text
.align 2
.global TestPushAndPop ;全局符号
.type TestPushAndPop, %function
TestPushAndPop: #label
stp x29, x30, [sp, -16]! ;入栈指令 将x29,x30中的值存入sp(栈底)地址减16byte的位置
add x29, sp, 0 ;将当前栈指针 sp 的值赋给寄存器 x29,即建立当前栈帧。
adrp x0, .LC0 ;获取.LC0的基地址,并将这个基地址的高20位加载到目标寄存器x0中。
add x0, x0, :lo12:.LC0
bl printf ;调用 printf 函数,将寄存器 x0 中保存的字符串地址作为参数传递给 printf 函数。
nop ;空操作,用于占位,没有实际作用。
ldp x29, x30, [sp], 16 ; 将栈中的值取出存放到x29, x30
ret ;返回
.size TestPushAndPop, .-TestPushAndPop
.ident "GCC: (Ubuntu/Linaro 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
注意:对栈的 分配/释放 操作只会对栈指针做加减法, 而不会对栈内存中的内容做任何修改(也不会把释 放的栈空间设置为 0)。