blob: eed6a162e040d1568b715fcd4ec9718066f8cd5d [file] [log] [blame]
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -08001#!/usr/bin/env python
2
3from bcc import BPF
4from time import sleep
5import argparse
6import subprocess
7
8examples = """
9EXAMPLES:
10
11memleak.py -p $(pidof allocs)
12 Trace allocations and display a summary of "leaked" (outstanding)
13 allocations every 5 seconds
14memleak.py -p $(pidof allocs) -t
15 Trace allocations and display each individual call to malloc/free
16memleak.py -p $(pidof allocs) -a -i 10
17 Trace allocations and display allocated addresses, sizes, and stacks
18 every 10 seconds for outstanding allocations
19memleak.py
20 Trace allocations in kernel mode and display a summary of outstanding
21 allocations every 5 seconds
22"""
23
24description = """
25Trace outstanding memory allocations that weren't freed.
26Supports both user-mode allocations made with malloc/free and kernel-mode
27allocations made with kmalloc/kfree.
28"""
29
30parser = argparse.ArgumentParser(description=description, formatter_class=argparse.RawDescriptionHelpFormatter, epilog=examples)
31parser.add_argument("-p", "--pid", help="the PID to trace; if not specified, trace kernel allocs")
32parser.add_argument("-t", "--trace", action="store_true", help="print trace messages for each alloc/free call")
33parser.add_argument("-i", "--interval", default=5, help="interval in seconds to print outstanding allocations")
34parser.add_argument("-a", "--show-allocs", default=False, action="store_true", help="show allocation addresses and sizes as well as call stacks")
35
36args = parser.parse_args()
37
38pid = -1 if args.pid is None else int(args.pid)
39kernel_trace = (pid == -1)
40trace_all = args.trace
41interval = int(args.interval)
42
43bpf_source = open("memleak.c").read()
44bpf_source = bpf_source.replace("SHOULD_PRINT", "1" if trace_all else "0")
45
46bpf_program = BPF(text=bpf_source)
47
48if not kernel_trace:
49 print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % pid)
50 bpf_program.attach_uprobe(name="c", sym="malloc", fn_name="alloc_enter", pid=pid)
51 bpf_program.attach_uretprobe(name="c", sym="malloc", fn_name="alloc_exit", pid=pid)
52 bpf_program.attach_uprobe(name="c", sym="free", fn_name="free_enter", pid=pid)
53else:
54 print("Attaching to kmalloc and kfree, Ctrl+C to quit.")
55 bpf_program.attach_kprobe(event="__kmalloc", fn_name="alloc_enter")
56 bpf_program.attach_kretprobe(event="__kmalloc", fn_name="alloc_exit")
57 bpf_program.attach_kprobe(event="kfree", fn_name="free_enter")
58
59def get_code_ranges(pid):
60 ranges = {}
61 raw_ranges = open("/proc/%d/maps" % pid).readlines()
62 for raw_range in raw_ranges:
63 parts = raw_range.split()
64 if len(parts) < 6 or parts[5][0] == '[' or not 'x' in parts[1]:
65 continue
66 binary = parts[5]
67 range_parts = parts[0].split('-')
68 addr_range = (int(range_parts[0], 16), int(range_parts[1], 16))
69 ranges[binary] = addr_range
70 return ranges
71
72def run_command(command):
73 p = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
74 return iter(p.stdout.readline, b'')
75
76ranges_cache = {}
77
78def get_sym_ranges(binary):
79 if binary in ranges_cache:
80 return ranges_cache[binary]
81 sym_ranges = {}
82 raw_symbols = run_command("objdump -t %s" % binary)
83 for raw_symbol in raw_symbols:
84 parts = raw_symbol.split()
85 if len(parts) < 6 or parts[3] != ".text" or parts[2] != "F":
86 continue
87 sym_start = int(parts[0], 16)
88 sym_len = int(parts[4], 16)
89 sym_name = parts[5]
90 sym_ranges[sym_name] = (sym_start, sym_len)
91 ranges_cache[binary] = sym_ranges
92 return sym_ranges
93
94def decode_sym(binary, offset):
95 sym_ranges = get_sym_ranges(binary)
96 for name, (start, length) in sym_ranges.items():
97 if offset >= start and offset <= (start + length):
98 return "%s+0x%x" % (name, offset - start)
99 return "%x" % offset
100
101def decode_addr(code_ranges, addr):
102 for binary, (start, end) in code_ranges.items():
103 if addr >= start and addr <= end:
104 offset = addr - start if binary.endswith(".so") else addr
105 return "%s %s" % (binary, decode_sym(binary, offset))
106 return "%x" % addr
107
108def decode_stack(info):
109 stack = ""
110 if info.num_frames <= 0:
111 return "???"
112 for i in range(0, info.num_frames):
113 addr = info.callstack[i]
114 if kernel_trace:
115 stack += " %s (%x) ;" % (bpf_program.ksym(addr), addr)
116 else:
117 stack += " %s (%x) ;" % (decode_addr(code_ranges, addr), addr)
118 return stack
119
120def print_outstanding():
121 stacks = {}
122 print("*** Outstanding allocations:")
123 allocs = bpf_program.get_table("allocs")
124 for address, info in sorted(allocs.items(), key=lambda a: -a[1].size):
125 stack = decode_stack(info)
126 if stack in stacks: stacks[stack] += info.size
127 else: stacks[stack] = info.size
128 if args.show_allocs:
129 print("\taddr = %x size = %s" % (address.value, info.size))
130 for stack, size in sorted(stacks.items(), key=lambda s: -s[1]):
131 print("\t%d bytes allocated from stack\n\t\t%s" % (size, stack.replace(";", "\n\t\t")))
132
133while True:
134 if trace_all:
135 print bpf_program.trace_fields()
136 else:
137 try:
138 sleep(interval)
139 except KeyboardInterrupt:
140 exit()
141 if not kernel_trace:
142 code_ranges = get_code_ranges(pid)
143 print_outstanding()
144