When a user-space application dereferences a null pointer, the operating system kills the process and moves on. The rest of the system remains perfectly stable. But when a kernel driver makes the same mistake, the stakes are much higher. Kernel code operates with unrestricted memory access. A single misstep by a third-party audio driver can corrupt critical memory pages. Platform engineers need a way to contain the damage without immediately halting the entire device.
This containment mechanism is called a Kernel Oops. An Oops is the kernel's way of handling a severe but potentially localized violation. Instead of pulling the plug on the machine entirely, the kernel intercepts the fault. It isolates the offending thread, dumps diagnostic data to the console, and forcefully terminates the thread by calling do_exit(). The scheduler removes that specific thread from the run queue, while the rest of the OS attempts to continue execution.
The following flowchart illustrates the decision path the kernel takes when it catches a fault. This visual helps clarify exactly where an Oops diverges from a full system panic. Pay attention to the configuration check, as that determines whether the device stays alive.
The exception handler catches the driver error and evaluates the system configuration. If configured to tolerate the fault, the kernel kills only the thread and enters an unstable state. Otherwise, it triggers a Kernel Panic and reboots.
But what happens when the OS tries to keep running after terminating a kernel thread? This is where theoretical resilience meets practical danger.
The Production Safety Net
Continuing execution after a thread is forcefully killed is a massive gamble. If that audio driver was holding a critical system lock when it died, that lock will never be released. The next time another CPU core tries to acquire it, the system will silently deadlock. Users will experience frozen screens and unresponsive buttons.
Android platform engineers prevent this gamble by explicitly forbidding a device from surviving an Oops in production. They use a kernel parameter called panic_on_oops to control this behavior. When this flag is enabled, the kernel intercepts the Oops logging routine and immediately upgrades the failure. The localized fault becomes a full-blown Kernel Panic. This guarantees a clean reboot instead of leaving the device in a zombified, unpredictable state.
You can verify if a device enforces this safety net by reading the sysctl node. A value of 1 confirms the kernel will panic and reboot on any Oops.
adb shell cat /proc/sys/kernel/panic_on_oops
Tip: Engineers often disable
panic_on_oopson local engineering builds. This allows you to inspect the memory state over a serial console after a crash, rather than watching the board immediately reboot.
Whether the kernel panics or limps along, it always leaves behind a highly structured block of text called the Oops log. Reading this evidence is the only way to figure out what went wrong.
Translating Hex to Source Code
The Oops message prints directly to the kernel ring buffer and contains four critical pieces of information. It states the exact memory violation, names the function that crashed, dumps the CPU registers at the microsecond of failure, and prints the raw call stack.
Internal error: Oops: 5 [#1] PREEMPT SMP ARM
PC is at my_buggy_driver_write+0x14/0x30 [buggy_module]
LR is at vfs_write+0xbc/0x184
...
[<c01053a4>] (my_buggy_driver_write) from [<c015b3c8>] (vfs_write+0xbc/0x184)
This output gives you the exact execution context that caused the fault. You can see the chain of function calls leading to the crash. However, the stack trace relies on raw hexadecimal memory addresses like <c01053a4>. Developers cannot fix C code using a hex pointer.
To fix the bug, you must map that hexadecimal instruction pointer back to a specific line in your source tree. Platform engineers bridge this gap using addr2line, a utility shipped with the Android NDK. This tool reads the DWARF debug information embedded inside an unstripped kernel binary. You provide the raw binary and the hex address. The utility parses the symbols and outputs the exact source file path and line number.
aarch64-linux-android-addr2line -e out/target/product/device/obj/KERNEL_OBJ/vmlinux c01053a4
Common Mistake: You must use the unstripped
vmlinuxbinary from your build output directory. Do not use the compressedImage.gzorzImageflashed to the device, as those have their debug symbols stripped to save space.
Running this command transforms a cryptic hex value into an actionable target, like drivers/audio/buggy_module.c:452. With the exact line of code identified, you can investigate why the driver attempted to access invalid memory.
You now understand how the kernel handles localized faults and how to track those faults back to your source code. Your toolkit is ready for debugging driver crashes when they occur. The next challenge is learning how to write drivers that do not crash the system in the first place.