1#!/usr/bin/env python 2from __future__ import print_function 3 4import cmd 5import optparse 6import os 7import shlex 8import struct 9import sys 10 11ARMAG = "!<arch>\n" 12SARMAG = 8 13ARFMAG = "`\n" 14AR_EFMT1 = "#1/" 15 16 17def memdump(src, bytes_per_line=16, address=0): 18 FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' 19 for x in range(256)]) 20 for i in range(0, len(src), bytes_per_line): 21 s = src[i:i+bytes_per_line] 22 hex_bytes = ' '.join(["%02x" % (ord(x)) for x in s]) 23 ascii = s.translate(FILTER) 24 print("%#08.8x: %-*s %s" % (address+i, bytes_per_line*3, hex_bytes, 25 ascii)) 26 27 28class Object(object): 29 def __init__(self, file): 30 def read_str(file, str_len): 31 return file.read(str_len).rstrip('\0 ') 32 33 def read_int(file, str_len, base): 34 return int(read_str(file, str_len), base) 35 36 self.offset = file.tell() 37 self.file = file 38 self.name = read_str(file, 16) 39 self.date = read_int(file, 12, 10) 40 self.uid = read_int(file, 6, 10) 41 self.gid = read_int(file, 6, 10) 42 self.mode = read_int(file, 8, 8) 43 self.size = read_int(file, 10, 10) 44 if file.read(2) != ARFMAG: 45 raise ValueError('invalid BSD object at offset %#08.8x' % ( 46 self.offset)) 47 # If we have an extended name read it. Extended names start with 48 name_len = 0 49 if self.name.startswith(AR_EFMT1): 50 name_len = int(self.name[len(AR_EFMT1):], 10) 51 self.name = read_str(file, name_len) 52 self.obj_offset = file.tell() 53 self.obj_size = self.size - name_len 54 file.seek(self.obj_size, 1) 55 56 def dump(self, f=sys.stdout, flat=True): 57 if flat: 58 f.write('%#08.8x: %#08.8x %5u %5u %6o %#08.8x %s\n' % (self.offset, 59 self.date, self.uid, self.gid, self.mode, self.size, 60 self.name)) 61 else: 62 f.write('%#08.8x: \n' % self.offset) 63 f.write(' name = "%s"\n' % self.name) 64 f.write(' date = %#08.8x\n' % self.date) 65 f.write(' uid = %i\n' % self.uid) 66 f.write(' gid = %i\n' % self.gid) 67 f.write(' mode = %o\n' % self.mode) 68 f.write(' size = %#08.8x\n' % (self.size)) 69 self.file.seek(self.obj_offset, 0) 70 first_bytes = self.file.read(4) 71 f.write('bytes = ') 72 memdump(first_bytes) 73 74 def get_bytes(self): 75 saved_pos = self.file.tell() 76 self.file.seek(self.obj_offset, 0) 77 bytes = self.file.read(self.obj_size) 78 self.file.seek(saved_pos, 0) 79 return bytes 80 81 def save(self, path=None, overwrite=False): 82 ''' 83 Save the contents of the object to disk using 'path' argument as 84 the path, or save it to the current working directory using the 85 object name. 86 ''' 87 88 if path is None: 89 path = self.name 90 if not overwrite and os.path.exists(path): 91 print('error: outfile "%s" already exists' % (path)) 92 return 93 print('Saving "%s" to "%s"...' % (self.name, path)) 94 with open(path, 'w') as f: 95 f.write(self.get_bytes()) 96 97 98class StringTable(object): 99 def __init__(self, bytes): 100 self.bytes = bytes 101 102 def get_string(self, offset): 103 length = len(self.bytes) 104 if offset >= length: 105 return None 106 return self.bytes[offset:self.bytes.find('\0', offset)] 107 108 109class Archive(object): 110 def __init__(self, path): 111 self.path = path 112 self.file = open(path, 'r') 113 self.objects = [] 114 self.offset_to_object = {} 115 if self.file.read(SARMAG) != ARMAG: 116 print("error: file isn't a BSD archive") 117 while True: 118 try: 119 self.objects.append(Object(self.file)) 120 except ValueError: 121 break 122 123 def get_object_at_offset(self, offset): 124 if offset in self.offset_to_object: 125 return self.offset_to_object[offset] 126 for obj in self.objects: 127 if obj.offset == offset: 128 self.offset_to_object[offset] = obj 129 return obj 130 return None 131 132 def find(self, name, mtime=None, f=sys.stdout): 133 ''' 134 Find an object(s) by name with optional modification time. There 135 can be multple objects with the same name inside and possibly with 136 the same modification time within a BSD archive so clients must be 137 prepared to get multiple results. 138 ''' 139 matches = [] 140 for obj in self.objects: 141 if obj.name == name and (mtime is None or mtime == obj.date): 142 matches.append(obj) 143 return matches 144 145 @classmethod 146 def dump_header(self, f=sys.stdout): 147 f.write(' DATE UID GID MODE SIZE NAME\n') 148 f.write(' ---------- ----- ----- ------ ---------- ' 149 '--------------\n') 150 151 def get_symdef(self): 152 def get_uint32(file): 153 '''Extract a uint32_t from the current file position.''' 154 v, = struct.unpack('=I', file.read(4)) 155 return v 156 157 for obj in self.objects: 158 symdef = [] 159 if obj.name.startswith("__.SYMDEF"): 160 self.file.seek(obj.obj_offset, 0) 161 ranlib_byte_size = get_uint32(self.file) 162 num_ranlib_structs = ranlib_byte_size/8 163 str_offset_pairs = [] 164 for _ in range(num_ranlib_structs): 165 strx = get_uint32(self.file) 166 offset = get_uint32(self.file) 167 str_offset_pairs.append((strx, offset)) 168 strtab_len = get_uint32(self.file) 169 strtab = StringTable(self.file.read(strtab_len)) 170 for s in str_offset_pairs: 171 symdef.append((strtab.get_string(s[0]), s[1])) 172 return symdef 173 174 def get_object_dicts(self): 175 ''' 176 Returns an array of object dictionaries that contain they following 177 keys: 178 'object': the actual bsd.Object instance 179 'symdefs': an array of symbol names that the object contains 180 as found in the "__.SYMDEF" item in the archive 181 ''' 182 symdefs = self.get_symdef() 183 symdef_dict = {} 184 if symdefs: 185 for (name, offset) in symdefs: 186 if offset in symdef_dict: 187 object_dict = symdef_dict[offset] 188 else: 189 object_dict = { 190 'object': self.get_object_at_offset(offset), 191 'symdefs': [] 192 } 193 symdef_dict[offset] = object_dict 194 object_dict['symdefs'].append(name) 195 object_dicts = [] 196 for offset in sorted(symdef_dict): 197 object_dicts.append(symdef_dict[offset]) 198 return object_dicts 199 200 def dump(self, f=sys.stdout, flat=True): 201 f.write('%s:\n' % self.path) 202 if flat: 203 self.dump_header(f=f) 204 for obj in self.objects: 205 obj.dump(f=f, flat=flat) 206 207class Interactive(cmd.Cmd): 208 '''Interactive prompt for exploring contents of BSD archive files, type 209 "help" to see a list of supported commands.''' 210 image_option_parser = None 211 212 def __init__(self, archives): 213 cmd.Cmd.__init__(self) 214 self.use_rawinput = False 215 self.intro = ('Interactive BSD archive prompt, type "help" to see a ' 216 'list of supported commands.') 217 self.archives = archives 218 self.prompt = '% ' 219 220 def default(self, line): 221 '''Catch all for unknown command, which will exit the interpreter.''' 222 print("unknown command: %s" % line) 223 return True 224 225 def do_q(self, line): 226 '''Quit command''' 227 return True 228 229 def do_quit(self, line): 230 '''Quit command''' 231 return True 232 233 def do_extract(self, line): 234 args = shlex.split(line) 235 if args: 236 extracted = False 237 for object_name in args: 238 for archive in self.archives: 239 matches = archive.find(object_name) 240 if matches: 241 for object in matches: 242 object.save(overwrite=False) 243 extracted = True 244 if not extracted: 245 print('error: no object matches "%s" in any archives' % ( 246 object_name)) 247 else: 248 print('error: must specify the name of an object to extract') 249 250 def do_ls(self, line): 251 args = shlex.split(line) 252 if args: 253 for object_name in args: 254 for archive in self.archives: 255 matches = archive.find(object_name) 256 if matches: 257 for object in matches: 258 object.dump(flat=False) 259 else: 260 print('error: no object matches "%s" in "%s"' % ( 261 object_name, archive.path)) 262 else: 263 for archive in self.archives: 264 archive.dump(flat=True) 265 print('') 266 267 268 269def main(): 270 parser = optparse.OptionParser( 271 prog='bsd', 272 description='Utility for BSD archives') 273 parser.add_option( 274 '--object', 275 type='string', 276 dest='object_name', 277 default=None, 278 help=('Specify the name of a object within the BSD archive to get ' 279 'information on')) 280 parser.add_option( 281 '-s', '--symbol', 282 type='string', 283 dest='find_symbol', 284 default=None, 285 help=('Specify the name of a symbol within the BSD archive to get ' 286 'information on from SYMDEF')) 287 parser.add_option( 288 '--symdef', 289 action='store_true', 290 dest='symdef', 291 default=False, 292 help=('Dump the information in the SYMDEF.')) 293 parser.add_option( 294 '-v', '--verbose', 295 action='store_true', 296 dest='verbose', 297 default=False, 298 help='Enable verbose output') 299 parser.add_option( 300 '-e', '--extract', 301 action='store_true', 302 dest='extract', 303 default=False, 304 help=('Specify this to extract the object specified with the --object ' 305 'option. There must be only one object with a matching name or ' 306 'the --mtime option must be specified to uniquely identify a ' 307 'single object.')) 308 parser.add_option( 309 '-m', '--mtime', 310 type='int', 311 dest='mtime', 312 default=None, 313 help=('Specify the modification time of the object an object. This ' 314 'option is used with either the --object or --extract options.')) 315 parser.add_option( 316 '-o', '--outfile', 317 type='string', 318 dest='outfile', 319 default=None, 320 help=('Specify a different name or path for the file to extract when ' 321 'using the --extract option. If this option isn\'t specified, ' 322 'then the extracted object file will be extracted into the ' 323 'current working directory if a file doesn\'t already exist ' 324 'with that name.')) 325 parser.add_option( 326 '-i', '--interactive', 327 action='store_true', 328 dest='interactive', 329 default=False, 330 help=('Enter an interactive shell that allows users to interactively ' 331 'explore contents of .a files.')) 332 333 (options, args) = parser.parse_args(sys.argv[1:]) 334 335 if options.interactive: 336 archives = [] 337 for path in args: 338 archives.append(Archive(path)) 339 interpreter = Interactive(archives) 340 interpreter.cmdloop() 341 return 342 343 for path in args: 344 archive = Archive(path) 345 if options.object_name: 346 print('%s:\n' % (path)) 347 matches = archive.find(options.object_name, options.mtime) 348 if matches: 349 dump_all = True 350 if options.extract: 351 if len(matches) == 1: 352 dump_all = False 353 matches[0].save(path=options.outfile, overwrite=False) 354 else: 355 print('error: multiple objects match "%s". Specify ' 356 'the modification time using --mtime.' % ( 357 options.object_name)) 358 if dump_all: 359 for obj in matches: 360 obj.dump(flat=False) 361 else: 362 print('error: object "%s" not found in archive' % ( 363 options.object_name)) 364 elif options.find_symbol: 365 symdefs = archive.get_symdef() 366 if symdefs: 367 success = False 368 for (name, offset) in symdefs: 369 obj = archive.get_object_at_offset(offset) 370 if name == options.find_symbol: 371 print('Found "%s" in:' % (options.find_symbol)) 372 obj.dump(flat=False) 373 success = True 374 if not success: 375 print('Didn\'t find "%s" in any objects' % ( 376 options.find_symbol)) 377 else: 378 print("error: no __.SYMDEF was found") 379 elif options.symdef: 380 object_dicts = archive.get_object_dicts() 381 for object_dict in object_dicts: 382 object_dict['object'].dump(flat=False) 383 print("symbols:") 384 for name in object_dict['symdefs']: 385 print(" %s" % (name)) 386 else: 387 archive.dump(flat=not options.verbose) 388 389 390if __name__ == '__main__': 391 main() 392 393 394def print_mtime_error(result, dmap_mtime, actual_mtime): 395 print("error: modification time in debug map (%#08.8x) doesn't " 396 "match the .o file modification time (%#08.8x)" % ( 397 dmap_mtime, actual_mtime), file=result) 398 399 400def print_file_missing_error(result, path): 401 print("error: file \"%s\" doesn't exist" % (path), file=result) 402 403 404def print_multiple_object_matches(result, object_name, mtime, matches): 405 print("error: multiple matches for object '%s' with with " 406 "modification time %#08.8x:" % (object_name, mtime), file=result) 407 Archive.dump_header(f=result) 408 for match in matches: 409 match.dump(f=result, flat=True) 410 411 412def print_archive_object_error(result, object_name, mtime, archive): 413 matches = archive.find(object_name, f=result) 414 if len(matches) > 0: 415 print("error: no objects have a modification time that " 416 "matches %#08.8x for '%s'. Potential matches:" % ( 417 mtime, object_name), file=result) 418 Archive.dump_header(f=result) 419 for match in matches: 420 match.dump(f=result, flat=True) 421 else: 422 print("error: no object named \"%s\" found in archive:" % ( 423 object_name), file=result) 424 Archive.dump_header(f=result) 425 for match in archive.objects: 426 match.dump(f=result, flat=True) 427 # archive.dump(f=result, flat=True) 428 429 430class VerifyDebugMapCommand: 431 name = "verify-debug-map-objects" 432 433 def create_options(self): 434 usage = "usage: %prog [options]" 435 description = '''This command reports any .o files that are missing 436or whose modification times don't match in the debug map of an executable.''' 437 438 self.parser = optparse.OptionParser( 439 description=description, 440 prog=self.name, 441 usage=usage, 442 add_help_option=False) 443 444 self.parser.add_option( 445 '-e', '--errors', 446 action='store_true', 447 dest='errors', 448 default=False, 449 help="Only show errors") 450 451 def get_short_help(self): 452 return "Verify debug map object files." 453 454 def get_long_help(self): 455 return self.help_string 456 457 def __init__(self, debugger, unused): 458 self.create_options() 459 self.help_string = self.parser.format_help() 460 461 def __call__(self, debugger, command, exe_ctx, result): 462 import lldb 463 # Use the Shell Lexer to properly parse up command options just like a 464 # shell would 465 command_args = shlex.split(command) 466 467 try: 468 (options, args) = self.parser.parse_args(command_args) 469 except: 470 result.SetError("option parsing failed") 471 return 472 473 # Always get program state from the SBExecutionContext passed in 474 target = exe_ctx.GetTarget() 475 if not target.IsValid(): 476 result.SetError("invalid target") 477 return 478 archives = {} 479 for module_spec in args: 480 module = target.module[module_spec] 481 if not (module and module.IsValid()): 482 result.SetError('error: invalid module specification: "%s". ' 483 'Specify the full path, basename, or UUID of ' 484 'a module ' % (module_spec)) 485 return 486 num_symbols = module.GetNumSymbols() 487 num_errors = 0 488 for i in range(num_symbols): 489 symbol = module.GetSymbolAtIndex(i) 490 if symbol.GetType() != lldb.eSymbolTypeObjectFile: 491 continue 492 path = symbol.GetName() 493 if not path: 494 continue 495 # Extract the value of the symbol by dumping the 496 # symbol. The value is the mod time. 497 dmap_mtime = int(str(symbol).split('value = ') 498 [1].split(',')[0], 16) 499 if not options.errors: 500 print('%s' % (path), file=result) 501 if os.path.exists(path): 502 actual_mtime = int(os.stat(path).st_mtime) 503 if dmap_mtime != actual_mtime: 504 num_errors += 1 505 if options.errors: 506 print('%s' % (path), end=' ', file=result) 507 print_mtime_error(result, dmap_mtime, 508 actual_mtime) 509 elif path[-1] == ')': 510 (archive_path, object_name) = path[0:-1].split('(') 511 if not archive_path and not object_name: 512 num_errors += 1 513 if options.errors: 514 print('%s' % (path), end=' ', file=result) 515 print_file_missing_error(path) 516 continue 517 if not os.path.exists(archive_path): 518 num_errors += 1 519 if options.errors: 520 print('%s' % (path), end=' ', file=result) 521 print_file_missing_error(archive_path) 522 continue 523 if archive_path in archives: 524 archive = archives[archive_path] 525 else: 526 archive = Archive(archive_path) 527 archives[archive_path] = archive 528 matches = archive.find(object_name, dmap_mtime) 529 num_matches = len(matches) 530 if num_matches == 1: 531 print('1 match', file=result) 532 obj = matches[0] 533 if obj.date != dmap_mtime: 534 num_errors += 1 535 if options.errors: 536 print('%s' % (path), end=' ', file=result) 537 print_mtime_error(result, dmap_mtime, obj.date) 538 elif num_matches == 0: 539 num_errors += 1 540 if options.errors: 541 print('%s' % (path), end=' ', file=result) 542 print_archive_object_error(result, object_name, 543 dmap_mtime, archive) 544 elif num_matches > 1: 545 num_errors += 1 546 if options.errors: 547 print('%s' % (path), end=' ', file=result) 548 print_multiple_object_matches(result, 549 object_name, 550 dmap_mtime, matches) 551 if num_errors > 0: 552 print("%u errors found" % (num_errors), file=result) 553 else: 554 print("No errors detected in debug map", file=result) 555 556 557def __lldb_init_module(debugger, dict): 558 # This initializer is being run from LLDB in the embedded command 559 # interpreter. 560 # Add any commands contained in this module to LLDB 561 debugger.HandleCommand( 562 'command script add -c %s.VerifyDebugMapCommand %s' % ( 563 __name__, VerifyDebugMapCommand.name)) 564 print('The "%s" command has been installed, type "help %s" for detailed ' 565 'help.' % (VerifyDebugMapCommand.name, VerifyDebugMapCommand.name)) 566