Skip to content

Commit 58a5d0a

Browse files
quark-zjumeta-codesync[bot]
authored andcommitted
backtrace-python: add offset-probe to detect IP and SP offsets
Summary: The disassambly can vary by compiler settings: Example buck build `Sapling_PyEvalFrame` disassambly: <+0>: pushq %rbp <+1>: movq %rsp, %rbp ; FP <+4>: subq $0x20, %rsp ; SP = FP - 0x20 <+8>: movq %rdi, -0x18(%rbp) <+12>: movq %rsi, -0x10(%rbp) ; PyFrame f at FP - 0x10 or SP + 0x10 <+16>: movl %edx, -0x4(%rbp) <+19>: movq -0x18(%rbp), %rdi <+23>: movq -0x10(%rbp), %rsi <+27>: movl -0x4(%rbp), %edx <+30>: callq 0x8d4eb0 ; symbol stub for: _PyEval_EvalFrameDefault <+35>: addq $0x20, %rsp ; OFFSET_IP = 35 <+39>: popq %rbp <+40>: retq Example cargo build `Sapling_PyEvalFrame` disassambly: <+0>: endbr64 ; -C link-arg=-fcf-protection=full <+4>: pushq %rbp <+5>: movq %rsp, %rbp <+8>: subq $0x20, %rsp <+12>: movq %rdi, -0x8(%rbp) <+16>: movq %rsi, -0x10(%rbp) ; OFFSET_SP = 0x10 <+20>: movl %edx, -0x14(%rbp) <+23>: movl -0x14(%rbp), %edx <+26>: movq -0x10(%rbp), %rcx <+30>: movq -0x8(%rbp), %rax <+34>: movq %rcx, %rsi <+37>: movq %rax, %rdi <+40>: callq 0x4e5e0b0 ; symbol stub for: _PyEval_EvalFrameDefault <+45>: leave ; OFFSET_IP = 45 <+46>: retq Add a `offset-probe` library intended to be used by cargo `build.rs` to extract offsets. A codegen binary is added for buck `genrule`. Reviewed By: muirdm Differential Revision: D92185212 fbshipit-source-id: d767f3ee715b074951f16dbd99ec8e3015a6f62b
1 parent a271550 commit 58a5d0a

5 files changed

Lines changed: 263 additions & 0 deletions

File tree

eden/scm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ members = [
1616
"lib/backtrace-ext",
1717
"lib/backtrace-python",
1818
"lib/backtrace-python/evalframe-sys",
19+
"lib/backtrace-python/offset-probe",
1920
"lib/blackbox",
2021
"lib/blackbox/serde_alt",
2122
"lib/blob",
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
load("@fbsource//tools/build_defs:rust_binary.bzl", "rust_binary")
2+
load("//eden/scm:targets.bzl", "rust_python_library")
3+
4+
oncall("sapling")
5+
6+
rust_python_library(
7+
name = "backtrace-python-offset-probe",
8+
srcs = glob(
9+
["src/**/*.rs"],
10+
exclude = ["src/bin/**"],
11+
),
12+
autocargo = {
13+
"cargo_toml_config": {
14+
"package": {
15+
"description": "Attmpt to obtain raw offsets used to extract Python frames. Intended to be used at build time.",
16+
"homepage": "https://sapling-scm.com/",
17+
"license": "MIT",
18+
"name": "sapling-backtrace-python-offset-probe",
19+
"repository": "https://github.com/facebook/sapling",
20+
},
21+
},
22+
},
23+
deps = [
24+
"fbsource//third-party/rust:backtrace",
25+
"fbsource//third-party/rust:libc",
26+
"//eden/scm/lib/backtrace-python/evalframe-sys:evalframe-sys",
27+
],
28+
)
29+
30+
rust_binary(
31+
name = "offset-codegen",
32+
srcs = ["src/bin/offset-codegen.rs"],
33+
crate_root = "src/bin/offset-codegen.rs",
34+
deps = [
35+
":backtrace-python-offset-probe",
36+
],
37+
)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# @generated by autocargo from //eden/scm/lib/backtrace-python/offset-probe:[backtrace-python-offset-probe,offset-codegen]
2+
3+
[package]
4+
name = "sapling-backtrace-python-offset-probe"
5+
version = "0.1.0"
6+
edition = "2024"
7+
description = "Attmpt to obtain raw offsets used to extract Python frames. Intended to be used at build time."
8+
homepage = "https://sapling-scm.com/"
9+
repository = "https://github.com/facebook/sapling"
10+
license = "MIT"
11+
12+
[lib]
13+
name = "backtrace_python_offset_probe"
14+
15+
[[bin]]
16+
name = "offset_codegen"
17+
path = "src/bin/offset-codegen.rs"
18+
19+
[dependencies]
20+
backtrace = "0.3"
21+
cpython = { version = "0.7.2", features = ["python3-sys"], default-features = false }
22+
libc = "0.2.139"
23+
sapling-evalframe-sys = { version = "0.1.0", path = "../evalframe-sys" }
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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+
//! Binary to probe for offsets and output Rust constants.
9+
//! Writes a Rust source file with OFFSET_IP and OFFSET_SP constants.
10+
11+
fn main() {
12+
let (ip, sp) = match backtrace_python_offset_probe::get_offsets() {
13+
Some((ip, sp)) => (Some(ip), Some(sp)),
14+
None => (None, None),
15+
};
16+
17+
#[allow(clippy::print_literal)]
18+
println!(
19+
r#"/*
20+
* Copyright (c) Meta Platforms, Inc. and affiliates.
21+
*
22+
* This source code is licensed under the MIT license found in the
23+
* LICENSE file in the root directory of this source tree.
24+
*/
25+
26+
// @{}enerated by offset-codegen. Do not edit.
27+
28+
/// IP offset within Sapling_PyEvalFrame where the PyFrame can be read.
29+
pub const OFFSET_IP: Option<usize> = {:?};
30+
31+
/// SP offset within Sapling_PyEvalFrame to read the PyFrame pointer.
32+
pub const OFFSET_SP: Option<usize> = {:?};
33+
"#,
34+
"g", ip, sp,
35+
);
36+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
use core::iter::Iterator;
9+
use std::sync::atomic::AtomicUsize;
10+
use std::sync::atomic::Ordering;
11+
12+
use cpython::*;
13+
use evalframe_sys::EvalFrameMode;
14+
15+
/// IP (PC) Offset in Sapling_PyEvalFrame after the `call ...`.
16+
static OFFSET_IP: AtomicUsize = AtomicUsize::new(0);
17+
18+
/// SP Offset to get the Python frame object.
19+
static OFFSET_SP: AtomicUsize = AtomicUsize::new(0);
20+
21+
/// Attempt to get the IP and SP offsets.
22+
pub fn get_offsets() -> Option<(usize, usize)> {
23+
// Unsupported Python version (Python C API)?
24+
if !evalframe_sys::resolve_frame_is_supported() {
25+
return None;
26+
}
27+
28+
let gil = Python::acquire_gil();
29+
let py = gil.python();
30+
31+
// Use probe mode to track last_frame for offset detection.
32+
unsafe { evalframe_sys::set_mode(EvalFrameMode::Probe) };
33+
34+
let m = PyModule::new(py, "probe").ok()?;
35+
m.add(py, "examine_backtrace", py_fn!(py, examine_backtrace()))
36+
.ok()?;
37+
38+
let code = r#"
39+
def call_native_examine_backtrace():
40+
examine_backtrace()
41+
call_native_examine_backtrace()
42+
"#;
43+
py.run(code, Some(&m.dict(py)), None).ok()?;
44+
45+
let offset_ip = OFFSET_IP.load(Ordering::Acquire);
46+
let offset_sp = OFFSET_SP.load(Ordering::Acquire);
47+
if offset_ip > 0 {
48+
Some((offset_ip, offset_sp))
49+
} else {
50+
None
51+
}
52+
}
53+
54+
/// Attempt to set OFFSET_IP and OFFSET_SP.
55+
/// Intended to be called from a pure Python function.
56+
fn examine_backtrace(_py: Python) -> PyResult<Option<bool>> {
57+
backtrace::trace(|frame| {
58+
let ip = frame.ip() as usize;
59+
let start = evalframe_sys::sapling_py_eval_frame_addr();
60+
// How many bytes the `Sapling_PyEvalFrame` function has at most?
61+
const CODE_SIZE_THRESHOLD: usize = 64;
62+
// How many bytes the `Sapling_PyEvalFrame` stack might be at most?
63+
const STACK_SIZE_THRESHOLD: usize = 48;
64+
if ip >= start && ip <= start + CODE_SIZE_THRESHOLD {
65+
let sp = frame.sp() as usize;
66+
let last_frame = evalframe_sys::get_last_frame();
67+
68+
// Try "known" SP offsets first. This reduces issues reading "bad" offsets.
69+
let sp_offset_hints: &[usize] = if cfg!(all(
70+
any(target_os = "linux", target_os = "macos"),
71+
any(target_arch = "x86_64", target_arch = "aarch64"),
72+
)) {
73+
// x86_64
74+
// Sapling_PyEvalFrame(PyThreadState* tstate, PyFrameObject* f, int exc)
75+
// (lldb) disassemble -n Sapling_PyEvalFrame
76+
// <+0>: pushq %rbp
77+
// <+1>: movq %rsp, %rbp ; FP
78+
// <+4>: subq $0x20, %rsp ; SP = FP - 0x20
79+
// <+8>: movq %rdi, -0x18(%rbp)
80+
// <+12>: movq %rsi, -0x10(%rbp) ; PyFrame f at FP - 0x10 or SP + 0x10
81+
// <+16>: movl %edx, -0x4(%rbp)
82+
// <+19>: movq -0x18(%rbp), %rdi
83+
// <+23>: movq -0x10(%rbp), %rsi
84+
// <+27>: movl -0x4(%rbp), %edx
85+
// <+30>: callq 0x8d4eb0 ; symbol stub for: _PyEval_EvalFrameDefault
86+
// <+35>: addq $0x20, %rsp
87+
// <+39>: popq %rbp
88+
// <+40>: retq
89+
//
90+
// x86_64 with FCF protection:
91+
// <+0>: endbr64
92+
// <+4>: pushq %rbp
93+
// <+5>: movq %rsp, %rbp
94+
// <+8>: subq $0x20, %rsp
95+
// <+12>: movq %rdi, -0x8(%rbp)
96+
// <+16>: movq %rsi, -0x10(%rbp) ; PyFrame f at SP + 0x10
97+
// <+20>: movl %edx, -0x14(%rbp)
98+
// <+23>: movl -0x14(%rbp), %edx
99+
// <+26>: movq -0x10(%rbp), %rcx
100+
// <+30>: movq -0x8(%rbp), %rax
101+
// <+34>: movq %rcx, %rsi
102+
// <+37>: movq %rax, %rdi
103+
// <+40>: callq 0x4e5e0b0 ; symbol stub for: _PyEval_EvalFrameDefault
104+
// <+45>: leave
105+
// <+46>: retq
106+
//
107+
// aarch64
108+
// <+0>: sub sp, sp, #0x30
109+
// <+4>: stp x29, x30, [sp, #0x20]
110+
// <+8>: add x29, sp, #0x20 ; FP (x29) = SP + 0x20
111+
// <+12>: stur x0, [x29, #-0x8] ; x0 is 1st arg (tstate)
112+
// <+16>: str x1, [sp, #0x10] ; x1 is 2nd arg (f), at SP + 0x10
113+
// <+20>: str w2, [sp, #0xc]
114+
// <+24>: ldur x0, [x29, #-0x8]
115+
// <+28>: ldr x1, [sp, #0x10]
116+
// <+32>: ldr w2, [sp, #0xc]
117+
// <+36>: bl 0x102c76340 ; symbol stub for: _PyEval_EvalFrameDefault
118+
// <+40>: ldp x29, x30, [sp, #0x20]
119+
// <+44>: add sp, sp, #0x30
120+
// <+48>: ret
121+
&[0x10]
122+
} else if cfg!(all(
123+
target_os = "windows",
124+
target_env = "msvc",
125+
target_arch = "x86_64"
126+
)) {
127+
// <+0>: pushq %rbp
128+
// <+1>: subq $0x40, %rsp
129+
// <+5>: leaq 0x40(%rsp), %rbp ; FP = SP + 0x40
130+
// <+10>: movl %r8d, -0x4(%rbp)
131+
// <+14>: movq %rdx, -0x18(%rbp) ; rdx is 2nd arg. FP - 0x18 = SP + 0x28
132+
// <+18>: movq %rcx, -0x10(%rbp)
133+
// <+22>: movl -0x4(%rbp), %r8d
134+
// <+26>: movq -0x18(%rbp), %rdx
135+
// <+30>: movq -0x10(%rbp), %rcx
136+
// <+34>: callq *0x517e830(%rip)
137+
// <+40>: nop
138+
// <+41>: addq $0x40, %rsp
139+
// <+45>: popq %rbp
140+
// <+46>: retq
141+
&[0x28]
142+
} else {
143+
// Unsupported OS or arch.
144+
&[]
145+
};
146+
147+
for sp_offset in sp_offset_hints
148+
.iter()
149+
.copied()
150+
.chain((0..=STACK_SIZE_THRESHOLD).step_by(std::mem::size_of::<usize>()))
151+
{
152+
let frame_ptr: *const *mut libc::c_void = (sp + sp_offset) as *const _;
153+
let frame: *mut libc::c_void = unsafe { *frame_ptr };
154+
if frame as usize == last_frame {
155+
// Got the SP offset and IP offset.
156+
OFFSET_IP.store(ip - start, Ordering::Release);
157+
OFFSET_SP.store(sp_offset, Ordering::Release);
158+
return false;
159+
}
160+
}
161+
}
162+
// Not Sapling_PyEvalFrame. Check the next frame.
163+
true
164+
});
165+
Ok(None)
166+
}

0 commit comments

Comments
 (0)