本文主要是介绍ARMv8汇编指令-adrp、adr、adr_l,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1.概述
在阅读Linux内核代码时,经常能碰到汇编代码,网上能查的资料千篇一律,大多都描述的很模糊。俗话说,实践是检验真理的唯一标准,我们就参考官方文档,自己写汇编代码并反汇编,探寻其中的奥妙。
2.adrp
在Linux内核启动代码primary_entry
中,使用adrp
指令获取Linux内核在内存中的起始页地址,页大小为4KB,由于内核启动的时候MMU还未打开,此时获取的Linux内核在内存中的起始页地址为物理地址。adrp
通过当前PC地址的偏移地址计算目标地址,和实际的物理无关,因此属于位置无关码。对于具体的计算过程,下面慢慢分析。
[arch/arm64/kernel/head.S]
SYM_CODE_START(primary_entry)......adrp x23, __PHYS_OFFSETand x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0......
SYM_CODE_END(primary_entry)[arch/arm64/kernel/head.S]
#define __PHYS_OFFSET KERNEL_START // 内核的物理地址
[arch/arm64/include/asm/memory.h]
// 内核的起始地址和结束地址在vmlinux.lds链接脚本中定义
#define KERNEL_START _text // 内核代码段的起始地址,也即内核的起始地址
#define KERNEL_END _end // 内核的结束地址
2.1.定义
adrp
指令根据PC的偏移地址计算目标页地址。首先adrp
将一个21位有符号立即数左移12位,得到一个33位的有符号数(最高位为符号位),接着将PC地址的低12位清零,这样就得到了当前PC地址所在页的地址,然后将当前PC地址所在页的地址加上33位的有符号数,就得到了目标页地址,最后将目标页地址写入通用寄存器。此处页大小为4KB,只是为了得到更大的地址范围,和虚拟内存的页大小没有关系。通过adrp
指令,可以获取当前PC地址±4GB范围内的地址。通常的使用场景是先通过adrp
获取一个基地址,然后再通过基地址的偏移地址获取具体变量的地址。
下面是adrp
指令的编码格式。立即数占用21位,在运行的时候,会将21位立即数扩展为33位有符号数。最高位为1,表示这是一个aarch64指令。
2.2.测试
Linux内核启动代码不好测试,需要写一个简单的测试代码。下面是本次adrp
的测试代码,使用adrp
指令获取g_val1
和g_val2
数组所在页的基地址,同时会打印数组的地址和调用函数的地址,由于是应用层的程序,这些地址都是虚拟地址,但是计算过程都是一样的。
#define PAGE_4KB (4096)
#define __stringify_1(x...) #x
#define __stringify(x...) __stringify_1(x)
uint64_t g_val1[PAGE_4KB / sizeof(uint64_t)];
uint64_t g_val2[PAGE_4KB / sizeof(uint64_t)];#define ADRP(label) ({ \uint64_t __adrp_val__ = 0; \asm volatile("adrp %0," __stringify(label) :"=r"(__adrp_val__)); \__adrp_val__; \
})static void adrp_test()
{printf("g_val1 addr 0x%lx, adrp_val1 0x%lx, adrp_test addr 0x%lx\n",(uint64_t)g_val1, ADRP(g_val1), (uint64_t)adrp_test);printf("g_val2 addr 0x%lx, adrp_val2 0x%lx, adrp_test addr 0x%lx\n",(uint64_t)g_val2, ADRP(g_val2), (uint64_t)adrp_test);
}
上面程序运行的输出结果如下,g_val1
和g_val2
的地址分别为0x5583e25028
和0x5583e26028
,g_val1
的页基地址为0x5583e25000
,g_val2
页的基地址为0x5583e26000
,adrp_test
函数的地址为0x5583e1479c
。
g_val1 addr 0x5583e25028, adrp_val1 0x5583e25000, adrp_test addr 0x5583e1479c
g_val2 addr 0x5583e26028, adrp_val2 0x5583e26000, adrp_test addr 0x5583e1479c
反汇编代码如下所示。下面分析一下g_val1
页基地址的计算过程,包括编译时和运行时,g_val2
页基地址的计算过程类似,这里不再赘述。
- 将
g_val1
地址(0x11028)低12位清零,得到0x11000,将当前adrp
指令所在地址(0x7b0)的低12清零,得到0x0(编译时完成) - 0x1100减去0x0得到偏移地址0x11000,偏移地址右移12位得到偏移页数量0x11,将立即数0x11保存到指令编码中(编译时完成)
- 取出立即数0x11,左移12位转换成偏移的字节数,即0x11000(运行时完成)
- 将PC地址的低12位清零得到0x5583e14000(运行时完成)
- 将0x5583e14000加上0x1100得到
g_val1
运行时页基地址0x5583e25000(运行时完成)
000000000000079c <adrp_test>: // 运行时的地址为0x5583e1479c
......7b0: b0000080 adrp x0, 11000 <__data_start> // 获取g_val1页基地址
......7e0: d0000080 adrp x0, 12000 <g_val1+0xfd8> // 获取g_val2页基地址Disassembly of section .data: // 数据段定义
0000000000011000 <__data_start>: // 运行时的地址为0x5583e25000...
......
Disassembly of section .bss: // bss段定义
0000000000011028 <g_val1>: // 运行时地址为0x5583e25028...
0000000000012028 <g_val2>: // 运行时地址为0x5583e26028...
从上面可以看出,编译时和运行时的地址不一样,但通过adrp
指令都能正确获取g_val1
页基地址和g_val2
页基地址。说明adrp
获取的地址是位置无关的,不管运行时的地址怎么变,都可以正确获取对应变量页基地址。当然我们也可以使用专业的反汇编工具,直接将机器码转换为汇编代码。上面两条adrp
指令转换的汇编代码如下,和上面一样,这里的偏移地址都已经做了左移12位的处理。
3.adr
3.1.定义
adr
指令根据PC的偏移地址计算目标地址。偏移地址是一个21位的有符号数,加上当前的PC地址得到目标地址。adr
可以获取当前PC地址±1MB范围内的地址。下面是adr
指令的编码格式。立即数占用21位。
3.2.测试
下面是测试代码,使用adr
指令获取变量g_val3
和g_val4
的地址,并与通过&
获取的地址进行对比。
uint64_t g_val3 = 0;
uint64_t g_val4 = 0;#define ADR(label) ({ \uint64_t __adr_val__ = 0; \asm volatile("adr %0," __stringify(label) :"=r"(__adr_val__)); \__adr_val__; \
})static void adr_test()
{printf("g_val3 addr 0x%lx, adr_val1 0x%lx, adr_test addr 0x%lx\n",(uint64_t)&g_val3, ADR(g_val3), (uint64_t)adr_test);printf("g_val4 addr 0x%lx, adr_val2 0x%lx, adr_test addr 0x%lx\n",(uint64_t)&g_val4, ADR(g_val4), (uint64_t)adr_test);
}
下面是测试结果,使用&
获取的地址和通过adr
获取的地址相同。
g_val3 addr 0x5583e25018, adr_val1 0x5583e25018, adr_test addr 0x5583e14810
g_val4 addr 0x5583e25020, adr_val2 0x5583e25020, adr_test addr 0x5583e14810
下面是反汇编的代码。可以看出,adr
汇编代码中的偏移地址被objdump使用符号地址代替了,没有使用真正的偏移地址。g_val3
真正的偏移地址为0x107f4,g_val4
真正的偏移地址为0x107cc。执行第一条adr
指令的PC地址为0x5583e14824,则0x5583e14824+0x107f4=0x5583e25018为g_val3的地址。g_val4
的计算过程类似,不再赘述。
0000000000000810 <adr_test>: // 运行地址为0x5583e14810
......824: 10083fa0 adr x0, 11018 <g_val3> // 偏移地址为0x11018-0x824=0x107f4
......854: 10083e60 adr x0, 11020 <g_val4> // 偏移地址为0x11020-0x854=0x107cc
......isassembly of section .data:0000000000011000 <__data_start>:...
......
Disassembly of section .bss:
......
0000000000011018 <g_val3>: // 运行地址为0x5583e25018...0000000000011020 <g_val4>: // 运行地址为0x5583e25020...
4. adr_l
adr_l
是Linux内核定义的一个宏,用于获取基于PC相对偏移+/- 4 GB内的符号地址。在内核上下文中,使用adrp和add指令获取符号地址,而在内核模块上下文中,使用mov指令获取符号地址。
[arch/arm64/include/asm/assembler.h].macro adr_l, dst, sym
#ifndef MODULE /* 内核上下文中 */adrp \dst, \sym /* 获取符号所在页的基地址 *//* :lo12:\sym - 获取符号sym的低12位地址。符号所在页的基地址加上低12位地址就得到符号的完整地址 */add \dst, \dst, :lo12:\sym
#else /* 内核模块上下中 *//* 将符号的bit[64:48]地址加载到dst寄存器中,同时做overflow check,其他位清零 */movz \dst, #:abs_g3:\sym/* 将符号的bit[47:32]地址加载到dst寄存器中,不做overflow check,其他位保持不变 */movk \dst, #:abs_g2_nc:\sym/* 将符号的bit[31:16]地址加载到dst寄存器中,不做overflow check,其他位保持不变 */movk \dst, #:abs_g1_nc:\sym/* 将符号的bit[15:0]地址加载到dst寄存器中,不做overflow check,其他位保持不变 */movk \dst, #:abs_g0_nc:\sym
#endif.endm
mov指令获取地址的操作如下图所示。
MOVZ将16位立即数移至寄存器,并且除该立即数之外的所有其他位均设置为零,同时也可以将立即数向左移0、16、32或48位。
instruction value of x0
movz x0, #0x1f88 | 0x1f88
movz x0, #0x1f88, lsl #16 | 0x1f880000
MOVK移动16位立即数并将其保存到寄存器中,但寄存器中其他位保持不变。
instruction value of x0
movk x0, #0xb7fb, lsl #16 | 0xb7fb1f88
movk x0, #0x7f, lsl #32 | 0x7fb7fb1f88
参考资料
- linux-5.10.81原代码
- Arm ® Architecture Reference Manual Armv8, for A-profile architecture
- https://sourceware.org/binutils/docs/as/AArch64_002dRelocations.html#AArch64_002dRelocations
- https://stackoverflow.com/questions/64838776/understanding-arm-relocation-example-str-x0-tmp-lo12zbi-paddr?noredirect=1
- https://stackoverflow.com/questions/38570495/aarch64-relocation-prefixes(import)
- https://stackoverflow.com/questions/53268118/whats-the-difference-between-mov-movz-movn-and-movk-in-armv8-assembly
这篇关于ARMv8汇编指令-adrp、adr、adr_l的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!