浅析ARMv8汇编指令adrp和adr

目录
  • 1.概述
  • 2.adrp
    • 2.1.定义
    • 2.2.测试
  • 3.adr
    • 3.1.定义
    • 3.2.测试
  • 参考资料

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_OFFSET
	and	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_val1g_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_val1g_val2的地址分别为0x5583e250280x5583e26028g_val1的页基地址为0x5583e25000g_val2页的基地址为0x5583e26000adrp_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址低低12位清零,得到0x1100,将当前adrp指令所在地址的低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_val3g_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
    ...

参考资料

  1. linux-5.10.81原代码
  2. Arm Architecture Reference Manual Armv8, for A-profile architecture

到此这篇关于ARMv8汇编指令-adrp和adr的文章就介绍到这了,更多相关ARMv8汇编指令内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • GNU ARM汇编语法原理及操作解析

    这篇文章主要介绍了GNU ARM汇编语法原理及操作解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 ARM汇编源程序有两种风格: ARM官方推荐的风格,所有的指令都大写.常用在windows下的IDE中. GNU风格的汇编风格,所有的指令都小写.常用在与Linux相关的工具中. 注释符号: GNU arm汇编所采用的注释符号是@符号,同样c语言中的 // 和 /* */ 两种类型的注释方法在GNU arm汇编中也被支持. 宏定义: 在GNU

  • ARM体系下的GCC内联汇编教程详解

    在操作系统级的编程中,有时候,C语言并不能完全的使用硬件的功能,这时候就需要嵌入一些汇编代码来实现功能. 有两种方式可以使C语言和assemly语言一起工作,一种是两种语言分开写成两个文件,链接的时候链接成一个文件;另一种就是在C语言中嵌入汇编代码.下面简单介绍一下如何在GCC中嵌入汇编代码. GCC规定了一个内联汇编的语法,不同硬件平台上的GCC内联汇编几乎都是这样的: asm( 汇编指令列表 :输出运算符列表 :输入运算符列表 :被更改的资源列表 }; 在GCC中插入汇编代码,需要以asm关

  • ARM汇编解决阶乘及大小写转换的问题

    环境以及硬件 一.硬件仿真基于 SAMSUNG's S3C44B0X 16/32-bit RISC microprocessor 芯片,仿真器为 J-LINK 二.编写指令软件为 Integrated Development Environment ,软件仿真为 ARMulate.dll 三.需要基于ARM7硬件平台的C语言启动代码,用于分配中断向量表,初始化ISR地址,初始化堆栈空间,初始化应用程序执行环境,配置存储器系统,设定时钟周期,呼叫主应用程序. 四.这里仅有关键算法代码 ARM汇编求

  • ARM汇编逆向iOS 实战

    我们先讲一些ARM汇编的基础知识.(我们以ARMV7为例,最新iPhone5s上的64位暂不讨论) 基础知识部分: 首先你介绍一下寄存器: R0-R3:用于函数参数及返回值的传递 R4-R6, R8,R10-R11:没有特殊规定,就是普通的通用寄存器 R7:栈帧指针(Frame Pointer).指向前一个保存的栈帧(stack frame)和链接寄存器(link register, lr)在栈上的地址. R9:操作系统保留 R12:又叫IP(intra-procedure scratch),

  • 使用ARM汇编破解iOS程序基础知识分享

    一.Thumb指令与ARM指令 Thumb指令为16位,因此存储代码的密度高,节省存储空间.但是功能不全,它只是ARM指令(32位)集的补充,是ARM指令集下的一个子集.在初级阶段我们不需要了解这些知识,只要有个概念知道有这么个东西就可以. 二.ARM的寄存器初步了解 R0-R3: 用于函数参数及返回值的传递,超过4个参数,其它参数存在栈中,在ARM中栈是向下生长的,R0还可以作为返回值. R4-R6, R8,R10-R11: 没有特殊规定,就是普通的通用寄存器 R7: 栈帧指针,指向母函数与被

  • 浅析ARMv8汇编指令adrp和adr

    目录 1.概述 2.adrp 2.1.定义 2.2.测试 3.adr 3.1.定义 3.2.测试 参考资料 1.概述 在阅读Linux内核代码时,经常能碰到汇编代码,网上能查的资料千篇一律,大多都描述的很模糊.俗话说,实践是检验真理的唯一标准,我们就参考官方文档,自己写汇编代码并反汇编,探寻其中的奥妙. 2.adrp 在Linux内核启动代码primary_entry中,使用adrp指令获取Linux内核在内存中的起始页地址,页大小为4KB,由于内核启动的时候MMU还未打开,此时获取的Linux

  • 浅析Go汇编语法和MatrixOne使用介绍

    目录 MatrixOne数据库是什么? Go汇编介绍 为什么使用Go汇编? 为什么不用CGO? Go汇编语法特点 操作数顺序 寄存器宽度标识 函数调用约定 对写Go汇编代码有帮助的工具 avo text/template 在Go汇编代码中使用宏 在MatrixOne数据库中的Go语言汇编应用 基本向量运算加速 Go语言无法直接调用的指令 编译器无法达到的特殊优化效果 MatrixOne是一个新一代超融合异构数据库,致力于打造单一架构处理TP.AP.流计算等多种负载的极简大数据引擎.MatrixO

  • 常用的汇编指令与技巧(收藏)

    1.数据传送指令:mov move r1,r2 /*r1=r2*/ move r1,#4096 /*r1=4096*/ 2.大范围的地址读取指令:ldr ldr r1,=0x123456789 /*r1=0x123456789*/ ldr r1,=label /*获取绝对地址,即label的地址*/ label: -- 3.内存访问指令(当ldr后面没有=号时为内存读取指令) 读取指令:ldr ldr r1 ,[r2,#4] /*将内存地址为r2+4的数据读取到r1中,相当于C语言中的*操作*/

  • C 表达式中的汇编指令

    asm 为 gcc 中的关键字,asm 表达式为在 C代码中嵌套汇编指令,该表达式只是单纯的替换出汇编代码,并不对汇编代码的含义进行解析. asm 表达式有两种形式,第二种 asm-qualifiers 包含了 goto 语句. 第一种形式为常见的用法,AssemblerTemplate 和 OutputOperands 必须存在, 其中 Clobbers 存在需要 InputOperands 也出现. asm asm-qualifiers ( AssemblerTemplate : Outpu

  • 汇编语言伪指令和汇编指令的区别

    [指令语句] 每一条指令语句在源程序汇编时都要产生可供计算机执行的指令代码(即目标代码),所以这种语句又叫可执行语句.每一条指令语句表示计算机具有的一个基本能力,如数据传送,两数相加或相减,移位等,而这种能力是在目标程序(指令代码的有序集合)运行时完成的,是依赖于汁算机内的中央处理器(CPU).存储器.I/O接口等硬件设备来实现的. [伪指令语句] 伪指令语句是用于指示汇编程序如何汇编源程序,所以这种语句又叫命令语句.例如源程序中的伪指令语句告诉汇编程序:该源程序如何分段,有哪些逻辑段在程序段中

  • 汇编指令:JO、JNO、JB..的使用方法

    汇编指令: JO.JNO.JB.JNB.JE.JNE.JBE.JA.JS.JNS.JP.JNP.JL.JNL.JNG.JG.JCXZ.JECXZ.JMP.JMPE 名称 功能 操作数 操作码 模数 寄存器1 寄存器2 或内存 位移量 立即数 符号 方向 芯片 型号 16位 32位 JO 溢出跳转 短 $70 无 无 无 无 10 无 无 8086 无 无 JNO 不溢出跳转 短 $71 无 无 无 无 10 无 无 8086 无 无 JB 低于跳转 短 $72 无 无 无 无 10 无 无 80

  • 汇编语言指令大全 X86和X87汇编指令大全(带注释)

    目录 一.数据传输指令 1. 通用数据传送指令. 2. 输入输出端口传送指令. 3. 目的地址传送指令. 4. 标志传送指令. 二.算术运算指令 三.逻辑运算指令 四.串指令 五.程序转移指令 六.伪指令 七.处理机控制指令:标志处理指令 浮点运算指令集 1.控制指令 2.数据传送指令 3.比较指令 4.运算指令 其它 一.机械码,又称机器码. 二.需要熟练掌握的全部汇编知识(只有这么多) 三.常见修改(机器码) 四.两种不同情况的不同修改方法 一.数据传输指令 它们在存贮器和寄存器.寄存器和输

  • 汇编语言:x86汇编指令大全及其注意事项

    目录 Part 1:instruction Part 2 2.1 (逻辑)运算.移位等常用指令 2.1 (逻辑)运算.移位等常用指令 2.2 循环移位指令 2.3 数据串操作指令 2.4 逻辑运算指令 2.5 基于大小关系的跳转指令 2.6 基于单标志位的转移指令 Part 1:instruction 积少成多,持续更新.(这将会是一个极其漫长的过程) 表格中各条指令的顺序根据笔者所认为的重要或常用程度进行排序,仅供参考. Part 2 本表格中所涉及的F是指状态寄存器,CF指进位标志位,其它以

  • 汇编语言入门汇编指令及寄存器详解教程

    目录 前言 什么是汇编语言 汇编语言产生的原因 汇编与二进制的关系 寄存器 寄存器作用 存取速度比较 寄存器分类 常用寄存器用途 寄存器EAX.AX.AH.AL的关系 汇编语言指令 数据传送指令 算术运算指令 逻辑运算指令 循环控制指令 转移指令 linux 和 windows 下汇编的区别 总结 前言 我们大都是被高级语言惯坏了的一代,源源不断的新特性正在逐步添加到各类高级语言之中,汇编作为最接近机器指令的低级语言,已经很少被直接拿来写程序了,不过我还真的遇到了一个,那是之前的一个同事,因为在

  • LyScript获取上一条与下一条汇编指令的方法详解

    LyScript 插件默认并没有提供上一条与下一条汇编指令的获取功能,当然你可以使用LyScriptTools工具包直接调用内置命令得到,不过这种方式显然在效率上并不理想,我们需要在LyScript插件API基础上自己封装实现这个功能. LyScript项目地址:https://github.com/lyshark/LyScript 获取下一条汇编指令 下一条汇编指令的获取需要注意如果是被命中的指令则此处应该是CC断点占用一个字节,如果不是则正常获取到当前指令即可. 1.我们需要检查当前内存断点

随机推荐