• 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, 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