Skip to content

Commit 1c75e0a

Browse files
committed
Initial public version of allocscope
Added allocscope-trace for tracing allocations made by another process. Added allocscope-view for viewing a trace produced by allocscope-trace. Added scripts for building static binaries.
0 parents  commit 1c75e0a

27 files changed

+4624
-0
lines changed

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Cargo.lock
2+
/target
3+
TODO.txt
4+
sigtrap/sigtrap
5+
build-static/release

COPYING

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[workspace]
2+
members = ["allocscope-trace", "allocscope-view"]

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# allocscope
2+
### a memory tracking tool
3+
4+
allocscope is a tool for tracking down where the most egregiously large allocations are occurring
5+
in a C, C++ or Rust codebase.
6+
7+
It is composed of two commands:
8+
9+
`allocscope-trace` attaches to another process as a debugger. By using breakpoints on memory
10+
allocation functions such as `malloc` it tracks allocations made by that process.
11+
12+
`allocscope-view` reads a trace file produced by `allocscope-trace`. It presents a summary of all
13+
allocations made in a call tree format, which can be sorted by largest concurrent allocation,
14+
total number of blocks, or number of unfreed allocation blocks.
15+
16+
# License
17+
18+
allocscope is licensed GNU General Public License version 3.

allocscope-trace/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "allocscope-trace"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
libc = "0.2"
8+
libunwind-sys = { version = "0.5.1", features = ["ptrace"] }
9+
object = "0.29.0"
10+
rusqlite = "0.28.0"

allocscope-trace/src/breakpoint.rs

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
/*
2+
allocscope - a memory tracking tool
3+
Copyright (C) 2023 Matt Kimball
4+
5+
This program is free software: you can redistribute it and/or modify it
6+
under the terms of the GNU General Public License as published by the
7+
Free Software Foundation, either version 3 of the License, or (at your
8+
option) any later version.
9+
10+
This program is distributed in the hope that it will be useful, but
11+
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
12+
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
13+
for more details.
14+
15+
You should have received a copy of the GNU General Public License along
16+
with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
use crate::context;
20+
use crate::process_map;
21+
use crate::ptrace;
22+
use crate::symbol_index;
23+
use crate::trace;
24+
use std::collections::{HashMap, HashSet};
25+
use std::error::Error;
26+
27+
// A callback invoked with a breakpoint it triggered in a traced process.
28+
pub type BreakpointCallback =
29+
fn(context: &mut context::TraceContext, pid: u32) -> Result<(), Box<dyn Error>>;
30+
31+
// A callback invoked when a system call is made by a traced process.
32+
//
33+
// 'complete' will be false as the system call is entered, and true as it
34+
// exits.
35+
pub type SyscallCallback =
36+
fn(context: &mut context::TraceContext, pid: u32, complete: bool) -> Result<(), Box<dyn Error>>;
37+
38+
// Tracking data for a breakpoint.
39+
pub struct Breakpoint {
40+
// The instruction address at which the breakpoint was inserted.
41+
pub address: u64,
42+
43+
// The original instructions at the 8-byte aligned address where
44+
// the breakpoint was insertered.
45+
pub original_instruction: u64,
46+
47+
// The callback to invoke when the breakpoint is hit.
48+
pub callback: BreakpointCallback,
49+
50+
// true if the breakpoint should remain after being encountered.
51+
// false for one shot breakpoints.
52+
pub persist: bool,
53+
54+
// A set of thread on which the callback should be invoked when
55+
// the breakpoint is encountered. Used by one shot breakpoints.
56+
pub one_shot_threads: HashSet<u32>,
57+
}
58+
59+
// The binding between an unresolved symbol name (function name) and
60+
// the callback to invoke when the breakpoint is encountered.
61+
pub struct BreakpointLooseBinding {
62+
// Name of function at which set set the breakpoint.
63+
pub function_name: String,
64+
65+
// The callback to invoke.
66+
pub callback: BreakpointCallback,
67+
}
68+
69+
// The set of all breakpoints relevant to a traced process.
70+
pub struct BreakpointSet {
71+
// The bindings between function names and callbacks.
72+
pub bindings: Vec<BreakpointLooseBinding>,
73+
74+
// The breakpoint instructions which have been inserted into the process.
75+
pub breakpoints: HashMap<u64, Breakpoint>,
76+
77+
// Intercepted system calls for the process.
78+
pub syscall_intercepts: HashMap<i64, SyscallCallback>,
79+
}
80+
81+
// Insert a breakpoint in the address space of the traced process.
82+
fn insert_breakpoint_instruction(pid: u32, address: u64) -> Result<(), Box<dyn Error>> {
83+
// The peektext / poketext are 8-byte aligned, but x86_64 instructions are
84+
// not, so we need to shift the appropriate byte.
85+
let shift = (address & 7) * 8;
86+
let code = ptrace::peektext(pid, address & !7);
87+
88+
// The x86_64 instruction 'int3' is encoded as 0xCC.
89+
let instruction = (0xCC << shift) | (code & !(0xFF << shift));
90+
91+
ptrace::poketext(pid, address & !7, instruction)?;
92+
93+
Ok(())
94+
}
95+
96+
// Remove a previously inserted breakpoint, restoring the original instruction.
97+
fn remove_breakpoint_instruction(
98+
pid: u32,
99+
address: u64,
100+
original_instruction: u64,
101+
) -> Result<(), Box<dyn Error>> {
102+
// The peektext / poketext are 8-byte aligned, but x86_64 instructions are
103+
// not, so we need to shift the appropriate byte.
104+
let shift = (address & 7) * 8;
105+
let code = ptrace::peektext(pid, address & !7);
106+
107+
// We want to restore only one byte, rather than the entire 8-byte word,
108+
// because there could be other inserted breakpoints within the same
109+
// word which we don't want to disrupt.
110+
let instruction = (original_instruction & (0xFF << shift)) | (code & !(0xFF << shift));
111+
112+
ptrace::poketext(pid, address & !7, instruction)?;
113+
114+
Ok(())
115+
}
116+
117+
// Insert a breakpoint into a traced process, taking care to handle the case
118+
// where a breakpoint has already been inserted at the same address, and
119+
// update the bookkeeping for one-shot breakpoints.
120+
fn add_breakpoint(
121+
breakpoints: &mut HashMap<u64, Breakpoint>,
122+
pid: u32,
123+
address: u64,
124+
callback: BreakpointCallback,
125+
persist: bool,
126+
) -> Result<(), Box<dyn Error>> {
127+
// It may be that another thread wants a one-shot breakpoint at the same
128+
// address. In such a case, avoid a double insert so that we don't
129+
// read the previously inserted breakpoint as the "original" instruction.
130+
if !breakpoints.contains_key(&address) {
131+
let original_instruction = ptrace::peektext(pid, address & !7);
132+
insert_breakpoint_instruction(pid, address)?;
133+
134+
let breakpoint = Breakpoint {
135+
address,
136+
original_instruction,
137+
callback,
138+
persist,
139+
one_shot_threads: HashSet::new(),
140+
};
141+
breakpoints.insert(address, breakpoint);
142+
}
143+
144+
// Regardless of whether we modified instructions above, track this
145+
// thread as interested if this is a one shot breakpoint.
146+
let breakpoint = breakpoints.get_mut(&address).ok_or("breakpoint missing")?;
147+
if !persist {
148+
breakpoint.one_shot_threads.insert(pid);
149+
}
150+
151+
Ok(())
152+
}
153+
154+
impl Breakpoint {
155+
// Step through a breakpoint by restoring the instruction which was
156+
// replaced when the breakpoint was set, stepping through that one
157+
// instruction, and then putting the breakpoint back.
158+
pub fn step_through(&self, pid: u32) -> Result<(), Box<dyn Error>> {
159+
self.remove_breakpoint_instruction(pid)?;
160+
ptrace::singlestep(pid)?;
161+
trace::wait_for_signal(pid, libc::SIGTRAP)?;
162+
insert_breakpoint_instruction(pid, self.address)?;
163+
164+
Ok(())
165+
}
166+
167+
// Remove the breakpoint by restoring the original instruction.
168+
fn remove_breakpoint_instruction(&self, pid: u32) -> Result<(), Box<dyn Error>> {
169+
remove_breakpoint_instruction(pid, self.address, self.original_instruction)
170+
}
171+
}
172+
173+
impl BreakpointSet {
174+
// Create a new empty set of breakpoints for a traced process.
175+
pub fn new() -> BreakpointSet {
176+
BreakpointSet {
177+
bindings: Vec::new(),
178+
breakpoints: HashMap::new(),
179+
syscall_intercepts: HashMap::new(),
180+
}
181+
}
182+
183+
// Add a one shot breakpoint at a specific address. This is used
184+
// following a stack trace to breakpoint at the return of a function.
185+
pub fn add_one_shot_breakpoint(
186+
&mut self,
187+
pid: u32,
188+
address: u64,
189+
callback: BreakpointCallback,
190+
) -> Result<(), Box<dyn Error>> {
191+
add_breakpoint(&mut self.breakpoints, pid, address, callback, false)
192+
}
193+
194+
// Disable a one shot breakpoint for a particular thread.
195+
pub fn remove_one_shot_breakpoint(
196+
&mut self,
197+
pid: u32,
198+
address: u64,
199+
) -> Result<(), Box<dyn Error>> {
200+
let breakpoint = self
201+
.breakpoints
202+
.get_mut(&address)
203+
.ok_or("breakpoint missing")?;
204+
breakpoint.one_shot_threads.remove(&pid);
205+
206+
Ok(())
207+
}
208+
209+
// Break at the entry point of a particular function name.
210+
// This only creates a loose binding (binding by function name) because
211+
// the relevant code may not be mapped into the process yet.
212+
pub fn breakpoint_on(&mut self, function_name: &str, callback: BreakpointCallback) {
213+
self.bindings.push(BreakpointLooseBinding {
214+
function_name: function_name.to_string(),
215+
callback: callback,
216+
});
217+
}
218+
219+
// Add a callback for a particular system call.
220+
pub fn add_syscall_intercept(&mut self, syscall_id: i64, callback: SyscallCallback) {
221+
self.syscall_intercepts.insert(syscall_id, callback);
222+
}
223+
224+
// Rebind all previously bound breakpoints. Used when new symbols may
225+
// have been resolved.
226+
fn rebind_breakpoints(&mut self, pid: u32) -> Result<(), Box<dyn Error>> {
227+
for breakpoint in self.breakpoints.values() {
228+
insert_breakpoint_instruction(pid, breakpoint.address)?;
229+
}
230+
231+
Ok(())
232+
}
233+
234+
// Resolve all loosely bound breakpoint using the current process map of
235+
// the traced process.
236+
pub fn resolve_breakpoints(&mut self, pid: u32) -> Result<(), Box<dyn Error>> {
237+
let process_map = process_map::ProcessMap::new(pid)?;
238+
let mut symbol_index = symbol_index::SymbolIndex::new();
239+
symbol_index.add_symbols(&process_map);
240+
241+
for binding in self.bindings.iter() {
242+
match symbol_index.symbols.get(&binding.function_name) {
243+
Some(entry) => {
244+
// For each address of the function, set a breakpoint.
245+
// Multiple addresses might be necessary, because there
246+
// might be multiple linked copies of a function with the
247+
// same name. (Consider multiple linked copies of libc
248+
// in the same process.)
249+
for address in &entry.addresses {
250+
if !self.breakpoints.contains_key(address) {
251+
add_breakpoint(
252+
&mut self.breakpoints,
253+
pid,
254+
*address,
255+
binding.callback,
256+
true,
257+
)?;
258+
}
259+
}
260+
}
261+
None => (),
262+
}
263+
}
264+
265+
// XXX: This will cause damage if a shared library is unmapped but we
266+
// still have the breakpoint.
267+
self.rebind_breakpoints(pid)?;
268+
269+
Ok(())
270+
}
271+
272+
// Remove all previously inserted breakpoints from the process. Used
273+
// when deatching from a process to leave it in a runnable state when
274+
// not being traced.
275+
pub fn clear_breakpoints(&mut self, pid: u32) -> Result<(), Box<dyn Error>> {
276+
for breakpoint in self.breakpoints.values() {
277+
breakpoint.remove_breakpoint_instruction(pid)?;
278+
}
279+
280+
Ok(())
281+
}
282+
}

0 commit comments

Comments
 (0)