Skip to content

Commit

Permalink
use libbacktrace to replace backtrace(3); add LEAK_AFTER and LEAK_LIB…
Browse files Browse the repository at this point in the history
…_BLACKLIST
  • Loading branch information
WuBingzheng committed Jun 15, 2020
1 parent 7b61ee1 commit fe8deb7
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ LDFLAGS = -Llibwuya

libleak.so: libleak.o
CFLAGS='-fPIC' make -C libwuya
$(CC) -shared -o $@ $^ $(LDFLAGS) -lwuya -lpthread -ldl
$(CC) -shared -o $@ $^ $(LDFLAGS) -lwuya -lpthread -ldl -lbacktrace

clean:
rm -f libleak.so *.o
31 changes: 22 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ GPLv2

3. Then you will read output in `/tmp/libleak.$pid` in time.

4. If some symbol names are absent in the output, try to compile your program with `-rdynamic` GCC flag.

### set expire threshold

As said above, you should set expire threshold according to your scenarios.
Expand Down Expand Up @@ -88,6 +86,24 @@ To disable detecting process pid=1234:

$ sed -i '/1234/d' /tmp/libleak.enabled

### disable shared libraries calling

If your program uses a shared library that allocates too much memory
which ruins the log file, AND you can make sure that there is no leak in
calling it, `LEAK_LIB_BLACKLIST` can be used to disable it.
Library name can be got from `ldd $your-program`.
If there are more than one libraries, use `,` to seperate them:

$ LD_PRELOAD=/path/of/libleak.so LEAK_LIB_BLACKLIST=libmysqlclient.so.20.3.8,librdkafka.so.1 ./a.out

### skip initial phase

Programs always allocate some memory in initial phase and do not free them.
`LEAK_AFTER` can be used to skip this. If it's set, `libleak`
starts to detect after this time (in second):

$ LD_PRELOAD=/path/of/libleak.so LEAK_AFTER=1 ./a.out

### for multi-thread program

`libleak` is multi-thread safe.
Expand All @@ -114,16 +130,13 @@ After the program running, you can check the output log (e.g. by `tail -f /tmp/l
The memory blocks that live longer than the threshold will be printed as:

callstack[1] expires. count=1 size=1024/1024 alloc=1 free=0
0x00007fd322bd8220 libc-2.17.so malloc()+0
0x000000000040084e test foo()+14
0x0000000000400875 test bar()+37
0x0000000000400acb test main()+364
0x00007fd322bd8220 libleak.so /path/libleak/libleak.c:674 malloc()
0x000000000040084e test /path/test/test.c:30 foo()
0x0000000000400875 test /path/test/test.c:60 bar()
0x0000000000400acb test /path/test/test.c:67 main()

`callstack[1]` is the ID of callstack where memory leak happens.

It only prints addresses and function names, but not line numbers. Tool `addr2line(1)` can
be used to transfer the addresses to line numbers.

The backtrace is showed only on the first time, while it only prints
the ID and counters if expiring again:

Expand Down
158 changes: 131 additions & 27 deletions libleak.c
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#define _GNU_SOURCE

#include <execinfo.h>
#include <backtrace.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <pthread.h>
#include <string.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>

/* libwuya */
Expand Down Expand Up @@ -37,6 +38,12 @@ static char *conf_pid_file = "/tmp/libleak.enabled";
/* LEAK_LOG_FILE: log file name. */
static char *conf_log_file = "/tmp/libleak";

/* LEAK_LIB_BLACKLIST: blacklist of shared libraries, seperated by `,`. */
static char *conf_lib_blacklist = NULL;

/* LEAK_AFTER: start detect after this time (in second). */
static long conf_start_ms = 0;


/* ### hooking symbols */
static void *(*leak_real_malloc)(size_t size);
Expand Down Expand Up @@ -69,7 +76,7 @@ struct leak_callstack {
pthread_mutex_t mutex;

int ip_num;
void *ips[0];
uintptr_t ips[0];
};
static wuy_dict_t *leak_callstack_dict;
static pthread_mutex_t leak_callstack_mutex = PTHREAD_MUTEX_INITIALIZER;
Expand All @@ -91,6 +98,8 @@ static wuy_dict_t *leak_memblock_dict;
static WUY_LIST(leak_memblock_list);
static pthread_mutex_t leak_memblock_mutex = PTHREAD_MUTEX_INITIALIZER;

struct backtrace_state *btstate;


static uint32_t leak_callstack_hash(const void *item)
{
Expand All @@ -116,18 +125,6 @@ static int leak_callstack_cmp(const void *a, const void *b)
- (csb->expired_count - csb->free_expired_count);
}

static void leak_callstack_print(struct leak_callstack *cs)
{
char **symbols = backtrace_symbols(cs->ips, cs->ip_num);

for (int i = 2; i < cs->ip_num; i++) {
fprintf(leak_log_filp, " %s\n", symbols[i]);
}

free(symbols);
}


static uint32_t leak_memblock_hash(const void *item)
{
const struct leak_memblock *mb = item;
Expand All @@ -141,6 +138,55 @@ static bool leak_memblock_equal(const void *a, const void *b)
}


/* ### build library maps by reading /proc */
struct lib_map {
const char *name;
size_t start;
size_t end;
bool enabled;
};
static struct lib_map lib_maps[100];
static int lib_map_num = 0;

static void lib_maps_build(void)
{
char fname[100];
sprintf(fname, "/proc/%d/maps", getpid());
FILE *filp = fopen(fname, "r");

int ia, ib, ic, id;
size_t start, end;
char line[1024], path[1024], perms[5], deleted[100];
while (fgets(line, sizeof(line), filp) != NULL) {
int ret = sscanf(line, "%zx-%zx %s %x %x:%x %d %s %s",
&start, &end, perms, &ia, &ib, &ic, &id, path, deleted);
if (ret != 8 || perms[2] != 'x' || path[0] != '/') {
continue;
}

struct lib_map *lib = &lib_maps[lib_map_num++];
lib->name = strdup(strrchr(path, '/') + 1);
lib->start = start;
lib->end = end;
if (conf_lib_blacklist != NULL) {
lib->enabled = strstr(conf_lib_blacklist, lib->name) == NULL;
} else {
lib->enabled = true;
}
}
}
static const char *lib_maps_search(uintptr_t pc)
{
for (int i = 0; i < lib_map_num; i++) {
struct lib_map *lib = &lib_maps[i];
if (pc >= lib->start && pc <= lib->end) {
return lib->enabled ? lib->name : NULL;
}
}
return "[unknown]";
}


/* ### a simple memory allocation used before init() */

static uint8_t tmp_buffer[1024 * 1024]; /* increase this if need */
Expand Down Expand Up @@ -225,6 +271,12 @@ static void leak_report(void)


/* ### module init */
static long leak_now_ms(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
static void __attribute__((constructor))init(void)
{
/* read configs from environment variables */
Expand All @@ -248,6 +300,14 @@ static void __attribute__((constructor))init(void)
if (ev != NULL) {
conf_pid_file = strdup(ev);
}
ev = getenv("LEAK_LIB_BLACKLIST");
if (ev != NULL) {
conf_lib_blacklist = strdup(ev);
}
ev = getenv("LEAK_AFTER");
if (ev != NULL) {
conf_start_ms = leak_now_ms() + atoi(ev) * 1000;
}

/* hook symbols */
leak_real_malloc = dlsym(RTLD_NEXT, "malloc");
Expand Down Expand Up @@ -276,6 +336,12 @@ static void __attribute__((constructor))init(void)

leak_memblock_pool = wuy_pool_new_type(struct leak_memblock);

/* build shared library maps */
lib_maps_build();

/* init backtrace state */
btstate = backtrace_create_state(NULL, 1, NULL, NULL);

/* log file */
char log_fname[100];
sprintf(log_fname, "%s.%d", conf_log_file, getpid());
Expand All @@ -293,6 +359,26 @@ static void __attribute__((constructor))init(void)


/* ### check, expire and log memory blocks */

static int leak_backtrace_full_cb (void *data, uintptr_t pc,
const char *filename, int lineno,
const char *function)
{
fprintf(leak_log_filp, " 0x%016zx %s", pc, lib_maps_search(pc));
if (filename != NULL || function != NULL) {
fprintf(leak_log_filp, " %s:%d %s()\n", filename, lineno, function);
} else {
fprintf(leak_log_filp, "\n");
}
return 0;
}
static void leak_callstack_print(struct leak_callstack *cs)
{
for (int i = 0; i < cs->ip_num; i++) {
backtrace_pcinfo(btstate, cs->ips[i], leak_backtrace_full_cb, NULL, NULL);
}
}

static void leak_expire(void)
{
time_t now = time(NULL);
Expand Down Expand Up @@ -383,14 +469,27 @@ static bool leak_enabled_check(void)
}


static int leak_backtrace_simple_cb(void *data, uintptr_t pc)
{
if (pc == (uintptr_t)-1) {
return 0;
}
if (conf_lib_blacklist != NULL && lib_maps_search(pc) == NULL) {
return 1;
}

struct leak_callstack *current = data;
current->ips[current->ip_num++] = pc;
return 0;
}

static struct leak_callstack *leak_current(void)
{
static int leak_callstack_id = 1;

struct leak_callstack current[20];
current->ip_num = backtrace(current->ips, 100);

if (current->ip_num == 0) {
current->ip_num = 0;
if (backtrace_simple(btstate, 2, leak_backtrace_simple_cb, NULL, current) != 0) {
return NULL;
}

Expand All @@ -414,12 +513,23 @@ static struct leak_callstack *leak_current(void)
return cs;
}

static void leak_process_alloc(void *p, size_t size)
static bool leak_do_skip(void)
{
if (!leak_inited) {
return;
return true;
}
if (leak_in_process) {
return true;
}
if (conf_start_ms != 0 && leak_now_ms() < conf_start_ms) {
return true;
}
return false;
}

static void leak_process_alloc(void *p, size_t size)
{
if (leak_do_skip()) {
return;
}
leak_in_process = true;
Expand Down Expand Up @@ -459,10 +569,7 @@ static void leak_process_alloc(void *p, size_t size)

static void leak_process_free(void *p)
{
if (!leak_inited) {
return;
}
if (leak_in_process) {
if (leak_do_skip()) {
return;
}
leak_in_process = true;
Expand Down Expand Up @@ -529,10 +636,7 @@ static void leak_process_free(void *p)

static void leak_process_update(void *p, size_t size)
{
if (!leak_inited) {
return;
}
if (leak_in_process) {
if (leak_do_skip()) {
return;
}
leak_in_process = true;
Expand Down

0 comments on commit fe8deb7

Please sign in to comment.