Skip to content

Commit

Permalink
Unwind through frame pointer (#116)
Browse files Browse the repository at this point in the history
* basic framepointer backtrace

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* implement the framepointer unwind and check

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* support aarch64 on linux

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* fix struct layout representation

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* fix addr to frame_pointer

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* format the codes

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* add back lib.rs

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* initialize the last_frame_pointer with a proper value

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* fix cargo clippy

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* fix the compilation on arm

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* only build frame pointer with nightly toolchain

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* only compile on ubuntu-latest

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* add changelog

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* add a addr validator through pipe

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* add benchmark for addr_validate

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* fine tune the clippy

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* only allow frame pointer in linux

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* extend the check length

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* only validate on linux, also build on macos

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* fix according to comments

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* fix grammar

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* build on macos

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* add NON_BLOCK for the pipe in macos

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* encapsulate set_flags function

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* support aarch64 macos

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>

* remove addr_validate conditional flag

Signed-off-by: YangKeao <yangkeao@chunibyo.icu>
  • Loading branch information
YangKeao authored May 9, 2022
1 parent 1f096ce commit 567c5d0
Show file tree
Hide file tree
Showing 12 changed files with 433 additions and 31 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ jobs:
command: build
args: --features flamegraph,protobuf-codec --target ${{ matrix.target }}

- name: Run cargo build frame pointer
if: ${{ matrix.toolchain == 'nightly' && matrix.os == 'ubuntu-latest' }}
uses: actions-rs/cargo@v1.0.3
with:
command: build
args: --no-default-features --features frame-pointer --target ${{ matrix.target }}

test:
name: Test
strategy:
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Add `frame-pointer` feature to unwind the stack with frame pointer (#116)

### Changed
- The user has to specify one unwind implementation (`backtrace-rs` or `frame-pointer`) in the features (#116)

## [0.8.0] - 2022-04-20

### Changed
Expand Down
14 changes: 12 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ documentation = "https://docs.rs/pprof/"
readme = "README.md"

[features]
default = ["cpp"]
default = ["cpp", "backtrace-rs"]
flamegraph = ["inferno"]

# A private feature to indicate either prost-codec or protobuf-codec is enabled.
_protobuf = []
prost-codec = ["prost", "prost-derive", "prost-build", "_protobuf"]
protobuf-codec = ["protobuf", "protobuf-codegen-pure", "_protobuf"]

backtrace-rs = ["backtrace"]
frame-pointer = ["backtrace"]

cpp = ["symbolic-demangle/cpp"]

[dependencies]
backtrace = "0.3"
backtrace = { version = "0.3", optional = true }
once_cell = "1.9"
libc = "^0.2.66"
log = "0.4"
Expand Down Expand Up @@ -71,5 +76,10 @@ name = "collector"
path = "benches/collector.rs"
harness = false

[[bench]]
name = "addr_validate"
path = "benches/addr_validate.rs"
harness = false

[package.metadata.docs.rs]
all-features = true
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ FRAME: backtrace::backtrace::trace::h3e91a3123a3049a5 -> FRAME: pprof::profiler:
- `flamegraph` enables the flamegraph report format.
- `prost-codec` enables the pprof protobuf report format through `prost`.
- `protobuf-codec` enables the pprof protobuf report format through `protobuf` crate.
- `backtrace-rs` unwind the backtrace through `backtrace-rs` (which calls the `Unwind_Backtrace`).
- `frame-pointer` gets the backtrace through frame pointer. **only available for nightly**

## Flamegraph

Expand Down Expand Up @@ -222,6 +224,12 @@ let guard = pprof::ProfilerGuardBuilder::default().frequency(1000).blocklist(&["

The `vdso` should also be added to the blocklist, because in some distribution (e.g. ubuntu 18.04), the dwarf information in vdso is incorrect.

### Frame Pointer

The `pprof-rs` also supports unwinding through frame pointer, without the need to use `libunwind`. However, the standard library shipped with the rust compiler does not have the correct frame pointer in every function, so you need to use `cargo +nightly -Z build-std` to build the standard library from source.

As we cannot get the stack boundaries inside the signal handler, it's also not possible to ensure the safety. If the frame pointer was set to a wrong value, the program will panic.

### Signal Safety

Signal safety is hard to guarantee. But it's not *that* hard.
Expand Down
29 changes: 29 additions & 0 deletions benches/addr_validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.

use criterion::{criterion_group, criterion_main, Criterion};
use pprof::validate;

fn bench_validate_addr(c: &mut Criterion) {
c.bench_function("validate stack addr", |b| {
let stack_addrs = [0; 100];

b.iter(|| {
stack_addrs.iter().for_each(|item| {
validate(item as *const _ as *const libc::c_void);
})
})
});

c.bench_function("validate heap addr", |b| {
let heap_addrs = vec![0; 100];

b.iter(|| {
heap_addrs.iter().for_each(|item| {
validate(item as *const _ as *const libc::c_void);
})
})
});
}

criterion_group!(benches, bench_validate_addr);
criterion_main!(benches);
122 changes: 122 additions & 0 deletions src/addr_validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use std::{cell::RefCell, mem::size_of};

use nix::{
errno::Errno,
unistd::{close, read, write},
};

thread_local! {
static MEM_VALIDATE_PIPE: RefCell<[i32; 2]> = RefCell::new([-1, -1]);
}

#[inline]
#[cfg(target_os = "linux")]
fn create_pipe() -> nix::Result<(i32, i32)> {
use nix::fcntl::OFlag;
use nix::unistd::pipe2;

pipe2(OFlag::O_CLOEXEC | OFlag::O_NONBLOCK)
}

#[inline]
#[cfg(target_os = "macos")]
fn create_pipe() -> nix::Result<(i32, i32)> {
use nix::fcntl::{fcntl, FcntlArg, FdFlag, OFlag};
use nix::unistd::pipe;
use std::os::unix::io::RawFd;

fn set_flags(fd: RawFd) -> nix::Result<()> {
let mut flags = FdFlag::from_bits(fcntl(fd, FcntlArg::F_GETFD)?).unwrap();
flags |= FdFlag::FD_CLOEXEC;
fcntl(fd, FcntlArg::F_SETFD(flags))?;
let mut flags = OFlag::from_bits(fcntl(fd, FcntlArg::F_GETFL)?).unwrap();
flags |= OFlag::O_NONBLOCK;
fcntl(fd, FcntlArg::F_SETFL(flags))?;
Ok(())
}

let (read_fd, write_fd) = pipe()?;
set_flags(read_fd)?;
set_flags(write_fd)?;
Ok((read_fd, write_fd))
}

fn open_pipe() -> nix::Result<()> {
MEM_VALIDATE_PIPE.with(|pipes| {
let mut pipes = pipes.borrow_mut();

// ignore the result
let _ = close(pipes[0]);
let _ = close(pipes[1]);

let (read_fd, write_fd) = create_pipe()?;

pipes[0] = read_fd;
pipes[1] = write_fd;

Ok(())
})
}

pub fn validate(addr: *const libc::c_void) -> bool {
const CHECK_LENGTH: usize = 2 * size_of::<*const libc::c_void>() / size_of::<u8>();

// read data in the pipe
let valid_read = MEM_VALIDATE_PIPE.with(|pipes| {
let pipes = pipes.borrow();
loop {
let mut buf = [0u8; CHECK_LENGTH];

match read(pipes[0], &mut buf) {
Ok(bytes) => break bytes > 0,
Err(_err @ Errno::EINTR) => continue,
Err(_err @ Errno::EAGAIN) => break true,
Err(_) => break false,
}
}
});

if !valid_read && open_pipe().is_err() {
return false;
}

MEM_VALIDATE_PIPE.with(|pipes| {
let pipes = pipes.borrow();
loop {
let buf = unsafe { std::slice::from_raw_parts(addr as *const u8, CHECK_LENGTH) };

match write(pipes[1], buf) {
Ok(bytes) => break bytes > 0,
Err(_err @ Errno::EINTR) => continue,
Err(_) => break false,
}
}
})
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn validate_stack() {
let i = 0;

assert_eq!(validate(&i as *const _ as *const libc::c_void), true);
}

#[test]
fn validate_heap() {
let vec = vec![0; 1000];

for i in vec.iter() {
assert_eq!(validate(i as *const _ as *const libc::c_void), true);
}
}

#[test]
fn failed_validate() {
assert_eq!(validate(0 as *const libc::c_void), false);
assert_eq!(validate((-1 as i32) as usize as *const libc::c_void), false)
}
}
28 changes: 28 additions & 0 deletions src/backtrace/backtrace_rs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
impl super::Frame for backtrace::Frame {
type S = backtrace::Symbol;

fn ip(&self) -> usize {
self.ip() as usize
}

fn resolve_symbol<F: FnMut(&Self::S)>(&self, cb: F) {
backtrace::resolve_frame(self, cb);
}

fn symbol_address(&self) -> *mut libc::c_void {
self.symbol_address()
}
}

pub struct Trace {}

impl super::Trace for Trace {
type Frame = backtrace::Frame;

fn trace<F: FnMut(&Self::Frame) -> bool>(_: *mut libc::c_void, cb: F) {
unsafe { backtrace::trace_unsynchronized(cb) }
}
}

pub use backtrace::Frame;
pub use backtrace::Symbol;
116 changes: 116 additions & 0 deletions src/backtrace/frame_pointer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright 2022 TiKV Project Authors. Licensed under Apache-2.0.

use std::ptr::null_mut;

use libc::c_void;

use crate::addr_validate::validate;

#[derive(Clone, Debug)]
pub struct Frame {
pub ip: usize,
}

extern "C" {
fn _Unwind_FindEnclosingFunction(pc: *mut c_void) -> *mut c_void;

}

impl super::Frame for Frame {
type S = backtrace::Symbol;

fn ip(&self) -> usize {
self.ip
}

fn resolve_symbol<F: FnMut(&Self::S)>(&self, cb: F) {
backtrace::resolve(self.ip as *mut c_void, cb);
}

fn symbol_address(&self) -> *mut libc::c_void {
if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
self.ip as *mut c_void
} else {
unsafe { _Unwind_FindEnclosingFunction(self.ip as *mut c_void) }
}
}
}

pub struct Trace {}
impl super::Trace for Trace {
type Frame = Frame;

fn trace<F: FnMut(&Self::Frame) -> bool>(ucontext: *mut libc::c_void, mut cb: F) {
let ucontext: *mut libc::ucontext_t = ucontext as *mut libc::ucontext_t;
if ucontext.is_null() {
return;
}

#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
let frame_pointer =
unsafe { (*ucontext).uc_mcontext.gregs[libc::REG_RBP as usize] as usize };

#[cfg(all(target_arch = "x86_64", target_os = "macos"))]
let frame_pointer = unsafe {
let mcontext = (*ucontext).uc_mcontext;
if mcontext.is_null() {
0
} else {
(*mcontext).__ss.__rbp as usize
}
};

#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
let frame_pointer = unsafe { (*ucontext).uc_mcontext.regs[29] as usize };

#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
let frame_pointer = unsafe {
let mcontext = (*ucontext).uc_mcontext;
if mcontext.is_null() {
0
} else {
(*mcontext).__ss.__fp as usize
}
};

let mut frame_pointer = frame_pointer as *mut FramePointerLayout;

let mut last_frame_pointer: *mut FramePointerLayout = null_mut();
loop {
// The stack grow from high address to low address.
// but we don't have a reasonable assumption for the hightest address
// the `__libc_stack_end` is not thread-local, and only represent the
// stack end of the main thread. For other thread, their stacks are allocated
// by the `pthread`.
//
// TODO: If we can hook the thread creation, we will have chance to get the
// stack end through `pthread_get_attr`.

// the frame pointer should never be smaller than the former one.
if !last_frame_pointer.is_null() && frame_pointer < last_frame_pointer {
break;
}

if !validate(frame_pointer as *const libc::c_void) {
break;
}
last_frame_pointer = frame_pointer;

// iterate to the next frame
let frame = Frame {
ip: unsafe { (*frame_pointer).ret },
};

if !cb(&frame) {
break;
}
frame_pointer = unsafe { (*frame_pointer).frame_pointer };
}
}
}

#[repr(C)]
struct FramePointerLayout {
frame_pointer: *mut FramePointerLayout,
ret: usize,
}
Loading

0 comments on commit 567c5d0

Please sign in to comment.