blob: b1af70cd1239af331e586e3795124770725b5688 [file] [log] [blame]
import os
import re
# If doing it on device with gdbserver
DEVICE = os.environ.get('GDBSCRIPT_ON_DEVICE', False)
# Path of the file on device
DEVICE_FILEPATH = os.environ.get('GDBSCRIPT_FILENAME', None)
# GDBServer's port
DEVICE_PORT = os.environ.get('GDBSCRIPT_DEVICE_PORT', 4444)
# Serial number of device for adb
DEVICE_SERIAL = os.environ.get('GDBSCRIPT_DEVICE_SERIAL', None)
def check_device_args():
"""
Checks if FILEPATH is provided if the execution is on device
"""
if not DEVICE:
return
if not DEVICE_FILEPATH:
raise ValueError("Filename (GDBSCRIPT_FILEPATH) not provided")
class RecordPoint(gdb.Breakpoint):
"""
A custom breakpoint that records the arguments when the breakpoint is hit and continues
Also enables the next breakpoint and disables all the ones after it
"""
def stop(self):
"""
The function that's called when a breakpoint is hit. If we return true,
it halts otherwise it continues
We always return false because we just need to record the value and we
can do it without halting the program
"""
self.args[self.times_hit % self.count] = get_function_args()
self.times_hit += 1
if self.next_bp != None:
self.next_bp.previous_hit()
return False
def previous_hit(self):
"""
This function is called if the previous breakpoint is hit so it can enable
itself and disable the next ones
"""
self.enabled = True
if self.next_bp != None:
self.next_bp.propagate_disable()
def propagate_disable(self):
"""
Disabled all the breakpoints after itself
"""
self.enabled = False
if self.next_bp != None:
self.next_bp.propagate_disable()
def process_arguments(self):
"""
Orders the recorded arguments into the right order (oldest to newest)
"""
current_hit_point = self.times_hit % self.count
# Split at the point of current_hit_point because all the entries after it
# are older than the ones before it
self.processed_args = self.args[current_hit_point:] + self.args[:current_hit_point]
self.current_arg_idx = 0
def get_arguments(self):
"""
Gets the current argument value.
Should be called the same amount of times as the function was called
in the stacktrace
First call returns the arguments recorded for the first call in the stacktrace
and so on.
"""
if self.current_arg_idx >= len(self.processed_args):
raise ValueError("Cannot get arguments more times than the function \
was present in stacktrace")
cur = self.processed_args[self.current_arg_idx]
self.current_arg_idx += 1
return cur
def init_gdb():
"""
Initialized the GDB specific stuff
"""
gdb.execute('set pagination off')
gdb.execute('set print frame-arguments all')
if DEVICE:
gdb.execute('target extended-remote :{}'.format(DEVICE_PORT))
gdb.execute('set remote exec-file /data/local/tmp/{}'.format(DEVICE_FILEPATH))
def initial_run():
"""
The initial run of the program which captures the stacktrace in init.log file
"""
gdb.execute('r > init.log 2>&1',from_tty=True, to_string=True)
if DEVICE:
if DEVICE_SERIAL:
os.system('adb -s "{}" pull /data/local/tmp/init.log'.format(DEVICE_SERIAL))
else:
os.system("adb pull /data/local/tmp/init.log")
with open("init.log", "rb") as f:
out = f.read().decode()
return out
def gdb_exit():
"""
Exits the GDB instance
"""
gdb.execute('q')
def get_stacktrace_functions(stacktrace):
"""
Gets the functions from ASAN/HWASAN's stacktrace
Args:
stacktrace: (string) ASAN/HWASAN's stacktrace output
Returns:
functions: (list) functions in the stacktrace
"""
stacktrace_start = stacktrace[stacktrace.index('==ERROR: '):].split("\n")
functions = []
# skip the first two lines of stacktrace
for line in stacktrace_start[2:]:
if line == "":
break
# Extracts the function name from a line like this
# "#0 0xaddress in function_name() file/path.cc:xx:yy"
func_name = line.strip().split(" ")[3]
if '(' in func_name:
func_name = func_name[:func_name.index('(')]
functions.append(func_name)
#remove last function from stacktrace because it would be _start
return functions
def parse_function_arguments(func_info):
"""
Parses the output of 'whatis' command into a list of arguments
"void (teststruct)" --> ["teststruct"]
"int (int (*)(int, char **, char **), int, char **, int (*)(int, char **, char **),
void (*)(void), void (*)(void), void *)" --> ['int (*)(int, char **, char **)',
'int', 'char **', 'int (*)(int, char **, char **)', 'void (*)(void)',
'void (*)(void)', ' void *']
Args:
func_info: (string) output of gdb's 'whatis' command for a function
Returns:
parsed_params: (list) parsed parameters of the function
"""
if '(' not in func_info:
return []
func_params = func_info[func_info.index('(')+1:-1]
parentheses_count = 0
current_param = ""
parsed_params = []
for token in func_params:
# Essentially trying to get the data types from a function declaration
if token == '(':
parentheses_count += 1
elif token == ')':
parentheses_count -= 1
# If we are not inside any paren and see a ',' it signals the start of
#the next parameter
if token == ',' and parentheses_count == 0:
parsed_params.append(current_param.strip())
current_param = ""
else:
current_param += token
parsed_params.append(current_param)
return parsed_params
def parse_stacktrace(stacktrace):
"""
Parses the ASAN/HWASAN's stacktrace to a list of functions, their addresses
and argument types
Args:
stacktrace: (string) ASAN/HWASAN's stacktrace output
Returns:
functions_info: (list) parsed function information as a dictionary
"""
stacktrace_functions = get_stacktrace_functions(stacktrace)[:-1]
functions_info = []
for function in stacktrace_functions:
# Gets the value right hand side of gdb's whatis command.
# "type = {function info}" -> "{function info}"
func_info = gdb.execute('whatis {}'.format(function),
to_string=True).split(' = ')[1].strip()
# Uses gdb's x/i to print its address and parse it from hex to int
address = int(gdb.execute("x/i {}".format(function),
to_string=True).strip().split(" ")[0], 16)
functions_info.append({'name': function, 'address':address,
'arguments' : parse_function_arguments(func_info)})
#In the order they are called in the execution
return functions_info[::-1]
def get_function_args():
"""
Gets the current function arguments
"""
args = gdb.execute('info args -q', to_string=True).strip()
return args
def functions_to_breakpoint(parsed_functions):
"""
Sets the breakpoint at every function and returns a dictionary mapping the
function to it's breakpoint
Args:
parsed_functions: (list) functions in the stacktrace (in the same order) as
dictionary with "name" referring to the function name
({"name" : function_name})
Returns:
function_breakpoints: (dictionary) maps the function name to its
breakpoint object
"""
function_breakpoints = {}
last_bp = None
for function in reversed(parsed_functions):
function_name = function['name']
if function_name in function_breakpoints:
function_breakpoints[function_name].count += 1
function_breakpoints[function_name].args.append(None)
continue
cur_bp = RecordPoint("{}".format(function_name))
cur_bp.count = 1
cur_bp.times_hit = 0
cur_bp.args = []
cur_bp.args.append(None)
cur_bp.next_bp = last_bp
function_breakpoints[function['name']] = cur_bp
last_bp = cur_bp
return function_breakpoints
def run(parsed_functions):
"""
Runs the whole thing by setting up breakpoints and printing them after
excecution is done
Args:
parsed_functions: A list of functions in the stacktrace (in the same order)
as dictionary with "name" referring to the function name
({"name" : function_name})
"""
names = [function['name'] for function in parsed_functions]
breakpoints = functions_to_breakpoint(parsed_functions)
#Disable all breakpoints at start
for bp in breakpoints:
breakpoints[bp].enabled = False
breakpoints[names[0]].enabled = True
gdb.execute('r')
for breakpoint in breakpoints:
breakpoints[breakpoint].process_arguments()
function_args = []
for name in names:
print("-----------")
print("Function -> {}".format(name))
function_args.append({'function':name,
'arguments' : breakpoints[name].get_arguments()})
print(function_args[-1]['arguments'])
return function_args
if __name__ == '__main__':
check_device_args()
init_gdb()
initial_out = initial_run()
function_data = parse_stacktrace(initial_out)
run(function_data)
gdb_exit()