• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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