Skip to content

Commit 510e62c

Browse files
committed
Auto merge of #1559 - Aaron1011:new-miri-backtrace, r=RalfJung
Add API for capturing backtrace This PR adds two new Miri-defined extern functions: `miri_get_backtrace` and `miri_resolve_frame`, which are documented in the README. Together, they allow obtaining a backtrace for the currently executing program. I've added a test showing how these APIs are used. I've also prepared a companion PR `backtrace-rs`, which will allow `backtrace::Backtrace::new()` to work automatically under Miri. Once these two PRs are merged, we will be able to print backtraces from the normal Rust panic hook (since libstd is now using backtrace-rs). A few notes: * Resolving the backtrace frames is *very* slow - you can actually see each line being printed out one at a time. Some local testing showed that this is not (primrary) caused by resolving a `Span` - it seems to be just Miri being slow. * For the first time, we now interact directly with a user-defined struct (instead of just executing the user-provided MIR that manipulates the struct). To allow for future changes, I've added a 'version' parameter (currently required to be 0). This should allow us to change the `MiriFrame` struct should the need ever arise. * I used the approach suggested by `@oli-obk` - a returned backtrace pointer consists of a base function allocation, with the 'offset' used to encode the `Span.lo`. This allows losslessly reconstructing the location information in `miri_resolve_frame`. * There are a few quirks on the `backtrace-rs` side: * `backtrace-rs` calls `getcwd()` by default to try to simplify the filename. This results in an isolation error by default, which could be annoying when printing a backtrace from libstd. * `backtrace-rs` tries to remove 'internal' frames (everything between the call to `Backtrace::new()` and the internal API call made by backtrace-rs) by comparing the returned frame pointer value to a Rust function pointer. This doesn't work due to the way we construct the frame pointers passed to the caller. We could attempt to support this kind of comparison, or just add a `#[cfg(miri)]` and ignore the frames ourselves.
2 parents aa832c1 + 7fba3c2 commit 510e62c

10 files changed

+253
-2
lines changed

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,34 @@ extern "Rust" {
266266
/// `ptr` has to point to the beginning of an allocated block.
267267
fn miri_static_root(ptr: *const u8);
268268

269+
/// Miri-provided extern function to obtain a backtrace of the current call stack.
270+
/// This returns a boxed slice of pointers - each pointer is an opaque value
271+
/// that is only useful when passed to `miri_resolve_frame`
272+
/// The `flags` argument must be `0`.
273+
fn miri_get_backtrace(flags: u64) -> Box<[*mut ()]>;
274+
275+
/// Miri-provided extern function to resolve a frame pointer obtained
276+
/// from `miri_get_backtrace`. The `flags` argument must be `0`,
277+
/// and `MiriFrame` should be declared as follows:
278+
///
279+
/// ```rust
280+
/// #[repr(C)]
281+
/// struct MiriFrame {
282+
/// // The name of the function being executed, encoded in UTF-8
283+
/// name: Box<[u8]>,
284+
/// // The filename of the function being executed, encoded in UTF-8
285+
/// filename: Box<[u8]>,
286+
/// // The line number currently being executed in `filename`, starting from '1'.
287+
/// lineno: u32,
288+
/// // The column number currently being executed in `filename`, starting from '1'.
289+
/// colno: u32,
290+
/// }
291+
/// ```
292+
///
293+
/// The fields must be declared in exactly the same order as they appear in `MiriFrame` above.
294+
/// This function can be called on any thread (not just the one which obtained `frame`).
295+
fn miri_resolve_frame(frame: *mut (), flags: u64) -> MiriFrame;
296+
269297
/// Miri-provided extern function to begin unwinding with the given payload.
270298
///
271299
/// This is internal and unstable and should not be used; we give it here

src/shims/backtrace.rs

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use crate::*;
2+
use helpers::check_arg_count;
3+
use rustc_middle::ty::{self, TypeAndMut};
4+
use rustc_ast::ast::Mutability;
5+
use rustc_span::BytePos;
6+
use rustc_target::abi::Size;
7+
use std::convert::TryInto as _;
8+
use crate::rustc_target::abi::LayoutOf as _;
9+
10+
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriEvalContext<'mir, 'tcx> {}
11+
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx> {
12+
13+
fn handle_miri_get_backtrace(
14+
&mut self,
15+
args: &[OpTy<'tcx, Tag>],
16+
dest: PlaceTy<'tcx, Tag>
17+
) -> InterpResult<'tcx> {
18+
let this = self.eval_context_mut();
19+
let tcx = this.tcx;
20+
let &[flags] = check_arg_count(args)?;
21+
22+
let flags = this.read_scalar(flags)?.to_u64()?;
23+
if flags != 0 {
24+
throw_unsup_format!("unknown `miri_get_backtrace` flags {}", flags);
25+
}
26+
27+
let mut data = Vec::new();
28+
for frame in this.active_thread_stack().iter().rev() {
29+
data.push((frame.instance, frame.current_span().lo()));
30+
}
31+
32+
let ptrs: Vec<_> = data.into_iter().map(|(instance, pos)| {
33+
// We represent a frame pointer by using the `span.lo` value
34+
// as an offset into the function's allocation. This gives us an
35+
// opaque pointer that we can return to user code, and allows us
36+
// to reconstruct the needed frame information in `handle_miri_resolve_frame`.
37+
// Note that we never actually read or write anything from/to this pointer -
38+
// all of the data is represented by the pointer value itself.
39+
let mut fn_ptr = this.memory.create_fn_alloc(FnVal::Instance(instance));
40+
fn_ptr.offset = Size::from_bytes(pos.0);
41+
Scalar::Ptr(fn_ptr)
42+
}).collect();
43+
44+
let len = ptrs.len();
45+
46+
let ptr_ty = tcx.mk_ptr(TypeAndMut {
47+
ty: tcx.types.unit,
48+
mutbl: Mutability::Mut
49+
});
50+
51+
let array_ty = tcx.mk_array(ptr_ty, ptrs.len().try_into().unwrap());
52+
53+
// Write pointers into array
54+
let alloc = this.allocate(this.layout_of(array_ty).unwrap(), MiriMemoryKind::Rust.into());
55+
for (i, ptr) in ptrs.into_iter().enumerate() {
56+
let place = this.mplace_index(alloc, i as u64)?;
57+
this.write_immediate_to_mplace(ptr.into(), place)?;
58+
}
59+
60+
this.write_immediate(Immediate::new_slice(alloc.ptr.into(), len.try_into().unwrap(), this), dest)?;
61+
Ok(())
62+
}
63+
64+
fn handle_miri_resolve_frame(
65+
&mut self,
66+
args: &[OpTy<'tcx, Tag>],
67+
dest: PlaceTy<'tcx, Tag>
68+
) -> InterpResult<'tcx> {
69+
let this = self.eval_context_mut();
70+
let tcx = this.tcx;
71+
let &[ptr, flags] = check_arg_count(args)?;
72+
73+
let flags = this.read_scalar(flags)?.to_u64()?;
74+
if flags != 0 {
75+
throw_unsup_format!("unknown `miri_resolve_frame` flags {}", flags);
76+
}
77+
78+
let ptr = match this.read_scalar(ptr)?.check_init()? {
79+
Scalar::Ptr(ptr) => ptr,
80+
Scalar::Raw { .. } => throw_ub_format!("expected a pointer in `rust_miri_resolve_frame`, found {:?}", ptr)
81+
};
82+
83+
let fn_instance = if let Some(GlobalAlloc::Function(instance)) = this.tcx.get_global_alloc(ptr.alloc_id) {
84+
instance
85+
} else {
86+
throw_ub_format!("expected function pointer, found {:?}", ptr);
87+
};
88+
89+
if dest.layout.layout.fields.count() != 4 {
90+
throw_ub_format!("bad declaration of miri_resolve_frame - should return a struct with 4 fields");
91+
}
92+
93+
let pos = BytePos(ptr.offset.bytes().try_into().unwrap());
94+
let name = fn_instance.to_string();
95+
96+
let lo = tcx.sess.source_map().lookup_char_pos(pos);
97+
98+
let filename = lo.file.name.to_string();
99+
let lineno: u32 = lo.line as u32;
100+
// `lo.col` is 0-based - add 1 to make it 1-based for the caller.
101+
let colno: u32 = lo.col.0 as u32 + 1;
102+
103+
let name_alloc = this.allocate_str(&name, MiriMemoryKind::Rust.into());
104+
let filename_alloc = this.allocate_str(&filename, MiriMemoryKind::Rust.into());
105+
let lineno_alloc = Scalar::from_u32(lineno);
106+
let colno_alloc = Scalar::from_u32(colno);
107+
108+
let dest = this.force_allocation(dest)?;
109+
if let ty::Adt(adt, _) = dest.layout.ty.kind() {
110+
if !adt.repr.c() {
111+
throw_ub_format!("miri_resolve_frame must be declared with a `#[repr(C)]` return type");
112+
}
113+
}
114+
115+
this.write_immediate(name_alloc.to_ref(), this.mplace_field(dest, 0)?.into())?;
116+
this.write_immediate(filename_alloc.to_ref(), this.mplace_field(dest, 1)?.into())?;
117+
this.write_scalar(lineno_alloc, this.mplace_field(dest, 2)?.into())?;
118+
this.write_scalar(colno_alloc, this.mplace_field(dest, 3)?.into())?;
119+
Ok(())
120+
}
121+
}

src/shims/foreign_items.rs

+14-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@ use std::{convert::{TryInto, TryFrom}, iter};
33
use log::trace;
44

55
use rustc_hir::def_id::DefId;
6-
use rustc_middle::{mir, ty};
6+
use rustc_middle::mir;
77
use rustc_target::{abi::{Align, Size}, spec::PanicStrategy};
8+
use rustc_middle::ty;
89
use rustc_apfloat::Float;
910
use rustc_span::symbol::sym;
1011

1112
use crate::*;
13+
use super::backtrace::EvalContextExt as _;
1214
use helpers::check_arg_count;
1315

1416
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriEvalContext<'mir, 'tcx> {}
@@ -211,6 +213,17 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
211213
this.machine.static_roots.push(ptr.alloc_id);
212214
}
213215

216+
// Obtains a Miri backtrace. See the README for details.
217+
"miri_get_backtrace" => {
218+
this.handle_miri_get_backtrace(args, dest)?;
219+
}
220+
221+
// Resolves a Miri backtrace frame. See the README for details.
222+
"miri_resolve_frame" => {
223+
this.handle_miri_resolve_frame(args, dest)?;
224+
}
225+
226+
214227
// Standard C allocation
215228
"malloc" => {
216229
let &[size] = check_arg_count(args)?;

src/shims/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
mod backtrace;
22
pub mod foreign_items;
33
pub mod intrinsics;
44
pub mod posix;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
extern "Rust" {
2+
fn miri_get_backtrace(flags: u64) -> Box<[*mut ()]>;
3+
fn miri_resolve_frame(ptr: *mut (), flags: u64);
4+
}
5+
6+
fn main() {
7+
let frames = unsafe { miri_get_backtrace(0) };
8+
for frame in frames.into_iter() {
9+
unsafe {
10+
miri_resolve_frame(*frame, 0); //~ ERROR Undefined Behavior: bad declaration of miri_resolve_frame - should return a struct with 4 fields
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
extern "Rust" {
2+
fn miri_resolve_frame(ptr: *mut (), flags: u64);
3+
}
4+
5+
fn main() {
6+
unsafe {
7+
miri_resolve_frame(0 as *mut _, 0); //~ ERROR Undefined Behavior: expected a pointer
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
extern "Rust" {
2+
fn miri_resolve_frame(ptr: *mut (), flags: u64);
3+
}
4+
5+
fn main() {
6+
unsafe {
7+
miri_resolve_frame(0 as *mut _, 1); //~ ERROR unsupported operation: unknown `miri_resolve_frame` flags 1
8+
}
9+
}

tests/run-pass/backtrace-api.rs

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// normalize-stderr-test ".*rustlib" -> "RUSTLIB"
2+
// normalize-stderr-test "RUSTLIB/(.*):\d+:\d+ "-> "RUSTLIB/$1:LL:COL "
3+
// normalize-stderr-test "::<.*>" -> ""
4+
5+
extern "Rust" {
6+
fn miri_get_backtrace(flags: u64) -> Box<[*mut ()]>;
7+
fn miri_resolve_frame(ptr: *mut (), flags: u64) -> MiriFrame;
8+
}
9+
10+
#[derive(Debug)]
11+
#[repr(C)]
12+
struct MiriFrame {
13+
name: Box<[u8]>,
14+
filename: Box<[u8]>,
15+
lineno: u32,
16+
colno: u32
17+
}
18+
19+
#[inline(never)] fn func_a() -> Box<[*mut ()]> { func_b::<u8>() }
20+
#[inline(never)] fn func_b<T>() -> Box<[*mut ()]> { func_c() }
21+
#[inline(never)] fn func_c() -> Box<[*mut ()]> { unsafe { miri_get_backtrace(0) } }
22+
23+
fn main() {
24+
let mut seen_main = false;
25+
let frames = func_a();
26+
for frame in frames.into_iter() {
27+
let miri_frame = unsafe { miri_resolve_frame(*frame, 0) };
28+
let name = String::from_utf8(miri_frame.name.into()).unwrap();
29+
let filename = String::from_utf8(miri_frame.filename.into()).unwrap();
30+
31+
// Print every frame to stderr.
32+
let out = format!("{}:{}:{} ({})", filename, miri_frame.lineno, miri_frame.colno, name);
33+
eprintln!("{}", out);
34+
// Print the 'main' frame (and everything before it) to stdout, skipping
35+
// the printing of internal (and possibly fragile) libstd frames.
36+
if !seen_main {
37+
println!("{}", out);
38+
seen_main = name == "main";
39+
}
40+
}
41+
}

tests/run-pass/backtrace-api.stderr

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
$DIR/backtrace-api.rs:21:59 (func_c)
2+
$DIR/backtrace-api.rs:20:53 (func_b)
3+
$DIR/backtrace-api.rs:19:50 (func_a)
4+
$DIR/backtrace-api.rs:25:18 (main)
5+
RUSTLIB/src/rust/library/core/src/ops/function.rs:LL:COL (<fn() as std::ops::FnOnce<()>>::call_once - shim(fn()))
6+
RUSTLIB/src/rust/library/std/src/sys_common/backtrace.rs:LL:COL (std::sys_common::backtrace::__rust_begin_short_backtrace)
7+
RUSTLIB/src/rust/library/std/src/rt.rs:LL:COL (std::rt::lang_start::{closure#0})
8+
RUSTLIB/src/rust/library/core/src/ops/function.rs:LL:COL (std::ops::function::impls::call_once)
9+
RUSTLIB/src/rust/library/std/src/panicking.rs:LL:COL (std::panicking::r#try::do_call)
10+
RUSTLIB/src/rust/library/std/src/panicking.rs:LL:COL (std::panicking::r#try)
11+
RUSTLIB/src/rust/library/std/src/panic.rs:LL:COL (std::panic::catch_unwind)
12+
RUSTLIB/src/rust/library/std/src/rt.rs:LL:COL (std::rt::lang_start_internal)
13+
RUSTLIB/src/rust/library/std/src/rt.rs:LL:COL (std::rt::lang_start)

tests/run-pass/backtrace-api.stdout

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
$DIR/backtrace-api.rs:21:59 (func_c)
2+
$DIR/backtrace-api.rs:20:53 (func_b::<u8>)
3+
$DIR/backtrace-api.rs:19:50 (func_a)
4+
$DIR/backtrace-api.rs:25:18 (main)

0 commit comments

Comments
 (0)