Skip to content

Commit 8134c2c

Browse files
quark-zjumeta-codesync[bot]
authored andcommitted
backtrace-python: use offset-probe provided offsets
Summary: Replaced manually hardcoded offsets with `offset-probe` detected ones. This should make both cargo and buck builds work. Reviewed By: muirdm Differential Revision: D92185214 fbshipit-source-id: 84608eaa05c2bb69927c07d4bea80d5dbf901289
1 parent 58a5d0a commit 8134c2c

5 files changed

Lines changed: 90 additions & 74 deletions

File tree

eden/scm/lib/backtrace-python/BUCK

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,30 @@
1+
load("@fbcode//eden/scm:targets.bzl", "exec_compatible_with_target")
2+
load("@fbcode_macros//build_defs:native_rules.bzl", "buck_genrule")
13
load("@fbsource//tools/build_defs:rust_library.bzl", "rust_library")
24

35
oncall("sapling")
46

7+
buck_genrule(
8+
name = "gen_offsets.rs",
9+
out = "offsets.rs",
10+
cmd = select({
11+
"DEFAULT": "$(exe_target //eden/scm/lib/backtrace-python/offset-probe:offset-codegen) > $OUT",
12+
# HACK: pythonXY.DLL not in PATH could fail remote execution.
13+
# Reported at https://fburl.com/workplace/3ep6oilj
14+
"ovr_config//os:windows": "set PATH=C:\\tools\\fb-python\\fb-python312;%PATH% && $(exe_target //eden/scm/lib/backtrace-python/offset-probe:offset-codegen) > $OUT",
15+
}),
16+
# Force execution platform to match target platform so the probed offsets
17+
# are correct for the target OS and architecture.
18+
exec_compatible_with = exec_compatible_with_target(),
19+
)
20+
521
rust_library(
622
name = "backtrace-python",
723
srcs = glob(["src/**/*.rs"]),
824
autocargo = {"cargo_toml_config": {
25+
"extra_buck_dependencies": {"build-dependencies": [
26+
"//eden/scm/lib/backtrace-python/offset-probe:backtrace-python-offset-probe",
27+
]},
928
"lib": {"name": "backtrace-python"},
1029
"package": {
1130
"authors": ["Meta Source Control Team <sourcecontrol-dev@meta.com>"],
@@ -17,6 +36,7 @@ rust_library(
1736
},
1837
}},
1938
crate_root = "src/lib.rs",
39+
mapped_srcs = {":gen_offsets.rs": "src/offsets.rs"},
2040
deps = [
2141
"fbsource//third-party/rust:libc",
2242
"//eden/scm/lib/backtrace-ext:backtrace-ext",

eden/scm/lib/backtrace-python/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ name = "backtrace_python"
1717
libc = "0.2.139"
1818
sapling-backtrace-ext = { version = "0.1.0", path = "../backtrace-ext" }
1919
sapling-evalframe-sys = { version = "0.1.0", path = "evalframe-sys" }
20+
21+
[build-dependencies]
22+
sapling-backtrace-python-offset-probe = { version = "0.1.0", path = "offset-probe" }
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
//! Build script for backtrace-python.
9+
//!
10+
//! This probes the offsets needed to extract Python frames from native stack
11+
//! traces and passes them to the compiler via environment variables.
12+
13+
fn main() {
14+
println!("cargo:rerun-if-changed=build.rs");
15+
16+
if let Some(offsets) = backtrace_python_offset_probe::get_offsets() {
17+
println!("cargo::rustc-env=BACKTRACE_PYTHON_OFFSET_IP={}", offsets.0);
18+
println!("cargo::rustc-env=BACKTRACE_PYTHON_OFFSET_SP={}", offsets.1);
19+
eprintln!("Got offsets: {offsets:?}");
20+
} else {
21+
eprintln!("No offset");
22+
}
23+
}

eden/scm/lib/backtrace-python/src/lib.rs

Lines changed: 6 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use backtrace_ext::SupplementalFrameResolver;
1919
use backtrace_ext::SupplementalInfo;
2020

2121
mod libpython_filter;
22+
mod offsets;
2223

2324
/// Setup backtrace-ext to resolve Python frames on supported platforms.
2425
/// This function is a no-op if the platform is not supported.
@@ -47,6 +48,7 @@ pub fn init() {
4748
#[derive(Clone, Copy, Debug)]
4849
pub struct SupportedInfo {
4950
/// Whether the (OS, architecture) combination is supported.
51+
/// Decided by whether the `offsets` can be detected at build time.
5052
pub os_arch: bool,
5153
/// Whether the C evalframe logic supports frame resolution.
5254
/// This is usually affected by the cpython version.
@@ -60,7 +62,7 @@ impl SupportedInfo {
6062

6163
fn new() -> Self {
6264
Self {
63-
os_arch: OFFSET.is_some(),
65+
os_arch: offsets::OFFSET_IP.is_some() && offsets::OFFSET_SP.is_some(),
6466
c_evalframe: evalframe_sys::resolve_frame_is_supported(),
6567
}
6668
}
@@ -71,80 +73,10 @@ pub static SUPPORTED_INFO: LazyLock<SupportedInfo> = LazyLock::new(SupportedInfo
7173
#[derive(Copy, Clone)]
7274
struct PythonSupplementalFrameResolver;
7375

74-
/// Raw offsets.
75-
/// When IP (program counter) is `OFFSET.0 + Sapling_PyEvalFrame`,
76-
/// the `PyFrame` can be read at `OFFSET.1 + SP`.
77-
const OFFSET: Option<(usize, usize)> = {
78-
if cfg!(all(
79-
any(target_os = "linux", target_os = "macos"),
80-
target_arch = "x86_64"
81-
)) {
82-
// Sapling_PyEvalFrame(PyThreadState* tstate, PyFrameObject* f, int exc)
83-
// (lldb) disassemble -n Sapling_PyEvalFrame
84-
// `Sapling_PyEvalFrame:
85-
// <+0>: pushq %rbp
86-
// <+1>: movq %rsp, %rbp ; FP
87-
// <+4>: subq $0x20, %rsp ; SP = FP - 0x20
88-
// <+8>: movq %rdi, -0x18(%rbp)
89-
// <+12>: movq %rsi, -0x10(%rbp) ; PyFrame f at FP - 0x10 or SP + 0x10
90-
// <+16>: movl %edx, -0x4(%rbp)
91-
// <+19>: movq -0x18(%rbp), %rdi
92-
// <+23>: movq -0x10(%rbp), %rsi
93-
// <+27>: movl -0x4(%rbp), %edx
94-
// <+30>: callq 0x8d4eb0 ; symbol stub for: _PyEval_EvalFrameDefault
95-
// <+35>: addq $0x20, %rsp
96-
// <+39>: popq %rbp
97-
// <+40>: retq
98-
Some((35, 0x10))
99-
} else if cfg!(all(
100-
any(target_os = "linux", target_os = "macos"),
101-
target_arch = "aarch64"
102-
)) {
103-
// <+0>: sub sp, sp, #0x30
104-
// <+4>: stp x29, x30, [sp, #0x20]
105-
// <+8>: add x29, sp, #0x20 ; FP (x29) = SP + 0x20
106-
// <+12>: stur x0, [x29, #-0x8] ; x0 is 1st arg (tstate)
107-
// <+16>: str x1, [sp, #0x10] ; x1 is 2nd arg (f), at SP + 0x10
108-
// <+20>: str w2, [sp, #0xc]
109-
// <+24>: ldur x0, [x29, #-0x8]
110-
// <+28>: ldr x1, [sp, #0x10]
111-
// <+32>: ldr w2, [sp, #0xc]
112-
// <+36>: bl 0x102c76340 ; symbol stub for: _PyEval_EvalFrameDefault
113-
// <+40>: ldp x29, x30, [sp, #0x20]
114-
// <+44>: add sp, sp, #0x30
115-
// <+48>: ret
116-
Some((40, 0x10))
117-
} else if cfg!(all(
118-
target_os = "windows",
119-
target_env = "msvc",
120-
target_arch = "x86_64"
121-
)) {
122-
// <+0>: pushq %rbp
123-
// <+1>: subq $0x40, %rsp
124-
// <+5>: leaq 0x40(%rsp), %rbp ; FP = SP + 0x40
125-
// <+10>: movl %r8d, -0x4(%rbp)
126-
// <+14>: movq %rdx, -0x18(%rbp) ; rdx is 2nd arg. FP - 0x18 = SP + 0x28
127-
// <+18>: movq %rcx, -0x10(%rbp)
128-
// <+22>: movl -0x4(%rbp), %r8d
129-
// <+26>: movq -0x18(%rbp), %rdx
130-
// <+30>: movq -0x10(%rbp), %rcx
131-
// <+34>: callq *0x517e830(%rip)
132-
// <+40>: nop
133-
// <+41>: addq $0x40, %rsp
134-
// <+45>: popq %rbp
135-
// <+46>: retq
136-
Some((40, 0x28))
137-
} else {
138-
// Unsupported OS or arch.
139-
None
140-
}
141-
};
142-
14376
impl SupplementalFrameResolver for PythonSupplementalFrameResolver {
14477
fn maybe_extract_supplemental_info(&self, ip: usize, sp: usize) -> FrameDecision {
145-
let offset: usize = match OFFSET {
146-
Some(o) => o.0,
147-
None => return FrameDecision::Keep,
78+
let Some(offset) = offsets::OFFSET_IP else {
79+
return FrameDecision::Keep;
14880
};
14981
if ip != (evalframe_sys::sapling_py_eval_frame_addr() + offset) {
15082
// Skip native python frames to reduce noise.
@@ -187,7 +119,7 @@ fn extract_python_supplemental_info(sp: usize) -> Option<SupplementalInfo> {
187119
return None;
188120
}
189121
// Read the `f` variable on stack. See sapling/dbgutil.py, D55728746
190-
let offset = OFFSET?.1;
122+
let offset = offsets::OFFSET_SP?;
191123
let addr = sp.checked_add(offset)?;
192124
unsafe {
193125
let frame_ptr: *const *mut libc::c_void = addr as *const _;
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
//! Offsets for extracting Python frames from native stack traces.
9+
//!
10+
//! For Cargo builds, these are read from environment variables set by build.rs.
11+
//! For Buck builds, this file is replaced by a generated version with constants.
12+
13+
/// IP offset within Sapling_PyEvalFrame where the PyFrame can be read.
14+
pub const OFFSET_IP: Option<usize> = match option_env!("BACKTRACE_PYTHON_OFFSET_IP") {
15+
Some(s) if !s.is_empty() => Some(parse_usize(s)),
16+
_ => None,
17+
};
18+
19+
/// SP offset to read the PyFrame pointer.
20+
pub const OFFSET_SP: Option<usize> = match option_env!("BACKTRACE_PYTHON_OFFSET_SP") {
21+
Some(s) if !s.is_empty() => Some(parse_usize(s)),
22+
_ => None,
23+
};
24+
25+
/// Parse a usize from a string at compile time.
26+
const fn parse_usize(s: &str) -> usize {
27+
let bytes = s.as_bytes();
28+
let mut result: usize = 0;
29+
let mut i = 0;
30+
while i < bytes.len() {
31+
let b = bytes[i];
32+
if b >= b'0' && b <= b'9' {
33+
result = result * 10 + (b - b'0') as usize;
34+
}
35+
i += 1;
36+
}
37+
result
38+
}

0 commit comments

Comments
 (0)