[PATCH 01/13] binman: Android boot image support
Sam Day via B4 Relay
devnull+me.samcday.com at kernel.org
Sat Jun 6 02:52:35 CEST 2026
From: Sam Day <me at samcday.com>
Introduce initial support for Android boot images (abootimgs). This
initial stab supports both v0 and v2 headers. The AOSP implementation
was used as a reference.
Since we're targeting U-Boot use cases here, a couple of things were
omitted from this impl, namely "second" and recovery_dtbo support.
Link: https://android.googlesource.com/platform/system/tools/mkbootimg/
Signed-off-by: Sam Day <me at samcday.com>
---
tools/binman/etype/android_boot.py | 337 +++++++++++++++++++++++++++
tools/binman/ftest.py | 66 ++++++
tools/binman/test/vendor/android_boot_v0.dts | 29 +++
tools/binman/test/vendor/android_boot_v2.dts | 46 ++++
4 files changed, 478 insertions(+)
diff --git a/tools/binman/etype/android_boot.py b/tools/binman/etype/android_boot.py
new file mode 100644
index 00000000000..adf8248ee12
--- /dev/null
+++ b/tools/binman/etype/android_boot.py
@@ -0,0 +1,337 @@
+# SPDX-License-Identifier: GPL-2.0+
+# Entry-type module for Android boot images
+
+import hashlib
+import struct
+
+from binman.entry import Entry
+from binman.etype.section import Entry_section
+from dtoc import fdt_util
+
+
+BOOT_MAGIC = b'ANDROID!'
+BOOT_NAME_SIZE = 16
+BOOT_ARGS_SIZE = 512
+IMAGE_ID_SIZE = 32
+BOOT_EXTRA_ARGS_SIZE = 1024
+
+BOOT_IMAGE_HEADER_V0 = '<{}s10I{}s{}s{}s'.format(len(BOOT_MAGIC),
+ BOOT_NAME_SIZE,
+ BOOT_ARGS_SIZE,
+ IMAGE_ID_SIZE)
+BOOT_IMAGE_HEADER_V0_SIZE = struct.calcsize(BOOT_IMAGE_HEADER_V0)
+BOOT_IMAGE_HEADER_V2 = (BOOT_IMAGE_HEADER_V0 +
+ '{}sIQIIQ'.format(BOOT_EXTRA_ARGS_SIZE))
+BOOT_IMAGE_HEADER_V2_SIZE = struct.calcsize(BOOT_IMAGE_HEADER_V2)
+
+def _align_up(value, align):
+ return (value + align - 1) & ~(align - 1)
+
+
+def _pad(data, align):
+ return data + b'\0' * (_align_up(len(data), align) - len(data))
+
+
+class Entry_android_boot(Entry_section):
+ """Android boot image
+
+ This creates an Android v0 or v2 boot image.
+
+ A kernel payload, optional ramdisk payload can be supplied. A DTB payload
+ can also be provided when header_version == v2.
+
+ Properties / Entry arguments:
+ - header-version: Android boot image header version, must be 0 or 2,
+ defaults to 0
+ - page-size: Image page size, defaults to 2048
+ - base: Base address added to the offsets below, defaults to 0x10000000
+ - kernel-offset: Kernel load offset from base, defaults to 0x00008000
+ - ramdisk-offset: Ramdisk load offset from base, defaults to 0x01000000
+ - tags-offset: ATAGS/FDT offset from base, defaults to 0x00000100
+ - dtb-offset: DTB load offset from base, defaults to 0x01f00000
+ - os-version: Encoded Android OS version and patch level, defaults to 0
+ - boot-name: Android boot image board name
+ - cmdline: Android boot command line
+
+ This entry uses the following subnodes:
+ - kernel: section containing the executable payload
+ - dtb: section containing the DTB payload, used by header version 2 only
+ - ramdisk: optional section containing a ramdisk payload
+
+ Example::
+ A v2 abootimg with control FDT placed in the DTB section:
+
+ android-boot {
+ header-version = <2>;
+ page-size = <4096>;
+ base = <0x12345678>;
+ kernel-offset = <0xCAFED00D>;
+ ramdisk-offset = <0xBEEFBABE>;
+ tags-offset = <0xFEEDDEAD>;
+ dtb-offset = <0x06660666>;
+ cmdline = "foo bar";
+
+ kernel {
+ u-boot-nodtb {
+ # Many Android bootloaders support gzipped kernels
+ compress = "gzip";
+ };
+ };
+
+ dtb {
+ u-boot-dtb {
+ };
+ };
+ };
+
+ A v0 abootimg with embedded control FDT (v0 doesn't support DTBs) and
+ an empty ramdisk (some bootloaders insist on a ramdisk being present):
+ android-boot {
+ header-version = <0>;
+ page-size = <2048>;
+ base = <0x80200000>;
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+ };
+ """
+
+ def ReadNode(self):
+ super().ReadNode()
+ self.header_version = fdt_util.GetInt(self._node, 'header-version', 0)
+ self.page_size = fdt_util.GetInt(self._node, 'page-size', 2048)
+ self.base = self._GetIntCells('base', 0x10000000)
+ self.kernel_offset = self._GetIntCells('kernel-offset', 0x00008000)
+ self.ramdisk_offset = self._GetIntCells('ramdisk-offset', 0x01000000)
+ self.tags_offset = self._GetIntCells('tags-offset', 0x00000100)
+ self.dtb_offset = self._GetIntCells('dtb-offset', 0x01f00000)
+ self.os_version = fdt_util.GetInt(self._node, 'os-version', 0)
+ self.boot_name = fdt_util.GetString(self._node, 'boot-name', '')
+ self.cmdline = fdt_util.GetString(self._node, 'cmdline', '')
+
+ if self.header_version not in (0, 2):
+ self.Raise('Only Android boot image header versions 0 and 2 are '
+ 'supported')
+ if self.page_size <= 0 or self.page_size & (self.page_size - 1):
+ self.Raise('page-size must be a power of two')
+ if 'kernel' not in self._entries:
+ self.Raise("Missing required subnode 'kernel'")
+
+ if self.header_version == 0:
+ if self.page_size < BOOT_IMAGE_HEADER_V0_SIZE:
+ self.Raise('page-size must fit the Android boot image header')
+ if 'dtb' in self._entries:
+ self.Raise("Subnode 'dtb' requires header-version 2")
+ else:
+ # v2
+ if self.page_size < BOOT_IMAGE_HEADER_V2_SIZE:
+ self.Raise('page-size must fit the Android boot image header')
+ if 'dtb' not in self._entries:
+ self.Raise("Missing required subnode 'dtb'")
+
+ def ReadEntries(self):
+ for node in self._node.subnodes:
+ if self.IsSpecialSubnode(node):
+ continue
+ if node.name not in ('kernel', 'ramdisk', 'dtb'):
+ self.Raise("Unexpected subnode '%s'" % node.name)
+
+ entry = Entry.Create(self, node, etype='section',
+ expanded=self.GetImage().use_expanded,
+ missing_etype=self.GetImage().missing_etype)
+ entry.ReadNode()
+ entry.SetPrefix(self._name_prefix)
+ self._entries[node.name] = entry
+
+ def _GetIntCells(self, propname, default):
+ prop = self._node.props.get(propname)
+ if not prop:
+ return default
+
+ values = prop.value if isinstance(prop.value, list) else [prop.value]
+ if len(values) > 2:
+ self.Raise("Property '%s' must contain one or two cells" %
+ propname)
+
+ value = 0
+ for cell in values:
+ value = value << 32 | fdt_util.fdt32_to_cpu(cell)
+
+ return value
+
+ def _GetAddr(self, offset, name, size=32):
+ addr = self.base + offset
+ if addr >= 1 << size:
+ self.Raise('%s address %#x does not fit in %d bits' %
+ (name, addr, size))
+
+ return addr
+
+ @staticmethod
+ def _CheckFit(name, data, size):
+ if len(data) > size:
+ raise ValueError('%s is %d bytes, maximum is %d' %
+ (name, len(data), size))
+
+ return data + b'\0' * (size - len(data))
+
+ @staticmethod
+ def _BootId(*payloads):
+ digest = hashlib.sha1()
+ for data in payloads:
+ digest.update(data)
+ digest.update(struct.pack('<I', len(data)))
+
+ return digest.digest() + b'\0' * 12
+
+ def _SplitCmdline(self):
+ cmdline = self.cmdline.encode('ascii') + b'\0'
+ return (self._CheckFit('cmdline', cmdline[:BOOT_ARGS_SIZE],
+ BOOT_ARGS_SIZE),
+ self._CheckFit('extra-cmdline', cmdline[BOOT_ARGS_SIZE:],
+ BOOT_EXTRA_ARGS_SIZE))
+
+ def _GetEntryData(self, name, required):
+ entry = self._entries.get(name)
+ data = entry.GetData(required)
+ if data is None and not required:
+ return None
+
+ return data
+
+ def _GetOptionalEntryData(self, name, required, default=b''):
+ entry = self._entries.get(name)
+ if not entry:
+ return default
+
+ data = entry.GetData(required)
+ if data is None and not required:
+ return None
+
+ return data
+
+ @staticmethod
+ def _BuildDtb(node):
+ import libfdt
+
+ fsw = libfdt.FdtSw()
+ fsw.INC_SIZE = 65536
+ fsw.finish_reservemap()
+
+ def _AddNode(in_node):
+ for pname, prop in in_node.props.items():
+ fsw.property(pname, prop.bytes)
+ for subnode in in_node.subnodes:
+ with fsw.add_node(subnode.name):
+ _AddNode(subnode)
+
+ with fsw.add_node(''):
+ _AddNode(node)
+ if not node.FindNode('chosen'):
+ with fsw.add_node('chosen'):
+ pass
+ fdt = fsw.as_fdt()
+ fdt.pack()
+ return bytes(fdt.as_bytearray())
+
+ def _BuildV0SectionData(self, required):
+ kernel = self._GetEntryData('kernel', required)
+ if kernel is None:
+ return None
+
+ ramdisk = self._GetOptionalEntryData('ramdisk', required)
+ if ramdisk is None:
+ return None
+
+ boot_name = self._CheckFit('boot-name', self.boot_name.encode('ascii'),
+ BOOT_NAME_SIZE)
+ cmdline = self._CheckFit('cmdline', self.cmdline.encode('ascii'),
+ BOOT_ARGS_SIZE)
+
+ boot_id_payloads = [kernel, ramdisk, b'']
+ image_id = self._BootId(*boot_id_payloads)
+
+ header = struct.pack(BOOT_IMAGE_HEADER_V0,
+ BOOT_MAGIC,
+ len(kernel),
+ self._GetAddr(self.kernel_offset, 'kernel'),
+ len(ramdisk),
+ self._GetAddr(self.ramdisk_offset, 'ramdisk'),
+ 0, # second_len
+ 0, # second_offset
+ self._GetAddr(self.tags_offset, 'tags'),
+ self.page_size,
+ self.header_version,
+ self.os_version,
+ boot_name,
+ cmdline,
+ image_id)
+
+ image = bytearray()
+ image += _pad(header, self.page_size)
+ image += _pad(kernel, self.page_size)
+ image += _pad(ramdisk, self.page_size)
+
+ return bytes(image)
+
+ def _BuildV2SectionData(self, required):
+ kernel = self._GetEntryData('kernel', required)
+ if kernel is None:
+ return None
+
+ dtb = self._GetEntryData('dtb', required)
+ if dtb is None:
+ return None
+
+ ramdisk = self._GetOptionalEntryData('ramdisk', required)
+ if ramdisk is None:
+ return None
+ boot_name = self._CheckFit('boot-name', self.boot_name.encode('ascii'),
+ BOOT_NAME_SIZE)
+ cmdline, extra_cmdline = self._SplitCmdline()
+ image_id = self._BootId(kernel, ramdisk, b'', b'', dtb)
+
+ header = struct.pack(BOOT_IMAGE_HEADER_V2,
+ BOOT_MAGIC,
+ len(kernel),
+ self._GetAddr(self.kernel_offset, 'kernel'),
+ len(ramdisk),
+ self._GetAddr(self.ramdisk_offset, 'ramdisk'),
+ 0, # second_len
+ 0, # second_offset
+ self._GetAddr(self.tags_offset, 'tags'),
+ self.page_size,
+ self.header_version,
+ self.os_version,
+ boot_name,
+ cmdline,
+ image_id,
+ extra_cmdline,
+ 0, # recovery_dtbo_len
+ 0, # recovery_dtbo_offset
+ BOOT_IMAGE_HEADER_V2_SIZE,
+ len(dtb),
+ self._GetAddr(self.dtb_offset, 'dtb', size=64))
+
+ image = bytearray()
+ image += _pad(header, self.page_size)
+ image += _pad(kernel, self.page_size)
+ image += _pad(ramdisk, self.page_size)
+ image += _pad(dtb, self.page_size)
+
+ return bytes(image)
+
+ def BuildSectionData(self, required):
+ if self.header_version == 0:
+ return self._BuildV0SectionData(required)
+
+ return self._BuildV2SectionData(required)
diff --git a/tools/binman/ftest.py b/tools/binman/ftest.py
index 9a3811c1732..e92a231417b 100644
--- a/tools/binman/ftest.py
+++ b/tools/binman/ftest.py
@@ -5598,6 +5598,72 @@ fdt fdtmap Extract the devicetree blob from the fdtmap
self.assertIn("Node '/binman/renesas-rcar4-sa0': SRAM data longer than 966656 Bytes",
str(exc.exception))
+ @staticmethod
+ def _AndroidBootId(*payloads):
+ digest = hashlib.sha1()
+ for data in payloads:
+ digest.update(data)
+ digest.update(struct.pack('<I', len(data)))
+
+ return digest.digest() + b'\0' * 12
+
+ def testAndroidBootV0(self):
+ """Test that binman can produce a plain legacy Android boot image"""
+ data = self._DoReadFile('vendor/android_boot_v0.dts')
+ header = struct.unpack_from('<8s10I16s512s32s', data, 0)
+
+ self.assertEqual(b'ANDROID!', header[0])
+ self.assertEqual(len(U_BOOT_DATA), header[1])
+ self.assertEqual(0x80208000, header[2])
+ self.assertEqual(1, header[3])
+ self.assertEqual(0x81200000, header[4])
+ self.assertEqual(0, header[5])
+ self.assertEqual(0, header[6])
+ self.assertEqual(0x80200100, header[7])
+ self.assertEqual(0x800, header[8])
+ self.assertEqual(0, header[9])
+ self.assertEqual(0, header[10])
+ self.assertEqual(b'foo', header[12].split(b'\0', 1)[0])
+ self.assertEqual(self._AndroidBootId(U_BOOT_DATA, b'\0', b''),
+ header[13])
+
+ def testAndroidBootV2(self):
+ """Test that binman can produce an Android boot image"""
+ data = self._DoReadFile('vendor/android_boot_v2.dts')
+ header = struct.unpack_from('<8s10I16s512s32s1024sIQIIQ', data, 0)
+
+ self.assertEqual(b'ANDROID!', header[0])
+ self.assertEqual(len(U_BOOT_DATA), header[1])
+ self.assertEqual(0x80008000, header[2])
+ self.assertEqual(0, header[3])
+ self.assertEqual(0x81000000, header[4])
+ self.assertEqual(0, header[5])
+ self.assertEqual(0, header[6])
+ self.assertEqual(0x80000100, header[7])
+ self.assertEqual(0x800, header[8])
+ self.assertEqual(2, header[9])
+ self.assertEqual(0, header[10])
+ self.assertEqual(b'test-board', header[11].split(b'\0', 1)[0])
+ self.assertEqual(0, header[15])
+ self.assertEqual(0, header[16])
+ self.assertEqual(1660, header[17])
+ self.assertEqual(len(U_BOOT_DTB_DATA), header[18])
+ self.assertEqual(0x81f00000, header[19])
+ self.assertEqual(self._AndroidBootId(U_BOOT_DATA, b'', b'', b'',
+ U_BOOT_DTB_DATA), header[13])
+
+ cmdline = header[12].split(b'\0', 1)[0]
+ extra_cmdline = header[14].split(b'\0', 1)[0]
+ self.assertEqual(b"tests.. ", cmdline[-8:])
+ self.assertEqual(512, len(cmdline))
+ self.assertEqual(b'sup', extra_cmdline)
+
+ self.assertEqual(U_BOOT_DATA, data[0x800:0x800 + len(U_BOOT_DATA)])
+ self.assertEqual(U_BOOT_DTB_DATA,
+ data[0x1000:0x1000 + len(U_BOOT_DTB_DATA)])
+
+ self.assertEqual(U_BOOT_DATA, data[0x800:0x800 + len(U_BOOT_DATA)])
+
def testFitFdtOper(self):
"""Check handling of a specified FIT operation"""
entry_args = {
diff --git a/tools/binman/test/vendor/android_boot_v0.dts b/tools/binman/test/vendor/android_boot_v0.dts
new file mode 100644
index 00000000000..ec97741e754
--- /dev/null
+++ b/tools/binman/test/vendor/android_boot_v0.dts
@@ -0,0 +1,29 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ android-boot {
+ header-version = <0>;
+ page-size = <0x800>;
+ base = <0x80200000>;
+ cmdline = "foo";
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ ramdisk {
+ fill {
+ size = <1>;
+ };
+ };
+ };
+ };
+};
diff --git a/tools/binman/test/vendor/android_boot_v2.dts b/tools/binman/test/vendor/android_boot_v2.dts
new file mode 100644
index 00000000000..20de6e2f1af
--- /dev/null
+++ b/tools/binman/test/vendor/android_boot_v2.dts
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-2.0+
+
+/dts-v1/;
+
+#define CMDLINE(...) #__VA_ARGS__
+
+/ {
+ #address-cells = <1>;
+ #size-cells = <1>;
+
+ binman {
+ android-boot {
+ header-version = <2>;
+ page-size = <0x800>;
+ base = <0x80000000>;
+ kernel-offset = <0x00008000>;
+ ramdisk-offset = <0x01000000>;
+ second-offset = <0x00f00000>;
+ tags-offset = <0x00000100>;
+ dtb-offset = <0x01f00000>;
+ boot-name = "test-board";
+ cmdline = CMDLINE(
+ This is a very long commandline that is sure to exceed the
+ 512 chars that is allotted to the cmdline and this should
+ spillover into extra_cmdline which is useful from a
+ function testing standpoint. Gosh, it sure it hard to come
+ up with enough filler text here to get over the 512 char
+ limit though, huh? Even for someone as loquacious as
+ myself. So anyway. How's your day going? I wrote a binman
+ functional test today. It was fun. Did you know that
+ binman is great. I like binman. I also like functional
+ tests.. sup);
+
+ kernel {
+ u-boot {
+ no-expanded;
+ };
+ };
+
+ dtb {
+ u-boot-dtb {
+ };
+ };
+ };
+ };
+};
--
2.54.0
More information about the U-Boot
mailing list