Merge branch 'master' of /home/djm/cvs/leakmalloc into merge-leakmalloc

This commit is contained in:
Damien Miller
2012-03-15 12:24:27 +11:00
7 changed files with 561 additions and 0 deletions

19
leakmalloc/README Normal file
View File

@@ -0,0 +1,19 @@
This is a very simple memory leak detector using libexecinfo (in
OpenBSD ports as devel/libexecinfo). It could be adapted to use the
backtrace function in glibc too.
It tracks memory allocations by redirecting malloc/free and friends
to its own functions using preprocessor defines. When an allocation
is made, leakmalloc grabs the call chain from the stack and stores it.
When the program exits, leakmalloc dumps all unfreed allocations.
leakresolve.py consumes one of these dumps and produces a fairly
readable report with leaks coalesced by stack trace and sorted by
number of leak instances. Usually the last entry on this report is
the one to care most about.
leaky.c in regress/ shows how you might use it (run 'make' in that directory
and run it manually, or look at the Makefile for tips).
Note that everything needs to be compiled without optimisation and with
debugging symbols.

View File

@@ -0,0 +1,28 @@
# $OpenBSD$
LIB= leakmalloc
SRCS= leakmalloc.c
HDRS= leakmalloc.h
#MAN= leakmalloc.3
NOMAN=1
NOPIC=1
NOPROFILE=1
CDIAGFLAGS= -Wall
CDIAGFLAGS+= -Werror
CDIAGFLAGS+= -Wstrict-prototypes
CDIAGFLAGS+= -Wmissing-prototypes
CDIAGFLAGS+= -Wmissing-declarations
CDIAGFLAGS+= -Wshadow
CDIAGFLAGS+= -Wpointer-arith
CDIAGFLAGS+= -Wcast-qual
CDIAGFLAGS+= -Wsign-compare
CDIAGFLAGS+= -Wcast-align
CDIAGFLAGS+= -Wbad-function-cast
CPPFLAGS+= -I/usr/local/include
DEBUG=-ggdb3
COPTS=-O0
INSTALL_STRIP=
.include <bsd.lib.mk>

View File

@@ -0,0 +1,229 @@
/*
* Copyright (c) 2012 Damien Miller <djm@mindrot.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include <sys/types.h>
#include <sys/tree.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <err.h>
#include <execinfo.h>
#define LEAKMALLOC_NO_REDIRECT
#include "leakmalloc.h"
#define OPT_EXIT_ON_LEAKS 0x01
#define OPT_DUMP_TO_FILE 0x02
#define OPT_QUIET 0x04
u_int leakmalloc_options = 0;
static int initialised;
static FILE *dumpfile;
struct alloc {
RB_ENTRY(alloc) entry;
void *addr;
size_t len;
void *bt[BT_MAX_DEPTH];
int depth;
};
static int
alloc_cmp(struct alloc *a, struct alloc *b)
{
if (a->addr == b->addr)
return 0;
if (a->addr > b->addr)
return 1;
else
return -1;
}
RB_HEAD(alloc_tree, alloc);
RB_GENERATE_STATIC(alloc_tree, alloc, entry, alloc_cmp);
static struct alloc_tree alloc_tree = RB_INITIALIZER(&alloc_tree);
static void
dump_leak(FILE *f, const char *tag, struct alloc *alloc)
{
int i;
fprintf(f, "%s %p %zu TRACE", tag, alloc->addr, alloc->len);
for (i = 1; i < alloc->depth; i++)
fprintf(f, " %p", alloc->bt[i]);
fprintf(f, "\n");
}
/* Called atexit to dump unfreed leak objects */
static void
dump_leaks(void)
{
struct alloc *alloc;
int i = 0;
if (initialised != 1)
return;
RB_FOREACH(alloc, alloc_tree, &alloc_tree) {
if ((leakmalloc_options & OPT_QUIET) == 0)
dump_leak(dumpfile ? dumpfile : stdout, "LEAK", alloc);
i++;
}
if (dumpfile)
fclose(dumpfile);
if ((leakmalloc_options & OPT_EXIT_ON_LEAKS) != 0)
_exit(99);
}
static void
internal_error(struct alloc *alloc)
{
alloc->addr = NULL;
alloc->len = -1;
dump_leak(stderr, "ERROR", alloc);
initialised = -1;
}
static struct alloc *
new_alloc(void *addr, size_t len)
{
struct alloc *alloc;
if ((alloc = calloc(1, sizeof(*alloc))) == NULL)
errx(1, "%s: calloc failed", __func__);
alloc->addr = addr;
alloc->len = len;
if ((alloc->depth = backtrace(alloc->bt, BT_MAX_DEPTH)) == -1)
errx(1, "%s: backtrace failed", __func__);
return alloc;
}
static void
__record_leak(void *addr, size_t len, void *oaddr)
{
struct alloc oalloc, *alloc;
char *cp;
if (initialised == -1)
return;
else if (initialised == 0) {
atexit(dump_leaks);
if (!issetugid() &&
(cp = getenv("LEAKMALLOC_OPTIONS")) != NULL) {
if (strchr(cp, 'X') != NULL)
leakmalloc_options |= OPT_EXIT_ON_LEAKS;
if (strchr(cp, 'D') != NULL)
leakmalloc_options |= OPT_DUMP_TO_FILE;
if (strchr(cp, 'Q') != NULL)
leakmalloc_options |= OPT_QUIET;
}
if ((leakmalloc_options & OPT_DUMP_TO_FILE) != 0 &&
(dumpfile = fopen("leakmalloc.out", "w+")) == NULL)
err(1, "fopen(\"leakmalloc.out\")");
initialised = 1;
}
if (oaddr == NULL) {
/*
* malloc/calloc/realloc(NULL,...): allocate a leak object
* and fill in the trace.
*/
if (addr == NULL)
return; /* alloc failed or free(NULL) */
alloc = new_alloc(addr, len);
if (RB_INSERT(alloc_tree, &alloc_tree, alloc) != NULL) {
internal_error(alloc);
errx(1, "%s: alloc for %p already exists",
__func__, addr);
}
} else {
oalloc.addr = oaddr;
alloc = RB_FIND(alloc_tree, &alloc_tree, &oalloc);
if (addr == NULL) {
/*
* free: delete the tracked leak.
*/
if (alloc == NULL)
return; /* Ignore untracked memory */
RB_REMOVE(alloc_tree, &alloc_tree, alloc);
free(alloc);
} else {
/*
* realloc: update the original address so we can
* trace it when it is freed.
*/
if (alloc == NULL) {
alloc = new_alloc(NULL, -1);
internal_error(alloc);
errx(1, "%s: realloc original addr %p missing",
__func__, oaddr);
}
RB_REMOVE(alloc_tree, &alloc_tree, alloc);
alloc->addr = addr;
alloc->len = len;
if (RB_INSERT(alloc_tree, &alloc_tree, alloc) != NULL) {
internal_error(alloc);
errx(1, "%s: alloc for %p already exists",
__func__, addr);
}
}
}
}
char *
leak_strdup(const char *s)
{
char *ret = strdup(s);
__record_leak(ret, ret == NULL ? 0 : strlen(ret), NULL);
return ret;
}
void *
leak_malloc(size_t len)
{
void *ret = malloc(len);
__record_leak(ret, len, NULL);
return ret;
}
void *
leak_calloc(size_t nmemb, size_t size)
{
void *ret = calloc(nmemb, size);
__record_leak(ret, nmemb * size, NULL);
return ret;
}
void *
leak_realloc(void *s, size_t len)
{
void *ret = realloc(s, len);
__record_leak(ret, len, s);
return ret;
}
void
leak_free(void *s)
{
free(s);
__record_leak(NULL, 0, s);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (c) 2012 Damien Miller <djm@mindrot.org>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#ifndef _LEAKMALLOC_H
#define _LEAKMALLOC_H
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
char *leak_strdup(const char *s);
void *leak_malloc(size_t len);
void *leak_calloc(size_t nmemb, size_t size);
void *leak_realloc(void *s, size_t len);
void leak_free(void *s);
#ifndef LEAKMALLOC_NO_REDIRECT
#define malloc leak_malloc
#define strdup leak_strdup
#define calloc leak_calloc
#define realloc leak_realloc
#define free leak_free
#endif /* LEAKMALLOC_NO_REDIRECT */
#endif /* _LEAKMALLOC_H */

129
leakmalloc/leakresolve.py Executable file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python
# Copyright (c) 2012 Damien Miller <djm@mindrot.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# Resolve leak dump to backtraces
import sys
import getopt
import subprocess
TRIM_TRACE=True
def usage():
print >> sys.stderr, "leakresolve.py -p executable < trace"
sys.exit(1);
class LineResolver:
"""Resolves addresses to source lines"""
def __init__(self, executable):
self.resolver = subprocess.Popen(
["addr2line", "-e", executable, '-C', '-f'],
bufsize=1, # line-buffered
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
)
self.cache = {}
def resolve(self, addr):
if addr not in self.cache:
self.resolver.stdin.write(addr + "\n")
func = self.resolver.stdout.readline()
loc = self.resolver.stdout.readline()
result = "%s: in %s()" % (loc.strip(), func.strip())
self.cache[addr] = result
return self.cache[addr]
class Leak:
"""Represents a memory leak site"""
def __init__(self, backtrace, resolver):
self.backtrace = backtrace;
self.resolver = resolver
self.nleaks = 0
self.nbytes = 0;
def leak(self, nbytes):
self.nbytes += nbytes
self.nleaks += 1
def __str__(self):
s = "Leaked %d objects totalling %d bytes\n" % \
(self.nleaks, self.nbytes)
s += "\n".join(map(self.resolver.resolve, self.backtrace))
return s
class LeakTracker:
"""Tracks all memory leaks"""
def __init__(self, executable):
self.resolver = LineResolver(executable)
self.leaks = {}
def addleak(self, nbytes, trace):
if TRIM_TRACE:
trace = trace[:-1]
trace = tuple(trace)
if trace not in self.leaks:
leak = Leak(trace, self.resolver)
self.leaks[trace] = leak
self.leaks[trace].leak(nbytes)
def _leakcmp(self, a, b):
r = cmp(self.leaks[a].nleaks, self.leaks[b].nleaks)
if r:
return r
return cmp(self.leaks[a].nbytes, self.leaks[b].nbytes)
def __str__(self):
s = "Memory leaks\n"
s+= "------------\n"
total_sites = 0
total_leaks = 0
total_bytes = 0
for trace in sorted(self.leaks.keys(), cmp=self._leakcmp):
s += str(self.leaks[trace]) + "\n\n"
total_sites += 1
total_leaks += self.leaks[trace].nleaks
total_bytes += self.leaks[trace].nbytes
s+= "Total: %d leaks from %d sites, containing %d bytes\n" % \
(total_leaks, total_sites, total_bytes)
return s
def main():
executable = None
try:
opts, args = getopt.getopt(sys.argv[1:], 'hp:')
except getopt.GetoptError:
print >> sys.stderr, "Invalid commandline arguments"
usage()
for o, a in opts:
if o in ('-h', '--help'):
usage()
if o in ('-p', '--program'):
executable = a
continue
if not executable:
print >> sys.stderr, "Missing executable name"
usage();
leaks = LeakTracker(executable)
for line in sys.stdin:
if line.startswith("LEAK "):
leakinfo = line.split()
if len(leakinfo) < 4 or leakinfo[3] != "TRACE":
sys.stdout.write(line);
continue
backtrace = leakinfo[4:]
nbytes = int(leakinfo[2])
leaks.addleak(nbytes, backtrace)
print leaks
if __name__ == '__main__': main()

View File

@@ -0,0 +1,57 @@
# $OpenBSD$
.include <bsd.own.mk>
.include <bsd.obj.mk>
CDIAGFLAGS= -Wall
CDIAGFLAGS+= -Werror
CDIAGFLAGS+= -Wpointer-arith
CDIAGFLAGS+= -Wstrict-prototypes
CDIAGFLAGS+= -Wmissing-prototypes
CDIAGFLAGS+= -Wunused
CDIAGFLAGS+= -Wsign-compare
CDIAGFLAGS+= -Wshadow
CDIAGFLAGS+= -Wformat
.if (${CC:L} == "gcc" || ${CC:L} == "cc")
CDIAGFLAGS+= -Wbounded
.endif
DEBUG=-g
COPTS=-O0
CPPFLAGS+=-I${.CURDIR}/../leakmalloc
.if exists(${.CURDIR}/../leakmalloc/${__objdir})
LDADD+=-L${.CURDIR}/../leakmalloc/${__objdir} -lleakmalloc
DPADD+=${.CURDIR}/../leakmalloc/${__objdir}/libleakmalloc.a
.else
LDADD+=-L${.CURDIR}/../leakmalloc -lleakmalloc
DPADD+=${.CURDIR}/../leakmalloc/libleakmalloc.a
.endif
LDFLAGS+=-L/usr/local/lib
LDADD+=-lexecinfo
PROG=leaky
SRCS=leaky.c
REGRESS_TARGETS=does-leak outfile exit-leak summary
does-leak: ${PROG}
./${PROG} | grep -q "^LEAK "
outfile: ${PROG}
rm -f leakmalloc.out
env LEAKMALLOC_OPTIONS=D ./${PROG}
grep -q "^LEAK " leakmalloc.out
exit-leak: ${PROG}
sh -c 'export LEAKMALLOC_OPTIONS=XQ ; ./${PROG} ; test $$? -eq 99'
summary: ${PROG}
test `./${PROG} | ${.CURDIR}/../leakresolve.py -p ${PROG} | \
grep "^Total:" | cut -d' ' -f 2` -eq 22
clean:
rm ${PROG} *.o *.core leakmalloc.out
.include <bsd.regress.mk>

View File

@@ -0,0 +1,60 @@
#include <stdlib.h>
#include <string.h>
#include "leakmalloc.h"
static void *a, *b, *c, *d, *e, *f;
static void
f6(void)
{
d = strdup("hello");
e = malloc(789);
}
static void
f5(void)
{
c = calloc(1, 678);
f6();
}
static void
f4(void)
{
b = malloc(456);
f5();
free(e);
}
static void
f3(void)
{
a = malloc(123);
f = realloc(NULL, 321);
}
static void
f2(void)
{
f4();
b = realloc(b, 567);
}
static void
f1(void)
{
int i;
f2();
for (i = 0; i < 10; i++)
f3();
}
int
main(void)
{
f1();
free(f);
return 0;
}