forked from envoyproxy/envoy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
stack_decode.py
executable file
·131 lines (113 loc) · 5.22 KB
/
stack_decode.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/env python3
# Call addr2line as needed to resolve addresses in a stack trace. The addresses
# will be replaced if they can be resolved into file and line numbers. The
# executable must include debugging information to get file and line numbers.
#
# Two ways to call:
# 1) Execute binary as a subprocess: stack_decode.py executable_file [args]
# 2) Read log data from stdin: stack_decode.py -s executable_file
#
# In each case this script will add file and line information to any backtrace log
# lines found and echo back all non-Backtrace lines untouched.
import re
import subprocess
import sys
# Process the log output looking for stacktrace snippets, for each line found to
# contain backtrace output extract the address and call add2line to get the file
# and line information. Output appended to end of original backtrace line. Output
# any nonmatching lines unmodified. End when EOF received.
def decode_stacktrace_log(object_file, input_source, address_offset=0):
traces = {}
# Match something like:
# [backtrace] [bazel-out/local-dbg/bin/source/server/_virtual_includes/backtrace_lib/server/backtrace.h:84]
backtrace_marker = "\[backtrace\] [^\s]+"
# Match something like:
# ${backtrace_marker} #10: SYMBOL [0xADDR]
# or:
# ${backtrace_marker} #10: [0xADDR]
stackaddr_re = re.compile("%s #\d+:(?: .*)? \[(0x[0-9a-fA-F]+)\]$" % backtrace_marker)
# Match something like:
# #10 0xLOCATION (BINARY+0xADDR)
asan_re = re.compile(" *#\d+ *0x[0-9a-fA-F]+ *\([^+]*\+(0x[0-9a-fA-F]+)\)")
try:
while True:
line = input_source.readline()
if line == "":
return # EOF
stackaddr_match = stackaddr_re.search(line)
if not stackaddr_match:
stackaddr_match = asan_re.search(line)
if stackaddr_match:
address = stackaddr_match.groups()[0]
if address_offset != 0:
address = hex(int(address, 16) - address_offset)
file_and_line_number = run_addr2line(object_file, address)
file_and_line_number = trim_proc_cwd(file_and_line_number)
if address_offset != 0:
sys.stdout.write("%s->[%s] %s" % (line.strip(), address, file_and_line_number))
else:
sys.stdout.write("%s %s" % (line.strip(), file_and_line_number))
continue
else:
# Pass through print all other log lines:
sys.stdout.write(line)
except KeyboardInterrupt:
return
# Execute addr2line with a particular object file and input string of addresses
# to resolve, one per line.
#
# Returns list of result lines
def run_addr2line(obj_file, addr_to_resolve):
return subprocess.check_output(["addr2line", "-Cpie", obj_file,
addr_to_resolve]).decode('utf-8')
# Because of how bazel compiles, addr2line reports file names that begin with
# "/proc/self/cwd/" and sometimes even "/proc/self/cwd/./". This isn't particularly
# useful information, so trim it out and make a perfectly useful relative path.
def trim_proc_cwd(file_and_line_number):
trim_regex = r'/proc/self/cwd/(\./)?'
return re.sub(trim_regex, '', file_and_line_number)
# Execute pmap with a pid to calculate the addr offset
#
# Returns list of extended process memory information.
def run_pmap(pid):
return subprocess.check_output(['pmap', '-qX', str(pid)]).decode('utf-8')[1:]
# Find the virtual address offset of the process. This may be needed due ASLR.
#
# Returns the virtual address offset as an integer, or 0 if unable to determine.
def find_address_offset(pid):
try:
proc_memory = run_pmap(pid)
match = re.search(r'([a-f0-9]+)\s+r-xp', proc_memory)
if match is None:
return 0
return int(match.group(1), 16)
except (subprocess.CalledProcessError, PermissionError):
return 0
# When setting the logging level to trace, it's possible that we'll bump
# into chars not accepted by the default encoding. It's fine to
# ignore these and keep going (instead of giving up and exiting
# while possibly bringing Envoy down).
def ignore_decoding_errors(io_wrapper):
# Only avail since 3.7.
# https://docs.python.org/3/library/io.html#io.TextIOWrapper.reconfigure
if hasattr(io_wrapper, 'reconfigure'):
try:
io_wrapper.reconfigure(errors='ignore')
except:
pass
return io_wrapper
if __name__ == "__main__":
if len(sys.argv) > 2 and sys.argv[1] == '-s':
decode_stacktrace_log(sys.argv[2], ignore_decoding_errors(sys.stdin))
sys.exit(0)
elif len(sys.argv) > 1:
rununder = subprocess.Popen(
sys.argv[1:], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True)
offset = find_address_offset(rununder.pid)
decode_stacktrace_log(sys.argv[1], ignore_decoding_errors(rununder.stdout), offset)
rununder.wait()
sys.exit(rununder.returncode) # Pass back test pass/fail result
else:
print("Usage (execute subprocess): stack_decode.py executable_file [additional args]")
print("Usage (read from stdin): stack_decode.py -s executable_file")
sys.exit(1)