Home | History | Annotate | Download | only in old
      1 #!/usr/bin/env python
      2 #
      3 # memleak   Trace and display outstanding allocations to detect
      4 #           memory leaks in user-mode processes and the kernel.
      5 #
      6 # USAGE: memleak [-h] [-p PID] [-t] [-a] [-o OLDER] [-c COMMAND]
      7 #                [-s SAMPLE_RATE] [-d STACK_DEPTH] [-T TOP] [-z MIN_SIZE]
      8 #                [-Z MAX_SIZE]
      9 #                [interval] [count]
     10 #
     11 # Licensed under the Apache License, Version 2.0 (the "License")
     12 # Copyright (C) 2016 Sasha Goldshtein.
     13 
     14 from bcc import BPF
     15 from time import sleep
     16 from datetime import datetime
     17 import argparse
     18 import subprocess
     19 import os
     20 
     21 def decode_stack(bpf, pid, info):
     22         stack = ""
     23         if info.num_frames <= 0:
     24                 return "???"
     25         for i in range(0, info.num_frames):
     26                 addr = info.callstack[i]
     27                 stack += " %s ;" % bpf.sym(addr, pid, show_offset=True)
     28         return stack
     29 
     30 def run_command_get_output(command):
     31         p = subprocess.Popen(command.split(),
     32                 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
     33         return iter(p.stdout.readline, b'')
     34 
     35 def run_command_get_pid(command):
     36         p = subprocess.Popen(command.split())
     37         return p.pid
     38 
     39 examples = """
     40 EXAMPLES:
     41 
     42 ./memleak -p $(pidof allocs)
     43         Trace allocations and display a summary of "leaked" (outstanding)
     44         allocations every 5 seconds
     45 ./memleak -p $(pidof allocs) -t
     46         Trace allocations and display each individual call to malloc/free
     47 ./memleak -ap $(pidof allocs) 10
     48         Trace allocations and display allocated addresses, sizes, and stacks
     49         every 10 seconds for outstanding allocations
     50 ./memleak -c "./allocs"
     51         Run the specified command and trace its allocations
     52 ./memleak
     53         Trace allocations in kernel mode and display a summary of outstanding
     54         allocations every 5 seconds
     55 ./memleak -o 60000
     56         Trace allocations in kernel mode and display a summary of outstanding
     57         allocations that are at least one minute (60 seconds) old
     58 ./memleak -s 5
     59         Trace roughly every 5th allocation, to reduce overhead
     60 """
     61 
     62 description = """
     63 Trace outstanding memory allocations that weren't freed.
     64 Supports both user-mode allocations made with malloc/free and kernel-mode
     65 allocations made with kmalloc/kfree.
     66 """
     67 
     68 parser = argparse.ArgumentParser(description=description,
     69         formatter_class=argparse.RawDescriptionHelpFormatter,
     70         epilog=examples)
     71 parser.add_argument("-p", "--pid", type=int, default=-1,
     72         help="the PID to trace; if not specified, trace kernel allocs")
     73 parser.add_argument("-t", "--trace", action="store_true",
     74         help="print trace messages for each alloc/free call")
     75 parser.add_argument("interval", nargs="?", default=5, type=int,
     76         help="interval in seconds to print outstanding allocations")
     77 parser.add_argument("count", nargs="?", type=int,
     78         help="number of times to print the report before exiting")
     79 parser.add_argument("-a", "--show-allocs", default=False, action="store_true",
     80         help="show allocation addresses and sizes as well as call stacks")
     81 parser.add_argument("-o", "--older", default=500, type=int,
     82         help="prune allocations younger than this age in milliseconds")
     83 parser.add_argument("-c", "--command",
     84         help="execute and trace the specified command")
     85 parser.add_argument("-s", "--sample-rate", default=1, type=int,
     86         help="sample every N-th allocation to decrease the overhead")
     87 parser.add_argument("-d", "--stack-depth", default=10, type=int,
     88         help="maximum stack depth to capture")
     89 parser.add_argument("-T", "--top", type=int, default=10,
     90         help="display only this many top allocating stacks (by size)")
     91 parser.add_argument("-z", "--min-size", type=int,
     92         help="capture only allocations larger than this size")
     93 parser.add_argument("-Z", "--max-size", type=int,
     94         help="capture only allocations smaller than this size")
     95 
     96 args = parser.parse_args()
     97 
     98 pid = args.pid
     99 command = args.command
    100 kernel_trace = (pid == -1 and command is None)
    101 trace_all = args.trace
    102 interval = args.interval
    103 min_age_ns = 1e6 * args.older
    104 sample_every_n = args.sample_rate
    105 num_prints = args.count
    106 max_stack_size = args.stack_depth + 2
    107 top_stacks = args.top
    108 min_size = args.min_size
    109 max_size = args.max_size
    110 
    111 if min_size is not None and max_size is not None and min_size > max_size:
    112         print("min_size (-z) can't be greater than max_size (-Z)")
    113         exit(1)
    114 
    115 if command is not None:
    116         print("Executing '%s' and tracing the resulting process." % command)
    117         pid = run_command_get_pid(command)
    118 
    119 bpf_source = """
    120 #include <uapi/linux/ptrace.h>
    121 
    122 struct alloc_info_t {
    123         u64 size;
    124         u64 timestamp_ns;
    125         int num_frames;
    126         u64 callstack[MAX_STACK_SIZE];
    127 };
    128 
    129 BPF_HASH(sizes, u64);
    130 BPF_HASH(allocs, u64, struct alloc_info_t);
    131 
    132 // Adapted from https://github.com/iovisor/bcc/tools/offcputime.py
    133 static u64 get_frame(u64 *bp) {
    134         if (*bp) {
    135                 // The following stack walker is x86_64 specific
    136                 u64 ret = 0;
    137                 if (bpf_probe_read(&ret, sizeof(ret), (void *)(*bp+8)))
    138                         return 0;
    139                 if (bpf_probe_read(bp, sizeof(*bp), (void *)*bp))
    140                         *bp = 0;
    141                 return ret;
    142         }
    143         return 0;
    144 }
    145 static int grab_stack(struct pt_regs *ctx, struct alloc_info_t *info)
    146 {
    147         int depth = 0;
    148         u64 bp = ctx->bp;
    149         GRAB_ONE_FRAME
    150         return depth;
    151 }
    152 
    153 int alloc_enter(struct pt_regs *ctx, size_t size)
    154 {
    155         SIZE_FILTER
    156         if (SAMPLE_EVERY_N > 1) {
    157                 u64 ts = bpf_ktime_get_ns();
    158                 if (ts % SAMPLE_EVERY_N != 0)
    159                         return 0;
    160         }
    161 
    162         u64 pid = bpf_get_current_pid_tgid();
    163         u64 size64 = size;
    164         sizes.update(&pid, &size64);
    165 
    166         if (SHOULD_PRINT)
    167                 bpf_trace_printk("alloc entered, size = %u\\n", size);
    168         return 0;
    169 }
    170 
    171 int alloc_exit(struct pt_regs *ctx)
    172 {
    173         u64 address = ctx->ax;
    174         u64 pid = bpf_get_current_pid_tgid();
    175         u64* size64 = sizes.lookup(&pid);
    176         struct alloc_info_t info = {0};
    177 
    178         if (size64 == 0)
    179                 return 0; // missed alloc entry
    180 
    181         info.size = *size64;
    182         sizes.delete(&pid);
    183 
    184         info.timestamp_ns = bpf_ktime_get_ns();
    185         info.num_frames = grab_stack(ctx, &info) - 2;
    186         allocs.update(&address, &info);
    187 
    188         if (SHOULD_PRINT) {
    189                 bpf_trace_printk("alloc exited, size = %lu, result = %lx,"
    190                                  "frames = %d\\n", info.size, address,
    191                                  info.num_frames);
    192         }
    193         return 0;
    194 }
    195 
    196 int free_enter(struct pt_regs *ctx, void *address)
    197 {
    198         u64 addr = (u64)address;
    199         struct alloc_info_t *info = allocs.lookup(&addr);
    200         if (info == 0)
    201                 return 0;
    202 
    203         allocs.delete(&addr);
    204 
    205         if (SHOULD_PRINT) {
    206                 bpf_trace_printk("free entered, address = %lx, size = %lu\\n",
    207                                  address, info->size);
    208         }
    209         return 0;
    210 }
    211 """
    212 bpf_source = bpf_source.replace("SHOULD_PRINT", "1" if trace_all else "0")
    213 bpf_source = bpf_source.replace("SAMPLE_EVERY_N", str(sample_every_n))
    214 bpf_source = bpf_source.replace("GRAB_ONE_FRAME", max_stack_size *
    215         "\tif (!(info->callstack[depth++] = get_frame(&bp))) return depth;\n")
    216 bpf_source = bpf_source.replace("MAX_STACK_SIZE", str(max_stack_size))
    217 
    218 size_filter = ""
    219 if min_size is not None and max_size is not None:
    220         size_filter = "if (size < %d || size > %d) return 0;" % \
    221                       (min_size, max_size)
    222 elif min_size is not None:
    223         size_filter = "if (size < %d) return 0;" % min_size
    224 elif max_size is not None:
    225         size_filter = "if (size > %d) return 0;" % max_size
    226 bpf_source = bpf_source.replace("SIZE_FILTER", size_filter)
    227 
    228 bpf_program = BPF(text=bpf_source)
    229 
    230 if not kernel_trace:
    231         print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % pid)
    232         bpf_program.attach_uprobe(name="c", sym="malloc",
    233                                   fn_name="alloc_enter", pid=pid)
    234         bpf_program.attach_uretprobe(name="c", sym="malloc",
    235                                      fn_name="alloc_exit", pid=pid)
    236         bpf_program.attach_uprobe(name="c", sym="free",
    237                                   fn_name="free_enter", pid=pid)
    238 else:
    239         print("Attaching to kmalloc and kfree, Ctrl+C to quit.")
    240         bpf_program.attach_kprobe(event="__kmalloc", fn_name="alloc_enter")
    241         bpf_program.attach_kretprobe(event="__kmalloc", fn_name="alloc_exit")
    242         bpf_program.attach_kprobe(event="kfree", fn_name="free_enter")
    243 
    244 def print_outstanding():
    245         stacks = {}
    246         print("[%s] Top %d stacks with outstanding allocations:" %
    247               (datetime.now().strftime("%H:%M:%S"), top_stacks))
    248         allocs = bpf_program.get_table("allocs")
    249         for address, info in sorted(allocs.items(), key=lambda a: a[1].size):
    250                 if BPF.monotonic_time() - min_age_ns < info.timestamp_ns:
    251                         continue
    252                 stack = decode_stack(bpf_program, pid, info)
    253                 if stack in stacks:
    254                         stacks[stack] = (stacks[stack][0] + 1,
    255                                          stacks[stack][1] + info.size)
    256                 else:
    257                         stacks[stack] = (1, info.size)
    258                 if args.show_allocs:
    259                         print("\taddr = %x size = %s" %
    260                               (address.value, info.size))
    261         to_show = sorted(stacks.items(), key=lambda s: s[1][1])[-top_stacks:]
    262         for stack, (count, size) in to_show:
    263                 print("\t%d bytes in %d allocations from stack\n\t\t%s" %
    264                       (size, count, stack.replace(";", "\n\t\t")))
    265 
    266 count_so_far = 0
    267 while True:
    268         if trace_all:
    269                 print(bpf_program.trace_fields())
    270         else:
    271                 try:
    272                         sleep(interval)
    273                 except KeyboardInterrupt:
    274                         exit()
    275                 print_outstanding()
    276                 count_so_far += 1
    277                 if num_prints is not None and count_so_far >= num_prints:
    278                         exit()
    279