从汇编代码调用的 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();
我正在尝试 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
.
通常,编译在一起的代码都非常接近,这已经足够了,但是对于 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();