汇编基础介绍——ARMv8指令集(五)

一、比较指令

1.1、CMN指令

我们已经介绍了 CMP 指令,CMP 指令还有另外一个变种——CMN,CMN 指令用来将一个数与另一个数的相反数进行比较。 指令的基本格式如下。

CMN <Xn|SP>, #<imm>{, <shift>}
CMN <Xn|SP>, <R><m>{, <extend> {#<amount>}}

上述两条 CMN 指令分别等同于如下的 ADDS 指令。

CMN 指令的计算过程就是把第一个操作数加上第二个操作数,计算结果会影响 PSTATE 寄存器的 N、 C、 V、Z 标志位。如果 CMN 后面的跳转指令使用与标志位相关的条件后缀,例如 CS 或者 CC 等,那么可以根据 N、 C、 V、Z 标志位来进行跳转。

1.2、CSEL 指令

CSEL 指令的格式如下。

CSEL <Xd>, <Xn>, <Xm>, <cond>

CSEL 指令的作用是判断 cond 是否为真。如果为真,则返回 Xn;否则,返回 Xm,把结果写入 Xd 寄存器中。

CSEL 指令常常需要和 CMP 比较指令搭配使用。

使用 CSEL 指令来实现下面的 C 语言函数。

unsigned long csel_test(unsigned long a, unsigned long b)
{
    if (a >= b)
        return b + 2;
    else
        return b - 1;
}

上述 C 语言代码可改成如下汇编代码。

.global csel_test
csel_test:
    cmp x0, x1
    add x2, x1, #2
    sub x3, x1, #1
    csel x0, x2, x3, ge
    ret

汇编函数采用 X0 寄存器的值作为第一个形参,以 X1 寄存器的值作为第二个形参。CSEL 指令里采用 GE 这个条件判断码,它会根据前面的 CMP 比较指令的结果进行判断,GE 表示无符号大于或者等于。

1.3、CSET 指令

CSET 指令的格式如下。

CSET <Xd>, <cond>

CSET 指令的意思是当 cond 条件为真时设置 Xd 寄存器为 1,否则设置为 0。

使用 CSET 指令来实现下面的 C 语言函数。

boot cset_test(unsigned long a, unsigned long b)
{
    return (a > b) ? true : false;
}

上述 C 语言代码改成汇编代码如下。

.global cset_test
cset_test:
    cmp x0, x1
    cset x0, hi
    ret

CSET 指令里采用 HI 这个条件判断码,它会根据前面的 CMP 比较指令的结果进行判断,HI 表示无符号大于。当满足条件时,cset_test 函数返回 1;否则,返回 0。

1.4、CSINC 指令

CSINC 指令的格式如下。

CSINC <Xd>, <Xn>, <Xm>, <cond>

CSINC 指令的意思是当 cond 为真时, 返回 Xn 寄存器的值; 否则, 返回 Xm寄存器的值加 1。

二、跳转与返回指令

2.1、跳转指令

编写汇编代码常常会使用跳转指令, A64 指令集提供了多种不同功能的跳转指令,如下表所示:

指令

描述

B

跳转指令。指令的格式如下。

B label

该跳转指令可以在当前 PC 偏移量 ±128 MB 的范围内无条件地跳转到 label 处

B.cond

有条件的跳转指令。指令的格式如下。

B.cond label

如 B.EQ,该跳转指令可以在当前 PC 偏移量 ±1 MB 的范围内有条件地跳转到 label 处

BL

带返回地址的跳转指令。指令的格式如下。

BL label
和 B 指令类似,不同的地方是,BL 指令将返回地址设置到 LR(X30 寄存器)中,保存的值为调用 BL 指令的当前 PC 值加上 4

BR

跳转到寄存器指定的地址。指令的格式如下。

BR Xn

BLR

跳转到寄存器指定的地址。指令的格式如下。

BLR Xn

和 BR 指令类似,不同的地方是,BLR 指令将返回地址设置到 LR(X30 寄存器)中

2.2、返回指令

A64 指令集提供了两条返回指令。

  • RET 指令:通常用于子函数的返回,其返回地址保存在 LR 里。
  • ERET 指令:从当前的异常模式返回。它会把 SPSR 的内容恢复到 PSTATE 寄存器中,从 ELR 中获取跳转地址并返回到该地址。ERET 指令可以实现处理器模式的切换,比如从 EL1 切换到 EL0。

2.3、比较并跳转指令

A64 指令集还提供了几个比较并跳转指令,如下表所示:

指令

描述

CBZ

比较并跳转指令。指令的格式如下。

CBZ Xt, label

判断 Xt 寄存器是否为 0,若为 0,则跳转到 label 处,跳转范围是当前 PC 相对偏移量 ±1 MB

CBNZ

比较并跳转指令。指令的格式如下。

CBNZ Xt, label

判断 Xt 寄存器是否不为 0,若不为 0,则跳转到 label 处,跳转范围是当前 PC 相对偏移量 ±1 MB

TBZ

测试位并跳转指令。指令的格式如下。

TBZ R<t>, #imm, label

判断 Rt 寄存器中第 imm 位是否为 0,若为 0,则跳转到 label 处,跳转范围是当前 PC 相对偏移量 ±32 KB

TBNZ

测试位并跳转指令。指令的格式如下。

TBNZ R<t>, #imm, label

判断 Rt 寄存器中第 imm 位是否不为 0,若不为 0,则跳转到 label 处,跳转范围是当前 PC 相对偏移量 ±32 KB

三、其他重要指令

3.1、PC相对地址加载指令

A64 指令集提供了 PC 相对地址加载指令——ADR 和 ADRP 指令。

ADR 指令的格式如下。

ADR <Xd>, <label>

ADR 指令加载一个在当前 PC 值±1 MB 范围内的 label 地址到 Xd 寄存器中。

ADRP 指令的格式如下。

ADRP <Xd>, <label>

ADRP 指令加载一个在当前 PC 值一定范围内的 label 地址到 Xd 寄存器中, 这个地址与 label 所在的地址按 4 KB 对齐,偏移量的范围为 -4 GB~4 GB。

既然 LDR 伪指令和 ADRP 指令都可以加载 label 的地址,而且 LDR 伪指令可以寻址 64 位的地址空间,而 ADRP 指令的寻址范围为当前 PC 地址 ±4 GB,那么有了 LDR 伪指令为什么还需要 ADRP 指令呢?

本文借助AI工具回答这个问题,仅供参考。

1、代码效率方面

ADRP指令在生成页面对齐地址时,指令格式更简洁,执行效率更高。编译器在处理ADRP指令时,相对更容易进行优化。而LDR伪指令在某些情况下,编译器需要额外的处理来生成合适的机器码。

示例
假设要加载一个距离当前PC地址不远的全局变量地址。

.global my_variable
my_variable:.word 0x12345678

    ADRP X0, my_variable
    ADD X0, X0, :lo12:my_variable

在上述代码中,ADRP指令快速生成my_variable所在页面的基地址到X0寄存器,然后通过ADD指令加上低 12 位偏移量获取准确地址。


 

如果使用LDR伪指令:

.global my_variable
my_variable:.word 0x12345678

    LDR X0, =my_variable

编译器可能会将其转换为更复杂的指令序列,比如先将地址存储在文字池(literal pool),再从文字池加载到寄存器,相比之下会多一些额外的操作,在代码执行效率上可能不如ADRP

2、位置无关代码(PIC)编写方面

在编写共享库等需要位置无关代码的场景下,ADRP指令有着天然的优势。它基于PC相对寻址,使得代码在不同的内存位置都能正确执行。而LDR伪指令虽然也能加载地址,但在位置无关性的处理上没有ADRP指令直接。
 

示例
在共享库中定义一个函数来访问全局变量:

// 共享库中的代码
.global shared_variable
shared_variable:.word 0x98765432

    .section ".text"
    .global my_shared_function
my_shared_function:
    ADRP X1, shared_variable
    ADD X1, X1, :lo12:shared_variable
    LDR W2, [X1]
    // 对加载到的变量值进行处理
    //...
    RET

当这个共享库被加载到不同进程的不同内存地址时,ADRP基于PC相对计算地址的方式,能保证正确获取到shared_variable的地址。

如果使用LDR伪指令:

// 尝试使用LDR伪指令的方式
.global shared_variable
shared_variable:.word 0x98765432

    .section ".text"
    .global my_shared_function
my_shared_function:
    LDR X1, =shared_variable
    LDR W2, [X1]
    // 对加载到的变量值进行处理
    //...
    RET

在共享库被加载到不同位置时,编译器处理LDR伪指令生成的代码可能无法正确适应地址的变化,导致获取到错误的地址。

3.2、内存独占访问指令

ARMv8 体系结构都提供独占内存访问(exclusive memory access)的指令。在 A64 指令集中, LDXR 指令尝试在内存总线中申请一个独占访问的锁,然后访问一个内存地址。 STXR 指令会往刚才 LDXR 指令已经申请独占访问的内存地址中写入新内容。通常组合使用 LDXR 和 STXR 指令来完成一些同步操作。

另外,ARMv8 还提供多字节( 字节)独占访问的指令,即 LDXP 和 STXP 指令。独占内存访问指令如下表所示:

指令

描述

LDXR

独占内存访问指令。指令的格式如下。

LDXR Xt, [Xn|SP{,#0}]

STXR

独占内存访问指令。指令的格式如下。

STXR Ws, Xt, [Xn|SP{,#0}]

LDXP

多字节独占内存访问指令。指令的格式如下。

LDXP Xt1, Xt2, [Xn|SP{,#0}]

STXP

多字节独占内存访问指令。指令的格式如下。

STXP Ws, Xt1, Xt2, [Xn|SP{,#0}]

3.3、异常处理指令

A64 指令集支持多个异常处理指令,如下表所示:

指令

描述

SVC

系统调用指令。指令的格式如下。

SVC #imm

允许应用程序通过 指令自陷到操作系统中,通常会陷入 EL1

HVC

虚拟化系统调用指令。指令的格式如下。
HVC #imm

允许主机操作系统通过 HVC 指令自陷到虚拟机管理程序(hypervisor)中,通常会陷入 EL2

SMC

安全监控系统调用指令。指令的格式如下。

SMC #imm
允许主机操作系统或者虚拟机管理程序通过 SMC 指令自陷到安全监管程序(secure monitor)中,通常会陷入 EL3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值