Advanced OS Customizations & Bootloaders

Reverse Engineering Android Init.rc to systemd Units: Porting Complex Boot Dependencies

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction

For developers working with embedded Linux systems, particularly those customizing Android-based platforms, understanding the intricate boot process is paramount. Android traditionally relies on its own init system, driven by configuration files like init.rc and its imported counterparts. These files define services, actions, and triggers that orchestrate the device’s startup. However, when migrating to a more standard Linux environment or adopting systemd on an Android-derived system, the challenge lies in translating these complex boot dependencies and actions into systemd unit files.

This article provides an expert-level guide on reverse engineering Android’s init.rc files and porting their complex boot dependencies to systemd. We’ll explore how to map Android’s declarative service definitions and event-driven actions to systemd‘s powerful and highly configurable unit types, focusing on maintaining the delicate order and functionality crucial for a stable system.

Understanding Android’s init.rc

The init.rc file, located typically at /init.rc in the root filesystem or within the ramdisk, is the primary configuration file for Android’s init process. It uses a custom language with several key constructs:

  • Services: Define long-running daemons. Each service has a name, executable path, and optional arguments and attributes (e.g., user, group, capabilities, class).
  • Actions: Blocks of commands executed in response to specific triggers (e.g., on boot, on property:name=value, on class_start:name).
  • Commands: Primitive operations like exec, start, stop, setprop, mount, chmod, chown, etc., executed within actions or services.
  • Properties: System-wide key-value pairs that influence system behavior and trigger actions.
  • Imports: Allow modularity by including other .rc files (e.g., import /init.usb.rc).

Here’s a simplified example of an init.rc service and action:

# Service definition
service my_daemon /vendor/bin/my_daemon_exec
    class main
    user system
    group system wifi
    capabilities NET_ADMIN
    onrestart restart_log_service

# Action triggered on boot
on boot
    setprop sys.usb.config adb
    start log_service
    mount debugfs /sys/kernel/debug /sys/kernel/debug type debugfs
    exec /vendor/bin/configure_hw_features

systemd Fundamentals

systemd, the prevalent init system in modern Linux distributions, manages system boot, services, and various other components through ‘unit files’. Key unit types relevant to this porting process include:

  • Service Units (.service): Define daemons or applications.
  • Target Units (.target): Group related units and define synchronization points.
  • Mount Units (.mount): Define filesystem mount points.
  • Path Units (.path): Trigger actions when specific filesystem paths change.
  • Socket Units (.socket): Define system sockets for service activation.

systemd excels in managing dependencies and execution order through directives like Requires=, Wants=, After=, Before=, PartOf=, and conditional execution using Condition* directives.

Reverse Engineering init.rc: A Step-by-Step Approach

Step 1: Identify Key Services, Actions, and Triggers

Begin by meticulously parsing the primary init.rc file and all files it imports. Create a comprehensive list of:

  1. All service declarations: Note their executable paths, arguments, user, group, capabilities, and class.
  2. All on action blocks: Identify the triggers (e.g., boot, fs, property:name=value) and the commands executed within them.
  3. All exec commands: These represent one-shot scripts or binaries that must run at specific points.
  4. All mount commands: Note the source, destination, type, and options.
  5. All setprop commands: These modify system properties.

Step 2: Map init.rc Services to systemd Service Units

Each Android service translates directly to a .service unit. Consider the my_daemon example:

# my_daemon.service
[Unit]
Description=My Custom Daemon
After=network.target multi-user.target

[Service]
ExecStart=/vendor/bin/my_daemon_exec
User=system
Group=system wifi
CapabilityBoundingSet=CAP_NET_ADMIN
Restart=on-failure
# For 'onrestart restart_log_service'
# This is complex and might require a custom script in ExecStopPost
# or a separate .path unit to detect service failure.

[Install]
WantedBy=multi-user.target
  • User= and Group= map directly. If the group doesn’t exist, create it.
  • capabilities NET_ADMIN maps to CapabilityBoundingSet=CAP_NET_ADMIN. For multiple capabilities, list them space-separated.
  • class main suggests it’s part of the core system. It can be targeted by a custom .target unit (e.g., android-main.target) which multi-user.target `Wants=`.
  • onrestart restart_log_service is more complex. systemd‘s Restart= handles basic restarts. For conditional actions on restart, you might need a wrapper script, or utilize ExecStopPost to trigger another service.

Step 3: Translate init.rc Actions and Triggers to systemd Dependencies and Events

This is the most challenging part, requiring a deep understanding of systemd‘s dependency model.

  • on boot: Commands under on boot typically run very early. For services, use WantedBy=multi-user.target or a custom target that multi-user.target depends on. For one-shot commands (exec), create a .service unit with Type=oneshot and use ExecStart=. Add RemainAfterExit=yes if its success implies a lasting state.
# configure_hw_features.service
[Unit]
Description=Configure Hardware Features
After=multi-user.target

[Service]
Type=oneshot
ExecStart=/vendor/bin/configure_hw_features
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
  • on property:name=value: This is highly dynamic. systemd doesn’t have a direct equivalent for arbitrary property changes.
    1. Polling Service: Create a small .service unit that periodically polls the property and triggers another service (e.g., using systemctl start another-service.service). This is less efficient.
    2. udev Rules: If the property change is tied to a hardware event, a udev rule can detect the event and trigger a .service unit.
    3. Custom Agent: Implement a lightweight agent that monitors Android properties (if using a hybrid system) and signals systemd when a specific property changes.
  • exec in actions: As shown with configure_hw_features.service, these become Type=oneshot service units. Their After= and Wants= directives determine when they run.
  • start : In systemd, this means Wants=.service or `Requires=.service` from the unit that needs to start it.

Step 4: Handling File System Mounts and Permissions

  • mount [options]: Translate to .mount units or entries in /etc/fstab.
# debugfs.mount
[Unit]
Description=Debug Filesystem Mount
Requires=systemd-modules-load.service
After=systemd-modules-load.service

[Mount]
What=debugfs
Where=/sys/kernel/debug
Type=debugfs
Options=defaults

[Install]
WantedBy=multi-user.target
  • chmod and chown : For persistent files, configure permissions through package installation. For temporary files or files created at runtime, use ExecStartPre/ExecStartPost in a relevant service unit, or systemd-tmpfiles for managing temporary and volatile files.
# Example within a .service file
[Service]
ExecStartPre=/bin/mkdir -p /data/misc/mydir
ExecStartPre=/bin/chown system:system /data/misc/mydir
ExecStartPre=/bin/chmod 0770 /data/misc/mydir
ExecStart=/vendor/bin/my_daemon_exec

Step 5: Managing Properties and Environment Variables

  • setprop : If these are simple environment variables, define them in the [Service] section using Environment=VAR=value. For system-wide properties, consider creating a small `oneshot` service that writes to a known configuration file or uses a custom `property-manager` service if the Android property system must be emulated.

Advanced Scenarios and Best Practices

  • Class Management: Android’s class_start/class_stop can be emulated by creating custom .target units (e.g., android-main.target, android-core.target). Services belonging to a class would have WantedBy=.target. Starting/stopping a class would involve systemctl start android-main.target.
  • Debugging: Use journalctl -xeu for detailed logs. systemd-analyze blame, systemd-analyze critical-chain help identify boot bottlenecks.
  • Security: Leverage systemd‘s extensive security directives: NoNewPrivileges=yes, PrivateTmp=yes, ReadOnlyDirectories=, CapabilityBoundingSet=, AmbientCapabilities=, RestrictAddressFamilies=, etc., to harden your services beyond what init.rc traditionally offers.
  • Custom Targets: Define specific .target units for different boot phases or functional groups of services to mirror Android’s more granular startup phases (e.g., early-init, late-init, boot, fs, post-fs).
# android-early-init.target
[Unit]
Description=Android Early Init Target
Documentation=man:systemd.special(7)
RefuseManualStart=yes

[Install]
WantedBy=basic.target

Conclusion

Porting complex Android init.rc boot dependencies to systemd is a non-trivial but highly rewarding process. It requires a systematic approach to dissecting init.rc logic and carefully mapping it to systemd‘s powerful and declarative unit system. While direct equivalents don’t always exist, especially for dynamic property-based triggers, systemd offers robust alternatives through its dependency management, conditional execution, and the flexibility to create custom units and targets. By adopting systemd, you gain enhanced control, better debugging capabilities, and a more standardized, maintainable, and secure boot environment for your embedded Linux systems.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner