]> git.lizzy.rs Git - rust.git/commitdiff
Auto merge of #22839 - lifthrasiir:better-backtrace, r=alexcrichton
authorbors <bors@rust-lang.org>
Sat, 28 Feb 2015 08:30:19 +0000 (08:30 +0000)
committerbors <bors@rust-lang.org>
Sat, 28 Feb 2015 08:30:19 +0000 (08:30 +0000)
Fixes #20978 for supported platforms (i.e. non-Android POSIX).

This uses `backtrace_pcinfo` to inspect the DWARF debug info and list the file and line pairs for given stack frame. Such pair is not unique due to the presence of inlined functions and the updated routine correctly handles this case. The code is modelled after libbacktrace's `backtrace_full` routine.

There is one known issue with this approach. Macros, when invoked, take over the current frame and shadows the file and line pair which has invoked a macro. In particular, this makes many panicking
macros a bit harder to inspect. This really is a debuginfo problem, and the backtrace routine should print them correctly with a correct debuginfo.

Some example trace:

```
thread '<main>' panicked at 'explicit panic', /home/arachneng/Works/git/rust/src/test/run-pass/backtrace-debuginfo.rs:74
stack backtrace:
   1:         0xd964702f - sys::backtrace::write::h32d93fffb64131b2yxC
   2:         0xd9670202 - panicking::on_panic::h3a4fcb37b873aefeooM
   3:         0xd95b396a - rt::unwind::begin_unwind_inner::h576b3df5f626902dJ2L
   4:         0xd9eb88df - rt::unwind::begin_unwind::h16852273847167740350
   5:         0xd9eb8afb - aux::callback::h15056955655605709172
                        at /home/arachneng/Works/git/rust/<std macros>:3
                        at src/test/run-pass/backtrace-debuginfo-aux.rs:15
   6:         0xd9eb8caa - outer::h2cf96412459fceb6ema
                        at src/test/run-pass/backtrace-debuginfo.rs:73
                        at src/test/run-pass/backtrace-debuginfo.rs:88
   7:         0xd9ebab24 - main::h3f701287441442edasa
                        at src/test/run-pass/backtrace-debuginfo.rs:134
   8:         0xd96daba8 - rust_try_inner
   9:         0xd96dab95 - rust_try
  10:         0xd9671af4 - rt::lang_start::h7da0de9529b4c394liM
  11:         0xd8f3aec4 - __libc_start_main
  12:         0xd9eb8148 - <unknown>
  13:         0xffffffff - <unknown>
```

src/libstd/sys/unix/backtrace.rs
src/test/run-pass/backtrace-debuginfo-aux.rs [new file with mode: 0644]
src/test/run-pass/backtrace-debuginfo.rs [new file with mode: 0644]

index 6267792ba745e5ffa82bcca807c959d8ed4dd2a5..3695b615f62b8fe17237684d631f6f9b733bf700 100644 (file)
@@ -128,7 +128,7 @@ fn backtrace(buf: *mut *mut libc::c_void,
 
     // skipping the first one as it is write itself
     let iter = (1..cnt).map(|i| {
-        print(w, i as int, buf[i])
+        print(w, i as int, buf[i], buf[i])
     });
     result::fold(iter, (), |_, _| ())
 }
@@ -172,7 +172,16 @@ struct Context<'a> {
     extern fn trace_fn(ctx: *mut uw::_Unwind_Context,
                        arg: *mut libc::c_void) -> uw::_Unwind_Reason_Code {
         let cx: &mut Context = unsafe { mem::transmute(arg) };
-        let ip = unsafe { uw::_Unwind_GetIP(ctx) as *mut libc::c_void };
+        let mut ip_before_insn = 0;
+        let mut ip = unsafe {
+            uw::_Unwind_GetIPInfo(ctx, &mut ip_before_insn) as *mut libc::c_void
+        };
+        if ip_before_insn == 0 {
+            // this is a non-signaling frame, so `ip` refers to the address
+            // after the calling instruction. account for that.
+            ip = (ip as usize - 1) as *mut _;
+        }
+
         // dladdr() on osx gets whiny when we use FindEnclosingFunction, and
         // it appears to work fine without it, so we only use
         // FindEnclosingFunction on non-osx platforms. In doing so, we get a
@@ -183,7 +192,7 @@ struct Context<'a> {
         // instructions after it. This means that the return instruction
         // pointer points *outside* of the calling function, and by
         // unwinding it we go back to the original function.
-        let ip = if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
+        let symaddr = if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
             ip
         } else {
             unsafe { uw::_Unwind_FindEnclosingFunction(ip) }
@@ -204,7 +213,7 @@ struct Context<'a> {
         // Once we hit an error, stop trying to print more frames
         if cx.last_error.is_some() { return uw::_URC_FAILURE }
 
-        match print(cx.writer, cx.idx, ip) {
+        match print(cx.writer, cx.idx, ip, symaddr) {
             Ok(()) => {}
             Err(e) => { cx.last_error = Some(e); }
         }
@@ -215,7 +224,8 @@ struct Context<'a> {
 }
 
 #[cfg(any(target_os = "macos", target_os = "ios"))]
-fn print(w: &mut Writer, idx: int, addr: *mut libc::c_void) -> IoResult<()> {
+fn print(w: &mut Writer, idx: int, addr: *mut libc::c_void,
+         _symaddr: *mut libc::c_void) -> IoResult<()> {
     use intrinsics;
     #[repr(C)]
     struct Dl_info {
@@ -240,7 +250,8 @@ fn dladdr(addr: *const libc::c_void,
 }
 
 #[cfg(not(any(target_os = "macos", target_os = "ios")))]
-fn print(w: &mut Writer, idx: int, addr: *mut libc::c_void) -> IoResult<()> {
+fn print(w: &mut Writer, idx: int, addr: *mut libc::c_void,
+         symaddr: *mut libc::c_void) -> IoResult<()> {
     use env;
     use ptr;
 
@@ -253,6 +264,12 @@ fn print(w: &mut Writer, idx: int, addr: *mut libc::c_void) -> IoResult<()> {
                       symname: *const libc::c_char,
                       symval: libc::uintptr_t,
                       symsize: libc::uintptr_t);
+    type backtrace_full_callback =
+        extern "C" fn(data: *mut libc::c_void,
+                      pc: libc::uintptr_t,
+                      filename: *const libc::c_char,
+                      lineno: libc::c_int,
+                      function: *const libc::c_char) -> libc::c_int;
     type backtrace_error_callback =
         extern "C" fn(data: *mut libc::c_void,
                       msg: *const libc::c_char,
@@ -273,12 +290,19 @@ fn backtrace_syminfo(state: *mut backtrace_state,
                              cb: backtrace_syminfo_callback,
                              error: backtrace_error_callback,
                              data: *mut libc::c_void) -> libc::c_int;
+        fn backtrace_pcinfo(state: *mut backtrace_state,
+                            addr: libc::uintptr_t,
+                            cb: backtrace_full_callback,
+                            error: backtrace_error_callback,
+                            data: *mut libc::c_void) -> libc::c_int;
     }
 
     ////////////////////////////////////////////////////////////////////////
     // helper callbacks
     ////////////////////////////////////////////////////////////////////////
 
+    type FileLine = (*const libc::c_char, libc::c_int);
+
     extern fn error_cb(_data: *mut libc::c_void, _msg: *const libc::c_char,
                        _errnum: libc::c_int) {
         // do nothing for now
@@ -291,6 +315,25 @@ fn backtrace_syminfo(state: *mut backtrace_state,
         let slot = data as *mut *const libc::c_char;
         unsafe { *slot = symname; }
     }
+    extern fn pcinfo_cb(data: *mut libc::c_void,
+                        _pc: libc::uintptr_t,
+                        filename: *const libc::c_char,
+                        lineno: libc::c_int,
+                        _function: *const libc::c_char) -> libc::c_int {
+        if !filename.is_null() {
+            let slot = data as *mut &mut [FileLine];
+            let buffer = unsafe {ptr::read(slot)};
+
+            // if the buffer is not full, add file:line to the buffer
+            // and adjust the buffer for next possible calls to pcinfo_cb.
+            if !buffer.is_empty() {
+                buffer[0] = (filename, lineno);
+                unsafe { ptr::write(slot, &mut buffer[1..]); }
+            }
+        }
+
+        0
+    }
 
     // The libbacktrace API supports creating a state, but it does not
     // support destroying a state. I personally take this to mean that a
@@ -359,15 +402,42 @@ unsafe fn init_state() -> *mut backtrace_state {
     let mut data = ptr::null();
     let data_addr = &mut data as *mut *const libc::c_char;
     let ret = unsafe {
-        backtrace_syminfo(state, addr as libc::uintptr_t,
+        backtrace_syminfo(state, symaddr as libc::uintptr_t,
                           syminfo_cb, error_cb,
                           data_addr as *mut libc::c_void)
     };
     if ret == 0 || data.is_null() {
-        output(w, idx, addr, None)
+        try!(output(w, idx, addr, None));
     } else {
-        output(w, idx, addr, Some(unsafe { CStr::from_ptr(data).to_bytes() }))
+        try!(output(w, idx, addr, Some(unsafe { CStr::from_ptr(data).to_bytes() })));
+    }
+
+    // pcinfo may return an arbitrary number of file:line pairs,
+    // in the order of stack trace (i.e. inlined calls first).
+    // in order to avoid allocation, we stack-allocate a fixed size of entries.
+    const FILELINE_SIZE: usize = 32;
+    let mut fileline_buf = [(ptr::null(), -1); FILELINE_SIZE];
+    let ret;
+    let fileline_count;
+    {
+        let mut fileline_win: &mut [FileLine] = &mut fileline_buf;
+        let fileline_addr = &mut fileline_win as *mut &mut [FileLine];
+        ret = unsafe {
+            backtrace_pcinfo(state, addr as libc::uintptr_t,
+                             pcinfo_cb, error_cb,
+                             fileline_addr as *mut libc::c_void)
+        };
+        fileline_count = FILELINE_SIZE - fileline_win.len();
     }
+    if ret == 0 {
+        for (i, &(file, line)) in fileline_buf[..fileline_count].iter().enumerate() {
+            if file.is_null() { continue; } // just to be sure
+            let file = unsafe { CStr::from_ptr(file).to_bytes() };
+            try!(output_fileline(w, file, line, i == FILELINE_SIZE - 1));
+        }
+    }
+
+    Ok(())
 }
 
 // Finally, after all that work above, we can emit a symbol.
@@ -381,6 +451,18 @@ fn output(w: &mut Writer, idx: int, addr: *mut libc::c_void,
     w.write_all(&['\n' as u8])
 }
 
+#[allow(dead_code)]
+fn output_fileline(w: &mut Writer, file: &[u8], line: libc::c_int,
+                   more: bool) -> IoResult<()> {
+    let file = str::from_utf8(file).ok().unwrap_or("<unknown>");
+    // prior line: "  ##: {:2$} - func"
+    try!(write!(w, "      {:3$}at {}:{}", "", file, line, HEX_WIDTH));
+    if more {
+        try!(write!(w, " <... and possibly more>"));
+    }
+    w.write_all(&['\n' as u8])
+}
+
 /// Unwind library interface used for backtraces
 ///
 /// Note that dead code is allowed as here are just bindings
@@ -421,9 +503,12 @@ pub fn _Unwind_Backtrace(trace: _Unwind_Trace_Fn,
                                  trace_argument: *mut libc::c_void)
                     -> _Unwind_Reason_Code;
 
+        // available since GCC 4.2.0, should be fine for our purpose
         #[cfg(all(not(all(target_os = "android", target_arch = "arm")),
                   not(all(target_os = "linux", target_arch = "arm"))))]
-        pub fn _Unwind_GetIP(ctx: *mut _Unwind_Context) -> libc::uintptr_t;
+        pub fn _Unwind_GetIPInfo(ctx: *mut _Unwind_Context,
+                                 ip_before_insn: *mut libc::c_int)
+                    -> libc::uintptr_t;
 
         #[cfg(all(not(target_os = "android"),
                   not(all(target_os = "linux", target_arch = "arm"))))]
@@ -479,6 +564,18 @@ fn _Unwind_VRS_Get(ctx: *mut _Unwind_Context,
         (val & !1) as libc::uintptr_t
     }
 
+    // This function doesn't exist on Android or ARM/Linux, so make it same
+    // to _Unwind_GetIP
+    #[cfg(any(target_os = "android",
+              all(target_os = "linux", target_arch = "arm")))]
+    pub unsafe fn _Unwind_GetIPInfo(ctx: *mut _Unwind_Context,
+                                    ip_before_insn: *mut libc::c_int)
+        -> libc::uintptr_t
+    {
+        *ip_before_insn = 0;
+        _Unwind_GetIP(ctx)
+    }
+
     // This function also doesn't exist on Android or ARM/Linux, so make it
     // a no-op
     #[cfg(any(target_os = "android",
diff --git a/src/test/run-pass/backtrace-debuginfo-aux.rs b/src/test/run-pass/backtrace-debuginfo-aux.rs
new file mode 100644 (file)
index 0000000..074ee97
--- /dev/null
@@ -0,0 +1,22 @@
+// Copyright 2015 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+// ignore-test: not a test, used by backtrace-debuginfo.rs to test file!()
+
+#[inline(never)]
+pub fn callback<F>(f: F) where F: FnOnce((&'static str, u32)) {
+    f((file!(), line!()))
+}
+
+#[inline(always)]
+pub fn callback_inlined<F>(f: F) where F: FnOnce((&'static str, u32)) {
+    f((file!(), line!()))
+}
+
diff --git a/src/test/run-pass/backtrace-debuginfo.rs b/src/test/run-pass/backtrace-debuginfo.rs
new file mode 100644 (file)
index 0000000..a2a63d4
--- /dev/null
@@ -0,0 +1,160 @@
+// Copyright 2015 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+// compile-flags:-g
+// ignore-pretty as this critically relies on line numbers
+
+use std::old_io::stderr;
+use std::env;
+
+#[path = "backtrace-debuginfo-aux.rs"] mod aux;
+
+macro_rules! pos {
+    () => ((file!(), line!()))
+}
+
+#[cfg(all(unix,
+          not(target_os = "macos"),
+          not(target_os = "ios"),
+          not(target_os = "android"),
+          not(all(target_os = "linux", target_arch = "arm"))))]
+macro_rules! dump_and_die {
+    ($($pos:expr),*) => ({
+        // FIXME(#18285): we cannot include the current position because
+        // the macro span takes over the last frame's file/line.
+        dump_filelines(&[$($pos),*]);
+        panic!();
+    })
+}
+
+// this does not work on Windows, Android, OSX or iOS
+#[cfg(any(not(unix),
+          target_os = "macos",
+          target_os = "ios",
+          target_os = "android",
+          all(target_os = "linux", target_arch = "arm")))]
+macro_rules! dump_and_die {
+    ($($pos:expr),*) => ({ let _ = [$($pos),*]; })
+}
+
+// we can't use a function as it will alter the backtrace
+macro_rules! check {
+    ($counter:expr; $($pos:expr),*) => ({
+        if *$counter == 0 {
+            dump_and_die!($($pos),*)
+        } else {
+            *$counter -= 1;
+        }
+    })
+}
+
+type Pos = (&'static str, u32);
+
+// this goes to stdout and each line has to be occurred
+// in the following backtrace to stderr with a correct order.
+fn dump_filelines(filelines: &[Pos]) {
+    for &(file, line) in filelines.iter().rev() {
+        // extract a basename
+        let basename = file.split(&['/', '\\'][..]).last().unwrap();
+        println!("{}:{}", basename, line);
+    }
+}
+
+#[inline(never)]
+fn inner(counter: &mut u32, main_pos: Pos, outer_pos: Pos) {
+    check!(counter; main_pos, outer_pos);
+    check!(counter; main_pos, outer_pos);
+    let inner_pos = pos!(); aux::callback(|aux_pos| {
+        check!(counter; main_pos, outer_pos, inner_pos, aux_pos);
+    });
+    let inner_pos = pos!(); aux::callback_inlined(|aux_pos| {
+        check!(counter; main_pos, outer_pos, inner_pos, aux_pos);
+    });
+}
+
+#[inline(always)]
+fn inner_inlined(counter: &mut u32, main_pos: Pos, outer_pos: Pos) {
+    check!(counter; main_pos, outer_pos);
+    check!(counter; main_pos, outer_pos);
+
+    #[inline(always)]
+    fn inner_further_inlined(counter: &mut u32, main_pos: Pos, outer_pos: Pos, inner_pos: Pos) {
+        check!(counter; main_pos, outer_pos, inner_pos);
+    }
+    inner_further_inlined(counter, main_pos, outer_pos, pos!());
+
+    let inner_pos = pos!(); aux::callback(|aux_pos| {
+        check!(counter; main_pos, outer_pos, inner_pos, aux_pos);
+    });
+    let inner_pos = pos!(); aux::callback_inlined(|aux_pos| {
+        check!(counter; main_pos, outer_pos, inner_pos, aux_pos);
+    });
+
+    // this tests a distinction between two independent calls to the inlined function.
+    // (un)fortunately, LLVM somehow merges two consecutive such calls into one node.
+    inner_further_inlined(counter, main_pos, outer_pos, pos!());
+}
+
+#[inline(never)]
+fn outer(mut counter: u32, main_pos: Pos) {
+    inner(&mut counter, main_pos, pos!());
+    inner_inlined(&mut counter, main_pos, pos!());
+}
+
+fn check_trace(output: &str, error: &str) {
+    // reverse the position list so we can start with the last item (which was the first line)
+    let mut remaining: Vec<&str> = output.lines().map(|s| s.trim()).rev().collect();
+
+    assert!(error.contains("stack backtrace"), "no backtrace in the error: {}", error);
+    for line in error.lines() {
+        if !remaining.is_empty() && line.contains(remaining.last().unwrap()) {
+            remaining.pop();
+        }
+    }
+    assert!(remaining.is_empty(),
+            "trace does not match position list: {}\n---\n{}", error, output);
+}
+
+fn run_test(me: &str) {
+    use std::str;
+    use std::old_io::process::Command;
+
+    let mut template = Command::new(me);
+    template.env("RUST_BACKTRACE", "1");
+
+    let mut i = 0;
+    loop {
+        let p = template.clone().arg(i.to_string()).spawn().unwrap();
+        let out = p.wait_with_output().unwrap();
+        let output = str::from_utf8(&out.output).unwrap();
+        let error = str::from_utf8(&out.error).unwrap();
+        if out.status.success() {
+            assert!(output.contains("done."), "bad output for successful run: {}", output);
+            break;
+        } else {
+            check_trace(output, error);
+        }
+        i += 1;
+    }
+}
+
+#[inline(never)]
+fn main() {
+    let args: Vec<String> = env::args().collect();
+    if args.len() >= 2 {
+        let case = args[1].parse().unwrap();
+        writeln!(&mut stderr(), "test case {}", case).unwrap();
+        outer(case, pos!());
+        println!("done.");
+    } else {
+        run_test(&args[0]);
+    }
+}
+