mirror of
https://github.com/eunomia-bpf/bpf-developer-tutorial.git
synced 2026-02-03 18:24:27 +08:00
981 lines
25 KiB
C
981 lines
25 KiB
C
// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
|
|
// Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
|
|
//
|
|
// Based on memleak(8) from BCC by Sasha Goldshtein and others.
|
|
// 1-Mar-2023 JP Kobryn Created this.
|
|
#include <argp.h>
|
|
#include <errno.h>
|
|
#include <signal.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/eventfd.h>
|
|
#include <sys/types.h>
|
|
#include <sys/wait.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
|
|
#include <bpf/libbpf.h>
|
|
#include <bpf/bpf.h>
|
|
|
|
#include "memleak.h"
|
|
#include "memleak.skel.h"
|
|
|
|
#include "blazesym.h"
|
|
|
|
static struct env {
|
|
int interval;
|
|
int nr_intervals;
|
|
pid_t pid;
|
|
bool trace_all;
|
|
bool show_allocs;
|
|
bool combined_only;
|
|
int min_age_ns;
|
|
uint64_t sample_rate;
|
|
int top_stacks;
|
|
size_t min_size;
|
|
size_t max_size;
|
|
char object[32];
|
|
|
|
bool wa_missing_free;
|
|
bool percpu;
|
|
int perf_max_stack_depth;
|
|
int stack_map_max_entries;
|
|
long page_size;
|
|
bool kernel_trace;
|
|
bool verbose;
|
|
char command[32];
|
|
} env = {
|
|
.interval = 5, // posarg 1
|
|
.nr_intervals = -1, // posarg 2
|
|
.pid = -1, // -p --pid
|
|
.trace_all = false, // -t --trace
|
|
.show_allocs = false, // -a --show-allocs
|
|
.combined_only = false, // --combined-only
|
|
.min_age_ns = 500, // -o --older (arg * 1e6)
|
|
.wa_missing_free = false, // --wa-missing-free
|
|
.sample_rate = 1, // -s --sample-rate
|
|
.top_stacks = 10, // -T --top
|
|
.min_size = 0, // -z --min-size
|
|
.max_size = -1, // -Z --max-size
|
|
.object = {0}, // -O --obj
|
|
.percpu = false, // --percpu
|
|
.perf_max_stack_depth = 127,
|
|
.stack_map_max_entries = 10240,
|
|
.page_size = 1,
|
|
.kernel_trace = true,
|
|
.verbose = false,
|
|
.command = {0}, // -c --command
|
|
};
|
|
|
|
struct allocation_node {
|
|
uint64_t address;
|
|
size_t size;
|
|
struct allocation_node* next;
|
|
};
|
|
|
|
struct allocation {
|
|
uint64_t stack_id;
|
|
size_t size;
|
|
size_t count;
|
|
struct allocation_node* allocations;
|
|
};
|
|
|
|
#ifndef NSEC_PER_SEC
|
|
#define NSEC_PER_SEC 1000000000L
|
|
#endif
|
|
|
|
#define __ATTACH_UPROBE(skel, sym_name, prog_name, is_retprobe) \
|
|
do { \
|
|
LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, \
|
|
.func_name = #sym_name, \
|
|
.retprobe = is_retprobe); \
|
|
skel->links.prog_name = bpf_program__attach_uprobe_opts( \
|
|
skel->progs.prog_name, \
|
|
env.pid, \
|
|
env.object, \
|
|
0, \
|
|
&uprobe_opts); \
|
|
} while (false)
|
|
|
|
#define __CHECK_PROGRAM(skel, prog_name) \
|
|
do { \
|
|
if (!skel->links.prog_name) { \
|
|
perror("no program attached for " #prog_name); \
|
|
return -errno; \
|
|
} \
|
|
} while (false)
|
|
|
|
#define __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, is_retprobe) \
|
|
do { \
|
|
__ATTACH_UPROBE(skel, sym_name, prog_name, is_retprobe); \
|
|
__CHECK_PROGRAM(skel, prog_name); \
|
|
} while (false)
|
|
|
|
#define ATTACH_UPROBE(skel, sym_name, prog_name) __ATTACH_UPROBE(skel, sym_name, prog_name, false)
|
|
#define ATTACH_URETPROBE(skel, sym_name, prog_name) __ATTACH_UPROBE(skel, sym_name, prog_name, true)
|
|
|
|
#define ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name) __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, false)
|
|
#define ATTACH_URETPROBE_CHECKED(skel, sym_name, prog_name) __ATTACH_UPROBE_CHECKED(skel, sym_name, prog_name, true)
|
|
|
|
static void sig_handler(int signo);
|
|
|
|
static long argp_parse_long(int key, const char *arg, struct argp_state *state);
|
|
static error_t argp_parse_arg(int key, char *arg, struct argp_state *state);
|
|
|
|
static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args);
|
|
|
|
static int event_init(int *fd);
|
|
static int event_wait(int fd, uint64_t expected_event);
|
|
static int event_notify(int fd, uint64_t event);
|
|
|
|
static pid_t fork_sync_exec(const char *command, int fd);
|
|
|
|
static void print_stack_frame_by_blazesym(size_t frame, uint64_t addr, const blazesym_csym *sym);
|
|
static void print_stack_frames_by_blazesym();
|
|
static int print_stack_frames(struct allocation *allocs, size_t nr_allocs, int stack_traces_fd);
|
|
|
|
static int alloc_size_compare(const void *a, const void *b);
|
|
|
|
static int print_outstanding_allocs(int allocs_fd, int stack_traces_fd);
|
|
static int print_outstanding_combined_allocs(int combined_allocs_fd, int stack_traces_fd);
|
|
|
|
static void disable_kernel_node_tracepoints(struct memleak_bpf *skel);
|
|
static void disable_kernel_percpu_tracepoints(struct memleak_bpf *skel);
|
|
static void disable_kernel_tracepoints(struct memleak_bpf *skel);
|
|
|
|
static int attach_uprobes(struct memleak_bpf *skel);
|
|
|
|
const char *argp_program_version = "memleak 0.1";
|
|
const char *argp_program_bug_address =
|
|
"https://github.com/iovisor/bcc/tree/master/libbpf-tools";
|
|
|
|
const char argp_args_doc[] =
|
|
"Trace outstanding memory allocations\n"
|
|
"\n"
|
|
"USAGE: memleak [-h] [-c COMMAND] [-p PID] [-t] [-n] [-a] [-o AGE_MS] [-C] [-F] [-s SAMPLE_RATE] [-T TOP_STACKS] [-z MIN_SIZE] [-Z MAX_SIZE] [-O OBJECT] [-P] [INTERVAL] [INTERVALS]\n"
|
|
"\n"
|
|
"EXAMPLES:\n"
|
|
"./memleak -p $(pidof allocs)\n"
|
|
" Trace allocations and display a summary of 'leaked' (outstanding)\n"
|
|
" allocations every 5 seconds\n"
|
|
"./memleak -p $(pidof allocs) -t\n"
|
|
" Trace allocations and display each individual allocator function call\n"
|
|
"./memleak -ap $(pidof allocs) 10\n"
|
|
" Trace allocations and display allocated addresses, sizes, and stacks\n"
|
|
" every 10 seconds for outstanding allocations\n"
|
|
"./memleak -c './allocs'\n"
|
|
" Run the specified command and trace its allocations\n"
|
|
"./memleak\n"
|
|
" Trace allocations in kernel mode and display a summary of outstanding\n"
|
|
" allocations every 5 seconds\n"
|
|
"./memleak -o 60000\n"
|
|
" Trace allocations in kernel mode and display a summary of outstanding\n"
|
|
" allocations that are at least one minute (60 seconds) old\n"
|
|
"./memleak -s 5\n"
|
|
" Trace roughly every 5th allocation, to reduce overhead\n"
|
|
"";
|
|
|
|
static const struct argp_option argp_options[] = {
|
|
// name/longopt:str, key/shortopt:int, arg:str, flags:int, doc:str
|
|
{"pid", 'p', "PID", 0, "process ID to trace. if not specified, trace kernel allocs"},
|
|
{"trace", 't', 0, 0, "print trace messages for each alloc/free call" },
|
|
{"show-allocs", 'a', 0, 0, "show allocation addresses and sizes as well as call stacks"},
|
|
{"older", 'o', "AGE_MS", 0, "prune allocations younger than this age in milliseconds"},
|
|
{"command", 'c', "COMMAND", 0, "execute and trace the specified command"},
|
|
{"combined-only", 'C', 0, 0, "show combined allocation statistics only"},
|
|
{"wa-missing-free", 'F', 0, 0, "workaround to alleviate misjudgments when free is missing"},
|
|
{"sample-rate", 's', "SAMPLE_RATE", 0, "sample every N-th allocation to decrease the overhead"},
|
|
{"top", 'T', "TOP_STACKS", 0, "display only this many top allocating stacks (by size)"},
|
|
{"min-size", 'z', "MIN_SIZE", 0, "capture only allocations larger than this size"},
|
|
{"max-size", 'Z', "MAX_SIZE", 0, "capture only allocations smaller than this size"},
|
|
{"obj", 'O', "OBJECT", 0, "attach to allocator functions in the specified object"},
|
|
{"percpu", 'P', NULL, 0, "trace percpu allocations"},
|
|
{},
|
|
};
|
|
|
|
static volatile sig_atomic_t exiting;
|
|
static volatile sig_atomic_t child_exited;
|
|
|
|
static struct sigaction sig_action = {
|
|
.sa_handler = sig_handler
|
|
};
|
|
|
|
static int child_exec_event_fd = -1;
|
|
|
|
static blazesym *symbolizer;
|
|
static sym_src_cfg src_cfg;
|
|
static void (*print_stack_frames_func)();
|
|
|
|
static uint64_t *stack;
|
|
|
|
static struct allocation *allocs;
|
|
|
|
static const char default_object[] = "libc.so.6";
|
|
|
|
unsigned long long get_ktime_ns(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return ts.tv_sec * NSEC_PER_SEC + ts.tv_nsec;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
int ret = 0;
|
|
struct memleak_bpf *skel = NULL;
|
|
|
|
static const struct argp argp = {
|
|
.options = argp_options,
|
|
.parser = argp_parse_arg,
|
|
.doc = argp_args_doc,
|
|
};
|
|
|
|
// parse command line args to env settings
|
|
if (argp_parse(&argp, argc, argv, 0, NULL, NULL)) {
|
|
fprintf(stderr, "failed to parse args\n");
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
// install signal handler
|
|
if (sigaction(SIGINT, &sig_action, NULL) || sigaction(SIGCHLD, &sig_action, NULL)) {
|
|
perror("failed to set up signal handling");
|
|
ret = -errno;
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
// post-processing and validation of env settings
|
|
if (env.min_size > env.max_size) {
|
|
fprintf(stderr, "min size (-z) can't be greater than max_size (-Z)\n");
|
|
return 1;
|
|
}
|
|
|
|
if (!strlen(env.object)) {
|
|
printf("using default object: %s\n", default_object);
|
|
strncpy(env.object, default_object, sizeof(env.object) - 1);
|
|
}
|
|
|
|
env.page_size = sysconf(_SC_PAGE_SIZE);
|
|
printf("using page size: %ld\n", env.page_size);
|
|
|
|
env.kernel_trace = env.pid < 0 && !strlen(env.command);
|
|
printf("tracing kernel: %s\n", env.kernel_trace ? "true" : "false");
|
|
|
|
// if specific userspace program was specified,
|
|
// create the child process and use an eventfd to synchronize the call to exec()
|
|
if (strlen(env.command)) {
|
|
if (env.pid >= 0) {
|
|
fprintf(stderr, "cannot specify both command and pid\n");
|
|
ret = 1;
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
if (event_init(&child_exec_event_fd)) {
|
|
fprintf(stderr, "failed to init child event\n");
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
const pid_t child_pid = fork_sync_exec(env.command, child_exec_event_fd);
|
|
if (child_pid < 0) {
|
|
perror("failed to spawn child process");
|
|
ret = -errno;
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
env.pid = child_pid;
|
|
}
|
|
|
|
// allocate space for storing a stack trace
|
|
stack = calloc(env.perf_max_stack_depth, sizeof(*stack));
|
|
if (!stack) {
|
|
fprintf(stderr, "failed to allocate stack array\n");
|
|
ret = -ENOMEM;
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
if (env.pid < 0) {
|
|
src_cfg.src_type = SRC_T_KERNEL;
|
|
src_cfg.params.kernel.kallsyms = NULL;
|
|
src_cfg.params.kernel.kernel_image = NULL;
|
|
} else {
|
|
src_cfg.src_type = SRC_T_PROCESS;
|
|
src_cfg.params.process.pid = env.pid;
|
|
}
|
|
|
|
// allocate space for storing "allocation" structs
|
|
if (env.combined_only)
|
|
allocs = calloc(COMBINED_ALLOCS_MAX_ENTRIES, sizeof(*allocs));
|
|
else
|
|
allocs = calloc(ALLOCS_MAX_ENTRIES, sizeof(*allocs));
|
|
|
|
if (!allocs) {
|
|
fprintf(stderr, "failed to allocate array\n");
|
|
ret = -ENOMEM;
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
libbpf_set_print(libbpf_print_fn);
|
|
|
|
skel = memleak_bpf__open();
|
|
if (!skel) {
|
|
fprintf(stderr, "failed to open bpf object\n");
|
|
ret = 1;
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
skel->rodata->min_size = env.min_size;
|
|
skel->rodata->max_size = env.max_size;
|
|
skel->rodata->page_size = env.page_size;
|
|
skel->rodata->sample_rate = env.sample_rate;
|
|
skel->rodata->trace_all = env.trace_all;
|
|
skel->rodata->stack_flags = env.kernel_trace ? 0 : BPF_F_USER_STACK;
|
|
skel->rodata->wa_missing_free = env.wa_missing_free;
|
|
|
|
bpf_map__set_value_size(skel->maps.stack_traces,
|
|
env.perf_max_stack_depth * sizeof(unsigned long));
|
|
bpf_map__set_max_entries(skel->maps.stack_traces, env.stack_map_max_entries);
|
|
|
|
// disable kernel tracepoints based on settings or availability
|
|
if (env.kernel_trace) {
|
|
disable_kernel_node_tracepoints(skel);
|
|
|
|
if (!env.percpu)
|
|
disable_kernel_percpu_tracepoints(skel);
|
|
} else {
|
|
disable_kernel_tracepoints(skel);
|
|
}
|
|
|
|
ret = memleak_bpf__load(skel);
|
|
if (ret) {
|
|
fprintf(stderr, "failed to load bpf object\n");
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
const int allocs_fd = bpf_map__fd(skel->maps.allocs);
|
|
const int combined_allocs_fd = bpf_map__fd(skel->maps.combined_allocs);
|
|
const int stack_traces_fd = bpf_map__fd(skel->maps.stack_traces);
|
|
|
|
// if userspace oriented, attach upbrobes
|
|
if (!env.kernel_trace) {
|
|
ret = attach_uprobes(skel);
|
|
if (ret) {
|
|
fprintf(stderr, "failed to attach uprobes\n");
|
|
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
ret = memleak_bpf__attach(skel);
|
|
if (ret) {
|
|
fprintf(stderr, "failed to attach bpf program(s)\n");
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
// if running a specific userspace program,
|
|
// notify the child process that it can exec its program
|
|
if (strlen(env.command)) {
|
|
ret = event_notify(child_exec_event_fd, 1);
|
|
if (ret) {
|
|
fprintf(stderr, "failed to notify child to perform exec\n");
|
|
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
symbolizer = blazesym_new();
|
|
if (!symbolizer) {
|
|
fprintf(stderr, "Failed to load blazesym\n");
|
|
ret = -ENOMEM;
|
|
|
|
goto cleanup;
|
|
}
|
|
print_stack_frames_func = print_stack_frames_by_blazesym;
|
|
printf("Tracing outstanding memory allocs... Hit Ctrl-C to end\n");
|
|
|
|
// main loop
|
|
while (!exiting && env.nr_intervals) {
|
|
env.nr_intervals--;
|
|
|
|
sleep(env.interval);
|
|
|
|
if (env.combined_only)
|
|
print_outstanding_combined_allocs(combined_allocs_fd, stack_traces_fd);
|
|
else
|
|
print_outstanding_allocs(allocs_fd, stack_traces_fd);
|
|
}
|
|
|
|
// after loop ends, check for child process and cleanup accordingly
|
|
if (env.pid > 0 && strlen(env.command)) {
|
|
if (!child_exited) {
|
|
if (kill(env.pid, SIGTERM)) {
|
|
perror("failed to signal child process");
|
|
ret = -errno;
|
|
|
|
goto cleanup;
|
|
}
|
|
printf("signaled child process\n");
|
|
}
|
|
|
|
if (waitpid(env.pid, NULL, 0) < 0) {
|
|
perror("failed to reap child process");
|
|
ret = -errno;
|
|
|
|
goto cleanup;
|
|
}
|
|
printf("reaped child process\n");
|
|
}
|
|
|
|
cleanup:
|
|
blazesym_free(symbolizer);
|
|
memleak_bpf__destroy(skel);
|
|
|
|
free(allocs);
|
|
free(stack);
|
|
|
|
printf("done\n");
|
|
|
|
return ret;
|
|
}
|
|
|
|
long argp_parse_long(int key, const char *arg, struct argp_state *state)
|
|
{
|
|
errno = 0;
|
|
const long temp = strtol(arg, NULL, 10);
|
|
if (errno || temp <= 0) {
|
|
fprintf(stderr, "error arg:%c %s\n", (char)key, arg);
|
|
argp_usage(state);
|
|
}
|
|
|
|
return temp;
|
|
}
|
|
|
|
error_t argp_parse_arg(int key, char *arg, struct argp_state *state)
|
|
{
|
|
static int pos_args = 0;
|
|
|
|
switch (key) {
|
|
case 'p':
|
|
env.pid = atoi(arg);
|
|
break;
|
|
case 't':
|
|
env.trace_all = true;
|
|
break;
|
|
case 'a':
|
|
env.show_allocs = true;
|
|
break;
|
|
case 'o':
|
|
env.min_age_ns = 1e6 * atoi(arg);
|
|
break;
|
|
case 'c':
|
|
strncpy(env.command, arg, sizeof(env.command) - 1);
|
|
break;
|
|
case 'C':
|
|
env.combined_only = true;
|
|
break;
|
|
case 'F':
|
|
env.wa_missing_free = true;
|
|
break;
|
|
case 's':
|
|
env.sample_rate = argp_parse_long(key, arg, state);
|
|
break;
|
|
case 'T':
|
|
env.top_stacks = atoi(arg);
|
|
break;
|
|
case 'z':
|
|
env.min_size = argp_parse_long(key, arg, state);
|
|
break;
|
|
case 'Z':
|
|
env.max_size = argp_parse_long(key, arg, state);
|
|
break;
|
|
case 'O':
|
|
strncpy(env.object, arg, sizeof(env.object) - 1);
|
|
break;
|
|
case 'P':
|
|
env.percpu = true;
|
|
break;
|
|
case ARGP_KEY_ARG:
|
|
pos_args++;
|
|
|
|
if (pos_args == 1) {
|
|
env.interval = argp_parse_long(key, arg, state);
|
|
}
|
|
else if (pos_args == 2) {
|
|
env.nr_intervals = argp_parse_long(key, arg, state);
|
|
} else {
|
|
fprintf(stderr, "Unrecognized positional argument: %s\n", arg);
|
|
argp_usage(state);
|
|
}
|
|
|
|
break;
|
|
default:
|
|
return ARGP_ERR_UNKNOWN;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
|
|
{
|
|
if (level == LIBBPF_DEBUG && !env.verbose)
|
|
return 0;
|
|
|
|
return vfprintf(stderr, format, args);
|
|
}
|
|
|
|
void sig_handler(int signo)
|
|
{
|
|
if (signo == SIGCHLD)
|
|
child_exited = 1;
|
|
|
|
exiting = 1;
|
|
}
|
|
|
|
int event_init(int *fd)
|
|
{
|
|
if (!fd) {
|
|
fprintf(stderr, "pointer to fd is null\n");
|
|
|
|
return 1;
|
|
}
|
|
|
|
const int tmp_fd = eventfd(0, EFD_CLOEXEC);
|
|
if (tmp_fd < 0) {
|
|
perror("failed to create event fd");
|
|
|
|
return -errno;
|
|
}
|
|
|
|
*fd = tmp_fd;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int event_wait(int fd, uint64_t expected_event)
|
|
{
|
|
uint64_t event = 0;
|
|
const ssize_t bytes = read(fd, &event, sizeof(event));
|
|
if (bytes < 0) {
|
|
perror("failed to read from fd");
|
|
|
|
return -errno;
|
|
} else if (bytes != sizeof(event)) {
|
|
fprintf(stderr, "read unexpected size\n");
|
|
|
|
return 1;
|
|
}
|
|
|
|
if (event != expected_event) {
|
|
fprintf(stderr, "read event %lu, expected %lu\n", event, expected_event);
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int event_notify(int fd, uint64_t event)
|
|
{
|
|
const ssize_t bytes = write(fd, &event, sizeof(event));
|
|
if (bytes < 0) {
|
|
perror("failed to write to fd");
|
|
|
|
return -errno;
|
|
} else if (bytes != sizeof(event)) {
|
|
fprintf(stderr, "attempted to write %zu bytes, wrote %zd bytes\n", sizeof(event), bytes);
|
|
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
pid_t fork_sync_exec(const char *command, int fd)
|
|
{
|
|
const pid_t pid = fork();
|
|
|
|
switch (pid) {
|
|
case -1:
|
|
perror("failed to create child process");
|
|
break;
|
|
case 0: {
|
|
const uint64_t event = 1;
|
|
if (event_wait(fd, event)) {
|
|
fprintf(stderr, "failed to wait on event");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
printf("received go event. executing child command\n");
|
|
|
|
const int err = execl(command, command, NULL);
|
|
if (err) {
|
|
perror("failed to execute child command");
|
|
return -1;
|
|
}
|
|
|
|
break;
|
|
}
|
|
default:
|
|
printf("child created with pid: %d\n", pid);
|
|
|
|
break;
|
|
}
|
|
|
|
return pid;
|
|
}
|
|
|
|
void print_stack_frame_by_blazesym(size_t frame, uint64_t addr, const blazesym_csym *sym)
|
|
{
|
|
if (!sym)
|
|
printf("\t%zu [<%016lx>] <%s>\n", frame, addr, "null sym");
|
|
else if (sym->path && strlen(sym->path))
|
|
printf("\t%zu [<%016lx>] %s+0x%lx %s:%ld\n", frame, addr, sym->symbol, addr - sym->start_address, sym->path, sym->line_no);
|
|
else
|
|
printf("\t%zu [<%016lx>] %s+0x%lx\n", frame, addr, sym->symbol, addr - sym->start_address);
|
|
}
|
|
|
|
void print_stack_frames_by_blazesym()
|
|
{
|
|
const blazesym_result *result = blazesym_symbolize(symbolizer, &src_cfg, 1, stack, env.perf_max_stack_depth);
|
|
|
|
for (size_t j = 0; j < result->size; ++j) {
|
|
const uint64_t addr = stack[j];
|
|
|
|
if (addr == 0)
|
|
break;
|
|
|
|
// no symbol found
|
|
if (!result || j >= result->size || result->entries[j].size == 0) {
|
|
print_stack_frame_by_blazesym(j, addr, NULL);
|
|
|
|
continue;
|
|
}
|
|
|
|
// single symbol found
|
|
if (result->entries[j].size == 1) {
|
|
const blazesym_csym *sym = &result->entries[j].syms[0];
|
|
print_stack_frame_by_blazesym(j, addr, sym);
|
|
|
|
continue;
|
|
}
|
|
|
|
// multi symbol found
|
|
printf("\t%zu [<%016lx>] (%lu entries)\n", j, addr, result->entries[j].size);
|
|
|
|
for (size_t k = 0; k < result->entries[j].size; ++k) {
|
|
const blazesym_csym *sym = &result->entries[j].syms[k];
|
|
if (sym->path && strlen(sym->path))
|
|
printf("\t\t%s@0x%lx %s:%ld\n", sym->symbol, sym->start_address, sym->path, sym->line_no);
|
|
else
|
|
printf("\t\t%s@0x%lx\n", sym->symbol, sym->start_address);
|
|
}
|
|
}
|
|
|
|
blazesym_result_free(result);
|
|
}
|
|
|
|
int print_stack_frames(struct allocation *allocs, size_t nr_allocs, int stack_traces_fd)
|
|
{
|
|
for (size_t i = 0; i < nr_allocs; ++i) {
|
|
const struct allocation *alloc = &allocs[i];
|
|
|
|
printf("%zu bytes in %zu allocations from stack\n", alloc->size, alloc->count);
|
|
|
|
if (env.show_allocs) {
|
|
struct allocation_node* it = alloc->allocations;
|
|
while (it != NULL) {
|
|
printf("\taddr = %#lx size = %zu\n", it->address, it->size);
|
|
it = it->next;
|
|
}
|
|
}
|
|
|
|
if (bpf_map_lookup_elem(stack_traces_fd, &alloc->stack_id, stack)) {
|
|
if (errno == ENOENT)
|
|
continue;
|
|
|
|
perror("failed to lookup stack trace");
|
|
|
|
return -errno;
|
|
}
|
|
|
|
(*print_stack_frames_func)();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int alloc_size_compare(const void *a, const void *b)
|
|
{
|
|
const struct allocation *x = (struct allocation *)a;
|
|
const struct allocation *y = (struct allocation *)b;
|
|
|
|
// descending order
|
|
|
|
if (x->size > y->size)
|
|
return -1;
|
|
|
|
if (x->size < y->size)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int print_outstanding_allocs(int allocs_fd, int stack_traces_fd)
|
|
{
|
|
time_t t = time(NULL);
|
|
struct tm *tm = localtime(&t);
|
|
|
|
size_t nr_allocs = 0;
|
|
|
|
// for each struct alloc_info "alloc_info" in the bpf map "allocs"
|
|
for (uint64_t prev_key = 0, curr_key = 0;; prev_key = curr_key) {
|
|
struct alloc_info alloc_info = {};
|
|
memset(&alloc_info, 0, sizeof(alloc_info));
|
|
|
|
if (bpf_map_get_next_key(allocs_fd, &prev_key, &curr_key)) {
|
|
if (errno == ENOENT) {
|
|
break; // no more keys, done
|
|
}
|
|
|
|
perror("map get next key error");
|
|
|
|
return -errno;
|
|
}
|
|
|
|
if (bpf_map_lookup_elem(allocs_fd, &curr_key, &alloc_info)) {
|
|
if (errno == ENOENT)
|
|
continue;
|
|
|
|
perror("map lookup error");
|
|
|
|
return -errno;
|
|
}
|
|
|
|
// filter by age
|
|
if (get_ktime_ns() - env.min_age_ns < alloc_info.timestamp_ns) {
|
|
continue;
|
|
}
|
|
|
|
// filter invalid stacks
|
|
if (alloc_info.stack_id < 0) {
|
|
continue;
|
|
}
|
|
|
|
// when the stack_id exists in the allocs array,
|
|
// increment size with alloc_info.size
|
|
bool stack_exists = false;
|
|
|
|
for (size_t i = 0; !stack_exists && i < nr_allocs; ++i) {
|
|
struct allocation *alloc = &allocs[i];
|
|
|
|
if (alloc->stack_id == alloc_info.stack_id) {
|
|
alloc->size += alloc_info.size;
|
|
alloc->count++;
|
|
|
|
if (env.show_allocs) {
|
|
struct allocation_node* node = malloc(sizeof(struct allocation_node));
|
|
if (!node) {
|
|
perror("malloc failed");
|
|
return -errno;
|
|
}
|
|
node->address = curr_key;
|
|
node->size = alloc_info.size;
|
|
node->next = alloc->allocations;
|
|
alloc->allocations = node;
|
|
}
|
|
|
|
stack_exists = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (stack_exists)
|
|
continue;
|
|
|
|
// when the stack_id does not exist in the allocs array,
|
|
// create a new entry in the array
|
|
struct allocation alloc = {
|
|
.stack_id = alloc_info.stack_id,
|
|
.size = alloc_info.size,
|
|
.count = 1,
|
|
.allocations = NULL
|
|
};
|
|
|
|
if (env.show_allocs) {
|
|
struct allocation_node* node = malloc(sizeof(struct allocation_node));
|
|
if (!node) {
|
|
perror("malloc failed");
|
|
return -errno;
|
|
}
|
|
node->address = curr_key;
|
|
node->size = alloc_info.size;
|
|
node->next = NULL;
|
|
alloc.allocations = node;
|
|
}
|
|
|
|
memcpy(&allocs[nr_allocs], &alloc, sizeof(alloc));
|
|
nr_allocs++;
|
|
}
|
|
|
|
// sort the allocs array in descending order
|
|
qsort(allocs, nr_allocs, sizeof(allocs[0]), alloc_size_compare);
|
|
|
|
// get min of allocs we stored vs the top N requested stacks
|
|
size_t nr_allocs_to_show = nr_allocs < env.top_stacks ? nr_allocs : env.top_stacks;
|
|
|
|
printf("[%d:%d:%d] Top %zu stacks with outstanding allocations:\n",
|
|
tm->tm_hour, tm->tm_min, tm->tm_sec, nr_allocs_to_show);
|
|
|
|
print_stack_frames(allocs, nr_allocs_to_show, stack_traces_fd);
|
|
|
|
// Reset allocs list so that we dont accidentaly reuse data the next time we call this function
|
|
for (size_t i = 0; i < nr_allocs; i++) {
|
|
allocs[i].stack_id = 0;
|
|
if (env.show_allocs) {
|
|
struct allocation_node *it = allocs[i].allocations;
|
|
while (it != NULL) {
|
|
struct allocation_node *this = it;
|
|
it = it->next;
|
|
free(this);
|
|
}
|
|
allocs[i].allocations = NULL;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int print_outstanding_combined_allocs(int combined_allocs_fd, int stack_traces_fd)
|
|
{
|
|
time_t t = time(NULL);
|
|
struct tm *tm = localtime(&t);
|
|
|
|
size_t nr_allocs = 0;
|
|
|
|
// for each stack_id "curr_key" and union combined_alloc_info "alloc"
|
|
// in bpf_map "combined_allocs"
|
|
for (uint64_t prev_key = 0, curr_key = 0;; prev_key = curr_key) {
|
|
union combined_alloc_info combined_alloc_info;
|
|
memset(&combined_alloc_info, 0, sizeof(combined_alloc_info));
|
|
|
|
if (bpf_map_get_next_key(combined_allocs_fd, &prev_key, &curr_key)) {
|
|
if (errno == ENOENT) {
|
|
break; // no more keys, done
|
|
}
|
|
|
|
perror("map get next key error");
|
|
|
|
return -errno;
|
|
}
|
|
|
|
if (bpf_map_lookup_elem(combined_allocs_fd, &curr_key, &combined_alloc_info)) {
|
|
if (errno == ENOENT)
|
|
continue;
|
|
|
|
perror("map lookup error");
|
|
|
|
return -errno;
|
|
}
|
|
|
|
const struct allocation alloc = {
|
|
.stack_id = curr_key,
|
|
.size = combined_alloc_info.total_size,
|
|
.count = combined_alloc_info.number_of_allocs,
|
|
.allocations = NULL
|
|
};
|
|
|
|
memcpy(&allocs[nr_allocs], &alloc, sizeof(alloc));
|
|
nr_allocs++;
|
|
}
|
|
|
|
qsort(allocs, nr_allocs, sizeof(allocs[0]), alloc_size_compare);
|
|
|
|
// get min of allocs we stored vs the top N requested stacks
|
|
nr_allocs = nr_allocs < env.top_stacks ? nr_allocs : env.top_stacks;
|
|
|
|
printf("[%d:%d:%d] Top %zu stacks with outstanding allocations:\n",
|
|
tm->tm_hour, tm->tm_min, tm->tm_sec, nr_allocs);
|
|
|
|
print_stack_frames(allocs, nr_allocs, stack_traces_fd);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void disable_kernel_node_tracepoints(struct memleak_bpf *skel)
|
|
{
|
|
bpf_program__set_autoload(skel->progs.memleak__kmalloc_node, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__kmem_cache_alloc_node, false);
|
|
}
|
|
|
|
void disable_kernel_percpu_tracepoints(struct memleak_bpf *skel)
|
|
{
|
|
bpf_program__set_autoload(skel->progs.memleak__percpu_alloc_percpu, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__percpu_free_percpu, false);
|
|
}
|
|
|
|
void disable_kernel_tracepoints(struct memleak_bpf *skel)
|
|
{
|
|
bpf_program__set_autoload(skel->progs.memleak__kmalloc, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__kmalloc_node, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__kfree, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__kmem_cache_alloc, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__kmem_cache_alloc_node, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__kmem_cache_free, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__mm_page_alloc, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__mm_page_free, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__percpu_alloc_percpu, false);
|
|
bpf_program__set_autoload(skel->progs.memleak__percpu_free_percpu, false);
|
|
}
|
|
|
|
int attach_uprobes(struct memleak_bpf *skel)
|
|
{
|
|
ATTACH_UPROBE_CHECKED(skel, malloc, malloc_enter);
|
|
ATTACH_URETPROBE_CHECKED(skel, malloc, malloc_exit);
|
|
|
|
ATTACH_UPROBE_CHECKED(skel, calloc, calloc_enter);
|
|
ATTACH_URETPROBE_CHECKED(skel, calloc, calloc_exit);
|
|
|
|
ATTACH_UPROBE_CHECKED(skel, realloc, realloc_enter);
|
|
ATTACH_URETPROBE_CHECKED(skel, realloc, realloc_exit);
|
|
|
|
ATTACH_UPROBE_CHECKED(skel, mmap, mmap_enter);
|
|
ATTACH_URETPROBE_CHECKED(skel, mmap, mmap_exit);
|
|
|
|
ATTACH_UPROBE_CHECKED(skel, posix_memalign, posix_memalign_enter);
|
|
ATTACH_URETPROBE_CHECKED(skel, posix_memalign, posix_memalign_exit);
|
|
|
|
ATTACH_UPROBE_CHECKED(skel, memalign, memalign_enter);
|
|
ATTACH_URETPROBE_CHECKED(skel, memalign, memalign_exit);
|
|
|
|
ATTACH_UPROBE_CHECKED(skel, free, free_enter);
|
|
ATTACH_UPROBE_CHECKED(skel, munmap, munmap_enter);
|
|
|
|
// the following probes are intentinally allowed to fail attachment
|
|
|
|
// deprecated in libc.so bionic
|
|
ATTACH_UPROBE(skel, valloc, valloc_enter);
|
|
ATTACH_URETPROBE(skel, valloc, valloc_exit);
|
|
|
|
// deprecated in libc.so bionic
|
|
ATTACH_UPROBE(skel, pvalloc, pvalloc_enter);
|
|
ATTACH_URETPROBE(skel, pvalloc, pvalloc_exit);
|
|
|
|
// added in C11
|
|
ATTACH_UPROBE(skel, aligned_alloc, aligned_alloc_enter);
|
|
ATTACH_URETPROBE(skel, aligned_alloc, aligned_alloc_exit);
|
|
|
|
return 0;
|
|
}
|