Skip to content

Debugging Decoding a bugcheck

pappadf edited this page Sep 14, 2026 · 1 revision

Decoding a bugcheck

This page owns: turning a blue screen — or a loader error — into a faulting instruction and a module name.

*** STOP: 0x00000050 (0xEE315C98,0x00000000,0x00000000,0x00000000)
PAGE_FAULT_IN_NONPAGED_AREA

That is four numbers and a name, and on its own it tells you almost nothing useful: not which instruction, not which driver, not why. This page is the route from there to an answer.

The short version: a bugcheck reports the address that was referenced, never the instruction that referenced it. Getting the second one is the whole job.


1. First, is it even a bugcheck?

Three different things print errors on this machine, and mistaking one for another wastes hours.

What you see Who is talking Where to look
DEFAULT CATCH!, Program complete, a 0 > prompt Open Firmware Open Firmware
OS Loader V4.00 then a sentence, or bare hex ids OSLOADER.EXE §5 below
*** STOP: 0x… on a blue screen the NT kernel the rest of this page

The third is good news even when it looks worst: it means the whole firmware and loader chain succeeded. → The NT boot chain §3


2. The parameters

*** STOP: 0x00000050 (P1, P2, P3, P4)
           ^^^^ the bugcheck code

Codes seen on this project, and what each turned out to mean here:

Code Name On this machine it meant
0x1E KMODE_EXCEPTION_NOT_HANDLED with P1 0x80000002: a misaligned access. Almost always the compiler folding byte accesses back into a wide one
0x50 PAGE_FAULT_IN_NONPAGED_AREA P1 is the referenced address, P2 0 for a read
0x0A / 0x09 IRQL_NOT_LESS_OR_EQUAL / NOT_GREATER_OR_EQUAL a bad pointer, or something called at the wrong IRQL
0x7B INACCESSIBLE_BOOT_DEVICE the loader could not reach the disk — usually firmware, not NT
0x4C FATAL_UNHANDLED_HARD_ERROR P1 carries the real status, e.g. 0xC000026C "unable to load device driver"

A bugcheck during a bugcheck is normal and not the interesting one. The kernel calls the HAL to paint the screen, and if that path faults you get a second, unrelated code. Diagnose the first.

The parameter is munged

0x50's first parameter — and the processor's DAR — are reported with the little-endian address munge still applied:

reported 0xEE315C98    ^ 4    →    really 0xEE315C9C

Un-munge before doing arithmetic. A diagnosis that comes out "four bytes off and I cannot see why" is this, every time. → When your instrumentation lies


3. Getting the faulting instruction

On PowerPC the processor saves it in SRR0 when it takes the exception. The trick is catching the machine at that moment, because a boot takes thousands of ordinary page faults.

A conditional breakpoint on the data-storage vector, filtered on the address you care about:

debug.breakpoints.add 0x300 "machine.cpu.dar == 0xEE315C98" "logical"

(Use the munged value there — that is what the register holds.)

mkbootscript.py --bp emits exactly this and dumps the registers on every hit:

BP 0x300  srr0=0xee326bc0  srr1=0xb031  dar=0xee315c98  dsisr=0x40000000
          lr=0xee31db04  r1=0xe6e84320  r3=0x4 …

Now you have:

  • srr0 — the faulting instruction,
  • dsisr — why (bit 1 set: translation not found),
  • lr — who called the function you are in,
  • r1 — the stack frame, which is often the key to the rest.

Add a breakpoint on KeBugCheckEx as a backstop: it catches the failure even when your condition never matches, and r3/r4 are the code and first parameter.


4. Getting the module

SRR0 is an address; you need a name.

If it is in a module the HAL listed, it is arithmetic. Every boot prints:

HAL: module ntoskrnl.exe   at 8064a000 size 00140000
HAL: module hal.dll        at 8078a000 size 0000e000
HAL: module VIDEOPRT.SYS   at 80705000 size 00007720

Find the range containing SRR0, subtract the base, and you have an RVA to look up. → Reading NT binaries

If it is not, the code is in a driver loaded after that list was printed. Then:

  1. Translate the address and read the instructions there — machine.cpu.mmu.translate(srr0), then peek with the ^ 4 word munge.
  2. Take a run of branch-free instructions, which are position-independent.
  3. Search every driver image on the disk for that byte sequence.

Seven instructions uniquely identified MGA_MIL.SYS among 53 drivers, and the file offset gave the load base. → Debugging recipes


5. Loader errors are a separate skill

OSLOADER.EXE does not bugcheck. It prints sentences — or, when its own resource loading has failed, bare hexadecimal message ids:

0000232e
00002333
00002350

Those are message-table ids. Decoding them needs the table out of OSLOADER.EXE's .rsrc:

9006  Windows NT could not start because of a computer disk hardware configuration problem.
9011  Could not access disk partition tables
9040  Please check the Windows NT(TM) documentation …

Seeing raw numbers is itself a diagnosis: it means the first failure was BlInitResources, which is what loads the message table, and everything after is downstream of that. → The NT boot chain §2.2


6. A worked example

Wall 49, start to finish, as a template.

Step What it gave
The screen 0x50, referenced 0xEE315C98
Conditional breakpoint on 0x300 SRR0 = 0xEE326BC0, DSISR = translation not found
mmu.translate on both SRR0 maps; DAR does not — the fault is real
Disassemble at SRR0 lwz r11,-32360(r2) — the first instruction of an import glue stub
Therefore r2 is wrong, not the memory
Fingerprint the caller against 53 drivers MGA_MIL.SYS, loaded at 0xEE319000
Its entry descriptor TOC should be 0xEE321280; r2 held 0xEE31DB04 — a code address
Read the stack at 4(r1) that same value, sitting in the saved-TOC slot
Conclusion the HAL's prologue had overwritten its caller's saved TOC

Two general lessons in that table. The faulting instruction's identity mattered more than its address — recognising glue is what turned a memory question into a register question. And the distance between cause and symptom was several calls and an unrelated module, which is normal for ABI bugs and is why "the driver that crashed" is rarely the driver at fault.


Next

Clone this wiki locally