This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This section of the wiki covers the process of customizing board-specific files after adding support for a new device. It also explains all the available APIs provided by kaeru and how to use them.
Project structure
Kaeru follows a modular architecture similar to the Linux kernel, organized into several key directories:
| Directory | Purpose | Key Components |
|---|---|---|
arch/ |
Architecture-specific code | ARM assembly, cache management, linker scripts |
board/ |
Device-specific implementations | Board files organized by vendor |
configs/ |
Device configuration files | Defconfig files for each supported device |
include/ |
Header files | API definitions, arch headers, library headers |
lib/ |
Core library functions | Fastboot, bootmode, debug utilities |
main/ |
Entry point | Main kaeru initialization |
scripts/ |
Build system utilities | Kconfig, build helpers |
utils/ |
Helper tools | Python utilities for patching and parsing |
Architecture layer
The architecture layer handles low-level ARM operations:
arm/cache.c: Cache management and memory barriersarm/ops.c: Atomic operations and CPU-specific functionsarm/linker.lds.S: Linker script for payload layout
Board support
Each vendor has a subdirectory containing device-specific board files:
board/
├── vendor1/
│ ├── board-device1.c
│ └── board-device2.c
├── vendor2/
│ └── board-device3.c
└── ...
Board files implement device-specific functions and configurations.
Configurations
Device-specific configuration files define addresses, offsets, and device parameters:
CONFIG_BOOTLOADER_BASE=0x48000000
CONFIG_BOOTLOADER_SIZE=0xD4FF0
CONFIG_APP_ADDRESS=0x4801F2BC
CONFIG_FASTBOOT_CONTINUE=0x48021154
CONFIG_BOOTMODE_ADDRESS=0x480D75E4
...
Library functions
The library contains core functionality used throughout kaeru:
| File | Purpose | Key Functions |
|---|---|---|
bootmode.c |
Boot mode management | get_bootmode(), set_bootmode(), show_bootmode() |
fastboot.c |
Fastboot implementation | fastboot_info(), fastboot_okay(), fastboot_register() |
common.c |
Common utilities | mtk_detect_key(), udelay(), print_kaeru_info() |
debug.c |
Debug functionality | Debug output and logging |
libc/ |
Standard C library functions | Memory and string operations |
Available APIs
Kaeru provides several APIs for customizing device behavior.
Debug API
With kaeru, you can debug your own code in several ways. It includes a debug library with multiple functions that will likely help you troubleshoot your board files or custom additions.
The first and most common debugging method is UART. This involves soldering wires to the phone's motherboard at the TX and RX pins, which is typically not ideal, as it also requires a USB-to-TTL adapter. Nevertheless, if you have access to UART, you can simply use the built-in printf function, and the output will appear in the serial console whenever the code is executed:
void board_late_init(void) {
printf("This will get printed to the UART console!\n");
}
Note
If your stock LK supports it (see
CONFIG_LK_LOG_STORE), this will also write output to theexpdbpartition, which stores boot logs on non-legacy MediaTek devices. So even if you don’t have UART access,printfmight still be useful.
If none of the above methods work for you, your last option, though not ideal, is to print messages directly to the screen using video_printf. It works similarly to the standard printf function, with the main difference being that it outputs to the display instead:
void board_late_init(void) {
video_printf("This will get printed to the screen!\n");
}
Warning
Try not to overuse this function for heavy logging, as it can quickly fill the video buffer and potentially cause screen corruption or crashes.
In addition to standard printing functions, kaeru also provides hexdump utilities for easily dumping memory. The output is similar to running hexdump -C file.bin, showing both the ASCII representation and hexadecimal values in a readable format.
The following example dumps the first 0x200 bytes of LK and prints the output to both the UART console and the screen:
void board_late_init(void) {
// Cast CONFIG_BOOTLOADER_BASE to (void *) because the hexdump functions
// expect a generic pointer type. This makes it clear that we're passing
// a raw memory address rather than a typed object.
video_hexdump((void *)CONFIG_BOOTLOADER_BASE, 0x200);
uart_hexdump((void *)CONFIG_BOOTLOADER_BASE, 0x200);
}
Fastboot API
One of the most interesting things you can do with this payload is implement your own fastboot commands. Due to the nature of the prebuilt fastboot binaries, you'll most likely need to prefix these commands with oem; otherwise, they won't be executable unless you build a custom client or use the raw fastboot protocol.
The function used to register these commands takes three parameters: the command prefix, a pointer to the function that handles the command, and a flag indicating whether the command can be used with a locked bootloader. In this case, we always want the last parameter to be set to 1:
void cmd_custom(const char *arg, void *data, unsigned size) {
fastboot_info("Processing custom command...");
fastboot_okay("");
}
void board_late_init(void) {
// Register command with security check disabled
fastboot_register("oem custom", cmd_custom, 1);
}
Boot mode API
Another thing you can take control of is the boot mode. MediaTek uses a specific enumeration to define the current boot mode, and kaeru includes this enumeration so you can easily reference all available modes:
typedef enum {
BOOTMODE_NORMAL = 0,
BOOTMODE_META = 1,
BOOTMODE_RECOVERY = 2,
BOOTMODE_FACTORY = 4,
BOOTMODE_ADVMETA = 5,
BOOTMODE_ATEFACT = 6,
BOOTMODE_ALARM = 7,
BOOTMODE_POWEROFF_CHARGING = 9,
BOOTMODE_FASTBOOT = 99,
BOOTMODE_ERECOVERY = 101
} bootmode_t;
Warning
Some OEMs, such as Huawei, implement additional custom boot modes. For example, Huawei's eRecovery boot mode is included as a reference (
101).
The boot mode is stored as a global variable in LK's memory at CONFIG_BOOTMODE_ADDRESS. For convenience, kaeru provides getter and setter functions to access and modify this variable:
void board_late_init(void) {
// Get the current boot mode from memory and show it
bootmode_t mode = get_bootmode();
show_bootmode(mode);
// Set boot mode to recovery
set_bootmode(BOOTMODE_RECOVERY);
video_printf("Boot mode set to: %s\n", bootmode2str(BOOTMODE_RECOVERY));
}
Note
If you prefer, you can always manage the boot mode using the low-level
WRITE32andREAD32macros (after all, that’s what the getter and setter functions use internally).
Memory API
Managing memory is one of the most important tasks of this payload, as it allows us to modify the behavior of the currently running bootloader. Kaeru provides several macros for memory management, including convenient utilities for easily patching functions. For example, there is a macro to locate a function's address by searching for its signature (i.e., the first few bytes of the function), another to force the function to return a specific value, and more. These tools make it possible to modify a function's behavior without needing to know its address in advance.
Caution
Keep in mind that LK runs at the EL1 privilege level, so you cannot access any memory regions that are restricted to higher privilege levels. For example, this means you cannot access BootROM or Preloader memory.
The most primitive macros come in 8-bit, 16-bit, and 32-bit variants, giving you precise control over how data is read from or written to memory, depending on the size of the value involved. The most fundamental macros are READ and WRITE, which let you read from and write to memory directly.
Warning
One important detail is that the WRITE macros also flush and invalidate the corresponding cache range. This ensures that any changes made to memory are immediately visible to the processor and aren't delayed or overridden due to cache inconsistencies.
In addition to simple read and write operations, kaeru includes macros for modifying specific bits or fields within a value. The SET macros allow you to set particular bits without affecting the others. For example, SET32(addr, mask) sets the bits specified in mask at the given 32-bit address, while preserving the state of all other bits. Conversely, the CLR macros clear specific bits by applying a bitwise AND operation with the inverted mask.
The following table summarizes the most primitive macros available in kaeru:
| Macro | Description | Example |
|---|---|---|
READ8(addr) |
Reads an 8-bit value from the specified address | uint8_t val = READ8(0x48000000); |
WRITE8(addr, value) |
Writes an 8-bit value to the specified address | WRITE8(0x48000000, 0xFF); |
READ16(addr) |
Reads a 16-bit value from the specified address | uint16_t val = READ16(0x48000000); |
WRITE16(addr, value) |
Writes a 16-bit value and flushes cache | WRITE16(0x48000000, 0xFFFF); |
READ32(addr) |
Reads a 32-bit value from the specified address | uint32_t val = READ32(0x48000000); |
WRITE32(addr, value) |
Writes a 32-bit value and flushes cache | WRITE32(0x48000000, 0xFFFFFFFF); |
SET8(addr, mask) |
Sets bits defined in mask at the 8-bit address |
SET8(0x48000000, 0x01); |
CLR8(addr, mask) |
Clears bits defined in mask at the 8-bit address |
CLR8(0x48000000, 0x01); |
MASK8(addr, mask, value) |
Updates specific bits at the 8-bit address | MASK8(0x48000000, 0x0F, 0x05); |
SET16(addr, mask) |
Sets bits at a 16-bit address | SET16(0x48000000, 0x00FF); |
CLR16(addr, mask) |
Clears bits at a 16-bit address | CLR16(0x48000000, 0x00FF); |
MASK16(addr, mask, value) |
Updates specific bits at a 16-bit address | MASK16(0x48000000, 0xFF00, 0x5500); |
SET32(addr, mask) |
Sets bits at a 32-bit address | SET32(0x48000000, 0x00010000); |
CLR32(addr, mask) |
Clears bits at a 32-bit address | CLR32(0x48000000, 0x00010000); |
MASK32(addr, mask, value) |
Updates specific bits at a 32-bit address | MASK32(0x48000000, 0xFF000000, 0x55000000); |
The most advanced macros allow you to modify function logic, inject branches or NOPs, override returns, and search for instruction sequences by signature. A cornerstone of this system is the ability to locate specific functions or instruction sequences in memory using their signature bytes.
This is accomplished with the SEARCH_PATTERN and SEARCH_PATTERN_ARM macros, which scan a defined address range for a given series of 16-bit or 32-bit values. These patterns are typically extracted from disassemblers like Ghidra, which reveal the instruction opcodes for a given function.
Imagine you want to find the address of a function that looks like this:
You'll need to note down the first few bytes of the function and use them as a signature for pattern matching. However, it's important to understand the byte ordering, as ARM stores instructions in little-endian format. Depending on the function's mode (ARM or THUMB), you will use either SEARCH_PATTERN_ARM (for ARM mode) or SEARCH_PATTERN (for THUMB mode).
Note
In Ghidra, you can determine whether a function is using THUMB or ARM mode by checking the value of
TMode. If it's is0x1, the function is in THUMB mode, otherwise it's in ARM mode.
For example, if we take the byte sequence 2D E9 F8 4F 7A 48 7B 4F, it would be written in memory as:
void board_late_init(void) {
uint32_t addr = SEARCH_PATTERN(LK_START, LK_END, 0xE92D, 0x4FF8, 0x487A, 0x4F7B);
if (addr) {
video_printf("Function found at address: 0x%08X\n", addr);
} else {
video_printf("Function not found!\n");
}
}
Warning
Some functions may begin with identical bytes, so you might need to include additional bytes in the signature to ensure it uniquely identifies the target function.
Once you have the address of the function, you can manipulate it in various ways. The most common example is to force the function to return a specific value. This is done using the FORCE_RETURN macro, which takes the address of the function and the desired return value as parameters:
void board_late_init(void) {
uint32_t addr = SEARCH_PATTERN(LK_START, LK_END, 0xE92D, 0x4FF8, 0x487A, 0x4F7B);
if (addr) {
// Imagine this function checks whether the device's bootloader is locked,
// returning 1 if it is and 0 if it's unlocked. With this line, we force it
// to always return 1, effectively spoofing the bootloader status as locked.
FORCE_RETURN(addr, 1);
}
}
Warning
If you're targeting a function in ARM mode, you should use the
FORCE_RETURN_ARMmacro instead, which works the same but uses the equivalent ARM mode instructions.
Another common use case is disabling a function call somewhere in the code. For example, if we want to disable the call to the function responsible for showing the bootloader unlock warning, we can do so by either scanning for the bytes surrounding the call or by directly referencing its address.
In this case, we'll directly reference the address of the call, which is at 0x48029D36. Looking at the image, we can see this is a THUMB mode BL instruction. The BL instruction in THUMB mode is a 32-bit instruction (4 bytes) composed of two 16-bit half-words. To disable this call, we'll replace it with NOPs:
void board_late_init(void) {
// This will make the processor skip the call to the function that shows
// the bootloader unlock warning, effectively disabling it. We use the
// NOP(address, count) macro where count is the number of 16-bit NOPs
// to insert at the specified address.
NOP(0x48029D36, 2);
}
The NOP macro inserts THUMB mode NOPs (encoded as 0xBF00) at the specified address. Since a BL instruction in THUMB mode is 32 bits (4 bytes) and each NOP is 16 bits (2 bytes), we need to insert 2 NOPs to completely replace the branch instruction.
Warning
If you're patching code in ARM mode rather than THUMB mode, you should use the
NOP_ARMmacro instead, which inserts ARM mode NOPs (encoded as0xE320F000).