]> git.lizzy.rs Git - rust.git/commitdiff
Add API for capturing backtrace
authorAaron Hill <aa1ronham@gmail.com>
Tue, 3 Dec 2019 02:15:58 +0000 (21:15 -0500)
committerAaron Hill <aa1ronham@gmail.com>
Mon, 28 Sep 2020 17:44:45 +0000 (13:44 -0400)
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.

README.md
src/shims/foreign_items.rs
tests/compile-fail/backtrace/bad-backtrace-decl.rs [new file with mode: 0644]
tests/compile-fail/backtrace/bad-backtrace-ptr.rs [new file with mode: 0644]
tests/compile-fail/backtrace/bad-backtrace-version.rs [new file with mode: 0644]
tests/run-pass/backtrace-api.rs [new file with mode: 0644]
tests/run-pass/backtrace-api.stderr [new file with mode: 0644]

index 657ea096e6c8aedfde596e4c85c6fd4d4135fd3c..524c8e10cf3653d7b31b749de1509e946abc50bc 100644 (file)
--- a/README.md
+++ b/README.md
@@ -266,6 +266,32 @@ extern "Rust" {
     /// `ptr` has to point to the beginning of an allocated block.
     fn miri_static_root(ptr: *const u8);
 
+    /// Miri-provided extern function to obtain a backtrace of the current call stack.
+    /// This returns a boxed slice of pointers - each pointer is an opaque value
+    /// that is only useful when passed to `miri_resolve_frame`
+    fn miri_get_backtrace() -> Box<[*mut ()]>;
+
+    /// Miri-provided extern function to resolve a frame pointer obtained
+    /// from `miri_get_backtrace`. The `version` argument must be `0`,
+    /// and `MiriFrame` should be declared as follows:
+    ///
+    /// ```rust
+    /// struct MiriFrame {
+    ///     // The name of the function being executed, encoded in UTF-8
+    ///     name: Box<[u8]>,
+    ///     // The filename of the function being executed, encoded in UTF-8
+    ///     filename: Box<[u8]>,
+    ///     // The line number currently being executed in `filename`, starting from '1'.
+    ///     lineno: u32,
+    ///     // The column number currently being executed in `filename`, starting from '1'.
+    ///     colno: u32,
+    /// }
+    /// ```
+    ///
+    /// The fields must be declared in exactly the same order as they appear in `MiriFrame` above.
+    /// This function can be called on any thread (not just the one which obtained `frame`)
+    fn miri_resolve_frame(version: u8, frame: *mut ()) -> MiriFrame;
+
     /// Miri-provided extern function to begin unwinding with the given payload.
     ///
     /// This is internal and unstable and should not be used; we give it here
index 5bcbd797ca3c95dd23d4c597eacc7145b1975abd..4d1ead8f0f82fda4a05fb70a5fc0e4a1055c699d 100644 (file)
@@ -3,10 +3,13 @@
 use log::trace;
 
 use rustc_hir::def_id::DefId;
-use rustc_middle::{mir, ty};
+use rustc_middle::mir;
 use rustc_target::{abi::{Align, Size}, spec::PanicStrategy};
+use rustc_middle::ty::{self, ParamEnv, TypeAndMut};
+use rustc_ast::ast::Mutability;
 use rustc_apfloat::Float;
 use rustc_span::symbol::sym;
+use rustc_span::BytePos;
 
 use crate::*;
 use helpers::check_arg_count;
@@ -211,6 +214,89 @@ fn emulate_foreign_item_by_name(
                 this.machine.static_roots.push(ptr.alloc_id);
             }
 
+            // Obtains a Miri backtrace. See the README for details.
+            "miri_get_backtrace" => {
+                let tcx = this.tcx;
+                let mut data = Vec::new();
+                for frame in this.active_thread_stack().iter().rev() {
+                    data.push((frame.instance, frame.current_span().lo()));
+                }
+
+                let ptrs: Vec<_> = data.into_iter().map(|(instance, pos)| {
+                    let mut fn_ptr = this.memory.create_fn_alloc(FnVal::Instance(instance));
+                    fn_ptr.offset = Size::from_bytes(pos.0);
+                    Scalar::Ptr(fn_ptr)
+                }).collect();
+
+                let len = ptrs.len();
+
+                let ptr_ty = tcx.mk_ptr(TypeAndMut {
+                    ty: tcx.types.unit,
+                    mutbl: Mutability::Mut
+                });
+
+                let array_ty = tcx.mk_array(ptr_ty, ptrs.len().try_into().unwrap());
+                let array_ty_and_env = ParamEnv::empty().and(array_ty);
+
+                // Write pointers into array
+                let alloc = this.allocate(tcx.layout_of(array_ty_and_env).unwrap(), MiriMemoryKind::Rust.into());
+                for (i, ptr) in ptrs.into_iter().enumerate() {
+                    let place = this.mplace_index(alloc, i as u64)?;
+                    this.write_immediate_to_mplace(ptr.into(), place)?;
+                }
+
+                this.write_immediate(Immediate::new_slice(alloc.ptr.into(), len.try_into().unwrap(), this), dest)?;
+            }
+
+            // Resolves a Miri backtrace frame. See the README for details.
+            "miri_resolve_frame" => {
+                let tcx = this.tcx;
+                let &[version, ptr] = check_arg_count(args)?;
+
+                let version = this.read_scalar(version)?.to_u8()?;
+                if version != 0 {
+                    throw_ub_format!("Unknown `miri_resolve_frame` version {}", version);
+                }
+
+                let ptr = match this.read_scalar(ptr)?.check_init()? {
+                    Scalar::Ptr(ptr) => ptr,
+                    Scalar::Raw { .. } => throw_ub_format!("Expected a pointer in `rust_miri_resolve_frame`, found {:?}", ptr)
+                };
+
+                let fn_instance = if let Some(GlobalAlloc::Function(instance)) = this.tcx.get_global_alloc(ptr.alloc_id) {
+                    instance
+                } else {
+                    throw_ub_format!("Expect function pointer, found {:?}", ptr);
+                };
+
+                if dest.layout.layout.fields.count() != 4 {
+                    throw_ub_format!("Bad declaration of miri_resolve_frame - should return a struct with 4 fields");
+                }
+
+                let pos = BytePos(ptr.offset.bytes().try_into().unwrap());
+                let name = fn_instance.to_string();
+
+                let lo = tcx.sess.source_map().lookup_char_pos(pos);
+
+                let filename = lo.file.name.to_string();
+                let lineno: u32 = lo.line as u32;
+                // `lo.col` is 0-based - add 1 to make it 1-based for the caller.
+                let colno: u32 = lo.col.0 as u32 + 1;
+
+                let name_alloc = this.allocate_str(&name, MiriMemoryKind::Rust.into());
+                let filename_alloc = this.allocate_str(&filename, MiriMemoryKind::Rust.into());
+                let lineno_alloc = Scalar::from_u32(lineno);
+                let colno_alloc = Scalar::from_u32(colno);
+
+                let dest = this.force_allocation_maybe_sized(dest, MemPlaceMeta::None)?.0;
+
+                this.write_immediate(name_alloc.to_ref(), this.mplace_field(dest, 0)?.into())?;
+                this.write_immediate(filename_alloc.to_ref(), this.mplace_field(dest, 1)?.into())?;
+                this.write_scalar(lineno_alloc, this.mplace_field(dest, 2)?.into())?;
+                this.write_scalar(colno_alloc, this.mplace_field(dest, 3)?.into())?;
+            }
+
+
             // Standard C allocation
             "malloc" => {
                 let &[size] = check_arg_count(args)?;
diff --git a/tests/compile-fail/backtrace/bad-backtrace-decl.rs b/tests/compile-fail/backtrace/bad-backtrace-decl.rs
new file mode 100644 (file)
index 0000000..7c250fb
--- /dev/null
@@ -0,0 +1,13 @@
+extern "Rust" {
+    fn miri_get_backtrace() -> Box<[*mut ()]>;
+    fn miri_resolve_frame(version: u8, ptr: *mut ());
+}
+
+fn main() {
+    let frames = unsafe { miri_get_backtrace() };
+    for frame in frames.into_iter() {
+        unsafe {
+            miri_resolve_frame(0, *frame); //~ ERROR Undefined Behavior: Bad declaration of miri_resolve_frame - should return a struct with 4 fields
+        }
+    }
+}
diff --git a/tests/compile-fail/backtrace/bad-backtrace-ptr.rs b/tests/compile-fail/backtrace/bad-backtrace-ptr.rs
new file mode 100644 (file)
index 0000000..49b8ac8
--- /dev/null
@@ -0,0 +1,9 @@
+extern "Rust" {
+    fn miri_resolve_frame(version: u8, ptr: *mut ());
+}
+
+fn main() {
+    unsafe {
+        miri_resolve_frame(0, 0 as *mut _); //~ ERROR Undefined Behavior: Expected a pointer
+    }
+}
diff --git a/tests/compile-fail/backtrace/bad-backtrace-version.rs b/tests/compile-fail/backtrace/bad-backtrace-version.rs
new file mode 100644 (file)
index 0000000..b0183ca
--- /dev/null
@@ -0,0 +1,9 @@
+extern "Rust" {
+    fn miri_resolve_frame(version: u8, ptr: *mut ());
+}
+
+fn main() {
+    unsafe {
+        miri_resolve_frame(1, 0 as *mut _); //~ ERROR  Undefined Behavior: Unknown `miri_resolve_frame` version 1
+    }
+}
diff --git a/tests/run-pass/backtrace-api.rs b/tests/run-pass/backtrace-api.rs
new file mode 100644 (file)
index 0000000..fa86deb
--- /dev/null
@@ -0,0 +1,24 @@
+// normalize-stderr-test ".*rustlib" -> "RUSTLIB"
+
+extern "Rust" {
+    fn miri_get_backtrace() -> Box<[*mut ()]>;
+    fn miri_resolve_frame(version: u8, ptr: *mut ()) -> MiriFrame;
+}
+
+#[derive(Debug)]
+struct MiriFrame {
+    name: Box<[u8]>,
+    filename: Box<[u8]>,
+    lineno: u32,
+    colno: u32
+}
+
+fn main() {
+    let frames = unsafe { miri_get_backtrace() };
+    for frame in frames.into_iter() {
+        let miri_frame = unsafe { miri_resolve_frame(0, *frame) };
+        let name = String::from_utf8(miri_frame.name.into()).unwrap();
+        let filename = String::from_utf8(miri_frame.filename.into()).unwrap();
+        eprintln!("{}:{}:{} ({})", filename, miri_frame.lineno, miri_frame.colno, name);
+    }
+}
diff --git a/tests/run-pass/backtrace-api.stderr b/tests/run-pass/backtrace-api.stderr
new file mode 100644 (file)
index 0000000..91a9907
--- /dev/null
@@ -0,0 +1,10 @@
+$DIR/backtrace-api.rs:17:27 (main)
+RUSTLIB/src/rust/library/core/src/ops/function.rs:227:5 (<fn() as std::ops::FnOnce<()>>::call_once - shim(fn()))
+RUSTLIB/src/rust/library/std/src/sys_common/backtrace.rs:137:18 (std::sys_common::backtrace::__rust_begin_short_backtrace::<fn(), ()>)
+RUSTLIB/src/rust/library/std/src/rt.rs:66:18 (std::rt::lang_start::<()>::{{closure}}#0)
+RUSTLIB/src/rust/library/core/src/ops/function.rs:259:13 (std::ops::function::impls::<impl std::ops::FnOnce<()> for &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>::call_once)
+RUSTLIB/src/rust/library/std/src/panicking.rs:381:40 (std::panicking::r#try::do_call::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>)
+RUSTLIB/src/rust/library/std/src/panicking.rs:345:19 (std::panicking::r#try::<i32, &dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe>)
+RUSTLIB/src/rust/library/std/src/panic.rs:382:14 (std::panic::catch_unwind::<&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe, i32>)
+RUSTLIB/src/rust/library/std/src/rt.rs:51:25 (std::rt::lang_start_internal)
+RUSTLIB/src/rust/library/std/src/rt.rs:65:5 (std::rt::lang_start::<()>)