Skip to content

Commit 0557997

Browse files
committed
Add framehow as an unwinder and support perfmaps
1 parent 3d4e696 commit 0557997

File tree

8 files changed

+813
-34
lines changed

8 files changed

+813
-34
lines changed

Cargo.lock

Lines changed: 359 additions & 28 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ frame-pointer = []
2020
_protobuf = []
2121
prost-codec = ["prost", "prost-derive", "prost-build", "sha2", "_protobuf"]
2222
protobuf-codec = ["protobuf", "protobuf-codegen", "_protobuf"]
23+
framehop-unwinder = ["framehop", "memmap2", "object"]
24+
perfmaps = ["notify-debouncer-mini"]
25+
large-depth = []
26+
huge-depth = []
2327

2428
[dependencies]
2529
backtrace = { version = "0.3" }
@@ -41,6 +45,12 @@ protobuf = { version = ">=3.7.2", optional = true }
4145
criterion = {version = "0.5", optional = true}
4246
aligned-vec = "0.6"
4347

48+
# framehop unwinder dependencies
49+
framehop = { version = "0.13", optional = true }
50+
memmap2 = { version = "0.5.5", optional = true }
51+
object = { version = "0.29.0", optional = true }
52+
notify-debouncer-mini = { version = "0.4.1", optional = true }
53+
4454
[dependencies.symbolic-demangle]
4555
version = "12.1"
4656
default-features = false

src/backtrace/framehop_unwinder.rs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
use framehop::{
2+
CacheNative, MustNotAllocateDuringUnwind, UnwindRegsNative, Unwinder, UnwinderNative,
3+
};
4+
use libc::{c_void, ucontext_t};
5+
use once_cell::sync::Lazy;
6+
mod shlib;
7+
8+
#[cfg(all(target_arch = "aarch64", target_os = "macos"))]
9+
fn get_regs_from_context(ucontext: *mut c_void) -> Option<(UnwindRegsNative, u64)> {
10+
let ucontext: *mut ucontext_t = ucontext as *mut ucontext_t;
11+
if ucontext.is_null() {
12+
return None;
13+
}
14+
15+
let thread_state = unsafe {
16+
let mcontext = (*ucontext).uc_mcontext;
17+
if mcontext.is_null() {
18+
return None;
19+
} else {
20+
(*mcontext).__ss
21+
}
22+
};
23+
24+
Some((
25+
UnwindRegsNative::new(thread_state.__lr, thread_state.__sp, thread_state.__fp),
26+
thread_state.__pc,
27+
))
28+
}
29+
30+
#[cfg(all(target_arch = "x86_64", target_os = "macos"))]
31+
fn get_regs_from_context(ucontext: *mut c_void) -> Option<(UnwindRegsNative, u64)> {
32+
let ucontext: *mut ucontext_t = ucontext as *mut ucontext_t;
33+
if ucontext.is_null() {
34+
return None;
35+
}
36+
37+
let thread_state = unsafe {
38+
let mcontext = (*ucontext).uc_mcontext;
39+
if mcontext.is_null() {
40+
return None;
41+
} else {
42+
(*mcontext).__ss
43+
}
44+
};
45+
46+
Some((
47+
UnwindRegsNative::new(thread_state.__rip, thread_state.__rsp, thread_state.__rbp),
48+
thread_state.__rip,
49+
))
50+
}
51+
52+
#[cfg(all(target_arch = "aarch64", target_os = "linux"))]
53+
fn get_regs_from_context(ucontext: *mut c_void) -> Option<(UnwindRegsNative, u64)> {
54+
let ucontext: *mut ucontext_t = ucontext as *mut ucontext_t;
55+
if ucontext.is_null() {
56+
return None;
57+
}
58+
59+
let regs = unsafe { &(*ucontext).uc_mcontext.regs };
60+
Some((
61+
UnwindRegsNative::new(regs[30], regs[31], regs[29]),
62+
regs[30],
63+
))
64+
}
65+
66+
#[cfg(all(target_arch = "x86_64", target_os = "linux"))]
67+
fn get_regs_from_context(ucontext: *mut c_void) -> Option<(UnwindRegsNative, u64)> {
68+
let ucontext: *mut ucontext_t = ucontext as *mut ucontext_t;
69+
if ucontext.is_null() {
70+
return None;
71+
}
72+
let regs = unsafe { &(*ucontext).uc_mcontext.gregs };
73+
74+
Some((
75+
UnwindRegsNative::new(
76+
regs[libc::REG_RIP as usize] as u64,
77+
regs[libc::REG_RSP as usize] as u64,
78+
regs[libc::REG_RBP as usize] as u64,
79+
),
80+
regs[libc::REG_RIP as usize] as u64,
81+
))
82+
}
83+
84+
struct FramehopUnwinder {
85+
unwinder: UnwinderNative<Vec<u8>, MustNotAllocateDuringUnwind>,
86+
cache: CacheNative<MustNotAllocateDuringUnwind>,
87+
}
88+
89+
impl FramehopUnwinder {
90+
pub fn new() -> Self {
91+
let mut unwinder = UnwinderNative::new();
92+
for obj in shlib::get_objects() {
93+
unwinder.add_module(obj.clone());
94+
}
95+
let cache = CacheNative::default();
96+
FramehopUnwinder { unwinder, cache }
97+
}
98+
99+
pub fn iter_frames<F: FnMut(&Frame) -> bool>(&mut self, ctx: *mut c_void, mut cb: F) {
100+
let (regs, pc) = match get_regs_from_context(ctx) {
101+
Some(fp) => fp,
102+
None => return,
103+
};
104+
105+
let mut closure = |addr| read_stack(addr);
106+
let mut iter = self
107+
.unwinder
108+
.iter_frames(pc, regs, &mut self.cache, &mut closure);
109+
while let Ok(Some(frame)) = iter.next() {
110+
if !cb(&Frame {
111+
ip: frame.address() as usize,
112+
}) {
113+
break;
114+
}
115+
}
116+
}
117+
}
118+
119+
fn read_stack(addr: u64) -> Result<u64, ()> {
120+
let aligned_addr = addr & !0b111;
121+
if crate::addr_validate::validate(aligned_addr as _) {
122+
Ok(unsafe { (aligned_addr as *const u64).read() })
123+
} else {
124+
Err(())
125+
}
126+
}
127+
128+
static mut UNWINDER: Lazy<FramehopUnwinder> = Lazy::new(|| FramehopUnwinder::new());
129+
#[derive(Clone, Debug)]
130+
pub struct Frame {
131+
pub ip: usize,
132+
}
133+
134+
extern "C" {
135+
fn _Unwind_FindEnclosingFunction(pc: *mut c_void) -> *mut c_void;
136+
137+
}
138+
139+
impl super::Frame for Frame {
140+
type S = backtrace::Symbol;
141+
fn ip(&self) -> usize {
142+
self.ip
143+
}
144+
145+
fn symbol_address(&self) -> *mut c_void {
146+
if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
147+
self.ip as *mut c_void
148+
} else {
149+
unsafe { _Unwind_FindEnclosingFunction(self.ip as *mut c_void) }
150+
}
151+
}
152+
153+
fn resolve_symbol<F: FnMut(&Self::S)>(&self, cb: F) {
154+
backtrace::resolve(self.ip as *mut c_void, cb);
155+
}
156+
}
157+
158+
pub struct Trace;
159+
160+
impl super::Trace for Trace {
161+
type Frame = Frame;
162+
163+
fn trace<F: FnMut(&Self::Frame) -> bool>(ctx: *mut c_void, cb: F)
164+
where
165+
Self: Sized,
166+
{
167+
unsafe { UNWINDER.iter_frames(ctx, cb) };
168+
}
169+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
use std::path::PathBuf;
2+
3+
use findshlibs::{SharedLibrary, TargetSharedLibrary};
4+
use framehop::{Module, ModuleSectionInfo};
5+
use memmap2::Mmap;
6+
use object::{Object, ObjectSection};
7+
use once_cell::sync::Lazy;
8+
9+
static OBJECTS: Lazy<Vec<Module<Vec<u8>>>> = Lazy::new(find_objects);
10+
11+
pub fn get_objects() -> &'static [Module<Vec<u8>>] {
12+
&OBJECTS
13+
}
14+
15+
pub struct ObjectInfo<'f>(object::File<'f>);
16+
17+
impl<'file, D> ModuleSectionInfo<D> for ObjectInfo<'file>
18+
where
19+
D: From<&'file [u8]>,
20+
{
21+
fn base_svma(&self) -> u64 {
22+
if let Some(section) = self.0.section_by_name("__TEXT") {
23+
// in mach-o addresses are relative to __TEXT
24+
section.address()
25+
} else {
26+
self.0.relative_address_base()
27+
}
28+
}
29+
30+
fn section_svma_range(&mut self, name: &[u8]) -> Option<std::ops::Range<u64>> {
31+
if let Some(section) = self.0.section_by_name_bytes(name) {
32+
let start = section.address();
33+
let end = start + section.size();
34+
Some(start..end)
35+
} else {
36+
None
37+
}
38+
}
39+
40+
fn section_data(&mut self, name: &[u8]) -> Option<D> {
41+
if let Some(section) = self.0.section_by_name_bytes(name) {
42+
let data = section.data().ok()?;
43+
Some(D::from(data))
44+
} else {
45+
None
46+
}
47+
}
48+
}
49+
50+
fn open_mmap(path: &PathBuf) -> Option<Mmap> {
51+
let file = std::fs::File::open(path).ok()?;
52+
let mmap = unsafe { Mmap::map(&file) }.ok()?;
53+
Some(mmap)
54+
}
55+
56+
fn find_objects() -> Vec<Module<Vec<u8>>> {
57+
let mut objects = Vec::new();
58+
// objects
59+
TargetSharedLibrary::each(|shlib| {
60+
let path = PathBuf::from(shlib.name());
61+
let base_avma = shlib.actual_load_addr().0 as u64;
62+
let avma_range = base_avma..base_avma + shlib.len() as u64;
63+
if let Some(mmap) = open_mmap(&path) {
64+
if let Ok(obj) = object::File::parse(&*mmap) {
65+
let section_info = ObjectInfo(obj);
66+
67+
objects.push(Module::new(
68+
path.to_string_lossy().to_string(),
69+
avma_range,
70+
base_avma,
71+
section_info,
72+
));
73+
}
74+
}
75+
});
76+
77+
objects
78+
}

src/backtrace/mod.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub trait Trace {
5555
target_arch = "riscv64",
5656
target_arch = "loongarch64"
5757
),
58-
feature = "frame-pointer"
58+
any(feature = "frame-pointer", feature = "framehop-unwinder")
5959
)))]
6060
mod backtrace_rs;
6161
#[cfg(not(all(
@@ -65,7 +65,7 @@ mod backtrace_rs;
6565
target_arch = "riscv64",
6666
target_arch = "loongarch64"
6767
),
68-
feature = "frame-pointer"
68+
any(feature = "frame-pointer", feature = "framehop-unwinder")
6969
)))]
7070
pub use backtrace_rs::Trace as TraceImpl;
7171

@@ -89,3 +89,17 @@ pub mod frame_pointer;
8989
feature = "frame-pointer"
9090
))]
9191
pub use frame_pointer::Trace as TraceImpl;
92+
93+
#[cfg(all(
94+
any(target_arch = "x86_64", target_arch = "aarch64",),
95+
any(target_os = "linux", target_os = "macos",),
96+
feature = "framehop-unwinder"
97+
))]
98+
pub mod framehop_unwinder;
99+
100+
#[cfg(all(
101+
any(target_arch = "x86_64", target_arch = "aarch64",),
102+
any(target_os = "linux", target_os = "macos",),
103+
feature = "framehop-unwinder"
104+
))]
105+
pub use framehop_unwinder::Trace as TraceImpl;

src/frames.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ use symbolic_demangle::demangle;
1313
use crate::backtrace::{Frame, Trace, TraceImpl};
1414
use crate::{MAX_DEPTH, MAX_THREAD_NAME};
1515

16+
#[cfg(feature = "perfmaps")]
17+
fn resolve_in_perfmap(ip: usize) -> Option<Symbol> {
18+
use crate::perfmap::get_resolver;
19+
20+
if let Ok(Some(perf_map_resolver)) = get_resolver() {
21+
if let Some(symbol) = perf_map_resolver.resolve(ip as _) {
22+
return Some(Symbol::from(symbol));
23+
}
24+
}
25+
26+
None
27+
}
28+
29+
#[cfg(not(feature = "perfmaps"))]
30+
fn resolve_in_perfmap(_ip: usize) -> Option<Symbol> {
31+
None
32+
}
33+
1634
#[derive(Clone)]
1735
pub struct UnresolvedFrames {
1836
pub frames: SmallVec<[<TraceImpl as Trace>::Frame; MAX_DEPTH]>,
@@ -192,10 +210,14 @@ impl From<UnresolvedFrames> for Frames {
192210
while let Some(frame) = frame_iter.next() {
193211
let mut symbols: Vec<Symbol> = Vec::new();
194212

195-
frame.resolve_symbol(|symbol| {
196-
let symbol = Symbol::from(symbol);
197-
symbols.push(symbol);
198-
});
213+
if let Some(perfmap_symbol) = resolve_in_perfmap(frame.ip()) {
214+
symbols.push(perfmap_symbol);
215+
} else {
216+
frame.resolve_symbol(|symbol| {
217+
let symbol = Symbol::from(symbol);
218+
symbols.push(symbol);
219+
});
220+
}
199221

200222
if symbols.iter().any(|symbol| {
201223
// macOS prepends an underscore even with `#[no_mangle]`

src/lib.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
//! [README.md](https://github.com/tikv/pprof-rs/blob/master/README.md)
4141
4242
/// Define the MAX supported stack depth. TODO: make this variable mutable.
43+
#[cfg(feature = "large-depth")]
44+
pub const MAX_DEPTH: usize = 1024;
45+
46+
#[cfg(all(feature = "huge-depth", not(feature = "large-depth")))]
47+
pub const MAX_DEPTH: usize = 512;
48+
49+
#[cfg(not(any(feature = "large-depth", feature = "huge-depth")))]
4350
pub const MAX_DEPTH: usize = 128;
4451

4552
/// Define the MAX supported thread name length. TODO: make this variable mutable.
@@ -51,6 +58,8 @@ mod backtrace;
5158
mod collector;
5259
mod error;
5360
mod frames;
61+
#[cfg(feature = "perfmaps")]
62+
mod perfmap;
5463
mod profiler;
5564
mod report;
5665
mod timer;
@@ -87,3 +96,6 @@ pub mod protos {
8796

8897
#[cfg(feature = "criterion")]
8998
pub mod criterion;
99+
100+
#[cfg(feature = "perfmaps")]
101+
pub use perfmap::init_perfmap_resolver;

0 commit comments

Comments
 (0)