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