Rust – RFC 2867 isa_attribute

功能名称:isa_attribute启动日期:2020-02-16RFC PR: rust-lang/rfcs#2867Rust Issue: rust-lang/rust#74727

摘要

本 RFC 提议引入一个新的函数属性
#[instruction_set(set)]
,允许你声明编译该函数时所用的指令集。同时为 ARM 架构初步支持两个可选值(
arm::a32

arm::t32
)。后续可为其他架构/值进行扩展。

动机


ARMv4T
起,许多 ARM CPU 支持两套不同的指令集。最初称为“ARM 代码”和“Thumb 代码”,随着
AArch64
的发展,现在称为
a32

t32
。与 x86_64 架构(x86/64 只能二选一)不同,ARM 可以在单个程序里交错使用
a32

t32
。特定分支指令允许 CPU 在跳转时切换模式,故可以针对每个函数指定为
a32
还是
t32

在 LLVM 中,选择生成
a32
还是
t32
代码是通过启用(
t32
)或禁用(
a32

thumb-mode
target feature 实现的。以往 Rust 通过
target_feature
属性可以实现此操作(既能增加也能移除 LLVM 特性)。但 RFC 2045 最终不再支持“减去”特性,其设计只适用于“开启”附加特性,不适用于
a32
/
t32
这种“二选一”的场景。

用户指南级说明

部分平台支持在单一程序中混用多种指令集,一般每种指令集适合不同场景。每个目标平台有默认指令集,若希望指定某函数用其他指令集,可用
#[instruction_set(set)]
属性。

目前该属性只适用于 ARM CPU,支持
arm::a32

arm::t32
。以
arm
开头的目标(如
arm-linux-androideabi
)默认用
arm::a32
,以
thumb
开头(如
thumbv7neon-linux-androideabi
)默认用
arm::t32


// 使用目标平台默认指令集
fn add_one(x: i32) -> i32 {
    x + 1
}

// 在 arm 和 thumb 目标上都以 a32 编译
#[instruction_set(arm::a32)]
fn add_five(x: i32) -> i32 {
    x + 5
}

若指定了目标不支持的指令集会编译报错。为了可移植性,建议用
cfg_attr
配合目标判断。


// 如果目标不支持 arm::a32,会编译失败
#[instruction_set(arm::a32)]
fn add_five(x: i32) -> i32 {
    x + 5
}

// 只在 ARM 目标上应用 instruction_set 属性
#[cfg_attr(target_cpu="arm", instruction_set(arm::a32))]
fn add_six(x: i32) -> i32 {
    x + 6
}

如上所示,写法有点繁琐,建议大量用到时写一个语法糖 proc-macro。

何时需要指定非默认指令集,完全视平台文档要求。若无特殊需求,一般无需关心该属性。

参考级说明

每个目标现在都被视为有一个默认指令集(无属性时使用),也可能支持额外指令集:


arm
开头的目标默认用
arm::a32
,也支持
arm::t32

thumb
开头的目标默认用
arm::t32
,也支持
arm::a32
目前该属性未对其他架构定义为避免命名冲突,属性值统一前缀 arch 名,例如
arm::a32

使用范围:

可用于任何有函数体的
fn
项(自由函数、固有方法、trait 默认方法、trait impl 方法)不可用于闭包或
extern
块声明(未来可能支持 trait 原型)

编译错误情况:

如指定不存在的指令集(如“unicorn”),则编译错误。后续编译器可扩展更多合法值。多次指定不同架构的指令集也是允许的。

保证:

若指定了指令集,编译器必须遵守。不是建议,是硬性保证。具体保证细节依赖目标平台特别注意该属性可能影响函数内联和内联汇编,具体表现依赖目标架构

ARM

(以下为更深入的技术细节)

ARM 有两种指令编码。汇编源码形式下 Thumb 汇编是 ARM 汇编的子集,但实际汇编出的二进制码完全不同。CPU 的程序状态寄存器有一位决定此时是否用
a32
(4 字节指令)还是
t32
(2 字节指令)。解码错误编码会导致未定义行为。

外部可通过函数地址判断类型:
a32
代码地址为偶数,
t32
为奇数。程序计数器会忽略最低位,所以
t32
依然“2 字节对齐”。用
bx

blx
指令跳转时,目标地址最低位决定切换模式;用
b

bl
不切换。

所以,编译器需确保:

保证函数地址奇偶与编码一致,且函数体起始采用正确编码建议整个函数体用单一编码若必要,允许编译器用一个正确编码和地址的存根跳转到另一种编码实现(技术上符合要求,但应作为兜底方案)

后端支持:

LLVM 可通过开启/关闭
thumb-mode
特性实现未来如 Cranelift 之类后端可用类似机制。粗暴方案是按不同指令集拆分到不同编译单元,再由链接器处理。当前 Cranelift 尚不支持 ARM。Miri 只运行在 MIR 层,与指令集无关。若将来支持 inline assembly,需考虑该属性或直接不支持。ARM 汇编/链接器有对应 interwork 标志。手工汇编并链接的用户需正确配置,但这主要是实现细节,我们应尽量在文档中提醒并提供合理默认值。

内联:

被内联后不再跳转地址,则属性失效若带有 instruction_set 的函数中含 inline assembly,情况更复杂。即便内联后的汇编文本在新指令集下有效,要判定这一点需分析汇编字符串,这违背了 inline assembly“黑盒”原则。此外,何时发生内联对程序员来说并不总是可知。如何解决该问题是一个未决问题(见下文)。

缺点

增加一个新属性会让 Rust 设计更复杂。

方案与替代

方案理由

下面是一个实际用例(GBA 游戏开发),省略了 MMIO 和启动相关的细节:


// GBA BIOS 提供的功能,通过软中断调用。假设已由汇编“运行时”导出。
extern "C" fn {
    /// 进入低功耗状态,等待垂直消隐中断,之后返回
    VBlankInterWait(isize, isize);
}

// 假设 MMIO 已 import,此处省略。
// use all_the_gba_mmio_definitions::*;

fn main() {
    unsafe {
        INTR_FN_ADDR.write_volatile(core::transmute(my_inter_fn));
        DISPSTAT.write_volatile(DISPSTAT_VBLANK);
        IME.write_volatile(IME_VBLANK);
        IE.write_volatile(true);
        DISPCNT.write_volatile(MODE3_BG2);
        let mut x = 0;
        loop {
            VBlankInterWait(0, 0);
            VRAM_MODE3.row(0).col(x).write(RED);
            x += 1;
            if x >= VRAM_MODE3::WIDTH { x = 0 }
        }
    }
}

/// 响应中断,清除所有中断标志后直接返回
#[instruction_set(arm::a32)]
fn my_inter_fn() {
    INTER_BIOS_FLAGS.write_volatile(ALL_INTER_FLAGS);
    INTER_STANDARD_FLAGS.write_volatile(ALL_INTER_FLAGS);
}

此例实际流程:

设置设备的中断处理函数配置设备每次垂直消隐开始触发中断设置显示模式并进入主循环每帧等待消隐,然后画一个红点

此平台 BIOS 是 a32 代码,使用 b 跳转而非 bx(会切换模式)。因此 BIOS 跳转到你的中断处理函数时仍处于 a32 状态。如果中断处理函数编译为 t32,立即造成未定义行为。

替代方案

扩展
target_feature
,如
#[target_feature(disable = "...")]
并将
thumb-mode
加入白名单,可以无需新属性实现此功能;但这不符合
target_feature
目前的设计理念,后者侧重于可选特性(如 AVX、SSE),其缺失并不一定意味着“只能二选一”。什么都不做也是一种选择;目前可以通过外部汇编和构建脚本引入不同指令集代码,但体验很差。需要注意的是,这一特性主要提升了对 ARM 旧设备的支持。新设备(内存充裕)一般不再需要 t32 带来的空间节省,直接全用 a32 即可。

相关先例

C 语言可用
__attribute__((target("arm")))

__attribute__((target("thumb")))
实现类似功能。这是编译器扩展(GCC、Clang 都支持),详见 LLVM/clang 的实现 PR。

未决问题

如何保证
instruction_set
与内联汇编始终正确交互?这不是实现阻碍,但需在属性稳定前解决。
目前 LLVM 不会将 a32 函数内联进 t32 函数,反之亦然,因为它们视为不同目标。但这并非 LLVM 的强保证,需继续调研。

未来可能性

若 Rust 支持 65C816,则
#[instruction_set(?)]
可扩展用于切换 65C02 兼容模式。MIPS 也有类似 ARM 的 16 位编码,函数地址最低位为 1 时采用该编码。未来可能允许该属性用于 trait 原型,这样所有 impl 都会继承;不过这样规范和实现会复杂,而实际收益有限。现在 impl 上用该属性已经足够。LLVM 未来可能支持跨指令集调用(如混合 PowerPC/RISC-V)。

原文地址:https://rust-lang.github.io/rfcs/2867-isa-attribute.html

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...