blob: 34e9ae6aee73eda714b7267b45c2b8d05b6ac581 [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
Sasha Goldshteincfce3112016-02-07 11:09:36 -08007import ctypes
8import os
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -08009
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -080010class Time(object):
11 # Adapted from http://stackoverflow.com/a/1205762
12 CLOCK_MONOTONIC_RAW = 4 # see <linux/time.h>
13
14 class timespec(ctypes.Structure):
15 _fields_ = [
16 ('tv_sec', ctypes.c_long),
17 ('tv_nsec', ctypes.c_long)
18 ]
19
20 librt = ctypes.CDLL('librt.so.1', use_errno=True)
21 clock_gettime = librt.clock_gettime
22 clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)]
23
24 @staticmethod
25 def monotonic_time():
26 """monotonic_time()
27 Returns the reading of the monotonic clock, in nanoseconds.
28 """
29 t = Time.timespec()
30 if Time.clock_gettime(Time.CLOCK_MONOTONIC_RAW , ctypes.pointer(t)) != 0:
31 errno_ = ctypes.get_errno()
32 raise OSError(errno_, os.strerror(errno_))
33 return t.tv_sec*1e9 + t.tv_nsec
34
Sasha Goldshtein29228612016-02-07 12:20:19 -080035class StackDecoder(object):
36 def __init__(self, pid, bpf):
37 """
38 TODO
39 """
40 self.pid = pid
41 self.bpf = bpf
42 self.ranges_cache = {}
43 self.refresh_code_ranges()
44
45 def refresh_code_ranges(self):
46 """
47 TODO
48 """
49 if self.pid == -1:
50 return
51 self.code_ranges = self._get_code_ranges()
52
53 def _get_code_ranges(self):
54 ranges = {}
55 raw_ranges = open("/proc/%d/maps" % self.pid).readlines()
56 for raw_range in raw_ranges:
57 parts = raw_range.split()
58 if len(parts) < 6 or parts[5][0] == '[' or not 'x' in parts[1]:
59 continue
60 binary = parts[5]
61 range_parts = parts[0].split('-')
62 addr_range = (int(range_parts[0], 16), int(range_parts[1], 16))
63 ranges[binary] = addr_range
64 return ranges
65
66 def _get_sym_ranges(self, binary):
67 if binary in self.ranges_cache:
68 return self.ranges_cache[binary]
69 sym_ranges = {}
70 raw_symbols = run_command("objdump -t %s" % binary)
71 for raw_symbol in raw_symbols:
72 parts = raw_symbol.split()
73 if len(parts) < 6 or parts[3] != ".text" or parts[2] != "F":
74 continue
75 sym_start = int(parts[0], 16)
76 sym_len = int(parts[4], 16)
77 sym_name = parts[5]
78 sym_ranges[sym_name] = (sym_start, sym_len)
79 self.ranges_cache[binary] = sym_ranges
80 return sym_ranges
81
82 def _decode_sym(self, binary, offset):
83 sym_ranges = self._get_sym_ranges(binary)
84 for name, (start, length) in sym_ranges.items():
85 if offset >= start and offset <= (start + length):
86 return "%s+0x%x" % (name, offset - start)
87 return "%x" % offset
88
89 def _decode_addr(self, addr):
90 code_ranges = self._get_code_ranges()
91 for binary, (start, end) in code_ranges.items():
92 if addr >= start and addr <= end:
93 offset = addr - start if binary.endswith(".so") else addr
94 return "%s [%s]" % (self._decode_sym(binary, offset), binary)
95 return "%x" % addr
96
97 def decode_stack(self, info):
98 """
99 TODO
100 """
101 stack = ""
102 if info.num_frames <= 0:
103 return "???"
104 for i in range(0, info.num_frames):
105 addr = info.callstack[i]
106 if kernel_trace:
107 stack += " %s [kernel] (%x) ;" % (self.bpf.ksym(addr), addr)
108 else:
109 stack += " %s (%x) ;" % (self._decode_addr(addr), addr)
110 return stack
111
112def run_command(command):
113 p = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
114 return iter(p.stdout.readline, b'')
115
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800116examples = """
117EXAMPLES:
118
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800119./memleak.py -p $(pidof allocs)
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800120 Trace allocations and display a summary of "leaked" (outstanding)
121 allocations every 5 seconds
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800122./memleak.py -p $(pidof allocs) -t
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800123 Trace allocations and display each individual call to malloc/free
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800124./memleak.py -p $(pidof allocs) -a -i 10
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800125 Trace allocations and display allocated addresses, sizes, and stacks
126 every 10 seconds for outstanding allocations
Sasha Goldshtein29228612016-02-07 12:20:19 -0800127./memleak.py -c "./allocs"
128 Run the specified command and trace its allocations
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800129./memleak.py
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800130 Trace allocations in kernel mode and display a summary of outstanding
131 allocations every 5 seconds
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800132./memleak.py -o 60000
133 Trace allocations in kernel mode and display a summary of outstanding
134 allocations that are at least one minute (60 seconds) old
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800135"""
136
137description = """
138Trace outstanding memory allocations that weren't freed.
139Supports both user-mode allocations made with malloc/free and kernel-mode
140allocations made with kmalloc/kfree.
141"""
142
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800143parser = argparse.ArgumentParser(description=description,
144 formatter_class=argparse.RawDescriptionHelpFormatter,
145 epilog=examples)
146parser.add_argument("-p", "--pid",
147 help="the PID to trace; if not specified, trace kernel allocs")
148parser.add_argument("-t", "--trace", action="store_true",
149 help="print trace messages for each alloc/free call")
150parser.add_argument("-i", "--interval", default=5,
151 help="interval in seconds to print outstanding allocations")
152parser.add_argument("-a", "--show-allocs", default=False, action="store_true",
153 help="show allocation addresses and sizes as well as call stacks")
154parser.add_argument("-o", "--older", default=500,
155 help="prune allocations younger than this age in milliseconds")
Sasha Goldshtein29228612016-02-07 12:20:19 -0800156parser.add_argument("-c", "--command",
157 help="execute and trace the specified command")
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800158
159args = parser.parse_args()
160
161pid = -1 if args.pid is None else int(args.pid)
Sasha Goldshtein29228612016-02-07 12:20:19 -0800162command = args.command
163kernel_trace = (pid == -1 and command is None)
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800164trace_all = args.trace
165interval = int(args.interval)
Sasha Goldshteincfce3112016-02-07 11:09:36 -0800166min_age_ns = 1e6*int(args.older)
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800167
Sasha Goldshtein29228612016-02-07 12:20:19 -0800168if not command is None:
169 pass # TODO Run command, get its pid and trace that
170
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800171bpf_source = open("memleak.c").read()
172bpf_source = bpf_source.replace("SHOULD_PRINT", "1" if trace_all else "0")
173
174bpf_program = BPF(text=bpf_source)
175
176if not kernel_trace:
177 print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % pid)
178 bpf_program.attach_uprobe(name="c", sym="malloc", fn_name="alloc_enter", pid=pid)
179 bpf_program.attach_uretprobe(name="c", sym="malloc", fn_name="alloc_exit", pid=pid)
180 bpf_program.attach_uprobe(name="c", sym="free", fn_name="free_enter", pid=pid)
181else:
182 print("Attaching to kmalloc and kfree, Ctrl+C to quit.")
183 bpf_program.attach_kprobe(event="__kmalloc", fn_name="alloc_enter")
184 bpf_program.attach_kretprobe(event="__kmalloc", fn_name="alloc_exit")
185 bpf_program.attach_kprobe(event="kfree", fn_name="free_enter")
186
Sasha Goldshtein29228612016-02-07 12:20:19 -0800187decoder = StackDecoder(pid, bpf_program)
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800188
189def print_outstanding():
190 stacks = {}
191 print("*** Outstanding allocations:")
192 allocs = bpf_program.get_table("allocs")
193 for address, info in sorted(allocs.items(), key=lambda a: -a[1].size):
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800194 if Time.monotonic_time() - min_age_ns < info.timestamp_ns:
Sasha Goldshteincfce3112016-02-07 11:09:36 -0800195 continue
Sasha Goldshtein29228612016-02-07 12:20:19 -0800196 stack = decoder.decode_stack(info)
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800197 if stack in stacks: stacks[stack] = (stacks[stack][0] + 1, stacks[stack][1] + info.size)
198 else: stacks[stack] = (1, info.size)
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800199 if args.show_allocs:
200 print("\taddr = %x size = %s" % (address.value, info.size))
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800201 for stack, (count, size) in sorted(stacks.items(), key=lambda s: -s[1][1]):
202 print("\t%d bytes in %d allocations from stack\n\t\t%s" % (size, count, stack.replace(";", "\n\t\t")))
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800203
204while True:
205 if trace_all:
206 print bpf_program.trace_fields()
207 else:
208 try:
209 sleep(interval)
210 except KeyboardInterrupt:
211 exit()
Sasha Goldshtein29228612016-02-07 12:20:19 -0800212 decoder.refresh_code_ranges()
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800213 print_outstanding()
214