A bootloader is a small piece of code that runs before any operating system is loaded. It is responsible for initializing hardware, setting up memory, and loading the operating system kernel and initial ramdisk into memory so that the operating system can boot. Understanding bootloader code can help developers customize and modify the boot process for their systems.
Bootloader Stages
A typical bootloader performs the following stages:
- Initialize hardware: The bootloader initializes key hardware components like the clock, memory controllers, GPIO pins, UARTs, and interrupt controllers.
- Setup memory: It sets up the different types of memory like DRAM, SRAM, and memory mapped peripherals. This includes configuring timings and memory controllers.
- Load kernel image: It loads the kernel image file (e.g. zImage on Linux) into RAM from boot media like flash memory or SD card.
- Uncompress kernel: If the kernel image is compressed, the bootloader uncompresses it into RAM.
- Load ramdisk: An initial ramdisk (initrd) with essential drivers and files is loaded into RAM.
- Kernel boot parameters: Any required kernel parameters like root partition info are set.
- Jump to kernel: Finally control is transferred to the kernel code by jumping to its entry point.
Example Bootloader Code Flow
Here is an example outline of the key functions in a simple C-based bootloader code:
- Reset handler: This code runs first after a reset and sets up the processor modes, stack pointer, and zeroes out .bss section.
- Initialize hardware: Clock, memory controllers, GPIOs, UARTs etc are initialized.
- Enable MMU: If an MMU is present, it is enabled to setup virtual memory.
- Load kernel image: Reads kernel binary from storage into RAM.
- Parse kernel image: Checks kernel headermagic number, architecture etc.
- Uncompress kernel: If kernel is compressed, uncompress into RAM.
- Load ramdisk: Optional ramdisk is loaded into RAM.
- Pass boot parameters: Passes information like ramdisk location in RAM to kernel.
- Flush caches: Clears caches to ensure no stale data exists.
- Jump to kernel: Transfers control to kernel entry point.
C Code Example
Here is a simplified C code example implementing some key stages of a bootloader: // Bootloader entry point void bootloader_start(void) { // Initialize hardware – clocks, memory, etc hardware_init(); // Initialize UART for debug prints uart_init(); // Initialize DRAM and map memory controllers dram_init(); // Enable MMU to setup virtual memory mmu_init(); // Read kernel image from storage (e.g. SD card) read_kernel(); // Uncompress kernel if needed uncompress_kernel(); // Load ramdisk into memory load_ramdisk(); // Pass boot parameters to kernel boot_params.ramdisk_addr = RAMDISK_ADDRESS; boot_params.ramdisk_size = ramdisk_size; // Flush caches flush_caches(); // Jump to kernel entry point jump_to_kernel(kernel_entry); } // Hardware init function void hardware_init() { // Init clock, gpio, interrupt control, timers } // Read compressed kernel from SD card void read_kernel() { // Open SD card device sd_fd = sd_open(“sd0”); // Seek to kernel position on card sd_seek(sd_fd, KERNEL_START_SECTOR); // Read compressed kernel into RAM buffer sd_read(sd_fd, kernel_buf, KERNEL_SIZE); // Close SD card device sd_close(sd_fd); } // Uncompress kernel void uncompress_kernel() { // Check kernel header magic if(kernel_buf[0] == 0x1F && kernel_buf[1] == 0x8B) { // Kernel is gzip compressed, uncompress it ungzip(kernel_buf, kernel_uncompressed_buf); } } // Load ramdisk file into memory void load_ramdisk() { // Similar SD card read logic to load ramdisk // into reserved RAM address } // Flush caches before jumping to kernel void flush_caches() { // Implementation depends on architecture // ARMv7 might just need flushing of // data cache and branch predictor } // Jump to kernel entry point void jump_to_kernel(uint32_t entry_point) { // Pass pointer to boot params structure register uint32_t r0 asm(“r0”) = (uint32_t)&boot_params; // Jump to kernel entry point asm volatile(“mov pc, %0” : : “r” (entry_point)); }
Bootloader Programming Tips
Here are some tips for developing bootloader code:
- Use clear modular code organization with well defined interfaces.
- Optimize for size – avoid higher level libraries and large stack usage.
- Minimize initialization code to speed up boot time.
- Use hardware abstraction layer for portability across SoCs.
- Debug with LED blink codes, serial prints and/or debug registers.
- Check return values and error codes from hardware accesses.
- Verify memory interfaces thoroughly – incorrect configs can lockup SoC.
- Test end-to-end frequently on actual hardware.
- Allow passing optional boot configurations like UART baudrate.
- Support features like recovery mode and firmware updates.
Conclusion
The bootloader performs the critical task of system initialization and loading the kernel. Understanding typical bootloader functionality like hardware init, memory setup, kernel loading and jumping to kernel entry helps developers customize their system boot process. Using well structured, modular code as per the hardware capabilities results in a robust, optimized bootloader. With thoughtful programming and sufficient testing on target hardware, developers can build effective bootloaders tailored to their system requirements.