1#!/usr/bin/env python3 2# 3# Copyright 2018, The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17"""Unpacks the boot image. 18 19Extracts the kernel, ramdisk, second bootloader, dtb and recovery dtbo images. 20""" 21 22from argparse import ArgumentParser, RawDescriptionHelpFormatter 23from struct import unpack 24import os 25import shlex 26 27BOOT_IMAGE_HEADER_V3_PAGESIZE = 4096 28VENDOR_RAMDISK_NAME_SIZE = 32 29VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE = 16 30 31 32def create_out_dir(dir_path): 33 """creates a directory 'dir_path' if it does not exist""" 34 if not os.path.exists(dir_path): 35 os.makedirs(dir_path) 36 37 38def extract_image(offset, size, bootimage, extracted_image_name): 39 """extracts an image from the bootimage""" 40 bootimage.seek(offset) 41 with open(extracted_image_name, 'wb') as file_out: 42 file_out.write(bootimage.read(size)) 43 44 45def get_number_of_pages(image_size, page_size): 46 """calculates the number of pages required for the image""" 47 return (image_size + page_size - 1) // page_size 48 49 50def cstr(s): 51 """Remove first NULL character and any character beyond.""" 52 return s.split('\0', 1)[0] 53 54 55def format_os_version(os_version): 56 if os_version == 0: 57 return None 58 a = os_version >> 14 59 b = os_version >> 7 & ((1<<7) - 1) 60 c = os_version & ((1<<7) - 1) 61 return f'{a}.{b}.{c}' 62 63 64def format_os_patch_level(os_patch_level): 65 if os_patch_level == 0: 66 return None 67 y = os_patch_level >> 4 68 y += 2000 69 m = os_patch_level & ((1<<4) - 1) 70 return f'{y:04d}-{m:02d}' 71 72 73def decode_os_version_patch_level(os_version_patch_level): 74 """Returns a tuple of (os_version, os_patch_level).""" 75 os_version = os_version_patch_level >> 11 76 os_patch_level = os_version_patch_level & ((1<<11) - 1) 77 return (format_os_version(os_version), 78 format_os_patch_level(os_patch_level)) 79 80 81class BootImageInfoFormatter: 82 """Formats the boot image info.""" 83 84 def format_pretty_text(self): 85 lines = [] 86 lines.append(f'boot magic: {self.boot_magic}') 87 88 if self.header_version < 3: 89 lines.append(f'kernel_size: {self.kernel_size}') 90 lines.append( 91 f'kernel load address: {self.kernel_load_address:#010x}') 92 lines.append(f'ramdisk size: {self.ramdisk_size}') 93 lines.append( 94 f'ramdisk load address: {self.ramdisk_load_address:#010x}') 95 lines.append(f'second bootloader size: {self.second_size}') 96 lines.append( 97 f'second bootloader load address: ' 98 f'{self.second_load_address:#010x}') 99 lines.append( 100 f'kernel tags load address: {self.tags_load_address:#010x}') 101 lines.append(f'page size: {self.page_size}') 102 else: 103 lines.append(f'kernel_size: {self.kernel_size}') 104 lines.append(f'ramdisk size: {self.ramdisk_size}') 105 106 lines.append(f'os version: {self.os_version}') 107 lines.append(f'os patch level: {self.os_patch_level}') 108 lines.append(f'boot image header version: {self.header_version}') 109 110 if self.header_version < 3: 111 lines.append(f'product name: {self.product_name}') 112 113 lines.append(f'command line args: {self.cmdline}') 114 115 if self.header_version < 3: 116 lines.append(f'additional command line args: {self.extra_cmdline}') 117 118 if self.header_version in {1, 2}: 119 lines.append(f'recovery dtbo size: {self.recovery_dtbo_size}') 120 lines.append( 121 f'recovery dtbo offset: {self.recovery_dtbo_offset:#018x}') 122 lines.append(f'boot header size: {self.boot_header_size}') 123 124 if self.header_version == 2: 125 lines.append(f'dtb size: {self.dtb_size}') 126 lines.append(f'dtb address: {self.dtb_load_address:#018x}') 127 128 if self.header_version >= 4: 129 lines.append( 130 f'boot.img signature size: {self.boot_signature_size}') 131 132 return '\n'.join(lines) 133 134 def format_mkbootimg_argument(self): 135 args = [] 136 args.extend(['--header_version', str(self.header_version)]) 137 if self.os_version: 138 args.extend(['--os_version', self.os_version]) 139 if self.os_patch_level: 140 args.extend(['--os_patch_level', self.os_patch_level]) 141 142 args.extend(['--kernel', os.path.join(self.image_dir, 'kernel')]) 143 args.extend(['--ramdisk', os.path.join(self.image_dir, 'ramdisk')]) 144 145 if self.header_version <= 2: 146 if self.second_size > 0: 147 args.extend(['--second', 148 os.path.join(self.image_dir, 'second')]) 149 if self.recovery_dtbo_size > 0: 150 args.extend(['--recovery_dtbo', 151 os.path.join(self.image_dir, 'recovery_dtbo')]) 152 if self.dtb_size > 0: 153 args.extend(['--dtb', os.path.join(self.image_dir, 'dtb')]) 154 155 args.extend(['--pagesize', f'{self.page_size:#010x}']) 156 157 # Kernel load address is base + kernel_offset in mkbootimg.py. 158 # However we don't know the value of 'base' when unpacking a boot 159 # image in this script, so we set 'base' to zero and 'kernel_offset' 160 # to the kernel load address, 'ramdisk_offset' to the ramdisk load 161 # address, ... etc. 162 args.extend(['--base', f'{0:#010x}']) 163 args.extend(['--kernel_offset', 164 f'{self.kernel_load_address:#010x}']) 165 args.extend(['--ramdisk_offset', 166 f'{self.ramdisk_load_address:#010x}']) 167 args.extend(['--second_offset', 168 f'{self.second_load_address:#010x}']) 169 args.extend(['--tags_offset', f'{self.tags_load_address:#010x}']) 170 171 # dtb is added in boot image v2, and is absent in v1 or v0. 172 if self.header_version == 2: 173 # dtb_offset is uint64_t. 174 args.extend(['--dtb_offset', f'{self.dtb_load_address:#018x}']) 175 176 args.extend(['--board', self.product_name]) 177 args.extend(['--cmdline', self.cmdline + self.extra_cmdline]) 178 else: 179 args.extend(['--cmdline', self.cmdline]) 180 181 return args 182 183 184def unpack_boot_image(boot_img, output_dir): 185 """extracts kernel, ramdisk, second bootloader and recovery dtbo""" 186 info = BootImageInfoFormatter() 187 info.boot_magic = unpack('8s', boot_img.read(8))[0].decode() 188 189 kernel_ramdisk_second_info = unpack('9I', boot_img.read(9 * 4)) 190 # header_version is always at [8] regardless of the value of header_version. 191 info.header_version = kernel_ramdisk_second_info[8] 192 193 if info.header_version < 3: 194 info.kernel_size = kernel_ramdisk_second_info[0] 195 info.kernel_load_address = kernel_ramdisk_second_info[1] 196 info.ramdisk_size = kernel_ramdisk_second_info[2] 197 info.ramdisk_load_address = kernel_ramdisk_second_info[3] 198 info.second_size = kernel_ramdisk_second_info[4] 199 info.second_load_address = kernel_ramdisk_second_info[5] 200 info.tags_load_address = kernel_ramdisk_second_info[6] 201 info.page_size = kernel_ramdisk_second_info[7] 202 os_version_patch_level = unpack('I', boot_img.read(1 * 4))[0] 203 else: 204 info.kernel_size = kernel_ramdisk_second_info[0] 205 info.ramdisk_size = kernel_ramdisk_second_info[1] 206 os_version_patch_level = kernel_ramdisk_second_info[2] 207 info.second_size = 0 208 info.page_size = BOOT_IMAGE_HEADER_V3_PAGESIZE 209 210 info.os_version, info.os_patch_level = decode_os_version_patch_level( 211 os_version_patch_level) 212 213 if info.header_version < 3: 214 info.product_name = cstr(unpack('16s', 215 boot_img.read(16))[0].decode()) 216 info.cmdline = cstr(unpack('512s', boot_img.read(512))[0].decode()) 217 boot_img.read(32) # ignore SHA 218 info.extra_cmdline = cstr(unpack('1024s', 219 boot_img.read(1024))[0].decode()) 220 else: 221 info.cmdline = cstr(unpack('1536s', 222 boot_img.read(1536))[0].decode()) 223 224 if info.header_version in {1, 2}: 225 info.recovery_dtbo_size = unpack('I', boot_img.read(1 * 4))[0] 226 info.recovery_dtbo_offset = unpack('Q', boot_img.read(8))[0] 227 info.boot_header_size = unpack('I', boot_img.read(4))[0] 228 else: 229 info.recovery_dtbo_size = 0 230 231 if info.header_version == 2: 232 info.dtb_size = unpack('I', boot_img.read(4))[0] 233 info.dtb_load_address = unpack('Q', boot_img.read(8))[0] 234 else: 235 info.dtb_size = 0 236 info.dtb_load_address = 0 237 238 if info.header_version >= 4: 239 info.boot_signature_size = unpack('I', boot_img.read(4))[0] 240 else: 241 info.boot_signature_size = 0 242 243 # The first page contains the boot header 244 num_header_pages = 1 245 246 # Convenient shorthand. 247 page_size = info.page_size 248 249 num_kernel_pages = get_number_of_pages(info.kernel_size, page_size) 250 kernel_offset = page_size * num_header_pages # header occupies a page 251 image_info_list = [(kernel_offset, info.kernel_size, 'kernel')] 252 253 num_ramdisk_pages = get_number_of_pages(info.ramdisk_size, page_size) 254 ramdisk_offset = page_size * (num_header_pages + num_kernel_pages 255 ) # header + kernel 256 image_info_list.append((ramdisk_offset, info.ramdisk_size, 'ramdisk')) 257 258 if info.second_size > 0: 259 second_offset = page_size * ( 260 num_header_pages + num_kernel_pages + num_ramdisk_pages 261 ) # header + kernel + ramdisk 262 image_info_list.append((second_offset, info.second_size, 'second')) 263 264 if info.recovery_dtbo_size > 0: 265 image_info_list.append((info.recovery_dtbo_offset, 266 info.recovery_dtbo_size, 267 'recovery_dtbo')) 268 if info.dtb_size > 0: 269 num_second_pages = get_number_of_pages(info.second_size, page_size) 270 num_recovery_dtbo_pages = get_number_of_pages( 271 info.recovery_dtbo_size, page_size) 272 dtb_offset = page_size * ( 273 num_header_pages + num_kernel_pages + num_ramdisk_pages + 274 num_second_pages + num_recovery_dtbo_pages) 275 276 image_info_list.append((dtb_offset, info.dtb_size, 'dtb')) 277 278 if info.boot_signature_size > 0: 279 # boot signature only exists in boot.img version >= v4. 280 # There are only kernel and ramdisk pages before the signature. 281 boot_signature_offset = page_size * ( 282 num_header_pages + num_kernel_pages + num_ramdisk_pages) 283 284 image_info_list.append((boot_signature_offset, info.boot_signature_size, 285 'boot_signature')) 286 287 create_out_dir(output_dir) 288 for offset, size, name in image_info_list: 289 extract_image(offset, size, boot_img, os.path.join(output_dir, name)) 290 info.image_dir = output_dir 291 292 return info 293 294 295class VendorBootImageInfoFormatter: 296 """Formats the vendor_boot image info.""" 297 298 def format_pretty_text(self): 299 lines = [] 300 lines.append(f'boot magic: {self.boot_magic}') 301 lines.append(f'vendor boot image header version: {self.header_version}') 302 lines.append(f'page size: {self.page_size:#010x}') 303 lines.append(f'kernel load address: {self.kernel_load_address:#010x}') 304 lines.append(f'ramdisk load address: {self.ramdisk_load_address:#010x}') 305 if self.header_version > 3: 306 lines.append( 307 f'vendor ramdisk total size: {self.vendor_ramdisk_size}') 308 else: 309 lines.append(f'vendor ramdisk size: {self.vendor_ramdisk_size}') 310 lines.append(f'vendor command line args: {self.cmdline}') 311 lines.append( 312 f'kernel tags load address: {self.tags_load_address:#010x}') 313 lines.append(f'product name: {self.product_name}') 314 lines.append(f'vendor boot image header size: {self.header_size}') 315 lines.append(f'dtb size: {self.dtb_size}') 316 lines.append(f'dtb address: {self.dtb_load_address:#018x}') 317 if self.header_version > 3: 318 lines.append( 319 f'vendor ramdisk table size: {self.vendor_ramdisk_table_size}') 320 lines.append('vendor ramdisk table: [') 321 indent = lambda level: ' ' * 4 * level 322 for entry in self.vendor_ramdisk_table: 323 (output_ramdisk_name, ramdisk_size, ramdisk_offset, 324 ramdisk_type, ramdisk_name, board_id) = entry 325 lines.append(indent(1) + f'{output_ramdisk_name}: ''{') 326 lines.append(indent(2) + f'size: {ramdisk_size}') 327 lines.append(indent(2) + f'offset: {ramdisk_offset}') 328 lines.append(indent(2) + f'type: {ramdisk_type:#x}') 329 lines.append(indent(2) + f'name: {ramdisk_name}') 330 lines.append(indent(2) + 'board_id: [') 331 stride = 4 332 for row_idx in range(0, len(board_id), stride): 333 row = board_id[row_idx:row_idx + stride] 334 lines.append( 335 indent(3) + ' '.join(f'{e:#010x},' for e in row)) 336 lines.append(indent(2) + ']') 337 lines.append(indent(1) + '}') 338 lines.append(']') 339 lines.append( 340 f'vendor bootconfig size: {self.vendor_bootconfig_size}') 341 342 return '\n'.join(lines) 343 344 def format_mkbootimg_argument(self): 345 args = [] 346 args.extend(['--header_version', str(self.header_version)]) 347 args.extend(['--pagesize', f'{self.page_size:#010x}']) 348 args.extend(['--base', f'{0:#010x}']) 349 args.extend(['--kernel_offset', f'{self.kernel_load_address:#010x}']) 350 args.extend(['--ramdisk_offset', f'{self.ramdisk_load_address:#010x}']) 351 args.extend(['--tags_offset', f'{self.tags_load_address:#010x}']) 352 args.extend(['--dtb_offset', f'{self.dtb_load_address:#018x}']) 353 args.extend(['--vendor_cmdline', self.cmdline]) 354 args.extend(['--board', self.product_name]) 355 356 if self.dtb_size > 0: 357 args.extend(['--dtb', os.path.join(self.image_dir, 'dtb')]) 358 359 if self.header_version > 3: 360 args.extend(['--vendor_bootconfig', 361 os.path.join(self.image_dir, 'bootconfig')]) 362 363 for entry in self.vendor_ramdisk_table: 364 (output_ramdisk_name, _, _, ramdisk_type, 365 ramdisk_name, board_id) = entry 366 args.extend(['--ramdisk_type', str(ramdisk_type)]) 367 args.extend(['--ramdisk_name', ramdisk_name]) 368 for idx, e in enumerate(board_id): 369 if e: 370 args.extend([f'--board_id{idx}', f'{e:#010x}']) 371 vendor_ramdisk_path = os.path.join( 372 self.image_dir, output_ramdisk_name) 373 args.extend(['--vendor_ramdisk_fragment', vendor_ramdisk_path]) 374 else: 375 args.extend(['--vendor_ramdisk', 376 os.path.join(self.image_dir, 'vendor_ramdisk')]) 377 378 return args 379 380 381def unpack_vendor_boot_image(boot_img, output_dir): 382 info = VendorBootImageInfoFormatter() 383 info.boot_magic = unpack('8s', boot_img.read(8))[0].decode() 384 info.header_version = unpack('I', boot_img.read(4))[0] 385 info.page_size = unpack('I', boot_img.read(4))[0] 386 info.kernel_load_address = unpack('I', boot_img.read(4))[0] 387 info.ramdisk_load_address = unpack('I', boot_img.read(4))[0] 388 info.vendor_ramdisk_size = unpack('I', boot_img.read(4))[0] 389 info.cmdline = cstr(unpack('2048s', boot_img.read(2048))[0].decode()) 390 info.tags_load_address = unpack('I', boot_img.read(4))[0] 391 info.product_name = cstr(unpack('16s', boot_img.read(16))[0].decode()) 392 info.header_size = unpack('I', boot_img.read(4))[0] 393 info.dtb_size = unpack('I', boot_img.read(4))[0] 394 info.dtb_load_address = unpack('Q', boot_img.read(8))[0] 395 396 # Convenient shorthand. 397 page_size = info.page_size 398 # The first pages contain the boot header 399 num_boot_header_pages = get_number_of_pages(info.header_size, page_size) 400 num_boot_ramdisk_pages = get_number_of_pages( 401 info.vendor_ramdisk_size, page_size) 402 num_boot_dtb_pages = get_number_of_pages(info.dtb_size, page_size) 403 404 ramdisk_offset_base = page_size * num_boot_header_pages 405 image_info_list = [] 406 407 if info.header_version > 3: 408 info.vendor_ramdisk_table_size = unpack('I', boot_img.read(4))[0] 409 vendor_ramdisk_table_entry_num = unpack('I', boot_img.read(4))[0] 410 vendor_ramdisk_table_entry_size = unpack('I', boot_img.read(4))[0] 411 info.vendor_bootconfig_size = unpack('I', boot_img.read(4))[0] 412 num_vendor_ramdisk_table_pages = get_number_of_pages( 413 info.vendor_ramdisk_table_size, page_size) 414 vendor_ramdisk_table_offset = page_size * ( 415 num_boot_header_pages + num_boot_ramdisk_pages + num_boot_dtb_pages) 416 417 vendor_ramdisk_table = [] 418 vendor_ramdisk_symlinks = [] 419 for idx in range(vendor_ramdisk_table_entry_num): 420 entry_offset = vendor_ramdisk_table_offset + ( 421 vendor_ramdisk_table_entry_size * idx) 422 boot_img.seek(entry_offset) 423 ramdisk_size = unpack('I', boot_img.read(4))[0] 424 ramdisk_offset = unpack('I', boot_img.read(4))[0] 425 ramdisk_type = unpack('I', boot_img.read(4))[0] 426 ramdisk_name = cstr(unpack( 427 f'{VENDOR_RAMDISK_NAME_SIZE}s', 428 boot_img.read(VENDOR_RAMDISK_NAME_SIZE))[0].decode()) 429 board_id = unpack( 430 f'{VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE}I', 431 boot_img.read( 432 4 * VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE)) 433 output_ramdisk_name = f'vendor_ramdisk{idx:02}' 434 435 image_info_list.append((ramdisk_offset_base + ramdisk_offset, 436 ramdisk_size, output_ramdisk_name)) 437 vendor_ramdisk_symlinks.append((output_ramdisk_name, ramdisk_name)) 438 vendor_ramdisk_table.append( 439 (output_ramdisk_name, ramdisk_size, ramdisk_offset, 440 ramdisk_type, ramdisk_name, board_id)) 441 442 info.vendor_ramdisk_table = vendor_ramdisk_table 443 444 bootconfig_offset = page_size * (num_boot_header_pages 445 + num_boot_ramdisk_pages + num_boot_dtb_pages 446 + num_vendor_ramdisk_table_pages) 447 image_info_list.append((bootconfig_offset, info.vendor_bootconfig_size, 448 'bootconfig')) 449 else: 450 image_info_list.append( 451 (ramdisk_offset_base, info.vendor_ramdisk_size, 'vendor_ramdisk')) 452 453 dtb_offset = page_size * (num_boot_header_pages + num_boot_ramdisk_pages 454 ) # header + vendor_ramdisk 455 if info.dtb_size > 0: 456 image_info_list.append((dtb_offset, info.dtb_size, 'dtb')) 457 458 create_out_dir(output_dir) 459 for offset, size, name in image_info_list: 460 extract_image(offset, size, boot_img, os.path.join(output_dir, name)) 461 info.image_dir = output_dir 462 463 if info.header_version > 3: 464 vendor_ramdisk_by_name_dir = os.path.join( 465 output_dir, 'vendor-ramdisk-by-name') 466 create_out_dir(vendor_ramdisk_by_name_dir) 467 for src, dst in vendor_ramdisk_symlinks: 468 src_pathname = os.path.join('..', src) 469 dst_pathname = os.path.join( 470 vendor_ramdisk_by_name_dir, f'ramdisk_{dst}') 471 if os.path.lexists(dst_pathname): 472 os.remove(dst_pathname) 473 os.symlink(src_pathname, dst_pathname) 474 475 return info 476 477 478def unpack_bootimg(boot_img, output_dir): 479 """Unpacks the |boot_img| to |output_dir|, and returns the 'info' object.""" 480 with open(boot_img, 'rb') as image_file: 481 boot_magic = unpack('8s', image_file.read(8))[0].decode() 482 image_file.seek(0) 483 if boot_magic == 'ANDROID!': 484 info = unpack_boot_image(image_file, output_dir) 485 elif boot_magic == 'VNDRBOOT': 486 info = unpack_vendor_boot_image(image_file, output_dir) 487 else: 488 raise ValueError(f'Not an Android boot image, magic: {boot_magic}') 489 490 return info 491 492 493def print_bootimg_info(info, output_format, null_separator): 494 """Format and print boot image info.""" 495 if output_format == 'mkbootimg': 496 mkbootimg_args = info.format_mkbootimg_argument() 497 if null_separator: 498 print('\0'.join(mkbootimg_args) + '\0', end='') 499 else: 500 print(shlex.join(mkbootimg_args)) 501 else: 502 print(info.format_pretty_text()) 503 504 505def get_unpack_usage(): 506 return """Output format: 507 508 * info 509 510 Pretty-printed info-rich text format suitable for human inspection. 511 512 * mkbootimg 513 514 Output shell-escaped (quoted) argument strings that can be used to 515 reconstruct the boot image. For example: 516 517 $ unpack_bootimg --boot_img vendor_boot.img --out out --format=mkbootimg | 518 tee mkbootimg_args 519 $ sh -c "mkbootimg $(cat mkbootimg_args) --vendor_boot repacked.img" 520 521 vendor_boot.img and repacked.img would be equivalent. 522 523 If the -0 option is specified, output unescaped null-terminated argument 524 strings that are suitable to be parsed by a shell script (xargs -0 format): 525 526 $ unpack_bootimg --boot_img vendor_boot.img --out out --format=mkbootimg \\ 527 -0 | tee mkbootimg_args 528 $ declare -a MKBOOTIMG_ARGS=() 529 $ while IFS= read -r -d '' ARG; do 530 MKBOOTIMG_ARGS+=("${ARG}") 531 done <mkbootimg_args 532 $ mkbootimg "${MKBOOTIMG_ARGS[@]}" --vendor_boot repacked.img 533""" 534 535 536def parse_cmdline(): 537 """parse command line arguments""" 538 parser = ArgumentParser( 539 formatter_class=RawDescriptionHelpFormatter, 540 description='Unpacks boot, recovery or vendor_boot image.', 541 epilog=get_unpack_usage(), 542 ) 543 parser.add_argument('--boot_img', required=True, 544 help='path to the boot, recovery or vendor_boot image') 545 parser.add_argument('--out', default='out', 546 help='output directory of the unpacked images') 547 parser.add_argument('--format', choices=['info', 'mkbootimg'], 548 default='info', 549 help='text output format (default: info)') 550 parser.add_argument('-0', '--null', action='store_true', 551 help='output null-terminated argument strings') 552 return parser.parse_args() 553 554 555def main(): 556 """parse arguments and unpack boot image""" 557 args = parse_cmdline() 558 info = unpack_bootimg(args.boot_img, args.out) 559 print_bootimg_info(info, args.format, args.null) 560 561 562if __name__ == '__main__': 563 main() 564