|  | #!/usr/bin/env bcc-lua | 
|  | --[[ | 
|  | Copyright 2016 GitHub, Inc | 
|  |  | 
|  | Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | you may not use this file except in compliance with the License. | 
|  | You may obtain a copy of the License at | 
|  |  | 
|  | http://www.apache.org/licenses/LICENSE-2.0 | 
|  |  | 
|  | Unless required by applicable law or agreed to in writing, software | 
|  | distributed under the License is distributed on an "AS IS" BASIS, | 
|  | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | See the License for the specific language governing permissions and | 
|  | limitations under the License. | 
|  | ]] | 
|  |  | 
|  | local bpf_source = [[ | 
|  | #include <uapi/linux/ptrace.h> | 
|  |  | 
|  | struct alloc_info_t { | 
|  | u64 size; | 
|  | u64 timestamp_ns; | 
|  | int stack_id; | 
|  | }; | 
|  |  | 
|  | BPF_HASH(sizes, u64); | 
|  | BPF_HASH(allocs, u64, struct alloc_info_t); | 
|  | BPF_STACK_TRACE(stack_traces, 10240) | 
|  |  | 
|  | int alloc_enter(struct pt_regs *ctx, size_t size) | 
|  | { | 
|  | SIZE_FILTER | 
|  | if (SAMPLE_EVERY_N > 1) { | 
|  | u64 ts = bpf_ktime_get_ns(); | 
|  | if (ts % SAMPLE_EVERY_N != 0) | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | u64 pid = bpf_get_current_pid_tgid(); | 
|  | u64 size64 = size; | 
|  | sizes.update(&pid, &size64); | 
|  |  | 
|  | if (SHOULD_PRINT) | 
|  | bpf_trace_printk("alloc entered, size = %u\n", size); | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | int alloc_exit(struct pt_regs *ctx) | 
|  | { | 
|  | u64 address = PT_REGS_RC(ctx); | 
|  | u64 pid = bpf_get_current_pid_tgid(); | 
|  | u64* size64 = sizes.lookup(&pid); | 
|  | struct alloc_info_t info = {0}; | 
|  |  | 
|  | if (size64 == 0) | 
|  | return 0; // missed alloc entry | 
|  |  | 
|  | info.size = *size64; | 
|  | sizes.delete(&pid); | 
|  |  | 
|  | info.timestamp_ns = bpf_ktime_get_ns(); | 
|  | info.stack_id = stack_traces.get_stackid(ctx, STACK_FLAGS); | 
|  |  | 
|  | allocs.update(&address, &info); | 
|  |  | 
|  | if (SHOULD_PRINT) { | 
|  | bpf_trace_printk("alloc exited, size = %lu, result = %lx\n", | 
|  | info.size, address); | 
|  | } | 
|  | return 0; | 
|  | } | 
|  |  | 
|  | int free_enter(struct pt_regs *ctx, void *address) | 
|  | { | 
|  | u64 addr = (u64)address; | 
|  | struct alloc_info_t *info = allocs.lookup(&addr); | 
|  | if (info == 0) | 
|  | return 0; | 
|  |  | 
|  | allocs.delete(&addr); | 
|  |  | 
|  | if (SHOULD_PRINT) { | 
|  | bpf_trace_printk("free entered, address = %lx, size = %lu\n", | 
|  | address, info->size); | 
|  | } | 
|  | return 0; | 
|  | } | 
|  | ]] | 
|  |  | 
|  | return function(BPF, utils) | 
|  | local parser = utils.argparse("memleak", "Catch memory leaks") | 
|  | parser:flag("-t --trace") | 
|  | parser:flag("-a --show-allocs") | 
|  | parser:option("-p --pid"):convert(tonumber) | 
|  |  | 
|  | parser:option("-i --interval", "", 5):convert(tonumber) | 
|  | parser:option("-o --older", "", 500):convert(tonumber) | 
|  | parser:option("-s --sample-rate", "", 1):convert(tonumber) | 
|  |  | 
|  | parser:option("-z --min-size", ""):convert(tonumber) | 
|  | parser:option("-Z --max-size", ""):convert(tonumber) | 
|  | parser:option("-T --top", "", 10):convert(tonumber) | 
|  |  | 
|  | local args = parser:parse() | 
|  |  | 
|  | local size_filter = "" | 
|  | if args.min_size and args.max_size then | 
|  | size_filter = "if (size < %d || size > %d) return 0;" %  {args.min_size, args.max_size} | 
|  | elseif args.min_size then | 
|  | size_filter = "if (size < %d) return 0;" % args.min_size | 
|  | elseif args.max_size then | 
|  | size_filter = "if (size > %d) return 0;" % args.max_size | 
|  | end | 
|  |  | 
|  | local stack_flags = "BPF_F_REUSE_STACKID" | 
|  | if args.pid then | 
|  | stack_flags = stack_flags .. "|BPF_F_USER_STACK" | 
|  | end | 
|  |  | 
|  | local text = bpf_source | 
|  | text = text:gsub("SIZE_FILTER", size_filter) | 
|  | text = text:gsub("STACK_FLAGS",  stack_flags) | 
|  | text = text:gsub("SHOULD_PRINT", args.trace and "1" or "0") | 
|  | text = text:gsub("SAMPLE_EVERY_N", tostring(args.sample_rate)) | 
|  |  | 
|  | local bpf = BPF:new{text=text, debug=0} | 
|  | local syms = nil | 
|  | local min_age_ns = args.older * 1e6 | 
|  |  | 
|  | if args.pid then | 
|  | print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % args.pid) | 
|  | bpf:attach_uprobe{name="c", sym="malloc", fn_name="alloc_enter", pid=args.pid} | 
|  | bpf:attach_uprobe{name="c", sym="malloc", fn_name="alloc_exit", pid=args.pid, retprobe=true} | 
|  | bpf:attach_uprobe{name="c", sym="free", fn_name="free_enter", pid=args.pid} | 
|  | else | 
|  | print("Attaching to kmalloc and kfree, Ctrl+C to quit.") | 
|  | bpf:attach_kprobe{event="__kmalloc", fn_name="alloc_enter"} | 
|  | bpf:attach_kprobe{event="__kmalloc", fn_name="alloc_exit", retprobe=true} -- TODO | 
|  | bpf:attach_kprobe{event="kfree", fn_name="free_enter"} | 
|  | end | 
|  |  | 
|  | local syms = BPF.SymbolCache(args.pid) | 
|  | local allocs = bpf:get_table("allocs") | 
|  | local stack_traces = bpf:get_table("stack_traces") | 
|  |  | 
|  | local function resolve(addr) | 
|  | local sym = syms:resolve(addr) | 
|  | if args.pid == nil then | 
|  | sym = sym .. " [kernel]" | 
|  | end | 
|  | return string.format("%s (%p)", sym, addr) | 
|  | end | 
|  |  | 
|  | local function print_outstanding() | 
|  | local alloc_info = {} | 
|  | local now = utils.posix.time_ns() | 
|  |  | 
|  | print("[%s] Top %d stacks with outstanding allocations:" % | 
|  | {os.date("%H:%M:%S"), args.top}) | 
|  |  | 
|  | for address, info in allocs:items() do | 
|  | if now - min_age_ns >= tonumber(info.timestamp_ns) then | 
|  | local stack_id = tonumber(info.stack_id) | 
|  |  | 
|  | if stack_id >= 0 then | 
|  | if alloc_info[stack_id] then | 
|  | local s = alloc_info[stack_id] | 
|  | s.count = s.count + 1 | 
|  | s.size = s.size + tonumber(info.size) | 
|  | else | 
|  | local stack = stack_traces:get(stack_id, resolve) | 
|  | alloc_info[stack_id] = { stack=stack, count=1, size=tonumber(info.size) } | 
|  | end | 
|  | end | 
|  |  | 
|  | if args.show_allocs then | 
|  | print("\taddr = %p size = %s" % {address, tonumber(info.size)}) | 
|  | end | 
|  | end | 
|  | end | 
|  |  | 
|  | local top = table.values(alloc_info) | 
|  | table.sort(top, function(a, b) return a.size > b.size end) | 
|  |  | 
|  | for n, alloc in ipairs(top) do | 
|  | print("\t%d bytes in %d allocations from stack\n\t\t%s" % | 
|  | {alloc.size, alloc.count, table.concat(alloc.stack, "\n\t\t")}) | 
|  | if n == args.top then break end | 
|  | end | 
|  | end | 
|  |  | 
|  | if args.trace then | 
|  | local pipe = bpf:pipe() | 
|  | while true do | 
|  | print(pipe:trace_fields()) | 
|  | end | 
|  | else | 
|  | while true do | 
|  | utils.posix.sleep(args.interval) | 
|  | syms:refresh() | 
|  | print_outstanding() | 
|  | end | 
|  | end | 
|  | end |