blob: 0a508a9b6e10d02516a73bcee153229b437bf407 [file] [log] [blame]
#!/usr/bin/env python3
#
# Copyright 2018, The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Unpacks the boot image.
Extracts the kernel, ramdisk, second bootloader, dtb and recovery dtbo images.
"""
from argparse import ArgumentParser, FileType, RawDescriptionHelpFormatter
from struct import unpack
import json
import os
import shlex
BOOT_IMAGE_HEADER_V3_PAGESIZE = 4096
VENDOR_RAMDISK_NAME_SIZE = 32
VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE = 16
MKBOOTIMG_ARGS_FILE = 'mkbootimg_args.json'
def create_out_dir(dir_path):
"""creates a directory 'dir_path' if it does not exist"""
if not os.path.exists(dir_path):
os.makedirs(dir_path)
def extract_image(offset, size, bootimage, extracted_image_name):
"""extracts an image from the bootimage"""
bootimage.seek(offset)
with open(extracted_image_name, 'wb') as file_out:
file_out.write(bootimage.read(size))
def get_number_of_pages(image_size, page_size):
"""calculates the number of pages required for the image"""
return (image_size + page_size - 1) // page_size
def cstr(s):
"""Remove first NULL character and any character beyond."""
return s.split('\0', 1)[0]
def format_os_version(os_version):
a = os_version >> 14
b = os_version >> 7 & ((1<<7) - 1)
c = os_version & ((1<<7) - 1)
return '{}.{}.{}'.format(a, b, c)
def format_os_patch_level(os_patch_level):
y = os_patch_level >> 4
y += 2000
m = os_patch_level & ((1<<4) - 1)
return '{:04d}-{:02d}'.format(y, m)
def decode_os_version_patch_level(os_version_patch_level):
"""Returns a tuple of (os_version, os_patch_level)."""
os_version = os_version_patch_level >> 11
os_patch_level = os_version_patch_level & ((1<<11) - 1)
return (format_os_version(os_version),
format_os_patch_level(os_patch_level))
class BootImageInfoFormatter:
"""Formats the boot image info."""
def format_pretty_text(self):
lines = []
lines.append(f'boot magic: {self.boot_magic}')
if self.header_version < 3:
lines.append(f'kernel_size: {self.kernel_size}')
lines.append(
f'kernel load address: {self.kernel_load_address:#010x}')
lines.append(f'ramdisk size: {self.ramdisk_size}')
lines.append(
f'ramdisk load address: {self.ramdisk_load_address:#010x}')
lines.append(f'second bootloader size: {self.second_size}')
lines.append(
f'second bootloader load address: '
f'{self.second_load_address:#010x}')
lines.append(
f'kernel tags load address: {self.tags_load_address:#010x}')
lines.append(f'page size: {self.page_size}')
else:
lines.append(f'kernel_size: {self.kernel_size}')
lines.append(f'ramdisk size: {self.ramdisk_size}')
lines.append(f'os version: {self.os_version}')
lines.append(f'os patch level: {self.os_patch_level}')
lines.append(f'boot image header version: {self.header_version}')
if self.header_version < 3:
lines.append(f'product name: {self.product_name}')
lines.append(f'command line args: {self.cmdline}')
if self.header_version < 3:
lines.append(f'additional command line args: {self.extra_cmdline}')
if self.header_version in {1, 2}:
lines.append(f'recovery dtbo size: {self.recovery_dtbo_size}')
lines.append(
f'recovery dtbo offset: {self.recovery_dtbo_offset:#018x}')
lines.append(f'boot header size: {self.boot_header_size}')
if self.header_version == 2:
lines.append(f'dtb size: {self.dtb_size}')
lines.append(f'dtb address: {self.dtb_load_address:#018x}')
if self.header_version >= 4:
lines.append(
f'boot.img signature size: {self.boot_signature_size}')
return '\n'.join(lines)
def _format_json_dict_boot_image_v2_and_below(self):
"""Returns a dict of mkbootimg.py arguments for v0-v2 boot.img."""
args_dict = {}
args_dict['header_version'] = str(self.header_version)
# The type of pagesize is uint32_t, using '0xFFFFFFFF' as the
# output format.
args_dict['pagesize'] = f'{self.page_size:#010x}'
# Kernel load address is base + kernel_offset in mkbootimg.py.
# However, we don't know the value of 'base' when unpack a boot.img
# in this script. So always set 'base' to be zero and 'kernel_offset'
# to be the kernel load address. Same for 'ramdisk_offset',
# 'second_offset', etc.
# The following types are uint32_t, using '0xFFFFFFFF' as the output
# format.
args_dict['base'] = f'{0:#010x}'
args_dict['kernel_offset'] = f'{self.kernel_load_address:#010x}'
args_dict['ramdisk_offset'] = f'{self.ramdisk_load_address:#010x}'
args_dict['second_offset'] = f'{self.second_load_address:#010x}'
args_dict['tags_offset'] = f'{self.tags_load_address:#010x}'
# dtb is added in boot image v2, and is absent in v1 or v0.
if self.header_version == 2:
# The type of dtb_offset is uint64_t, using '0xFFFFFFFFEEEEEEEE' as
# the output format.
args_dict['dtb_offset'] = f'{self.dtb_load_address:#018x}'
args_dict['os_version'] = self.os_version
args_dict['os_patch_level'] = self.os_patch_level
args_dict['cmdline'] = self.cmdline + self.extra_cmdline
args_dict['board'] = self.product_name
return args_dict
def _format_json_dict_boot_image_v3_and_above(self):
"""Returns a dict of mkbootimg.py arguments for >= v3 boot.img."""
args_dict = {}
args_dict['header_version'] = str(self.header_version)
args_dict['os_version'] = self.os_version
args_dict['os_patch_level'] = self.os_patch_level
args_dict['cmdline'] = self.cmdline
return args_dict
def format_json_dict(self):
"""Returns a dict of arguments to be used in mkbootimg.py later."""
if self.header_version <= 2:
return self._format_json_dict_boot_image_v2_and_below()
else:
return self._format_json_dict_boot_image_v3_and_above()
def unpack_boot_image(args):
"""extracts kernel, ramdisk, second bootloader and recovery dtbo"""
info = BootImageInfoFormatter()
info.boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
kernel_ramdisk_second_info = unpack('9I', args.boot_img.read(9 * 4))
# header_version is always at [8] regardless of the value of header_version.
info.header_version = kernel_ramdisk_second_info[8]
if info.header_version < 3:
info.kernel_size = kernel_ramdisk_second_info[0]
info.kernel_load_address = kernel_ramdisk_second_info[1]
info.ramdisk_size = kernel_ramdisk_second_info[2]
info.ramdisk_load_address = kernel_ramdisk_second_info[3]
info.second_size = kernel_ramdisk_second_info[4]
info.second_load_address = kernel_ramdisk_second_info[5]
info.tags_load_address = kernel_ramdisk_second_info[6]
info.page_size = kernel_ramdisk_second_info[7]
os_version_patch_level = unpack('I', args.boot_img.read(1 * 4))[0]
else:
info.kernel_size = kernel_ramdisk_second_info[0]
info.ramdisk_size = kernel_ramdisk_second_info[1]
os_version_patch_level = kernel_ramdisk_second_info[2]
info.second_size = 0
info.page_size = BOOT_IMAGE_HEADER_V3_PAGESIZE
info.os_version, info.os_patch_level = decode_os_version_patch_level(
os_version_patch_level)
if info.header_version < 3:
info.product_name = cstr(unpack('16s',
args.boot_img.read(16))[0].decode())
info.cmdline = cstr(unpack('512s', args.boot_img.read(512))[0].decode())
args.boot_img.read(32) # ignore SHA
info.extra_cmdline = cstr(unpack('1024s',
args.boot_img.read(1024))[0].decode())
else:
info.cmdline = cstr(unpack('1536s',
args.boot_img.read(1536))[0].decode())
if info.header_version in {1, 2}:
info.recovery_dtbo_size = unpack('I', args.boot_img.read(1 * 4))[0]
info.recovery_dtbo_offset = unpack('Q', args.boot_img.read(8))[0]
info.boot_header_size = unpack('I', args.boot_img.read(4))[0]
else:
info.recovery_dtbo_size = 0
if info.header_version == 2:
info.dtb_size = unpack('I', args.boot_img.read(4))[0]
info.dtb_load_address = unpack('Q', args.boot_img.read(8))[0]
else:
info.dtb_size = 0
info.dtb_load_address = 0
if info.header_version >= 4:
info.boot_signature_size = unpack('I', args.boot_img.read(4))[0]
else:
info.boot_signature_size = 0
# The first page contains the boot header
num_header_pages = 1
# Convenient shorthand.
page_size = info.page_size
num_kernel_pages = get_number_of_pages(info.kernel_size, page_size)
kernel_offset = page_size * num_header_pages # header occupies a page
image_info_list = [(kernel_offset, info.kernel_size, 'kernel')]
num_ramdisk_pages = get_number_of_pages(info.ramdisk_size, page_size)
ramdisk_offset = page_size * (num_header_pages + num_kernel_pages
) # header + kernel
image_info_list.append((ramdisk_offset, info.ramdisk_size, 'ramdisk'))
if info.second_size > 0:
second_offset = page_size * (
num_header_pages + num_kernel_pages + num_ramdisk_pages
) # header + kernel + ramdisk
image_info_list.append((second_offset, info.second_size, 'second'))
if info.recovery_dtbo_size > 0:
image_info_list.append((info.recovery_dtbo_offset,
info.recovery_dtbo_size,
'recovery_dtbo'))
if info.dtb_size > 0:
num_second_pages = get_number_of_pages(info.second_size, page_size)
num_recovery_dtbo_pages = get_number_of_pages(
info.recovery_dtbo_size, page_size)
dtb_offset = page_size * (
num_header_pages + num_kernel_pages + num_ramdisk_pages +
num_second_pages + num_recovery_dtbo_pages)
image_info_list.append((dtb_offset, info.dtb_size, 'dtb'))
if info.boot_signature_size > 0:
# boot signature only exists in boot.img version >= v4.
# There are only kernel and ramdisk pages before the signature.
boot_signature_offset = page_size * (
num_header_pages + num_kernel_pages + num_ramdisk_pages)
image_info_list.append((boot_signature_offset, info.boot_signature_size,
'boot_signature'))
for image_info in image_info_list:
extract_image(image_info[0], image_info[1], args.boot_img,
os.path.join(args.out, image_info[2]))
# Saves the arguments to be reused in mkbootimg.py later.
mkbootimg_args = info.format_json_dict()
with open(os.path.join(args.out, MKBOOTIMG_ARGS_FILE), 'w') as f:
json.dump(mkbootimg_args, f, sort_keys=True, indent=4)
# TODO(yochiang): Support --format=mkbootimg
print(info.format_pretty_text())
class VendorBootImageInfoFormatter:
"""Formats the vendor_boot image info."""
def format_pretty_text(self):
lines = []
lines.append(f'boot magic: {self.boot_magic}')
lines.append(f'vendor boot image header version: {self.header_version}')
lines.append(f'page size: {self.page_size:#010x}')
lines.append(f'kernel load address: {self.kernel_load_address:#010x}')
lines.append(f'ramdisk load address: {self.ramdisk_load_address:#010x}')
if self.header_version > 3:
lines.append(
f'vendor ramdisk total size: {self.vendor_ramdisk_size}')
else:
lines.append(f'vendor ramdisk size: {self.vendor_ramdisk_size}')
lines.append(f'vendor command line args: {self.cmdline}')
lines.append(
f'kernel tags load address: {self.tags_load_address:#010x}')
lines.append(f'product name: {self.product_name}')
lines.append(f'vendor boot image header size: {self.header_size}')
lines.append(f'dtb size: {self.dtb_size}')
lines.append(f'dtb address: {self.dtb_load_address:#018x}')
if self.header_version > 3:
lines.append(
f'vendor ramdisk table size: {self.vendor_ramdisk_table_size}')
lines.append('vendor ramdisk table: [')
indent = lambda level: ' ' * 4 * level
for entry in self.vendor_ramdisk_table:
(output_ramdisk_name, ramdisk_size, ramdisk_offset,
ramdisk_type, ramdisk_name, board_id) = entry
lines.append(indent(1) + f'{output_ramdisk_name}: ''{')
lines.append(indent(2) + f'size: {ramdisk_size}')
lines.append(indent(2) + f'offset: {ramdisk_offset}')
lines.append(indent(2) + f'type: {ramdisk_type:#x}')
lines.append(indent(2) + f'name: {ramdisk_name}')
lines.append(indent(2) + 'board_id: [')
stride = 4
for row_idx in range(0, len(board_id), stride):
row = board_id[row_idx:row_idx + stride]
lines.append(
indent(3) + ' '.join(f'{e:#010x},' for e in row))
lines.append(indent(2) + ']')
lines.append(indent(1) + '}')
lines.append(']')
lines.append(
f'vendor bootconfig size: {self.vendor_bootconfig_size}')
return '\n'.join(lines)
def format_mkbootimg_argument(self, null=False):
args = []
args.extend(['--header_version', str(self.header_version)])
args.extend(['--pagesize', f'{self.page_size:#010x}'])
args.extend(['--base', f'{0:#010x}'])
args.extend(['--kernel_offset', f'{self.kernel_load_address:#010x}'])
args.extend(['--ramdisk_offset', f'{self.ramdisk_load_address:#010x}'])
args.extend(['--tags_offset', f'{self.tags_load_address:#010x}'])
args.extend(['--dtb_offset', f'{self.dtb_load_address:#018x}'])
args.extend(['--vendor_cmdline', self.cmdline])
args.extend(['--board', self.product_name])
dtb_path = os.path.join(self.image_dir, 'dtb')
args.extend(['--dtb', dtb_path])
if self.header_version > 3:
bootconfig_path = os.path.join(self.image_dir, 'bootconfig')
args.extend(['--vendor_bootconfig', bootconfig_path])
for entry in self.vendor_ramdisk_table:
(output_ramdisk_name, _, _, ramdisk_type,
ramdisk_name, board_id) = entry
args.extend(['--ramdisk_type', str(ramdisk_type)])
args.extend(['--ramdisk_name', ramdisk_name])
for idx, e in enumerate(board_id):
if e:
args.extend([f'--board_id{idx}', f'{e:#010x}'])
vendor_ramdisk_path = os.path.join(
self.image_dir, output_ramdisk_name)
args.extend(['--vendor_ramdisk_fragment', vendor_ramdisk_path])
else:
vendor_ramdisk_path = os.path.join(self.image_dir, 'vendor_ramdisk')
args.extend(['--vendor_ramdisk', vendor_ramdisk_path])
if null:
return '\0'.join(args) + '\0'
return shlex.join(args)
def format_json_dict(self):
"""Returns a dict of arguments to be used in mkbootimg.py later."""
args_dict = {}
args_dict['header_version'] = str(self.header_version)
# Format uint32_t as '0xFFFFFFFF', uint64_t as '0xFFFFFFFFEEEEEEEE'.
args_dict['pagesize'] = f'{self.page_size:#010x}'
# Kernel load address is base + kernel_offset in mkbootimg.py.
# However, we don't know the value of 'base' when unpacking a
# vendor_boot.img in this script. So always set 'base' to be zero and
# 'kernel_offset' to be the kernel load address. Same for
# 'ramdisk_offset', 'tags_offset' and 'dtb_offset'.
args_dict['base'] = f'{0:#010x}'
args_dict['kernel_offset'] = f'{self.kernel_load_address:#010x}'
args_dict['ramdisk_offset'] = f'{self.ramdisk_load_address:#010x}'
args_dict['tags_offset'] = f'{self.tags_load_address:#010x}'
# The type of dtb_offset is uint64_t.
args_dict['dtb_offset'] = f'{self.dtb_load_address:#018x}'
args_dict['vendor_cmdline'] = self.cmdline
args_dict['board'] = self.product_name
# TODO(bowgotsai): support for multiple vendor ramdisk (vendor boot v4).
return args_dict
def unpack_vendor_boot_image(args):
info = VendorBootImageInfoFormatter()
info.boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
info.header_version = unpack('I', args.boot_img.read(4))[0]
info.page_size = unpack('I', args.boot_img.read(4))[0]
info.kernel_load_address = unpack('I', args.boot_img.read(4))[0]
info.ramdisk_load_address = unpack('I', args.boot_img.read(4))[0]
info.vendor_ramdisk_size = unpack('I', args.boot_img.read(4))[0]
info.cmdline = cstr(unpack('2048s', args.boot_img.read(2048))[0].decode())
info.tags_load_address = unpack('I', args.boot_img.read(4))[0]
info.product_name = cstr(unpack('16s', args.boot_img.read(16))[0].decode())
info.header_size = unpack('I', args.boot_img.read(4))[0]
info.dtb_size = unpack('I', args.boot_img.read(4))[0]
info.dtb_load_address = unpack('Q', args.boot_img.read(8))[0]
# Convenient shorthand.
page_size = info.page_size
# The first pages contain the boot header
num_boot_header_pages = get_number_of_pages(info.header_size, page_size)
num_boot_ramdisk_pages = get_number_of_pages(
info.vendor_ramdisk_size, page_size)
num_boot_dtb_pages = get_number_of_pages(info.dtb_size, page_size)
ramdisk_offset_base = page_size * num_boot_header_pages
image_info_list = []
if info.header_version > 3:
info.vendor_ramdisk_table_size = unpack('I', args.boot_img.read(4))[0]
vendor_ramdisk_table_entry_num = unpack('I', args.boot_img.read(4))[0]
vendor_ramdisk_table_entry_size = unpack('I', args.boot_img.read(4))[0]
info.vendor_bootconfig_size = unpack('I', args.boot_img.read(4))[0]
num_vendor_ramdisk_table_pages = get_number_of_pages(
info.vendor_ramdisk_table_size, page_size)
vendor_ramdisk_table_offset = page_size * (
num_boot_header_pages + num_boot_ramdisk_pages + num_boot_dtb_pages)
vendor_ramdisk_table = []
vendor_ramdisk_symlinks = []
for idx in range(vendor_ramdisk_table_entry_num):
entry_offset = vendor_ramdisk_table_offset + (
vendor_ramdisk_table_entry_size * idx)
args.boot_img.seek(entry_offset)
ramdisk_size = unpack('I', args.boot_img.read(4))[0]
ramdisk_offset = unpack('I', args.boot_img.read(4))[0]
ramdisk_type = unpack('I', args.boot_img.read(4))[0]
ramdisk_name = cstr(unpack(
f'{VENDOR_RAMDISK_NAME_SIZE}s',
args.boot_img.read(VENDOR_RAMDISK_NAME_SIZE))[0].decode())
board_id = unpack(
f'{VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE}I',
args.boot_img.read(
4 * VENDOR_RAMDISK_TABLE_ENTRY_BOARD_ID_SIZE))
output_ramdisk_name = f'vendor_ramdisk{idx:02}'
image_info_list.append((ramdisk_offset_base + ramdisk_offset,
ramdisk_size, output_ramdisk_name))
vendor_ramdisk_symlinks.append((output_ramdisk_name, ramdisk_name))
vendor_ramdisk_table.append(
(output_ramdisk_name, ramdisk_size, ramdisk_offset,
ramdisk_type, ramdisk_name, board_id))
info.vendor_ramdisk_table = vendor_ramdisk_table
bootconfig_offset = page_size * (num_boot_header_pages
+ num_boot_ramdisk_pages + num_boot_dtb_pages
+ num_vendor_ramdisk_table_pages)
image_info_list.append((bootconfig_offset, info.vendor_bootconfig_size,
'bootconfig'))
else:
image_info_list.append(
(ramdisk_offset_base, info.vendor_ramdisk_size, 'vendor_ramdisk'))
dtb_offset = page_size * (num_boot_header_pages + num_boot_ramdisk_pages
) # header + vendor_ramdisk
image_info_list.append((dtb_offset, info.dtb_size, 'dtb'))
for image_info in image_info_list:
extract_image(image_info[0], image_info[1], args.boot_img,
os.path.join(args.out, image_info[2]))
if info.header_version > 3:
vendor_ramdisk_by_name_dir = os.path.join(
args.out, 'vendor-ramdisk-by-name')
create_out_dir(vendor_ramdisk_by_name_dir)
for src, dst in vendor_ramdisk_symlinks:
src_pathname = os.path.join('..', src)
dst_pathname = os.path.join(
vendor_ramdisk_by_name_dir, f'ramdisk_{dst}')
if os.path.lexists(dst_pathname):
os.remove(dst_pathname)
os.symlink(src_pathname, dst_pathname)
info.image_dir = args.out
# Saves the arguments to be reused in mkbootimg.py later.
mkbootimg_args = info.format_json_dict()
with open(os.path.join(args.out, MKBOOTIMG_ARGS_FILE), 'w') as f:
json.dump(mkbootimg_args, f, sort_keys=True, indent=4)
if args.format == 'mkbootimg':
print(info.format_mkbootimg_argument(null=args.null),
end='' if args.null else None)
else:
print(info.format_pretty_text())
def unpack_image(args):
boot_magic = unpack('8s', args.boot_img.read(8))[0].decode()
args.boot_img.seek(0)
if boot_magic == 'ANDROID!':
unpack_boot_image(args)
elif boot_magic == 'VNDRBOOT':
unpack_vendor_boot_image(args)
def get_unpack_usage():
return """Output format:
* info
Pretty-printed info-rich text format suitable for human inspection.
* mkbootimg
Output shell-escaped (quoted) argument strings that can be used to
reconstruct the boot image. For example:
$ unpack_bootimg --boot_img vendor_boot.img --out out --format=mkbootimg |
tee mkbootimg_args
$ sh -c "mkbootimg $(cat mkbootimg_args) --vendor_boot repacked.img"
vendor_boot.img and repacked.img would be equivalent.
If the -0 option is specified, output unescaped null-terminated argument
strings that are suitable to be parsed by a shell script (xargs -0 format):
$ unpack_bootimg --boot_img vendor_boot.img --out out --format=mkbootimg \\
-0 | tee mkbootimg_args
$ declare -a MKBOOTIMG_ARGS=()
$ while IFS= read -r -d '' ARG; do
MKBOOTIMG_ARGS+=("${ARG}")
done <mkbootimg_args
$ mkbootimg "${MKBOOTIMG_ARGS[@]}" --vendor_boot repacked.img
"""
def parse_cmdline():
"""parse command line arguments"""
parser = ArgumentParser(
formatter_class=RawDescriptionHelpFormatter,
description='Unpacks boot, recovery or vendor_boot image.',
epilog=get_unpack_usage(),
)
parser.add_argument('--boot_img', type=FileType('rb'), required=True,
help='path to the boot, recovery or vendor_boot image')
parser.add_argument('--out', default='out',
help='output directory of the unpacked images')
parser.add_argument('--format', choices=['info', 'mkbootimg'],
default='info',
help='text output format (default: info)')
parser.add_argument('-0', '--null', action='store_true',
help='output null-terminated argument strings')
return parser.parse_args()
def main():
"""parse arguments and unpack boot image"""
args = parse_cmdline()
create_out_dir(args.out)
unpack_image(args)
if __name__ == '__main__':
main()