1#!/usr/bin/env bcc-lua 2--[[ 3Copyright 2016 GitHub, Inc 4 5Licensed under the Apache License, Version 2.0 (the "License"); 6you may not use this file except in compliance with the License. 7You may obtain a copy of the License at 8 9http://www.apache.org/licenses/LICENSE-2.0 10 11Unless required by applicable law or agreed to in writing, software 12distributed under the License is distributed on an "AS IS" BASIS, 13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14See the License for the specific language governing permissions and 15limitations under the License. 16]] 17 18local bpf_source = [[ 19#include <uapi/linux/ptrace.h> 20 21struct alloc_info_t { 22 u64 size; 23 u64 timestamp_ns; 24 int stack_id; 25}; 26 27BPF_HASH(sizes, u64); 28BPF_HASH(allocs, u64, struct alloc_info_t); 29BPF_STACK_TRACE(stack_traces, 10240); 30 31int alloc_enter(struct pt_regs *ctx, size_t size) 32{ 33 SIZE_FILTER 34 if (SAMPLE_EVERY_N > 1) { 35 u64 ts = bpf_ktime_get_ns(); 36 if (ts % SAMPLE_EVERY_N != 0) 37 return 0; 38 } 39 40 u64 pid = bpf_get_current_pid_tgid(); 41 u64 size64 = size; 42 sizes.update(&pid, &size64); 43 44 if (SHOULD_PRINT) 45 bpf_trace_printk("alloc entered, size = %u\n", size); 46 return 0; 47} 48 49int alloc_exit(struct pt_regs *ctx) 50{ 51 u64 address = PT_REGS_RC(ctx); 52 u64 pid = bpf_get_current_pid_tgid(); 53 u64* size64 = sizes.lookup(&pid); 54 struct alloc_info_t info = {0}; 55 56 if (size64 == 0) 57 return 0; // missed alloc entry 58 59 info.size = *size64; 60 sizes.delete(&pid); 61 62 info.timestamp_ns = bpf_ktime_get_ns(); 63 info.stack_id = stack_traces.get_stackid(ctx, STACK_FLAGS); 64 65 allocs.update(&address, &info); 66 67 if (SHOULD_PRINT) { 68 bpf_trace_printk("alloc exited, size = %lu, result = %lx\n", 69 info.size, address); 70 } 71 return 0; 72} 73 74int free_enter(struct pt_regs *ctx, void *address) 75{ 76 u64 addr = (u64)address; 77 struct alloc_info_t *info = allocs.lookup(&addr); 78 if (info == 0) 79 return 0; 80 81 allocs.delete(&addr); 82 83 if (SHOULD_PRINT) { 84 bpf_trace_printk("free entered, address = %lx, size = %lu\n", 85 address, info->size); 86 } 87 return 0; 88} 89]] 90 91return function(BPF, utils) 92 local parser = utils.argparse("memleak", "Catch memory leaks") 93 parser:flag("-t --trace") 94 parser:flag("-a --show-allocs") 95 parser:option("-p --pid"):convert(tonumber) 96 97 parser:option("-i --interval", "", 5):convert(tonumber) 98 parser:option("-o --older", "", 500):convert(tonumber) 99 parser:option("-s --sample-rate", "", 1):convert(tonumber) 100 101 parser:option("-z --min-size", ""):convert(tonumber) 102 parser:option("-Z --max-size", ""):convert(tonumber) 103 parser:option("-T --top", "", 10):convert(tonumber) 104 105 local args = parser:parse() 106 107 local size_filter = "" 108 if args.min_size and args.max_size then 109 size_filter = "if (size < %d || size > %d) return 0;" % {args.min_size, args.max_size} 110 elseif args.min_size then 111 size_filter = "if (size < %d) return 0;" % args.min_size 112 elseif args.max_size then 113 size_filter = "if (size > %d) return 0;" % args.max_size 114 end 115 116 local stack_flags = "0" 117 if args.pid then 118 stack_flags = stack_flags .. "|BPF_F_USER_STACK" 119 end 120 121 local text = bpf_source 122 text = text:gsub("SIZE_FILTER", size_filter) 123 text = text:gsub("STACK_FLAGS", stack_flags) 124 text = text:gsub("SHOULD_PRINT", args.trace and "1" or "0") 125 text = text:gsub("SAMPLE_EVERY_N", tostring(args.sample_rate)) 126 127 local bpf = BPF:new{text=text, debug=0} 128 local syms = nil 129 local min_age_ns = args.older * 1e6 130 131 if args.pid then 132 print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % args.pid) 133 bpf:attach_uprobe{name="c", sym="malloc", fn_name="alloc_enter", pid=args.pid} 134 bpf:attach_uprobe{name="c", sym="malloc", fn_name="alloc_exit", pid=args.pid, retprobe=true} 135 bpf:attach_uprobe{name="c", sym="free", fn_name="free_enter", pid=args.pid} 136 else 137 print("Attaching to kmalloc and kfree, Ctrl+C to quit.") 138 bpf:attach_kprobe{event="__kmalloc", fn_name="alloc_enter"} 139 bpf:attach_kprobe{event="__kmalloc", fn_name="alloc_exit", retprobe=true} -- TODO 140 bpf:attach_kprobe{event="kfree", fn_name="free_enter"} 141 end 142 143 local syms = BPF.SymbolCache(args.pid) 144 local allocs = bpf:get_table("allocs") 145 local stack_traces = bpf:get_table("stack_traces") 146 147 local function resolve(addr) 148 local sym = syms:resolve(addr) 149 if args.pid == nil then 150 sym = sym .. " [kernel]" 151 end 152 return string.format("%s (%p)", sym, addr) 153 end 154 155 local function print_outstanding() 156 local alloc_info = {} 157 local now = utils.posix.time_ns() 158 159 print("[%s] Top %d stacks with outstanding allocations:" % 160 {os.date("%H:%M:%S"), args.top}) 161 162 for address, info in allocs:items() do 163 if now - min_age_ns >= tonumber(info.timestamp_ns) then 164 local stack_id = tonumber(info.stack_id) 165 166 if stack_id >= 0 then 167 if alloc_info[stack_id] then 168 local s = alloc_info[stack_id] 169 s.count = s.count + 1 170 s.size = s.size + tonumber(info.size) 171 else 172 local stack = stack_traces:get(stack_id, resolve) 173 alloc_info[stack_id] = { stack=stack, count=1, size=tonumber(info.size) } 174 end 175 end 176 177 if args.show_allocs then 178 print("\taddr = %p size = %s" % {address, tonumber(info.size)}) 179 end 180 end 181 end 182 183 local top = table.values(alloc_info) 184 table.sort(top, function(a, b) return a.size > b.size end) 185 186 for n, alloc in ipairs(top) do 187 print("\t%d bytes in %d allocations from stack\n\t\t%s" % 188 {alloc.size, alloc.count, table.concat(alloc.stack, "\n\t\t")}) 189 if n == args.top then break end 190 end 191 end 192 193 if args.trace then 194 local pipe = bpf:pipe() 195 while true do 196 print(pipe:trace_fields()) 197 end 198 else 199 while true do 200 utils.posix.sleep(args.interval) 201 syms:refresh() 202 print_outstanding() 203 end 204 end 205end 206