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