Android Boot and Recovery Images
To fully grasp the role the Android boot and recovery images play, we first need to understand the general boot-up process of an Android phone. While embedded software is no longer part of my day-to-day work at TextNow, I used to wear the embedded-engineer hat in one of my previous jobs and I dabble with embedded electronics in my spare time, so I am intimately familiar with modern microprocessor boot-up routines. I must caution you that this is a very technical post, but one that is hopefully useful to somebody looking for a reference material on how this all fits together, with links to where to delve more in-depth if required. This type of post would have been very useful when I was learning it :)
The Android Boot-Up Process
When power and reset is applied to the processor, it wakes up and needs to figure out what to do. In modern processors, there is usually a very small program baked-in to the processor whose function is to verify the next component’s integrity. This first component is sometimes referred to as the secure boot ROM, and its job is to ensure the next component it loads is trusted, verified software. This prevents unwanted tampering of the system software and, as such, increases the security of the device. The verification usually involves some sort of cryptographic cipher using a public/private key scheme, with the private key usually kept a closely guarded secret by the manufacturer. Note: usually the public key is programmed in at assembly time by the phone manufacturer and its contents can never be changed, thereby requiring that processor to always run a signed bootloader, with the private counterpart of the public key burned in at assembly time.
The next component loaded by the secure boot ROM is the what people commonly refer to as the bootloader. The bootloader, as I mentioned, is usually signed with the phone manufacturer’s private key, and it has a few jobs:
1. Load a boot image from non-volatile memory (usually NAND flash nowadays) into volatile memory (RAM) and run it.
2. Optionally, that image needs to be cryptographically verified before being loaded (as is the case with more and more phones these days which don’t even provide a way to unlock the bootloader to run unsigned images).
3. Provide the ability to flash new boot and other images to the non-volatile memory for future upgrades and recovery from failure.
4. Display some sort of information on the screen, and optionally provide an interface for the user to interact with. This is commonly known as fastboot mode on some Motorola, Google, and other manufacturer’s devices.
Android Boot Images
We now come to the final component of the boot-up process — loading a boot or recovery image into RAM and running it.
Technically speaking, there are no structural differences between boot and recovery images. The only difference is which kernel and which initial ramdisk is loaded, but they are packaged the same. We will come back to how these are packaged to make a neat and tidy boot image.There are a few sub-steps involved in running the boot image:
1. Cryptographic verification
2. Loading of the (Linux) kernel at the required memory address
3. Loading of the initial ramdisk at the required memory address
4. Jump to the start address of the kernel
The cryptographic verification is usually manufacturer-specific and it is typically very hard to reverse-engineer this method, though not impossible — it’s been done before for certain phones (for example, older Kit-Kat era LG phones).
The kernel is the heart of the operating system, as it ticks its constant heartbeat of interrupts and events which keep a modern operating system running. This magic has very meager origin as a simple chunk of bytes that was compiled in such a way to wake up and start running from a particular address of memory. This address varies depending on the particular phone and memory layout. Once execution starts, the kernel will start loading drivers and setup more advanced facilities so that it can start running some of its own programs.
In and of itself, the kernel wouldn’t know what to do once it’s loaded. It would sit there and do nothing. But what the Linux kernel expects is an initial ramdisk at a predefined memory address. This ramdisk contains a very rudimentary and usually read-only filesystem which contains the most basic root filesystem possible, and a file under /sbin/init which the kernel executes as the last thing it does as part of initialization. This init program starts running some scripts that start other programs talking to each other and finally deliver the full experience the system has to offer.
So now we come back to the difference between a boot and recovery image. While the kernel sometimes is the same as the boot image, the recovery image ramdisk has a distinct sets of init scripts which don’t start the Java VM, but instead start a small C program called (did you guess it?) recovery. Recovery displays a limited user interface that allows the user to start the system, factory reset, clear the cache partition, etc. Some custom recoveries have their own program that offers even more functionality.
Packing an Android Boot Image
The Android boot images have a specific structure whose internal organization reflects the memory in which it resides, namely flash. Flash is a specific type of memory that is not random-access, but is read in pages whose sizes vary depending on the particular chip, often within the 4K bytes range. For this reason, the boot image’s components are aligned to the page size, so that loading is more efficient (amongst other reasons I won’t go into detail here). The structure of the boot image is as shown in the figure below:
I should mention something of this second stage. To be completely honest, I do not know its raison d’être, the original intent of it. I have seen it used in more recent times to hold the kernel’s device tree block (DTB, another long and deeply technical topic best explained elsewhere) by certain phone manufacturers. The older kernels had the DTB as part of the kernel image, now they’ve split it out into its own separate piece to make it easier to build a more generic kernel that can be adapted to a variety of hardware configurations within the same architecture.
Moving swiftly along (and back!) to the boot image header, this primarily tells the bootloader how to load the components of the boot image, and it looks something like this:
As you can see, it has a few “bookkeeping” fields about how large the other sub-components of the boot image are, what address to load them into, etc.
It also has a field called magic. The magic field is simply a string of well-known characters to identify this as an Android boot image. In this case the string is “ANDROID!”, which fits neatly into 8 bytes.
Somebody familiar with the Linux kernel and boot-up process will recognize the kernel tags and command line argument fields as ways to pass more custom information into the kernel, such as where the console should be, and where to find that initial ramdisk from where to load its init program.
Working With Android Boot Images
So, since we have custom recovery and boot images for our Android phones (such as TWRP, CWM, etc.), obviously, someone had to create their own recovery image or modify an already existing boot image. Most of the work was done using the already open-sourced Google Android code, but there only was code there to create a boot image from its components. There was no tool to unpack a boot image for modification. Now, you can use a tool like https://github.com/osm0sis/mkbootimg to pack and unpack boot images and change out the kernel, or modify the initial ramdisk.
Further Areas to Explore
There are some interesting aspects relating to the initial ramdisk that apply to both the boot and recovery Android images that this article won’t cover in detail due to its already burgeoning size, but that are worth mentioning.
- The filesystem that initial ramdisks use is usually a gzipped CPIO archive
- You can pack and unpack the initial ramdisks and modify them
- init scripts are written in their own domain specific language (DSL) and you can read their README for more information
- Default SELinux file contexts and policy usually reside in the ramdisk’s root
We can spend many hours talking about and playing with the above items, but at least for now I will leave them as an exercise to the keen reader.
Until next time!