blob: 41063c72ad83570810b878d79823b4516d294a83 [file] [log] [blame]
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -08001#!/usr/bin/env python
2
3from bcc import BPF
4from time import sleep
Sasha Goldshteinc8148c82016-02-09 11:15:41 -08005from datetime import datetime
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -08006import argparse
7import subprocess
Sasha Goldshteincfce3112016-02-07 11:09:36 -08008import ctypes
9import os
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -080010
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -080011class Time(object):
Sasha Goldshtein33522d72016-02-08 03:39:44 -080012 # BPF timestamps come from the monotonic clock. To be able to filter
13 # and compare them from Python, we need to invoke clock_gettime.
14 # Adapted from http://stackoverflow.com/a/1205762
15 CLOCK_MONOTONIC_RAW = 4 # see <linux/time.h>
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -080016
Sasha Goldshtein33522d72016-02-08 03:39:44 -080017 class timespec(ctypes.Structure):
18 _fields_ = [
19 ('tv_sec', ctypes.c_long),
20 ('tv_nsec', ctypes.c_long)
21 ]
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -080022
Sasha Goldshtein33522d72016-02-08 03:39:44 -080023 librt = ctypes.CDLL('librt.so.1', use_errno=True)
24 clock_gettime = librt.clock_gettime
25 clock_gettime.argtypes = [ctypes.c_int, ctypes.POINTER(timespec)]
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -080026
Sasha Goldshtein33522d72016-02-08 03:39:44 -080027 @staticmethod
28 def monotonic_time():
29 t = Time.timespec()
30 if Time.clock_gettime(
31 Time.CLOCK_MONOTONIC_RAW, ctypes.pointer(t)) != 0:
32 errno_ = ctypes.get_errno()
33 raise OSError(errno_, os.strerror(errno_))
34 return t.tv_sec * 1e9 + t.tv_nsec
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -080035
Sasha Goldshtein29228612016-02-07 12:20:19 -080036class StackDecoder(object):
Sasha Goldshtein33522d72016-02-08 03:39:44 -080037 def __init__(self, pid, bpf):
38 self.pid = pid
39 self.bpf = bpf
40 self.ranges_cache = {}
41 self.refresh_code_ranges()
Sasha Goldshtein29228612016-02-07 12:20:19 -080042
Sasha Goldshtein33522d72016-02-08 03:39:44 -080043 def refresh_code_ranges(self):
44 if self.pid == -1:
45 return
46 self.code_ranges = self._get_code_ranges()
Sasha Goldshtein29228612016-02-07 12:20:19 -080047
Sasha Goldshtein33522d72016-02-08 03:39:44 -080048 @staticmethod
49 def _is_binary_segment(parts):
50 return len(parts) == 6 and \
Sasha Goldshtein521ab4f2016-02-08 05:48:31 -080051 parts[5][0] != '[' and 'x' in parts[1]
Sasha Goldshtein29228612016-02-07 12:20:19 -080052
Sasha Goldshtein33522d72016-02-08 03:39:44 -080053 def _get_code_ranges(self):
54 ranges = {}
55 raw_ranges = open("/proc/%d/maps" % self.pid).readlines()
56 # A typical line from /proc/PID/maps looks like this:
57 # 7f21b6635000-7f21b67eb000 r-xp ... /usr/lib64/libc-2.21.so
58 # We are looking for executable segments that have a .so file
59 # or the main executable. The first two lines are the range of
60 # that memory segment, which we index by binary name.
61 for raw_range in raw_ranges:
62 parts = raw_range.split()
63 if not StackDecoder._is_binary_segment(parts):
64 continue
65 binary = parts[5]
66 range_parts = parts[0].split('-')
67 addr_range = (int(range_parts[0], 16),
68 int(range_parts[1], 16))
69 ranges[binary] = addr_range
70 return ranges
Sasha Goldshtein29228612016-02-07 12:20:19 -080071
Sasha Goldshtein33522d72016-02-08 03:39:44 -080072 @staticmethod
73 def _is_function_symbol(parts):
74 return len(parts) == 6 and parts[3] == ".text" \
75 and parts[2] == "F"
Sasha Goldshtein29228612016-02-07 12:20:19 -080076
Sasha Goldshtein33522d72016-02-08 03:39:44 -080077 def _get_sym_ranges(self, binary):
78 if binary in self.ranges_cache:
79 return self.ranges_cache[binary]
80 sym_ranges = {}
81 raw_symbols = run_command_get_output("objdump -t %s" % binary)
82 for raw_symbol in raw_symbols:
83 # A typical line from objdump -t looks like this:
84 # 00000000004007f5 g F .text 000000000000010e main
85 # We only care about functions in the .text segment.
86 # The first number is the start address, and the second
87 # number is the length.
88 parts = raw_symbol.split()
89 if not StackDecoder._is_function_symbol(parts):
90 continue
91 sym_start = int(parts[0], 16)
92 sym_len = int(parts[4], 16)
93 sym_name = parts[5]
94 sym_ranges[sym_name] = (sym_start, sym_len)
95 self.ranges_cache[binary] = sym_ranges
96 return sym_ranges
Sasha Goldshtein29228612016-02-07 12:20:19 -080097
Sasha Goldshtein33522d72016-02-08 03:39:44 -080098 def _decode_sym(self, binary, offset):
99 sym_ranges = self._get_sym_ranges(binary)
100 # Find the symbol that contains the specified offset.
101 # There might not be one.
102 for name, (start, length) in sym_ranges.items():
103 if offset >= start and offset <= (start + length):
104 return "%s+0x%x" % (name, offset - start)
105 return "%x" % offset
106
107 def _decode_addr(self, addr):
108 code_ranges = self._get_code_ranges()
109 # Find the binary that contains the specified address.
110 # For .so files, look at the relative address; for the main
111 # executable, look at the absolute address.
112 for binary, (start, end) in code_ranges.items():
113 if addr >= start and addr <= end:
114 offset = addr - start \
115 if binary.endswith(".so") else addr
116 return "%s [%s]" % (self._decode_sym(binary,
117 offset), binary)
118 return "%x" % addr
119
120 def decode_stack(self, info, is_kernel_trace):
121 stack = ""
122 if info.num_frames <= 0:
123 return "???"
124 for i in range(0, info.num_frames):
125 addr = info.callstack[i]
126 if is_kernel_trace:
127 stack += " %s [kernel] (%x) ;" % \
128 (self.bpf.ksym(addr), addr)
129 else:
130 # At some point, we hope to have native BPF
131 # user-mode symbol decoding, but for now we
132 # have to use our own.
133 stack += " %s (%x) ;" % \
134 (self._decode_addr(addr), addr)
135 return stack
Sasha Goldshtein29228612016-02-07 12:20:19 -0800136
Sasha Goldshtein751fce52016-02-08 02:57:02 -0800137def run_command_get_output(command):
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800138 p = subprocess.Popen(command.split(),
139 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
140 return iter(p.stdout.readline, b'')
Sasha Goldshtein29228612016-02-07 12:20:19 -0800141
Sasha Goldshtein751fce52016-02-08 02:57:02 -0800142def run_command_get_pid(command):
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800143 p = subprocess.Popen(command.split())
144 return p.pid
Sasha Goldshtein751fce52016-02-08 02:57:02 -0800145
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800146examples = """
147EXAMPLES:
148
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800149./memleak.py -p $(pidof allocs)
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800150 Trace allocations and display a summary of "leaked" (outstanding)
151 allocations every 5 seconds
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800152./memleak.py -p $(pidof allocs) -t
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800153 Trace allocations and display each individual call to malloc/free
Sasha Goldshtein75ba13f2016-02-09 06:03:46 -0800154./memleak.py -ap $(pidof allocs) 10
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800155 Trace allocations and display allocated addresses, sizes, and stacks
156 every 10 seconds for outstanding allocations
Sasha Goldshtein29228612016-02-07 12:20:19 -0800157./memleak.py -c "./allocs"
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800158 Run the specified command and trace its allocations
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800159./memleak.py
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800160 Trace allocations in kernel mode and display a summary of outstanding
161 allocations every 5 seconds
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800162./memleak.py -o 60000
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800163 Trace allocations in kernel mode and display a summary of outstanding
164 allocations that are at least one minute (60 seconds) old
Sasha Goldshtein521ab4f2016-02-08 05:48:31 -0800165./memleak.py -s 5
166 Trace roughly every 5th allocation, to reduce overhead
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800167"""
168
169description = """
170Trace outstanding memory allocations that weren't freed.
171Supports both user-mode allocations made with malloc/free and kernel-mode
172allocations made with kmalloc/kfree.
173"""
174
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800175parser = argparse.ArgumentParser(description=description,
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800176 formatter_class=argparse.RawDescriptionHelpFormatter,
177 epilog=examples)
Sasha Goldshteind2241f42016-02-09 06:23:10 -0800178parser.add_argument("-p", "--pid", type=int, default=-1,
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800179 help="the PID to trace; if not specified, trace kernel allocs")
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800180parser.add_argument("-t", "--trace", action="store_true",
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800181 help="print trace messages for each alloc/free call")
Sasha Goldshtein40e55ba2016-02-09 05:53:48 -0800182parser.add_argument("interval", nargs="?", default=5, type=int,
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800183 help="interval in seconds to print outstanding allocations")
Sasha Goldshtein40e55ba2016-02-09 05:53:48 -0800184parser.add_argument("count", nargs="?", type=int,
185 help="number of times to print the report before exiting")
Sasha Goldshteina7cc6c22016-02-07 12:03:54 -0800186parser.add_argument("-a", "--show-allocs", default=False, action="store_true",
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800187 help="show allocation addresses and sizes as well as call stacks")
Sasha Goldshtein40e55ba2016-02-09 05:53:48 -0800188parser.add_argument("-o", "--older", default=500, type=int,
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800189 help="prune allocations younger than this age in milliseconds")
Sasha Goldshtein29228612016-02-07 12:20:19 -0800190parser.add_argument("-c", "--command",
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800191 help="execute and trace the specified command")
Sasha Goldshtein40e55ba2016-02-09 05:53:48 -0800192parser.add_argument("-s", "--sample-rate", default=1, type=int,
Sasha Goldshtein521ab4f2016-02-08 05:48:31 -0800193 help="sample every N-th allocation to decrease the overhead")
Sasha Goldshteindcee30d2016-02-09 06:24:33 -0800194parser.add_argument("-d", "--stack-depth", default=10, type=int,
Sasha Goldshteind2241f42016-02-09 06:23:10 -0800195 help="maximum stack depth to capture")
Sasha Goldshteinc8148c82016-02-09 11:15:41 -0800196parser.add_argument("-T", "--top", type=int, default=10,
197 help="display only this many top allocating stacks (by size)")
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800198
199args = parser.parse_args()
200
Sasha Goldshteind2241f42016-02-09 06:23:10 -0800201pid = args.pid
Sasha Goldshtein29228612016-02-07 12:20:19 -0800202command = args.command
203kernel_trace = (pid == -1 and command is None)
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800204trace_all = args.trace
Sasha Goldshtein40e55ba2016-02-09 05:53:48 -0800205interval = args.interval
206min_age_ns = 1e6 * args.older
Sasha Goldshtein521ab4f2016-02-08 05:48:31 -0800207sample_every_n = args.sample_rate
Sasha Goldshtein40e55ba2016-02-09 05:53:48 -0800208num_prints = args.count
Sasha Goldshteind2241f42016-02-09 06:23:10 -0800209max_stack_size = args.stack_depth + 2
Sasha Goldshteinc8148c82016-02-09 11:15:41 -0800210top_stacks = args.top
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800211
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800212if command is not None:
213 print("Executing '%s' and tracing the resulting process." % command)
214 pid = run_command_get_pid(command)
Sasha Goldshtein29228612016-02-07 12:20:19 -0800215
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800216bpf_source = open("memleak.c").read()
217bpf_source = bpf_source.replace("SHOULD_PRINT", "1" if trace_all else "0")
Sasha Goldshtein521ab4f2016-02-08 05:48:31 -0800218bpf_source = bpf_source.replace("SAMPLE_EVERY_N", str(sample_every_n))
Sasha Goldshteind2241f42016-02-09 06:23:10 -0800219bpf_source = bpf_source.replace("GRAB_ONE_FRAME", max_stack_size *
220 "\tif (!(info->callstack[depth++] = get_frame(&bp))) return depth;\n")
221bpf_source = bpf_source.replace("MAX_STACK_SIZE", str(max_stack_size))
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800222bpf_program = BPF(text=bpf_source)
223
224if not kernel_trace:
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800225 print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % pid)
226 bpf_program.attach_uprobe(name="c", sym="malloc",
227 fn_name="alloc_enter", pid=pid)
228 bpf_program.attach_uretprobe(name="c", sym="malloc",
229 fn_name="alloc_exit", pid=pid)
230 bpf_program.attach_uprobe(name="c", sym="free",
231 fn_name="free_enter", pid=pid)
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800232else:
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800233 print("Attaching to kmalloc and kfree, Ctrl+C to quit.")
234 bpf_program.attach_kprobe(event="__kmalloc", fn_name="alloc_enter")
235 bpf_program.attach_kretprobe(event="__kmalloc", fn_name="alloc_exit")
236 bpf_program.attach_kprobe(event="kfree", fn_name="free_enter")
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800237
Sasha Goldshtein29228612016-02-07 12:20:19 -0800238decoder = StackDecoder(pid, bpf_program)
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800239
240def print_outstanding():
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800241 stacks = {}
Sasha Goldshteinc8148c82016-02-09 11:15:41 -0800242 print("[%s] Top %d stacks with outstanding allocations:" %
243 (datetime.now().strftime("%H:%M:%S"), top_stacks))
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800244 allocs = bpf_program.get_table("allocs")
245 for address, info in sorted(allocs.items(), key=lambda a: a[1].size):
246 if Time.monotonic_time() - min_age_ns < info.timestamp_ns:
247 continue
248 stack = decoder.decode_stack(info, kernel_trace)
249 if stack in stacks:
250 stacks[stack] = (stacks[stack][0] + 1,
251 stacks[stack][1] + info.size)
252 else:
253 stacks[stack] = (1, info.size)
254 if args.show_allocs:
255 print("\taddr = %x size = %s" %
256 (address.value, info.size))
Sasha Goldshteinc8148c82016-02-09 11:15:41 -0800257 to_show = sorted(stacks.items(), key=lambda s: s[1][1])[-top_stacks:]
258 for stack, (count, size) in to_show:
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800259 print("\t%d bytes in %d allocations from stack\n\t\t%s" %
260 (size, count, stack.replace(";", "\n\t\t")))
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800261
Sasha Goldshtein40e55ba2016-02-09 05:53:48 -0800262count_so_far = 0
Sasha Goldshtein4f1ea672016-02-07 01:57:42 -0800263while True:
Sasha Goldshtein33522d72016-02-08 03:39:44 -0800264 if trace_all:
265 print bpf_program.trace_fields()
266 else:
267 try:
268 sleep(interval)
269 except KeyboardInterrupt:
270 exit()
271 decoder.refresh_code_ranges()
272 print_outstanding()
Sasha Goldshtein40e55ba2016-02-09 05:53:48 -0800273 count_so_far += 1
274 if num_prints is not None and count_so_far >= num_prints:
275 exit()