背景
本文只是简单记录一下偶尔遇到的丢人问题……
众所周知,标记指针(tagged pointer)可以在内存地址的高位或者低位进行打标记操作。其中,高位能打标记的原因是虚拟地址不会用满 64 位。
但是问题在于,我能不能不移除标记,直接解引用?毕竟这样至少能节省一条 andq 指令。
不过很可惜,程序崩溃了。
canonical form
1.1.1 Memory Addressing
Long mode defines a 64-bit effective-address length. If a processor implementation does not support the full 64-bit virtual-address space, the effective address must be in canonical form (see “Canonical Address Form” on page 4).1.1.3 Canonical Address Form
Long mode defines 64 bits of virtual-address space, but processor implementations can support less. Although some processor implementations do not use all 64 bits of the virtual address, they all check bits 63 through the most-significant implemented bit to see if those bits are all zeros or all ones.来源:AMD 手册(卷二)
简单查询资料得知:即使虚拟地址超出四级分页/五级分页的范围,x86 处理器也会在地址翻译前做额外检查而非忽略。因此这是程序会崩掉的原因。
但是这么简单的需求,x86 处理器真的没有提供吗?(安卓早就用上某司 Top-byte Ignore 特性了!)
LAM/UAI
其实 Intel/AMD 已经分别提供了 Linear Address Masking (LAM) 和 Upper Address Ignore (UAI) 特性来支持高位地址屏蔽,只是一些安全原因没有尽早支持 Linux。
NOTES:
- Intel LAM 在 Linux 内核版本 v6.4 合入主线,理论上能依赖 arch_prctl 系统调用使能该特性。
- 而 AMD UAI 迄今没有任何消息,至少我在 git 找不到记录。
复现 UAI
我的主机是 AMD ZEN 4,因此尝试在 Linux 平台复现 UAI 特性。
5.10 Upper Address Ignore
The Upper Address Ignore feature provides a way for software to use bits 63:57 of an address as an
arbitrary software-assigned and software-interpreted tag. When this feature is enabled, these address
bits are excluded from the canonicality check that’s done when the address is used for certain memory
references.
5.10.1 Detecting and Enabling Upper Address Ignore
Support for the Upper Address Ignore feature is indicated by CPUID
Fn8000_0021_EAX[UpperAddressIgnore](bit 7)=1. The Upper Address Ignore feature is enabled by
setting EFER.UAIE (bit 20) in 64-bit mode (EFER.LMA=CS.L=1). EFER.UAIE is ignored outside of
64-bit mode.
这里提供一些 AMD 手册(卷二 §5.10)的说明。总之这里的寄存器操作要求必须是有特权级才能做到的事情,因此尝试使用内核模块。
uai.c - 内核模块
test.cpp - 测试程序
Makefile - 构建
后面总共用到三个文件。
uai.c
// 文件:uai.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cpu.h>
#include <linux/smp.h>
#include <linux/processor.h>
#include <asm/msr.h>
#include <asm/cpufeature.h>
#define MODULE_NAME "amd_uai_enabler"
// AMD CPUID function for UAI detection
#define UAI_CPUID_FN 0x80000021
#define UAI_CPUID_BIT (1 << 7) // EAX bit 7
// EFER MSR definitions
#ifndef MSR_EFER
#define MSR_EFER 0xC0000080
#endif
#define EFER_UAIE_BIT (1 << 20) // Bit 20
static bool uai_supported = false;
// 检查单个CPU是否支持UAI
static bool check_cpu_uai_support(unsigned int cpu)
{
struct cpuinfo_x86 *c = &cpu_data(cpu);
u32 eax, ebx, ecx, edx;
// 检查是否支持扩展CPUID函数
if (c->extended_cpuid_level < UAI_CPUID_FN) {
pr_info("CPU%d: UAI CPUID function not supported\n", cpu);
return false;
}
cpuid_count(UAI_CPUID_FN, 0, &eax, &ebx, &ecx, &edx);
if (eax & UAI_CPUID_BIT) {
pr_info("CPU%d: UAI supported\n", cpu);
return true;
}
pr_info("CPU%d: UAI not supported\n", cpu);
return false;
}
// 在单个CPU上启用UAI
static void enable_uai_on_cpu(void *unused)
{
u64 efer;
int cpu = smp_processor_id();
if (!check_cpu_uai_support(cpu)) {
pr_warn("CPU%d: Cannot enable UAI - not supported\n", cpu);
return;
}
rdmsrl(MSR_EFER, efer);
if (efer & EFER_UAIE_BIT) {
pr_info("CPU%d: UAI already enabled (EFER=0x%llx)\n", cpu, efer);
return;
}
wrmsrl(MSR_EFER, efer | EFER_UAIE_BIT);
rdmsrl(MSR_EFER, efer); // 验证
if (efer & EFER_UAIE_BIT) {
pr_info("CPU%d: UAI enabled successfully (EFER=0x%llx)\n", cpu, efer);
} else {
pr_err("CPU%d: Failed to enable UAI (EFER=0x%llx)\n", cpu, efer);
}
}
// 热插拔CPU回调
static int uai_cpu_online(unsigned int cpu)
{
if (uai_supported) {
smp_call_function_single(cpu, enable_uai_on_cpu, NULL, 1);
}
return 0;
}
static int uai_cpu_down_prep(unsigned int cpu)
{
// 不需要特别操作
return 0;
}
// 检查所有CPU的支持性
static bool check_all_cpus_support(void)
{
unsigned int cpu;
for_each_online_cpu(cpu) {
if (!check_cpu_uai_support(cpu)) {
pr_err("UAI not supported on all CPUs\n");
return false;
}
}
return true;
}
// 热插拔回调状态
static enum cpuhp_state uai_hp_state;
// 模块初始化
static int __init uai_enabler_init(void)
{
int ret;
pr_info("AMD UAI Enabler Module Loading\n");
// 检查所有CPU是否支持UAI
if (!check_all_cpus_support()) {
pr_err("System does not support UAI on all CPUs\n");
return -ENODEV;
}
uai_supported = true;
// 在所有在线CPU上启用UAI
on_each_cpu(enable_uai_on_cpu, NULL, 1);
// 注册热插拔回调
ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN, "uai:online",
uai_cpu_online, uai_cpu_down_prep);
if (ret < 0) {
pr_err("Failed to register CPU hotplug callback: %d\n", ret);
return ret;
}
uai_hp_state = ret;
pr_info("UAI enabled on all CPUs and hotplug registered\n");
return 0;
}
// 模块退出
static void __exit uai_enabler_exit(void)
{
if (uai_supported) {
// 注销热插拔回调
cpuhp_remove_state(uai_hp_state);
pr_info("CPU hotplug callback unregistered\n");
// 注意:通常不应禁用UAI,因为其他组件可能依赖它
// 这里仅显示如何禁用的示例(实际使用不建议)
pr_info("UAI remains enabled on all CPUs\n");
}
pr_info("AMD UAI Enabler Module Unloaded\n");
}
module_init(uai_enabler_init);
module_exit(uai_enabler_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("滴扑息克");
MODULE_DESCRIPTION("AMD Upper Address Ignore (UAI) Enabler");
MODULE_VERSION("1.0");
问了滴扑息克有啥意见,它建议每一个 CPU 都做一遍寄存器操作以避免不一致的行为,并且顺手做了 CPU 热插拔回调,因此有了上述内核模块的实现。
test.cpp
// 文件:test.cpp
#include <iostream>
#include <cstdint>
#include <cstdlib>
#include <cstring>
// 危险:高位Tagging (使用第58位)
void dangerous_high_bit_tagging() {
// 分配内存
int* ptr = new int(42);
std::cout << "危险高位Tagging - 原始地址: " << ptr
<< ", 值: " << *ptr << "\n";
// 设置第58位为1 (从0开始计数)
constexpr uintptr_t HIGH_BIT_58 = (1ULL << 58);
uintptr_t tagged_ptr = reinterpret_cast<uintptr_t>(ptr) | HIGH_BIT_58;
std::cout << "设置第58位的指针: "
<< reinterpret_cast<void*>(tagged_ptr) << "\n";
std::cout << " 原始地址的第58位: "
<< ((reinterpret_cast<uintptr_t>(ptr) & HIGH_BIT_58 ? "1" : "0"))
<< "\n";
std::cout << " 带Tag地址的第58位: "
<< ((tagged_ptr & HIGH_BIT_58) ? "1" : "0") << "\n";
// ==== 危险操作 ====
std::cout << "尝试解引用高位Tagging指针...\n";
int value = *reinterpret_cast<int*>(tagged_ptr); // 此处应崩溃!
std::cout << "值: " << value << "\n"; // 这行不会执行
delete ptr;
}
int main() {
std::cout << "==== 运行高位Tagging测试 ====\n";
dangerous_high_bit_tagging();
return 0;
}
Makefile
obj-m += uai.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
$(CXX) test.cpp -o test
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
rm -f test
使用 make 命令构建即可。
运行测试
场景 | 结果 |
---|---|
未加载模块运行 test | 触发段错误,dmesg 报告 GP 异常 |
加载模块后运行 test | 成功输出解引用的值 42 |
构建后的测试方法:
- 执行 ./test,理论上会在高位场景解引用失败。
- 执行 sudo insmod uai.ko 加载内核模块。
- (可选)sudo dmesg 查看日志是否开启 UAI 成功。
- 再次执行 ./test,解引用成功。
总结
因此,只有在使能 LAM/UAI 的情况下,程序才能避免 canonical form 的检查,从而减少标记指针场景下的 and mask 指令数。