Skip to content

hassansys2/mac-docker-linux-kernel-arm64

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

2 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Embedded Linux Learning β€” ARM64 on macOS (Apple Silicon)

A hands-on learning repo progressing from raw kernel builds to full Yocto Project based embedded Linux images, all running on Apple Silicon via Docker + QEMU.

Roadmap

Phase Topic Status
1 Build Linux kernel (ARM64) from source Done
2 BusyBox initramfs + custom init Done
3 Kernel modules Done
4 Yocto Project β€” first image (core-image-minimal) Next
5 Yocto β€” custom layer + recipe Planned
6 Yocto β€” BSP layer for QEMU AArch64 Planned

Phase 1–3: Linux Kernel Build & Run (ARM64)

This guide documents a reproducible workflow to:

  • Build the Linux kernel (ARM64) using Docker
  • Build a BusyBox-based initramfs
  • Boot the kernel using QEMU (AArch64) on macOS (Apple Silicon / M1–M3)

System Requirements

Host (macOS)

  • macOS on Apple Silicon (M1 / M2 / M3)
  • Docker Desktop (Compose v2)
  • Homebrew
  • QEMU (AArch64)

Install QEMU:

brew install qemu

Directory Layout (Host)

.
β”œβ”€β”€ Dockerfile
β”œβ”€β”€ docker-compose.yml
β”œβ”€β”€ hello.c                        # Phase 2: minimal static-C init (no shell)
β”œβ”€β”€ init.c                         # Phase 2: pure-C init with ls/cd/cat shell
β”œβ”€β”€ kernel-modules-practice/
β”‚   β”œβ”€β”€ kernel-param-module/       # Phase 3: module parameters + callbacks
β”‚   └── kernel-sample-module/      # Phase 3: simple hello-world module
β”œβ”€β”€ minimal_shell_init/
β”‚   └── etc/                       # Phase 2: motd + resolv.conf for shell init
β”œβ”€β”€ initramfs/
β”‚   └── init                       # Phase 1: BusyBox init script
└── out/                           # build output (gitignored)

Step 1: Build Kernel Builder Image

docker compose -f docker-compose.yml build

Step 2: Enter Builder Container

docker compose -f docker-compose.yml run --rm kernel-builder

Or one-shot build:

git clean -fdx
make ARCH=arm64 mrproper
make ARCH=arm64 defconfig
make O=/work/out ARCH=arm64 -j$(nproc)
docker compose run --rm kernel-builder make -C /work/linux O=/work/out ARCH=arm64 -j$(nproc)

Or one-shot background build:

docker compose run -d --rm kernel-builder /bin/bash -c 'make -C /work/linux ARCH=arm64 defconfig && make -C /work/linux O=/work/out ARCH=arm64 -j$(nproc)'

docker compose run -d --rm kernel-builder /bin/bash -c 'make -C /work/linux O=/work/out ARCH=arm64 -j$(nproc)'
docker exec -d kernel-builder /bin/bash -c 'make -C /work/linux O=/work/out ARCH=arm64 -j$(nproc)'

docker exec kernel-builder /bin/bash -c 'make -C /linux-kernel-source O=/work/out ARCH=arm64 -j$(nproc)'


# View logs
docker compose logs -f kernel-builder

Step 3: Build Linux Kernel

cd /work

git clone --depth 1 --branch v6.12 https://github.com/torvalds/linux.git /linux-kernel-source
# or git clone --depth 1 https://github.com/torvalds/linux.git

cd linux
# if you have older linux cloned folder to cleanup
# or git clean -idx for intractive clean
git clean -fdx 

make ARCH=arm64 O=/work/out defconfig
make ARCH=arm64 O=/work/out -j$(nproc)

Kernel image produced:

/work/out/arch/arm64/boot/Image

Step 4: Build BusyBox (Static)

clone BusyBox β†’ /work/busybox

cd /work

git clone --depth 1 --branch 1_36_1 https://github.com/mirror/busybox.git /busybox
# or git clone https://github.com/mirror/busybox.git

cd /busybox
make distclean
make defconfig ARCH=arm64
make menuconfig

BusyBox Fixes (ARM64)

Disable:

  • Settings β†’ SHA1/SHA256 hardware acceleration
  • Networking Utilities β†’ tc

Enable static build:

sed -i.bak 's/# CONFIG_STATIC is not set/CONFIG_STATIC=y/' .config

Build and install:

make ARCH=arm64 -j$(nproc)
make ARCH=arm64 CONFIG_PREFIX=/work/initramfs install

This installs BusyBox into:

/work/initramfs/{bin,sbin,usr/bin,...}


Step 5: Create an /init script (the first userspace process)

cat > /work/initramfs/init <<'EOF'
#!/bin/sh

# 1. Mount basic filesystems
mkdir -p /proc /sys /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp

# 2. Mount devtmpfs FIRST
# This populates /dev with device nodes like console, null, etc.
mount -t devtmpfs none /dev

# 3. NOW create pts inside the newly mounted /dev and mount devpts
mkdir -p /dev/pts
mount -t devpts none /dev/pts

echo "Booted into initramfs!"
echo "Kernel: $(uname -a)"

# drop into a shell
# exec /bin/sh
# cttyhack detects the console and runs the shell with a controlling terminal
exec setsid cttyhack /bin/sh
EOF

Step 5: Make init script executable

chmod +x /work/initramfs/init

Step 6: Create initramfs

cd /work/initramfs

# mkdir -p proc sys dev dev/pts tmp # Not required as init script will do it
find . -print0 | cpio --null -ov --format=newc | gzip -9 > /work/initramfs.cpio.gz

# exit from container
exit

Now you have:

  • /work/out/arch/arm64/boot/Image
  • /work/initramfs.cpio.gz

Step 7: Boot with QEMU (on your Mac host)

qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a72 \
  -m 2048 \
  -nographic \
  -kernel ./out/arch/arm64/boot/Image \
  -initrd ./initramfs.cpio.gz \
  -append "console=ttyAMA0 loglevel=8 rdinit=/init"

Exit QEMU:

Ctrl + A, then X

Done πŸŽ‰

You are now running a custom ARM64 Linux kernel in QEMU on macOS.

Building using static C Binary πŸŽ‰

To test a Static C Binary as your init process, you will replace the entire BusyBox userland with a single compiled executable. Since you are on a Mac (Apple Silicon), you can use your existing Docker container to cross-compile the C code for the Linux ARM64 target.

Create a file named hello.c in your workspace:

#include <stdio.h>
#include <unistd.h>

int main() {
    printf("\n--- Starting Minimal Custom Init ---\n");
    printf("Hello from a pure C static binary!\n");
    printf("No BusyBox, no shell, just code.\n");

    // The init process must never exit. 
    // If it does, the kernel will panic.
    while(1) {
        sleep(10);
    }
    return 0;
}

Compile it Statically

You must compile this as a static binary so that it contains all its own library code. If you use a dynamic binary, the kernel will fail to boot because there is no linker (ld-linux) or libc.so in your empty filesystem.

Inside your Docker container, run:

gcc -static hello.c -o init

Note: Ensure you are using the ARM64 cross-compiler inside the container.

Create the Minimal initramfs

You don't need any folders (/bin, /sbin, etc.) for this testβ€”just the init file.

# Move the compiled binary to a fresh directory
mkdir -p /work/minimal_init
cp /work/init /work/minimal_init/init
chmod +x /work/minimal_init/init
# Create the cpio archive
cd /work/minimal_init
find . | cpio -ov --format=newc | gzip -9 > /work/minimal_init.cpio.gz

Boot with QEMU Run the QEMU command from your Mac host, pointing to the new minimal_init.cpio.gz.

qemu-system-aarch64 \
  -machine virt \
  -cpu cortex-a72 \
  -m 512 \
  -nographic \
  -kernel ./out/arch/arm64/boot/Image \
  -initrd ./minimal_init.cpio.gz \
  -append "console=ttyAMA0 rdinit=/init"

Phase 4: Yocto Project (Next)

Yocto lets you build a complete, reproducible embedded Linux distro from source β€” kernel, rootfs, packages β€” using BitBake recipes and layers.

Goal

Build core-image-minimal for qemuarm64 and boot it in QEMU on macOS.

Host Requirements

  • Docker Desktop (existing setup works)
  • ~100 GB free disk (Yocto build caches are large)
  • Poky reference distro (the starting point for all Yocto builds)

Quick Start (planned)

# Clone Poky (Yocto reference distro)
git clone --branch scarthgap https://git.yoctoproject.org/poky

cd poky
source oe-init-build-env build

# Set machine to QEMU AArch64
echo 'MACHINE = "qemuarm64"' >> conf/local.conf

# Build minimal image (~1-2 hrs first time)
bitbake core-image-minimal

# Boot in QEMU
runqemu qemuarm64 nographic

Key Concepts to Learn

Concept What it is
BitBake The task execution engine (like make, but for recipes)
Recipe (.bb) Describes how to fetch, patch, compile, and install one package
Layer (meta-*) A collection of recipes/configs β€” stacked to build an image
local.conf Your per-build settings (machine, distro, parallelism)
bblayers.conf Which layers are active in this build
core-image-minimal Smallest bootable image β€” shell + basic tools
devshell Drop into a recipe's build env for debugging

Learning Path

  1. Build core-image-minimal for qemuarm64
  2. Add a custom meta-mylayer with a hello-world recipe
  3. Add a custom kernel config fragment via a .bbappend
  4. Build core-image-full-cmdline and explore the rootfs
  5. Port one of the kernel modules in kernel-modules-practice/ to a Yocto recipe

About

A complete workflow for Linux kernel development on Apple Silicon. This project provides a containerized toolchain to build the ARM64 Linux kernel and a minimal BusyBox-based initramfs, with a pre-configured QEMU setup for immediate testing on macOS.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors