| 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() |