Why Linux Isn't Enough: The Gap Between Kernel and Framework
Why can't you just use standard Linux utilities on an Android device? You might boot up a phone, drop into a root shell, and discover standard GNU tools are missing. Google faced this problem because they needed to run a complex software stack on devices with constrained memory. Standard GNU binaries were too large. They also carried restrictive software licenses.
Google solved this by building custom, stripped-down replacements. The result is a middle layer of native code acting as connective tissue for the entire platform. This custom native code lives in the system/ directory. The Linux kernel is the raw engine. The Java framework is the dashboard. The custom code in system/ provides the transmission and electrical wiring connecting the two.
First, the upcoming diagram visualizes the architecture stack starting with the kernel at the base. Second, it shows the system directory sitting above the kernel to provide native services. Third, it places the Java framework on top to demonstrate how it relies on the underlying layers.
Hardware initializes under the kernel first. The kernel then hands off control to the native daemons running inside the system layer.
You can inspect the file system to see what commands are available. Executing the ls command lists directory contents.
ls /system/bin
This command reveals exactly which utilities Android provides out of the box. A common mistake is assuming standard Linux commands behave exactly like their GNU counterparts.
Tip: Many familiar commands in Android are not standalone binaries, but symlinks pointing to a single executable called
toyboxto save space.
Knowing why these custom native tools exist naturally leads to the question of what happens first when the kernel hands over control to this layer.
Process ID 1: How system/core Wakes Up the Device
After the kernel finishes setting up hardware drivers, it must start the operating system services. What is the very first piece of user-space code that executes, and how does it start everything else? Linux searches for a single executable to run. In Android, that executable is init.
The init process receives Process ID 1. It acts as the orchestra conductor for the entire boot sequence. It does not play the instruments itself. It reads sheet music to signal different sections to start playing at precise times. The source code for this master process lives in system/core/init.
These configuration files use a custom syntax based on actions, triggers, and services. The init process parses these .rc scripts to spawn critical system daemons in exact order.
First, the upcoming diagram outlines the chronological boot sequence starting from the kernel. Second, it tracks how the kernel spawns the initial process. Third, it demonstrates how that process subsequently launches other background daemons.
A fork operation starts the sequence inside the kernel. The init process then reads the configuration files to launch the log daemon and the zygote process.
Developers use custom init.rc syntax to define background services. This block defines the Android Debug Bridge daemon and triggers its startup based on a system property.
service adbd /system/bin/adbd
socket adbd stream 660 system system
disabled
on property:sys.usb.config=adb
start adbd
This syntax explicitly maps the service to its binary and sets property condition triggers.
Warning: A common mistake is adding a custom daemon to an
.rcscript without configuring the proper SELinux context. The executable will fail silently during boot.
With core daemons securely running, developers need a way to write their own native C++ code without causing dangerous system crashes.
Writing Safe Native Code: libbase and logging
Native code offers complete control over memory and hardware. Why shouldn't you use standard C++ file I/O or printf in your AOSP native code? Standard approaches are full of boilerplate and are highly prone to buffer overflows. A simple memory leak in a native daemon will eventually cause a total system crash.
Google created custom libraries to prevent these catastrophic failures. The system/libbase directory provides optimized C++ utility classes. Using libbase is like wearing safety goggles in a chemistry lab. The system/logging directory contains liblog to handle diagnostics safely.
The libbase library provides safe methods for reading files and manipulating strings. The liblog library replaces standard print statements with specialized functions. These functions write outputs to circular memory buffers instead of a standard console.
First, the upcoming diagram maps the path of a log message originating in a native daemon. Second, it shows the message traveling through the logging middleware. Third, it details how the final utility extracts the message for the developer.
A native daemon emits a message using the liblog interface. The logd daemon stores the message, and the developer retrieves it using logcat.
Calling the android::base::ReadFileToString function handles complex file descriptor management automatically.
#include <android-base/file.h>
#include <string>
std::string content;
if (android::base::ReadFileToString("/sys/class/power_supply/battery/capacity", &content)) {
// Parse battery capacity
}
This code safely reads hardware state from the file system without manual memory management.
Common Mistake: Using standard
std::coutorprintfin a native daemon means the text disappears into the void becauseliblogoverrides standard output.
Now that we have native processes running and logging safely, we must ensure a maliciously compromised process cannot read another process's data.
The Unforgiving Bouncer: Mandatory Access Control in sepolicy
You create a background service and set it to run as the root user. Why is your root-privileged daemon getting a "Permission Denied" error when reading a file? Android uses Discretionary Access Control through standard Linux user permissions, but that is not enough.
Discretionary access is a hotel room key. If stolen, anyone can enter. Mandatory Access Control is a security guard checking IDs against a master list at the door. The guard grants identity-based access regardless of keys.
The system/sepolicy directory holds the Type Enforcement files that build this master list. These .te files compile into strict rules. The SELinux kernel module enforces these rules on every single access attempt.
First, the upcoming diagram illustrates how the kernel evaluates an access attempt based on SELinux rules. Second, it depicts a compromised daemon being blocked by the module. Third, it shows a permitted daemon successfully accessing the target partition.
An unprivileged daemon attempts a read operation, which the SELinux module forcefully blocks. The permitted daemon makes the same request and successfully accesses the data partition.
Filtering the system logs reveals exactly which process was stopped.
adb logcat | grep 'avc: denied'
This command highlights the exact resource the daemon tried to access.
Warning: A common mistake is using
setenforce 0to bypass SELinux as a permanent fix. Permissive mode is strictly for local debugging and will result in a broken production build.
System daemons safely manage the environment using the init orchestrator, libbase guardrails, and strict sepolicy security. They still need a standardized interface to talk to physical hardware components. This limitation requires an entirely separate layer to bridge the gap between software daemons and physical cameras or flashlights.