从汇编代码调用的 Rust 函数访问共享内存时的一般保护错误

General protection fault when a Rust function called from assembly code accesses shared memory

我正在尝试 assemble 并在运行时调用用 Rust 编写的解释器项目中的代码。我为此使用 assembler crate。我想要 extern "C" fn 围绕 JIT 代码可以直接调用的重要运行时功能的包装器。为了尽量减少生成的代码段的复杂性,我想将解释器状态保留为全局变量。

但是,每当我尝试访问 and/or 修改任何全局状态时,程序总是崩溃。一个简单的 println!("hello world") 崩溃并出现一般保护错误,似乎是在 stdio::_print() 访问 stdout 时。一个带有 static mut 变量的更简单示例也会转储核心。有趣的是,虽然前者在 Valgrind 下崩溃并带有整洁的堆栈跟踪,但后者的示例运行无误,实际上传递了暗示程序按预期工作的断言。请注意,静态变量不需要是可变的,读取它足以导致崩溃。

我完全不知道 mmap 详细信息 assembler 板条箱在下面使用,但我找不到任何提示说明我的方法为什么会崩溃。任何指导将不胜感激。

我创建了一个 Repl.it repl with an MRE

use anyhow::Result;
use assembler::*;
use assembler::mnemonic_parameter_types::{registers::*, immediates::*};

const CHUNK_LENGTH: usize = 4096;
const LABEL_COUNT: usize = 64;

static mut X: u32 = 0;

#[no_mangle]
unsafe extern "C" fn foo() {
    // printing here will lead to a coredump,
    // Valgrind will provide more insight (general protection fault)
  
    // println!("hello world");

    // modifying a global variable instead will also dump
    // core but will run without fail in Valgrind
    X += 1
}

fn main() -> Result<()> {
    let mut memory_map = ExecutableAnonymousMemoryMap::new(CHUNK_LENGTH, true, true)?;
    let mut instr_stream = memory_map.instruction_stream(&InstructionStreamHints {
        number_of_labels: LABEL_COUNT,
        ..Default::default()
    });

    let f = instr_stream.nullary_function_pointer::<i64>();
    instr_stream.call_function(foo as unsafe extern "C" fn());
    instr_stream.mov_Register64Bit_Immediate64Bit(Register64Bit::RAX, Immediate64Bit(0x123456789abcdef0));
    instr_stream.ret();
    instr_stream.finish();

    assert_eq!(unsafe { f() }, 0x123456789abcdef0);
    assert_eq!(unsafe { X }, 1);
    Ok(())
}

这是两种情况下的 Valgrind 输出。

全局变量:

==4186== Memcheck, a memory error detector
==4186== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==4186== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info
==4186== Command: target/debug/jit-ffi-fault-mre
==4186== 
==4186== 
==4186== HEAP SUMMARY:
==4186==     in use at exit: 0 bytes in 0 blocks
==4186==   total heap usage: 14 allocs, 14 frees, 199,277 bytes allocated
==4186== 
==4186== All heap blocks were freed -- no leaks are possible
==4186== 
==4186== For lists of detected and suppressed errors, rerun with: -s
==4186== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

println!:

==4341== Memcheck, a memory error detector
==4341== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==4341== Using Valgrind-3.16.1 and LibVEX; rerun with -h for copyright info
==4341== Command: target/debug/jit-ffi-fault-mre
==4341== 
==4341== 
==4341== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==4341==  General Protection Fault
==4341==    at 0x13FB7A: std::io::stdio::_print (stdio.rs:1028)
==4341==    by 0x1174B0: foo (main.rs:15)
==4341==    by 0x4E58004: ???
==4341==    by 0x11A1BC: jit_ffi_fault_mre::main (main.rs:35)
==4341==    by 0x113FEA: core::ops::function::FnOnce::call_once (function.rs:248)
==4341==    by 0x11497D: std::sys_common::backtrace::__rust_begin_short_backtrace (backtrace.rs:122)
==4341==    by 0x114E70: std::rt::lang_start::{{closure}} (rt.rs:145)
==4341==    by 0x13CA95: call_once<(), (dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe)> (function.rs:280)
==4341==    by 0x13CA95: do_call<&(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe), i32> (panicking.rs:492)
==4341==    by 0x13CA95: try<i32, &(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe)> (panicking.rs:456)
==4341==    by 0x13CA95: catch_unwind<&(dyn core::ops::function::Fn<(), Output=i32> + core::marker::Sync + core::panic::unwind_safe::RefUnwindSafe), i32> (panic.rs:137)
==4341==    by 0x13CA95: {closure#2} (rt.rs:128)
==4341==    by 0x13CA95: do_call<std::rt::lang_start_internal::{closure_env#2}, isize> (panicking.rs:492)
==4341==    by 0x13CA95: try<isize, std::rt::lang_start_internal::{closure_env#2}> (panicking.rs:456)
==4341==    by 0x13CA95: catch_unwind<std::rt::lang_start_internal::{closure_env#2}, isize> (panic.rs:137)
==4341==    by 0x13CA95: std::rt::lang_start_internal (rt.rs:128)
==4341==    by 0x114E3F: std::rt::lang_start (rt.rs:144)
==4341==    by 0x11A37B: main (in /home/runner/UnsightlyAwfulPhases/jit-ffi-fault-mre/target/debug/jit-ffi-fault-mre)
==4341== 
==4341== HEAP SUMMARY:
==4341==     in use at exit: 85 bytes in 3 blocks
==4341==   total heap usage: 14 allocs, 11 frees, 199,277 bytes allocated
==4341== 
==4341== LEAK SUMMARY:
==4341==    definitely lost: 0 bytes in 0 blocks
==4341==    indirectly lost: 0 bytes in 0 blocks
==4341==      possibly lost: 0 bytes in 0 blocks
==4341==    still reachable: 85 bytes in 3 blocks
==4341==         suppressed: 0 bytes in 0 blocks
==4341== Rerun with --leak-check=full to see details of leaked memory
==4341== 
==4341== For lists of detected and suppressed errors, rerun with: -s
==4341== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
/tmp/nix-shell-4318-0/rc: line 1:  4341 Segmentation fault      (core dumped) valgrind target/debug/jit-ffi-fault-mre

我知道这段代码有两个问题。

一个来自call_function文档中的评论:

32-bit displacement sign extended to 64-bits in 64-bit mode.

WARNING: The location of emitted code may be such that if it is more than 2Gb away from common library function calls (eg printf); it may be preferrable to use an absolute address indirectly in this case, eg call_Register64Bit or call_Any64BitMemory.

这是一种奇特的说法,这个 call 的参数是一个相对于当前 rip.

的 32 位值

通常,编译在一起的代码都非常接近,这已经足够了,但是对于 Rust 编译函数和动态分配的匿名映射,不能保证它们相距小于 2GB。

例如,在我的系统中,foo 位于地址 0x559a8039f5a0 而匿名内存位于 0x40000000。那超过了 87657 GB!

解决方案是按照文档说明进行操作,并进行 64 位绝对跳转,例如使用 rax.

另一个问题是在 x86_64 ABI 中,堆栈必须对齐到 16 字节。但是对 nullary 函数执行 call 只会将 8 个字节压入堆栈并且它会错位。

要解决此问题,函数需要以某种方式重新对齐堆栈。如果函数有本地自动存储,它是通过保留 16 的倍数加 8 的字节数来完成的。不使用自动存储的函数,比如你的,通常只是在开始时做一个随机的 push 和一个末尾对应pop.

工作代码类似于:

// push %rax
instr_stream.push_Register64Bit_r64(Register64Bit::RAX);
// movabs foo, %rax
instr_stream.mov_Register64Bit_Immediate64Bit(
    Register64Bit::RAX,
    Immediate64Bit(foo as i64)
);
// call *%rax
instr_stream.call_Register64Bit(Register64Bit::RAX);
// movabs 0x123456789abcdef0, %rax
instr_stream.mov_Register64Bit_Immediate64Bit(Register64Bit::RAX, Immediate64Bit(0x123456789abcdef0));
// pop %ecx
instr_stream.pop_Register64Bit_r64(Register64Bit::RCX);
// ret
instr_stream.ret();
instr_stream.finish();