Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unwind through frame pointer #116

Merged
merged 27 commits into from May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c59e11e
basic framepointer backtrace
YangKeao Mar 29, 2022
2639a57
implement the framepointer unwind and check
YangKeao Mar 30, 2022
5479357
support aarch64 on linux
YangKeao Mar 30, 2022
5bf4384
fix struct layout representation
YangKeao Mar 30, 2022
4a5be7b
fix addr to frame_pointer
YangKeao Mar 30, 2022
833b925
format the codes
YangKeao Mar 30, 2022
ed70355
add back lib.rs
YangKeao Apr 2, 2022
6197f2e
initialize the last_frame_pointer with a proper value
YangKeao Apr 5, 2022
825b61d
fix cargo clippy
YangKeao Apr 6, 2022
1b2014a
fix the compilation on arm
YangKeao Apr 6, 2022
bf99a44
only build frame pointer with nightly toolchain
YangKeao Apr 6, 2022
23c10cf
only compile on ubuntu-latest
YangKeao Apr 6, 2022
a29259f
add changelog
YangKeao Apr 6, 2022
708d1df
add a addr validator through pipe
YangKeao Apr 11, 2022
e0ebabb
add benchmark for addr_validate
YangKeao Apr 11, 2022
93e9f6b
fine tune the clippy
YangKeao Apr 11, 2022
c212ffa
only allow frame pointer in linux
YangKeao Apr 11, 2022
95fd25e
extend the check length
YangKeao Apr 11, 2022
e7228d2
only validate on linux, also build on macos
YangKeao Apr 12, 2022
004a56e
fix according to comments
YangKeao May 7, 2022
7f21f1d
Merge remote-tracking branch 'upstream/master' into frame-pointer
YangKeao May 7, 2022
6a177ec
fix grammar
YangKeao May 7, 2022
ecc540e
build on macos
YangKeao May 7, 2022
95bd349
add NON_BLOCK for the pipe in macos
YangKeao May 9, 2022
ef4e2fc
encapsulate set_flags function
YangKeao May 9, 2022
c408854
support aarch64 macos
YangKeao May 9, 2022
3214fc1
remove addr_validate conditional flag
YangKeao May 9, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/rust.yml
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
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
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
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
@@ -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
@@ -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")]
Copy link
Contributor

@sticnarf sticnarf May 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently, addr validation is only enabled on Linux. But there is an implementation for macos?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. It's a mistake. I'll remove the conditional flag on addr validate.

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,
}
}
});
YangKeao marked this conversation as resolved.
Show resolved Hide resolved

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
@@ -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
@@ -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) {
YangKeao marked this conversation as resolved.
Show resolved Hide resolved
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,
}