1import os 2import re 3 4# If doing it on device with gdbserver 5DEVICE = os.environ.get('GDBSCRIPT_ON_DEVICE', False) 6# Path of the file on device 7DEVICE_FILEPATH = os.environ.get('GDBSCRIPT_FILENAME', None) 8# GDBServer's port 9DEVICE_PORT = os.environ.get('GDBSCRIPT_DEVICE_PORT', 4444) 10# Serial number of device for adb 11DEVICE_SERIAL = os.environ.get('GDBSCRIPT_DEVICE_SERIAL', None) 12 13def check_device_args(): 14 """ 15 Checks if FILEPATH is provided if the execution is on device 16 """ 17 if not DEVICE: 18 return 19 20 if not DEVICE_FILEPATH: 21 raise ValueError("Filename (GDBSCRIPT_FILEPATH) not provided") 22 23class RecordPoint(gdb.Breakpoint): 24 """ 25 A custom breakpoint that records the arguments when the breakpoint is hit and continues 26 Also enables the next breakpoint and disables all the ones after it 27 """ 28 def stop(self): 29 """ 30 The function that's called when a breakpoint is hit. If we return true, 31 it halts otherwise it continues 32 We always return false because we just need to record the value and we 33 can do it without halting the program 34 """ 35 self.args[self.times_hit % self.count] = get_function_args() 36 self.times_hit += 1 37 38 if self.next_bp != None: 39 self.next_bp.previous_hit() 40 41 return False 42 43 def previous_hit(self): 44 """ 45 This function is called if the previous breakpoint is hit so it can enable 46 itself and disable the next ones 47 """ 48 self.enabled = True 49 50 if self.next_bp != None: 51 self.next_bp.propagate_disable() 52 53 def propagate_disable(self): 54 """ 55 Disabled all the breakpoints after itself 56 """ 57 self.enabled = False 58 if self.next_bp != None: 59 self.next_bp.propagate_disable() 60 61 def process_arguments(self): 62 """ 63 Orders the recorded arguments into the right order (oldest to newest) 64 """ 65 current_hit_point = self.times_hit % self.count 66 # Split at the point of current_hit_point because all the entries after it 67 # are older than the ones before it 68 self.processed_args = self.args[current_hit_point:] + self.args[:current_hit_point] 69 self.current_arg_idx = 0 70 71 def get_arguments(self): 72 """ 73 Gets the current argument value. 74 Should be called the same amount of times as the function was called 75 in the stacktrace 76 First call returns the arguments recorded for the first call in the stacktrace 77 and so on. 78 """ 79 if self.current_arg_idx >= len(self.processed_args): 80 raise ValueError("Cannot get arguments more times than the function \ 81 was present in stacktrace") 82 83 cur = self.processed_args[self.current_arg_idx] 84 self.current_arg_idx += 1 85 return cur 86 87def init_gdb(): 88 """ 89 Initialized the GDB specific stuff 90 """ 91 gdb.execute('set pagination off') 92 gdb.execute('set print frame-arguments all') 93 if DEVICE: 94 gdb.execute('target extended-remote :{}'.format(DEVICE_PORT)) 95 gdb.execute('set remote exec-file /data/local/tmp/{}'.format(DEVICE_FILEPATH)) 96 97def initial_run(): 98 """ 99 The initial run of the program which captures the stacktrace in init.log file 100 """ 101 gdb.execute('r > init.log 2>&1',from_tty=True, to_string=True) 102 if DEVICE: 103 if DEVICE_SERIAL: 104 os.system('adb -s "{}" pull /data/local/tmp/init.log'.format(DEVICE_SERIAL)) 105 else: 106 os.system("adb pull /data/local/tmp/init.log") 107 with open("init.log", "rb") as f: 108 out = f.read().decode() 109 return out 110 111def gdb_exit(): 112 """ 113 Exits the GDB instance 114 """ 115 gdb.execute('q') 116 117def get_stacktrace_functions(stacktrace): 118 """ 119 Gets the functions from ASAN/HWASAN's stacktrace 120 Args: 121 stacktrace: (string) ASAN/HWASAN's stacktrace output 122 Returns: 123 functions: (list) functions in the stacktrace 124 """ 125 stacktrace_start = stacktrace[stacktrace.index('==ERROR: '):].split("\n") 126 functions = [] 127 128 # skip the first two lines of stacktrace 129 for line in stacktrace_start[2:]: 130 if line == "": 131 break 132 133 # Extracts the function name from a line like this 134 # "#0 0xaddress in function_name() file/path.cc:xx:yy" 135 func_name = line.strip().split(" ")[3] 136 if '(' in func_name: 137 func_name = func_name[:func_name.index('(')] 138 139 functions.append(func_name) 140 141 #remove last function from stacktrace because it would be _start 142 return functions 143 144def parse_function_arguments(func_info): 145 """ 146 Parses the output of 'whatis' command into a list of arguments 147 "void (teststruct)" --> ["teststruct"] 148 "int (int (*)(int, char **, char **), int, char **, int (*)(int, char **, char **), 149 void (*)(void), void (*)(void), void *)" --> ['int (*)(int, char **, char **)', 150 'int', 'char **', 'int (*)(int, char **, char **)', 'void (*)(void)', 151 'void (*)(void)', ' void *'] 152 153 Args: 154 func_info: (string) output of gdb's 'whatis' command for a function 155 Returns: 156 parsed_params: (list) parsed parameters of the function 157 """ 158 if '(' not in func_info: 159 return [] 160 func_params = func_info[func_info.index('(')+1:-1] 161 parentheses_count = 0 162 current_param = "" 163 parsed_params = [] 164 165 for token in func_params: 166 # Essentially trying to get the data types from a function declaration 167 if token == '(': 168 parentheses_count += 1 169 elif token == ')': 170 parentheses_count -= 1 171 172 # If we are not inside any paren and see a ',' it signals the start of 173 #the next parameter 174 if token == ',' and parentheses_count == 0: 175 parsed_params.append(current_param.strip()) 176 current_param = "" 177 else: 178 current_param += token 179 180 parsed_params.append(current_param) 181 return parsed_params 182 183def parse_stacktrace(stacktrace): 184 """ 185 Parses the ASAN/HWASAN's stacktrace to a list of functions, their addresses 186 and argument types 187 Args: 188 stacktrace: (string) ASAN/HWASAN's stacktrace output 189 Returns: 190 functions_info: (list) parsed function information as a dictionary 191 """ 192 stacktrace_functions = get_stacktrace_functions(stacktrace)[:-1] 193 functions_info = [] 194 for function in stacktrace_functions: 195 # Gets the value right hand side of gdb's whatis command. 196 # "type = {function info}" -> "{function info}" 197 func_info = gdb.execute('whatis {}'.format(function), 198 to_string=True).split(' = ')[1].strip() 199 # Uses gdb's x/i to print its address and parse it from hex to int 200 address = int(gdb.execute("x/i {}".format(function), 201 to_string=True).strip().split(" ")[0], 16) 202 functions_info.append({'name': function, 'address':address, 203 'arguments' : parse_function_arguments(func_info)}) 204 #In the order they are called in the execution 205 return functions_info[::-1] 206 207def get_function_args(): 208 """ 209 Gets the current function arguments 210 """ 211 args = gdb.execute('info args -q', to_string=True).strip() 212 return args 213 214def functions_to_breakpoint(parsed_functions): 215 """ 216 Sets the breakpoint at every function and returns a dictionary mapping the 217 function to it's breakpoint 218 Args: 219 parsed_functions: (list) functions in the stacktrace (in the same order) as 220 dictionary with "name" referring to the function name 221 ({"name" : function_name}) 222 Returns: 223 function_breakpoints: (dictionary) maps the function name to its 224 breakpoint object 225 """ 226 function_breakpoints = {} 227 last_bp = None 228 229 for function in reversed(parsed_functions): 230 function_name = function['name'] 231 if function_name in function_breakpoints: 232 function_breakpoints[function_name].count += 1 233 function_breakpoints[function_name].args.append(None) 234 continue 235 236 cur_bp = RecordPoint("{}".format(function_name)) 237 cur_bp.count = 1 238 cur_bp.times_hit = 0 239 cur_bp.args = [] 240 cur_bp.args.append(None) 241 cur_bp.next_bp = last_bp 242 243 function_breakpoints[function['name']] = cur_bp 244 last_bp = cur_bp 245 246 return function_breakpoints 247 248def run(parsed_functions): 249 """ 250 Runs the whole thing by setting up breakpoints and printing them after 251 excecution is done 252 Args: 253 parsed_functions: A list of functions in the stacktrace (in the same order) 254 as dictionary with "name" referring to the function name 255 ({"name" : function_name}) 256 """ 257 names = [function['name'] for function in parsed_functions] 258 breakpoints = functions_to_breakpoint(parsed_functions) 259 260 #Disable all breakpoints at start 261 for bp in breakpoints: 262 breakpoints[bp].enabled = False 263 264 breakpoints[names[0]].enabled = True 265 266 gdb.execute('r') 267 for breakpoint in breakpoints: 268 breakpoints[breakpoint].process_arguments() 269 270 function_args = [] 271 for name in names: 272 print("-----------") 273 print("Function -> {}".format(name)) 274 275 function_args.append({'function':name, 276 'arguments' : breakpoints[name].get_arguments()}) 277 print(function_args[-1]['arguments']) 278 279 return function_args 280 281 282if __name__ == '__main__': 283 check_device_args() 284 init_gdb() 285 initial_out = initial_run() 286 function_data = parse_stacktrace(initial_out) 287 run(function_data) 288 gdb_exit() 289