Adventures In A 32-bit Minidump, Part 2 We are discussing "registers", not the registry
The last of the main items of minidump information is the "context" of the thread that caused the crash. This is the most esoteric and least practical of the topics covered so far, but it is also fundamental to understanding how a computer functions at the interface between software and hardware.
The concept of a "memory hierachy" is frequently drawn (in computer science textbooks) as a pyramid. Since I can't easily draw here, imagine that "registers" are at the top (pointy) end, and "offline storage" is the wide pyramid base:
- L1 processor cache
- L2 processor cache (if present)
- L3 processor cache (if present)
- Physical memory (RAM)
- Secondary storage (HDD)
- Offline storage (backups, DVDs, USB sticks...)
The further down the list, the greater the storage capacity, but at the cost of slower access speed. Registers are tiny regions of extremely fast-access memory built right into the processor itself. When discussing whether a particular processor is in "32-bit" or "64-bit" modes, what that really refers to is the width (size) of each individual register.
The IA-32 processor architecture, more commonly called x86 when it's running Windows, has a mere 8 registers which can be termed "general purpose". Almost everything that a PC does, from games, to spreadsheets, to playing music, all happens because the contents of those 8 registers are added to each other, subtracted, compared, multipled, moved, and so on almost ad infinitum
. Here is what the contents of those registers might look like, with the aid of the debugger's 'r' ("display registers") command: kd> r eax=00000001 ebx=005a0737 ecx=8054a14c edx=000003f8 esi=00000089 edi=91e65b50 eip=80526fc8 esp=80548fd0 ebp=80548fe0 iopl=0 nv up ei pl nz na po nc
The names of the registers are deceptively simple:
- A: (Accumulator). General purpose, sometimes used as the destination register for results of arithmetic operations, and also for function return values.
- B: General purpose
- C: ("Counter"). General purpose, sometimes used to store loop counter interators.
- D: General purpose
- SI: "Source Index" pointing to source data for some memory and string operations. Also general purpose.
- DI: "Destination Index" counterpart to SI. Also general purpose.
- IP: "Instruction pointer" that contains the address in memory of the next instruction which is to be fetched and executed by the processor. This is not a general purpose register.
- SP: "Stack pointer" which maintains information regaring the current stack position.
- BP: "Base pointer" which mostly (but not always) contains the address of the beginning of the current stack frame.
The E and X name prefix and suffix, as in E
, are used to address either the the lower 16 bits of the register (just AX), or all 32 bits (EAX).
The cryptic-looking "nv up ei pl nz na po nc" letter combinations reveal the current contents of the FLAGS register. Because it is a bitmask, the bits in FLAGS are displayed (as above) according to their meaning, and not as a number. "nz" means "not zero" (zero flag not set), "po" means "parity odd", "nc" is "no carry" (carry flag not set), and so on for roughly 20 different flags (the debugger's 'r' command doesn't display all 20). Individual bits within that register are set or cleared for specific reasons, including comparison operations. For example, if the result of a comparison between EAX and EBX deduces that they contain the same value, the "zero flag" within the FLAGS register is set to TRUE (1) - there is zero difference between them. Subsequently, a JZ (jump if zero flag set) instruction may cause the execution to continue at a different point in code which is there to deal with the implications of those two numbers being equal. That is an example of conditional branching which is the cornerstone of computing - it was theorised about by Charles Babbage in the 19th century. Here is what a typical series of processor instructions looks like, "unassembled" using the debugger's 'u' command (I added the comments in green): 8052701c mov eax,edi // MOVe contents of EDI into EAX (copy them) 8052701e lea ecx,[eax+1] // Load Effective Address, here simply setting ECX = EAX +1
80527021 mov dl,byte ptr [eax] // MOVe contents of byte pointed at by EAX into the beginning of the D register.
80527023 inc eax // INCrement EAX by one
This is the only "language" that a processor understands. The vast majority of NTOSKRNL, user32.dll, ntdll.dll, and all other executable files consists of incredibly complex and intricate series of similar instructions which are usually translated ("compiled") from an original source written in a higher-level programming language such as C.
By initiating the execution of such a series of instructions at a given starting point, a "thread" (of execution) is brought into temporary existence. Each processor core is capable of following the instructions of only one such thread at any given time. Once every 15 milliseconds, the OS makes a decision as to whether it is appropriate to allow a different thread to have its slice of processor time, called "pre-empting" the currently running thread, or whether the current thread should be allowed to continue. By quickly swapping between many different threads in that manner, the OS makes it look like several things are happening at once, although in reality the number of things (threads) happening concurrently is exactly equal to the number of processor cores.
In order to temporarily suspend a thread in favour of another one, the OS must be able to record the current state of each of the processor registers for the thread being suspended. Later, when the suspended thread is to be reanimated, the previously saved register values for that thread are restored into corresponding processor registers, and execution is allowed to resume exactly at the point where it was previously suspended. The thread is not aware of being parked aside occasionally; from its point of view, it is merely executing a sequence of instructions until the end of the sequence is encountered.
A CONTEXT structure is used by the OS to store (in memory) the register values for a given thread. We can use the debugger's 'dt' ("dump type") command to see the members of the CONTEXT struct: kd> dt nt!_CONTEXT +0x000 ContextFlags : Uint4B ... omitted for clarity... +0x09c Edi : Uint4B +0x0a0 Esi : Uint4B
+0x0a4 Ebx : Uint4B
+0x0a8 Edx : Uint4B
+0x0ac Ecx : Uint4B
+0x0b0 Eax : Uint4B
+0x0b4 Ebp : Uint4B
+0x0b8 Eip : Uint4B
+0x0bc SegCs : Uint4B
+0x0c0 EFlags : Uint4B
+0x0c4 Esp : Uint4B
+0x0c8 SegSs : Uint4B
+0x0cc ExtendedRegisters :  UChar
The computer's memory normally contains many instances of the CONTEXT struct, each of which pertains to a single thread. A bugcheck minidump contains (at least) the context of the thread that caused the crash - i.e. the content of the processor's registers at the point where the crash condition was detected. That information can subsequently be used by developers for the purpose of understanding the code-level reasons behind a crash.