Commit 33522d7b authored by Sasha Goldshtein's avatar Sasha Goldshtein

Fixed indentation and Python style issues from pep

parent dda47697
...@@ -9,7 +9,7 @@ import os ...@@ -9,7 +9,7 @@ import os
class Time(object): class Time(object):
# BPF timestamps come from the monotonic clock. To be able to filter # BPF timestamps come from the monotonic clock. To be able to filter
# and compare them from Python, we need to invoke clock_gettime from librt. # and compare them from Python, we need to invoke clock_gettime.
# Adapted from http://stackoverflow.com/a/1205762 # Adapted from http://stackoverflow.com/a/1205762
CLOCK_MONOTONIC_RAW = 4 # see <linux/time.h> CLOCK_MONOTONIC_RAW = 4 # see <linux/time.h>
...@@ -26,10 +26,11 @@ class Time(object): ...@@ -26,10 +26,11 @@ class Time(object):
@staticmethod @staticmethod
def monotonic_time(): def monotonic_time():
t = Time.timespec() t = Time.timespec()
if Time.clock_gettime(Time.CLOCK_MONOTONIC_RAW , ctypes.pointer(t)) != 0: if Time.clock_gettime(
Time.CLOCK_MONOTONIC_RAW, ctypes.pointer(t)) != 0:
errno_ = ctypes.get_errno() errno_ = ctypes.get_errno()
raise OSError(errno_, os.strerror(errno_)) raise OSError(errno_, os.strerror(errno_))
return t.tv_sec*1e9 + t.tv_nsec return t.tv_sec * 1e9 + t.tv_nsec
class StackDecoder(object): class StackDecoder(object):
def __init__(self, pid, bpf): def __init__(self, pid, bpf):
...@@ -43,24 +44,35 @@ class StackDecoder(object): ...@@ -43,24 +44,35 @@ class StackDecoder(object):
return return
self.code_ranges = self._get_code_ranges() self.code_ranges = self._get_code_ranges()
@staticmethod
def _is_binary_segment(parts):
return len(parts) == 6 and \
parts[5][0] == '[' and 'x' in parts[1]
def _get_code_ranges(self): def _get_code_ranges(self):
ranges = {} ranges = {}
raw_ranges = open("/proc/%d/maps" % self.pid).readlines() raw_ranges = open("/proc/%d/maps" % self.pid).readlines()
for raw_range in raw_ranges:
# A typical line from /proc/PID/maps looks like this: # A typical line from /proc/PID/maps looks like this:
# 7f21b6635000-7f21b67eb000 r-xp 00000000 fd:00 1442606 /usr/lib64/libc-2.21.so # 7f21b6635000-7f21b67eb000 r-xp ... /usr/lib64/libc-2.21.so
# We are looking for executable segments that have a binary (.so # We are looking for executable segments that have a .so file
# or the main executable). The first two lines are the range of # or the main executable. The first two lines are the range of
# that memory segment, which we index by binary name. # that memory segment, which we index by binary name.
for raw_range in raw_ranges:
parts = raw_range.split() parts = raw_range.split()
if len(parts) < 6 or parts[5][0] == '[' or not 'x' in parts[1]: if not StackDecoder._is_binary_segment(parts):
continue continue
binary = parts[5] binary = parts[5]
range_parts = parts[0].split('-') range_parts = parts[0].split('-')
addr_range = (int(range_parts[0], 16), int(range_parts[1], 16)) addr_range = (int(range_parts[0], 16),
int(range_parts[1], 16))
ranges[binary] = addr_range ranges[binary] = addr_range
return ranges return ranges
@staticmethod
def _is_function_symbol(parts):
return len(parts) == 6 and parts[3] == ".text" \
and parts[2] == "F"
def _get_sym_ranges(self, binary): def _get_sym_ranges(self, binary):
if binary in self.ranges_cache: if binary in self.ranges_cache:
return self.ranges_cache[binary] return self.ranges_cache[binary]
...@@ -69,10 +81,11 @@ class StackDecoder(object): ...@@ -69,10 +81,11 @@ class StackDecoder(object):
for raw_symbol in raw_symbols: for raw_symbol in raw_symbols:
# A typical line from objdump -t looks like this: # A typical line from objdump -t looks like this:
# 00000000004007f5 g F .text 000000000000010e main # 00000000004007f5 g F .text 000000000000010e main
# We only care about functions (F) in the .text segment. The first # We only care about functions in the .text segment.
# number is the start address, and the second number is the length. # The first number is the start address, and the second
# number is the length.
parts = raw_symbol.split() parts = raw_symbol.split()
if len(parts) < 6 or parts[3] != ".text" or parts[2] != "F": if not StackDecoder._is_function_symbol(parts):
continue continue
sym_start = int(parts[0], 16) sym_start = int(parts[0], 16)
sym_len = int(parts[4], 16) sym_len = int(parts[4], 16)
...@@ -83,7 +96,8 @@ class StackDecoder(object): ...@@ -83,7 +96,8 @@ class StackDecoder(object):
def _decode_sym(self, binary, offset): def _decode_sym(self, binary, offset):
sym_ranges = self._get_sym_ranges(binary) sym_ranges = self._get_sym_ranges(binary)
# Find the symbol that contains the specified offset. There might not be one. # Find the symbol that contains the specified offset.
# There might not be one.
for name, (start, length) in sym_ranges.items(): for name, (start, length) in sym_ranges.items():
if offset >= start and offset <= (start + length): if offset >= start and offset <= (start + length):
return "%s+0x%x" % (name, offset - start) return "%s+0x%x" % (name, offset - start)
...@@ -91,13 +105,15 @@ class StackDecoder(object): ...@@ -91,13 +105,15 @@ class StackDecoder(object):
def _decode_addr(self, addr): def _decode_addr(self, addr):
code_ranges = self._get_code_ranges() code_ranges = self._get_code_ranges()
# Find the binary that contains the specified address. For .so files, look # Find the binary that contains the specified address.
# at the relative address; for the main executable, look at the absolute # For .so files, look at the relative address; for the main
# address. # executable, look at the absolute address.
for binary, (start, end) in code_ranges.items(): for binary, (start, end) in code_ranges.items():
if addr >= start and addr <= end: if addr >= start and addr <= end:
offset = addr - start if binary.endswith(".so") else addr offset = addr - start \
return "%s [%s]" % (self._decode_sym(binary, offset), binary) if binary.endswith(".so") else addr
return "%s [%s]" % (self._decode_sym(binary,
offset), binary)
return "%x" % addr return "%x" % addr
def decode_stack(self, info, is_kernel_trace): def decode_stack(self, info, is_kernel_trace):
...@@ -107,15 +123,19 @@ class StackDecoder(object): ...@@ -107,15 +123,19 @@ class StackDecoder(object):
for i in range(0, info.num_frames): for i in range(0, info.num_frames):
addr = info.callstack[i] addr = info.callstack[i]
if is_kernel_trace: if is_kernel_trace:
stack += " %s [kernel] (%x) ;" % (self.bpf.ksym(addr), addr) stack += " %s [kernel] (%x) ;" % \
(self.bpf.ksym(addr), addr)
else: else:
# At some point, we hope to have native BPF user-mode symbol # At some point, we hope to have native BPF
# decoding, but for now we have to use our own # user-mode symbol decoding, but for now we
stack += " %s (%x) ;" % (self._decode_addr(addr), addr) # have to use our own.
stack += " %s (%x) ;" % \
(self._decode_addr(addr), addr)
return stack return stack
def run_command_get_output(command): def run_command_get_output(command):
p = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p = subprocess.Popen(command.split(),
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return iter(p.stdout.readline, b'') return iter(p.stdout.readline, b'')
def run_command_get_pid(command): def run_command_get_pid(command):
...@@ -172,9 +192,9 @@ command = args.command ...@@ -172,9 +192,9 @@ command = args.command
kernel_trace = (pid == -1 and command is None) kernel_trace = (pid == -1 and command is None)
trace_all = args.trace trace_all = args.trace
interval = int(args.interval) interval = int(args.interval)
min_age_ns = 1e6*int(args.older) min_age_ns = 1e6 * int(args.older)
if not command is None: if command is not None:
print("Executing '%s' and tracing the resulting process." % command) print("Executing '%s' and tracing the resulting process." % command)
pid = run_command_get_pid(command) pid = run_command_get_pid(command)
...@@ -185,9 +205,12 @@ bpf_program = BPF(text=bpf_source) ...@@ -185,9 +205,12 @@ bpf_program = BPF(text=bpf_source)
if not kernel_trace: if not kernel_trace:
print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % pid) print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % pid)
bpf_program.attach_uprobe(name="c", sym="malloc", fn_name="alloc_enter", pid=pid) bpf_program.attach_uprobe(name="c", sym="malloc",
bpf_program.attach_uretprobe(name="c", sym="malloc", fn_name="alloc_exit", pid=pid) fn_name="alloc_enter", pid=pid)
bpf_program.attach_uprobe(name="c", sym="free", fn_name="free_enter", pid=pid) bpf_program.attach_uretprobe(name="c", sym="malloc",
fn_name="alloc_exit", pid=pid)
bpf_program.attach_uprobe(name="c", sym="free",
fn_name="free_enter", pid=pid)
else: else:
print("Attaching to kmalloc and kfree, Ctrl+C to quit.") print("Attaching to kmalloc and kfree, Ctrl+C to quit.")
bpf_program.attach_kprobe(event="__kmalloc", fn_name="alloc_enter") bpf_program.attach_kprobe(event="__kmalloc", fn_name="alloc_enter")
...@@ -204,12 +227,18 @@ def print_outstanding(): ...@@ -204,12 +227,18 @@ def print_outstanding():
if Time.monotonic_time() - min_age_ns < info.timestamp_ns: if Time.monotonic_time() - min_age_ns < info.timestamp_ns:
continue continue
stack = decoder.decode_stack(info, kernel_trace) stack = decoder.decode_stack(info, kernel_trace)
if stack in stacks: stacks[stack] = (stacks[stack][0] + 1, stacks[stack][1] + info.size) if stack in stacks:
else: stacks[stack] = (1, info.size) stacks[stack] = (stacks[stack][0] + 1,
stacks[stack][1] + info.size)
else:
stacks[stack] = (1, info.size)
if args.show_allocs: if args.show_allocs:
print("\taddr = %x size = %s" % (address.value, info.size)) print("\taddr = %x size = %s" %
for stack, (count, size) in sorted(stacks.items(), key=lambda s: s[1][1]): (address.value, info.size))
print("\t%d bytes in %d allocations from stack\n\t\t%s" % (size, count, stack.replace(";", "\n\t\t"))) for stack, (count, size) in sorted(stacks.items(),
key=lambda s: s[1][1]):
print("\t%d bytes in %d allocations from stack\n\t\t%s" %
(size, count, stack.replace(";", "\n\t\t")))
while True: while True:
if trace_all: if trace_all:
...@@ -221,4 +250,3 @@ while True: ...@@ -221,4 +250,3 @@ while True:
exit() exit()
decoder.refresh_code_ranges() decoder.refresh_code_ranges()
print_outstanding() print_outstanding()
...@@ -19,6 +19,42 @@ Attaching to malloc and free in pid 5193, Ctrl+C to quit. ...@@ -19,6 +19,42 @@ Attaching to malloc and free in pid 5193, Ctrl+C to quit.
__libc_start_main+0xf0 [/usr/lib64/libc-2.21.so] (7fd460ac2790) __libc_start_main+0xf0 [/usr/lib64/libc-2.21.so] (7fd460ac2790)
As time goes on, it becomes apparent that the main function in the allocs
process is leaking memory, 16 bytes at a time. Fortunately, you don't have to
inspect each allocation individually -- you get a nice summary of which stack
is responsible for a large leak.
Occasionally, you do want the individual allocation details. Perhaps the same
stack is allocating various sizes and you want to confirm which sizes are
prevalent. Use the -a switch:
# ./memleak.py -p $(pidof allocs) -a
Attaching to malloc and free in pid 5193, Ctrl+C to quit.
*** Outstanding allocations:
addr = 948cd0 size = 16
addr = 948d10 size = 16
addr = 948d30 size = 16
addr = 948cf0 size = 16
64 bytes in 4 allocations from stack
main+0x6d [/home/vagrant/allocs] (400862)
__libc_start_main+0xf0 [/usr/lib64/libc-2.21.so] (7fd460ac2790)
*** Outstanding allocations:
addr = 948d50 size = 16
addr = 948cd0 size = 16
addr = 948d10 size = 16
addr = 948d30 size = 16
addr = 948cf0 size = 16
addr = 948dd0 size = 16
addr = 948d90 size = 16
addr = 948db0 size = 16
addr = 948d70 size = 16
addr = 948df0 size = 16
160 bytes in 10 allocations from stack
main+0x6d [/home/vagrant/allocs] (400862)
__libc_start_main+0xf0 [/usr/lib64/libc-2.21.so] (7fd460ac2790)
When using the -p switch, memleak traces the allocations of a particular When using the -p switch, memleak traces the allocations of a particular
process. Without this switch, kernel allocations (kmalloc) are traced instead. process. Without this switch, kernel allocations (kmalloc) are traced instead.
For example: For example:
...@@ -58,6 +94,10 @@ Attaching to kmalloc and kfree, Ctrl+C to quit. ...@@ -58,6 +94,10 @@ Attaching to kmalloc and kfree, Ctrl+C to quit.
perf_tp_event_init [kernel] (ffffffff81192479) perf_tp_event_init [kernel] (ffffffff81192479)
Here you can see that arming the kprobe to which our eBPF program is attached
consumed 8KB of memory. Loading the BPF program also consumed a couple hundred
bytes (in bpf_prog_load).
memleak stores each allocated block along with its size, timestamp, and the memleak stores each allocated block along with its size, timestamp, and the
stack that allocated it. When the block is deleted, this information is freed stack that allocated it. When the block is deleted, this information is freed
to reduce the memory overhead. to reduce the memory overhead.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment