咨询Linux内核中获取指定指令虚拟地址的正确方法
我需要获取Linux内核代码段中某一特定指令的地址,尝试用GCC的Labels as Values特性实现,最初的示例代码如下:
u64 address; noinline void foo() { address = (u64)&&label; // some code label: // 我需要的虚拟地址 // some code }
但得到的address值不正确,实际是&foo的地址,猜测原因是address是全局变量,这里是赋值而非初始化操作。
为了更清晰说明场景,我提供完整的测试代码:
u64 sgt[16][3]; void my_foo(void) { sgt[2][0] = (u64) &&begin; sgt[2][1] = (u64) &&end; begin: __asm__ volatile(" .byte 0x02, 0x00, 0x00, 0x55 \n\t"); end: return; }
我期望sgt[2][0]和sgt[2][1]分别记录&&begin和&&end的值,也就是内嵌汇编语句和return;对应的虚拟地址。但反汇编结果不符合预期:
void my_foo(void) { ffffffc008026d40: d503245f bti c sgt[2][0] = (u64) &&begin; ffffffc008026d44: d00062e0 adrp x0, ffffffc008c84000 <cpu_release_addr+0x688> ffffffc008026d48: 91070000 add x0, x0, #0x1c0 { ffffffc008026d4c: d503233f paciasp sgt[2][0] = (u64) &&begin; ffffffc008026d50: 90000002 adrp x2, ffffffc008026000 <arch_prctl_spec_ctrl_set+0x270> sgt[2][1] = (u64) &&end; ffffffc008026d54: 90000001 adrp x1, ffffffc008026000 <arch_prctl_spec_ctrl_set+0x270> sgt[2][0] = (u64) &&begin; ffffffc008026d58: 91351042 add x2, x2, #0xd44 sgt[2][1] = (u64) &&end; ffffffc008026d5c: 9135c021 add x1, x1, #0xd70 ffffffc008026d60: a9030402 stp x2, x1, [x0, #48] begin: __asm__ volatile(" .byte 0x02, 0x00, 0x00, 0x55 \n\t"); ffffffc008026d64: 55000002 .word 0x55000002 end: return; } ffffffc008026d68: d50323bf autiasp ffffffc008026d6c: d65f03c0 ret
我期望x2和x1的值分别为0xffffffc008026d64和0xffffffc008026d68,但实际得到的是0xffffffc008026d44和0xffffffc008026d70,求正确的实现方法。
问题根源
你遇到的问题是GCC的Labels as Values在内核编译环境下,受编译器优化、内核安全特性(如PAC/BTI指令插入)影响,标签地址的计算出现偏差,导致获取的地址并非目标指令的实际位置。
正确实现方法
方法1:内嵌汇编精准获取标签地址
通过内嵌汇编直接基于标签生成正确的虚拟地址,绕过编译器的优化干扰,示例如下:
u64 sgt[16][3]; void my_foo(void) { // 写入begin标签的地址到sgt[2][0] __asm__ volatile( "adrp x0, %0\n" "add x0, x0, :lo12:%0\n" "str x0, %1\n" : : "p" (begin), "m" (sgt[2][0]) : "x0" ); begin: __asm__ volatile(" .byte 0x02, 0x00, 0x00, 0x55 \n\t"); // 写入end标签的地址到sgt[2][1] __asm__ volatile( "adrp x0, %0\n" "add x0, x0, :lo12:%0\n" "str x0, %1\n" : : "p" (end), "m" (sgt[2][1]) : "x0" ); end: return; }
这段代码通过adrp+add组合生成标签的完整虚拟地址,直接写入目标变量,避免编译器优化带来的地址偏移。
方法2:禁用函数局部优化(临时方案)
给目标函数添加属性,强制编译器不优化函数内的代码布局,确保标签地址与指令位置一致:
u64 sgt[16][3]; __attribute__((noinline, optimize("O0"))) void my_foo(void) { sgt[2][0] = (u64) &&begin; sgt[2][1] = (u64) &&end; begin: __asm__ volatile(" .byte 0x02, 0x00, 0x00, 0x55 \n\t"); end: return; }
方法3:利用内核内置宏辅助验证
如果需要确认物理地址与虚拟地址的对应关系,可以使用内核提供的__pa_symbol()(虚拟地址转物理地址)和__va()(物理地址转虚拟地址)宏,但核心还是要确保初始的虚拟地址获取准确。
验证方式
编译内核模块或代码后,使用objdump -d查看反汇编,检查存储到sgt中的值是否与目标指令的地址完全匹配。若内核开启KASLR,虚拟地址会随机化,但同一编译镜像内的相对地址固定,不影响地址的正确性验证。
内容的提问来源于stack exchange,提问作者Thysrael

