Sitemap
Najmudheen A

Najmudheen A

DREAM - DEVELOP - DEPLOY | Android Automotive | Mobile App developer | Android developer | Kotlin | Java | Flutter | Programmer |

From Kotlin to C++: Day 17, Meet The Linux Kernel

6 min readOct 28, 2026

--

Press enter or click to view image in full size

Sixteen days of Android-specific material, and now the ground shifts one layer deeper. Everything you’ve learned — HALs, VHAL, Binder, Zygote — all of it runs on top of a Linux kernel doing the actual work of talking to hardware. Today you meet that kernel directly, just enough to understand exactly what “bare metal” is about to change underneath you.

What The Kernel Actually Does

Strip away everything Android adds on top, and the Linux kernel’s job is simple to state: manage hardware, memory, and processes, and expose all of it to user-space programs through a controlled, deliberate set of interfaces. Your C++ programs all week have been user-space programs — when you called new to get heap memory, or std::thread to start a thread, you were really asking the kernel to do that work on your behalf, just through layers of library code that hid the asking from you. Today, you finally look at some of those “controlled interfaces” directly.

Device Tree: How The Kernel Learns What Hardware Exists

Here’s a genuinely important idea, and it’s the one your whole bare-metal project circles around. The same Linux kernel binary can run on wildly different boards — different chips, different memory layouts, different peripherals wired to different pins. So how does one kernel know what’s actually attached to this particular board?

The answer is the device tree: a separate file, written in a format called .dts (device tree source) and compiled into a .dtb (device tree blob), that describes exactly what hardware exists on one specific board — memory addresses, interrupt lines, clocks, which peripherals are present at all. At boot, the bootloader hands this .dtb to the kernel, and the kernel uses it to know what to even look for.

Here’s a simplified, representative device tree node for a CAN bus controller — deliberately chosen, since this is almost exactly the kind of hardware your real project will eventually need described correctly:

can0: can@40006400 {
compatible = "bosch,m_can";
reg = <0x40006400 0x400>;
interrupts = <19>;
clocks = <&can_clk>;
status = "okay";
};

Read it like a hardware fact sheet: compatible tells the kernel which driver knows how to handle this device. reg gives the exact memory address and size the kernel should map to actually talk to the hardware registers. interrupts says which interrupt line this device will use to signal the CPU. status = "okay" confirms this device is actually present and enabled on this board (some nodes exist in a shared base file but get disabled per-board). Every single field here matters: get the reg address wrong, and the driver reads garbage from the wrong location entirely.

Kernel Modules: Drivers You Can Load And Unload

A kernel module is a piece of driver code compiled separately from the main kernel, as a .ko file, that can be loaded in or removed while the system is running, using insmod (load) and rmmod (remove), or the smarter modprobe (which also pulls in anything that module depends on). This matters practically because it means you don’t need to rebuild and reflash an entire kernel just to test a driver change — you build the one module, load it, test it, unload it, and try again.

How User-Space Actually Talks To A Kernel Driver

This is the part that connects directly back to everything you’ve built this week. Once a driver is loaded and working, user-space code — like a HAL — talks to it through one of two doors:

Character device nodes, under /dev/. A driver exposes itself as a file — /dev/can0, /dev/ttyUSB0, whatever makes sense for that hardware — and user-space code talks to it using exactly the same open(), read(), write(), and ioctl() calls you used on Day 13’s /dev/null example. ioctl() is the one genuinely new piece: it’s a general-purpose “send this device a specific, device-defined command” call, used for anything that doesn’t fit neatly into a plain read or write.

Get Najmudheen A’s stories in your inbox

Join Medium for free to get updates from this writer.

sysfs, under /sys/. Many drivers also expose simple settings and status as plain text files here — read a file to get a value, write a file to change one. It’s a much simpler interface than ioctl(), and you’ll often see both used by the same driver for different purposes.

Why This Matters For Your Job

A hypervisor guest usually gets a small, simplified, virtual device tree, describing paravirtualized devices that don’t really exist as physical hardware. A bare-metal build needs the real board’s full device tree, describing the real chip’s real peripherals, with real memory addresses and real interrupt lines. This is one of the very first things that has to be correct in your porting project — before a single line of new HAL code can talk to real hardware, the kernel has to already know that hardware exists at all.

Today’s Homework — With Worked Answers

1. Run lsmod, dmesg | less, and browse /sys and /dev.

Realistic lsmod output — a list of currently loaded modules, with a usage count and what else depends on them:

Module                  Size  Used by
can_raw 20480 0
can_dev 24576 1 can_raw
vcan 16384 0

That vcan entry is worth knowing about by name: it’s a virtual CAN interface, meant purely for testing CAN code on a machine with no real CAN hardware attached at all — genuinely useful once you start writing CAN-facing code yourself.

Realistic dmesg output — raw kernel log messages, often from boot:

[    2.145032] m_can 40006400.can: m_can device registered (irq=19, version=32)
[ 2.145980] can: controller area network core

And /dev and /sys are just real directories you can ls directly:

$ ls /dev/can*
/dev/can0

$ ls /sys/class/net/
can0 eth0 lo

Notice can0 shows up under /sys/class/net/ — on Linux, CAN interfaces are treated as a kind of network interface, which is exactly why the tools for working with them (ip link, candump) feel like networking tools, not file I/O tools.

2. Write and run the open()/write() program from Day 13, practicing the pattern you’ll use on a real device node.

#include <fcntl.h>
#include <unistd.h>
#include <iostream>

int main() {
int fd = open("/dev/null", O_WRONLY);
if (fd < 0) { std::cerr << "open failed\n"; return 1; }

const char* msg = "hello device\n";
write(fd, msg, 13);
close(fd);
std::cout << "wrote to /dev/null\n";
return 0;
}

Running this prints just wrote to /dev/null — /dev/null is a special device that silently discards anything written to it, which is exactly why it’s a safe target for practicing this pattern. The real exercise here is noticing how little this code would need to change to target real hardware: swap "/dev/null" for "/dev/can0", add an ioctl() call or two to configure it, and you’ve got the opening lines of a real CAN-reading program — the exact shape your future IVehicleHardware implementation will use.

3. Find and read a simple device tree node example.

You just did, above — the can@40006400 node. Here’s the one-sentence version of what to take from it: a device tree node is really just a small, structured fact sheet — what kind of device this is (compatible), where it lives in memory (reg), how it signals the CPU (interrupts), and whether it’s actually turned on for this board (status). Every single peripheral on a bare-metal board — the CAN controller, the display, every sensor — needs a correct node like this before the kernel will even attempt to talk to it, which is exactly why “get the real device tree for our board” sits at the very top of the question list from Day 18.

--

--

Najmudheen A
Najmudheen A

Written by Najmudheen A

DREAM - DEVELOP - DEPLOY | Android Automotive | Mobile App developer | Android developer | Kotlin | Java | Flutter | Programmer |