Repository: luser/rust-minidump
Pull request #457 (2022-02-18 02:03:02)

Attempt to parse out build and version info from csd_version if it can't be found
In minidumps created by breakpad on Linux, the OS's version number can only be found in its MinidumpSystemInfo's csd_version field. See the minidump writer [here](https://chromium.googlesource.com/breakpad/breakpad/+/refs/heads/main/src/client/linux/minidump_writer/minidump_writer.cc#1281), which shows where it sources the contents of `csd_version` from. A quick search for`major_version`/`minor_version`/`build_version` in the same file also turns up nothing.

This adds in a helper to parse out the OS version number and build number if it turns out there's a chance that a Linux minidump is being examined. This is an adaptation of [symbolic's implementation](https://github.com/getsentry/symbolic/blob/6b3f424a60dcffa31fbde1deccde3ac6e1bbc0bd/symbolic-minidump/src/processor.rs#L856-L865) of the same functionality, but with string splitting instead of using regexes. The original `csd_version` field is left intact on the `MinidumpSystemInfo` in case the value is needed for any reason by any consumers. 

I've stolen the linux minidump used by symbolic to be used here for testing purposes. This isn't notably larger than the other minidumps already in this repository, so I'm hoping it's fine for me to include this. The tests targeting the linux minidump aren't particularly thorough, though: There's literally just the one to check that this patch works. 

Fun breadcrumbs: Hopefully to nobody's surprise, this is related to https://github.com/getsentry/symbolicator/issues/620.


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..8e4aa66
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,396 @@
+# Contributing to rust-minidump
+
+The top-level [README.md](README.md) documents the structure of the project. Each crate's README should further discuss itself and how to use it.
+
+This document discusses how to actually make changes and properly test/document them.
+
+TL;DR
+
+* Document/Comment your code
+* Test your code
+* List your change in the upcoming release in RELEASES.md (optional)
+* cargo fmt (in project root)
+* cargo clippy (in project root)
+* (maybe) cargo insta review
+    * may also involve updating minidump-stackwalk's README which contains generated docs
+
+
+
+# Document Folklore
+
+Minidumps are basically a fractal of corner cases and situations that only happen on certain platforms. If you're introducing some special handling for these kinds of things, it's good to include a comment in the code.
+
+This helps maintain and propagate the "folklore" of our field so more people know about the issue in the future.
+
+Specifically, it's ideal to:
+
+* describe the situation and why it's a problem
+* provide a concrete example where this occurs
+* describe what is needed to handle it
+
+(Also of course, check in a test that covers exactly this situation!)
+
+Here's an example comment in the current `instruction_seemd_valid_by_symbols` impl:
+
+```rust
+// Our input is a candidate return address, but we *really* want to validate the address
+// of the call instruction *before* the return address. In theory this symbol-based
+// analysis shouldn't *care* whether we're looking at the call or the instruction
+// after it, but there is one corner case where the return address can be invalid
+// but the instruction before it isn't: noreturn.
+//
+// If the *callee* is noreturn, then the caller has no obligation to have any instructions
+// after the call! So e.g. on x86 if you CALL a noreturn function, the return address
+// that's implicitly pushed *could* be one-past-the-end of the "function".
+//
+// This has been observed in practice with `+[NSThread exit]`!
+//
+// We don't otherwise need the instruction pointer to be terribly precise, so
+// subtracting 1 from the address should be sufficient to handle this corner case.
+let instruction = instruction.saturating_sub(1);
+```
+
+
+# Adding a Minidump Stream
+
+The minidump crate is designed in a very modular way, because minidumps themselves are very modular. To help users navigate this modularity, there are several places where we redundantly list and document what the crate knows about and implements.
+
+If you ever introduce a new stream, you should consider adjusting the following.
+
+Core Functionality / Testing:
+
+* [minidump's stream fuzzing tests](https://github.com/luser/rust-minidump/blob/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/minidump/fuzz/fuzz_targets/parse.rs#L13)
+* [synth-minidump's stream generator](https://github.com/luser/rust-minidump/blob/master/synth-minidump/src/lib.rs#L31) (make it so that we can test the stream)
+    * This will require filling in quite a bit of code!
+* [minidump-stackwalk's raw stream dumper](https://github.com/luser/rust-minidump/blob/master/minidump-stackwalk/src/main.rs#L516) (make it so we can debug the stream)
+    * This generally requires implementing a "print" function for it
+
+Documentation / Reporting:
+
+* [unimplemented_streams](https://github.com/luser/rust-minidump/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/master/minidump/src/minidump.rs#L4582) (remove it)
+* [stream_vendor](https://github.com/luser/rust-minidump/blob/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/minidump/src/minidump.rs#L4726) (if this stream has a new custom vendor)
+* [get_stream's listing of implemented streams](https://github.com/luser/rust-minidump/blob/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/minidump/src/minidump.rs#L4520)
+* [minidump's listing of implemented streams](https://github.com/luser/rust-minidump/blob/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/minidump/src/lib.rs#L71)
+* [minidump's listing of stream families](https://github.com/luser/rust-minidump/blob/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/minidump/src/lib.rs#L276) (optional, but nice)
+
+Wow that's a lot!
+
+Since streams are rarely perfectly unique, you can usually fill in this stuff by basing it (read: copy-pasting) the code for similar streams. For instance, if you're implementing something similar to MemoryList, then CTRL+Fing for "memory_list" in synth-minidump will generally show you what needs to be added.
+
+
+
+
+
+# Testing
+
+Whether you're adding a new feature or fixing a bug, **you should always add a test to verify your change works as you expect.** Our code generally tries to be robust in the face of messed up stuff, so **it's very easy to write a test that you think checks for the issue but is actually handled by some other guards elsewhere.** If you're doing some kind of bugfix, it's highly recommended that you run the test both with and without your change to verify that it went from passing to failing!
+
+Major locations for tests include:
+
+* [minidump parsing unit tests](https://github.com/luser/rust-minidump/blob/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/minidump/src/minidump.rs#L4739) (see the `synth-minidump` section)
+* [minidump-processor integration tests](https://github.com/luser/rust-minidump/blob/master/minidump-processor/tests/test_processor.rs) (see the `synth-minidump` section)
+* [minidump-stackwalk integration tests](https://github.com/luser/rust-minidump/blob/master/minidump-stackwalk/tests/test-minidump-stackwalk.rs) (see the `insta` section)
+* [breakpad-symbols symbol file parser tests](https://github.com/luser/rust-minidump/blob/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/breakpad-symbols/src/lib.rs#L819)
+* [breakpad-symbols cfi interpreter tests](https://github.com/luser/rust-minidump/blob/master/breakpad-symbols/src/sym_file/walker.rs#L1032)
+
+minidump-processor stackwalker tests (see `test_assembler` section):
+
+* [x86_unittest.rs](https://github.com/luser/rust-minidump/blob/master/minidump-processor/src/stackwalker/x86_unittest.rs)
+* [amd64_unittest.rs](https://github.com/luser/rust-minidump/blob/master/minidump-processor/src/stackwalker/amd64_unittest.rs)
+* [arm_unittest.rs](https://github.com/luser/rust-minidump/blob/master/minidump-processor/src/stackwalker/arm_unittest.rs)
+* [arm64_unittest.rs](https://github.com/luser/rust-minidump/blob/master/minidump-processor/src/stackwalker/arm64_unittest.rs)
+
+fuzzing tests (see the `cargo fuzz` section):
+
+* [minidump fuzz_targets](https://github.com/luser/rust-minidump/tree/master/minidump/fuzz/fuzz_targets)
+* [minidump-processor fuzz targets](https://github.com/luser/rust-minidump/tree/master/minidump-processor/fuzz/fuzz_targets)
+* [breakpad-symbols fuzz targets](https://github.com/luser/rust-minidump/tree/master/breakpad-symbols/fuzz/fuzz_targets)
+
+
+
+## synth-minidump: Synthetic Minidumps for Tests
+
+rust-minidump includes a [synthetic minidump generator](synth-minidump) which lets you come up with a high-level description of the contents of a minidump, and then produces an actual minidump binary that we can feed it into the full parser.
+
+This is used throughout the codebase. Being able to use synth-minidump and add things to it is very important! This can in turn involve the test_assembler crate, which we discuss in the next section. Thankfully you don't need to master these systems: there's usually already code/tests for something *similar* which you can copy-paste and tweak. 
+
+Most streams in the minidump format are "some kind of list", and there are a few common formats for those lists. synth-minidump makes it relatively easy to add streams like that. See: ListStream, ExListStream, and SimpleStream.
+
+If your stream is more complicated you might instead want to model your code off of the more adhoc streams like SystemInfo, Exception, or MiscStream.
+
+It's hard for me to know what exactly to say here... the copy-paste method really is super effective here, so I don't really think about the high-level design of this stuff!
+
+------
+
+Example test using synth-minidump (from the minidump crate's unit tests):
+
+```rust ,ignore
+#[test]
+fn test_crashpad_info_annotations() {
+    // Build a synth minidump
+    let module = ModuleCrashpadInfo::new(42, Endian::Little)
+        .add_list_annotation("annotation")
+        .add_simple_annotation("simple", "module")
+        .add_annotation_object("string", AnnotationValue::String("value".to_owned()))
+        .add_annotation_object("invalid", AnnotationValue::Invalid)
+        .add_annotation_object("custom", AnnotationValue::Custom(0x8001, vec![42]));
+
+    let crashpad_info = CrashpadInfo::new(Endian::Little)
+        .add_module(module)
+        .add_simple_annotation("simple", "info");
+
+    let dump = SynthMinidump::with_endian(Endian::Little).add_crashpad_info(crashpad_info);
+    let dump = read_synth_dump(dump).unwrap();
+
+
+    // Actual tests
+    let crashpad_info = dump.get_stream::<MinidumpCrashpadInfo>().unwrap();
+    let module = &crashpad_info.module_list[0];
+
+    assert_eq!(crashpad_info.simple_annotations["simple"], "info");
+    assert_eq!(module.module_index, 42);
+    assert_eq!(module.list_annotations, vec!["annotation".to_owned()]);
+    assert_eq!(module.simple_annotations["simple"], "module");
+    assert_eq!(
+        module.annotation_objects["string"],
+        MinidumpAnnotation::String("value".to_owned())
+    );
+    assert_eq!(
+        module.annotation_objects["invalid"],
+        MinidumpAnnotation::Invalid
+    );
+}
+```
+
+
+## test_assembler: building binaries
+
+One of the major pieces of testing infra we rely on is [test_assembler](https://github.com/luser/rust-test-assembler), which allows us to construct artificial binaries with a combination of the builder pattern and *labels* which essentially represent variables which will have their values filled in later. The primary purpose of labels is that they let us refer to offsets in the binary we're writing, *even in the binary itself*, **even when those offsets aren't defined yet**.
+
+The place where you'll see this most is in our stackwalker tests, where we artificially construct the memory a stack we want to walk. Things like frame pointers are *precisely* pointers to later parts of the stack. Here's an [example frame pointery stack in the amd64 tests](https://github.com/luser/rust-minidump/blob/2001547fcf4aa0f28f52b8b1ab5da9bd99c8ac87/minidump-processor/src/stackwalker/amd64_unittest.rs#L82-L116):
+
+```rust ,ignore
+// Functions typically push their %rbp upon entry and set %rbp pointing
+// there.  If stackwalking finds a plausible address for the next frame's
+// %rbp directly below the return address, assume that it is indeed the
+// next frame's %rbp.
+let mut f = TestFixture::new();
+
+// test_assembler lets you connect up many Sections, but here we only need one.
+let mut stack = Section::new();           
+let stack_start = 0x8000000080000000;
+let return_address = 0x00007500b0000110;
+
+// The `start` constant is the address this section should claim to start at.
+// This affects the addresses that Labels will report when we query offsets
+// in the binary. In this way we can easily test corner cases and get exact
+// addresses when we want to test overflow/underflow/truncaction bugs.
+stack.start().set_const(stack_start);
+
+// We will be querying these 3 offsets in the binary.
+let frame0_rbp = Label::new();
+let frame1_sp = Label::new();
+let frame1_rbp = Label::new();
+
+// Now we build the actual stack binary. Key methods:
+//
+// * D64(val): push a 64-bit value (may be a label!)
+// * append_repeated(val, count): push `count` bytes that contain `val`
+// * mark(label): write the current offset to the given label
+//
+// Note that we `D64(&frame1_rbp)` **before** we `mark(&frame1_rbp)`!
+// This is allowed! test_assembler will record that the location of
+// the D64 must contain whatever value is written to the label. This
+// is done ~magically, and you really don't need to worry about how
+// this works. If you mess it up, test_assembler will panic.
+stack = stack
+    // frame 0
+    .append_repeated(0, 16)  // space (16 bytes, all 0)
+    .D64(0x00007400b0000000) // junk that's not
+    .D64(0x00007500b0000000) // a return address
+    .D64(0x00007400c0001000) // a couple of plausible addresses
+    .D64(0x00007500b000aaaa) // that are not within functions
+    .mark(&frame0_rbp)
+    .D64(&frame1_rbp)        // caller-pushed %rbp
+    .D64(return_address)     // actual return address
+    // frame 1
+    .mark(&frame1_sp)
+    .append_repeated(0, 32)  // body of frame1
+    .mark(&frame1_rbp)       // end of stack
+    .D64(0);
+
+f.raw.rip = 0x00007400c0000200;
+f.raw.rbp = frame0_rbp.value().unwrap();    // can read the label's value
+f.raw.rsp = stack.start().value().unwrap();
+
+let s = f.walk_stack(stack).await; // process the binary
+```
+
+**TEST_ASSEMBLER IS VERY "DO WHAT I MEAN" AND HAS SOME FOOTGUNS!** Most operations are generic over all the integer types, which means it will happily widen and truncate an integer to fit the operation you requested. This in turn means Rust can happily infer integers to be the wrong width and you will get very nasty to debug issues.
+
+It's advisable to be extra explicit about integer types and the exact values you want when using test_assembler!
+
+
+
+## insta: Snapshot Testing
+
+[Insta](https://github.com/mitsuhiko/insta) is a tool for writing snapshot tests. What are snapshot tests? Well basically we write a test which produces some output and ask insta "hey, is this value the same as last time?". If it's not, insta will fail the test and spit out a diff.
+
+We have [integration tests for the minidump-stackwalk CLI application that snapshot its output](https://github.com/luser/rust-minidump/blob/40c3390f5705890f932f78b7db4fc02866e012b8/minidump-stackwalk/tests/test-minidump-stackwalk.rs) to confirm that we never *accidentally* change the results. This also lets everyone see exactly what your changes look like in practice!
+
+Now of course, this raises two questions:
+
+* How does it know what the old value was?
+* What do I do when the value changes?
+
+When we ask insta about a snapshot, we give it a name like "json-pretty-evil-symbols". Insta maintains [a directory of files containing snapshots](https://github.com/luser/rust-minidump/tree/master/minidump-stackwalk/tests/snapshots) and will lookup the checked in snapshot with that name.
+
+Whenever a snapshot *doesn't* match (including when insta has no record of that snapshot name at all), it will write out some temporary files to disk recording the diffs. You can then review and accept/reject those diffs with [cargo-insta](https://insta.rs/docs/cli/), a CLI-application you should be able to easily install and run:
+
+```text
+cargo install cargo-insta
+cargo insta review
+
+<some fancy terminal ui will show up>
+
+<mash "A" a bunch of times if you like all the changes>
+```
+
+If the snapshot is totally new, be sure to `git add` the file. Otherwise, git will take care of the rest like any other changes.
+
+Here's what one of the snapshot tests looks like:
+
+```rust ,ignore
+#[test]
+fn test_evil_json() {
+    let bin = env!("CARGO_BIN_EXE_minidump-stackwalk");
+    let output = Command::new(bin)
+        .arg("--json")
+        .arg("--pretty")
+        .arg("--raw-json")
+        .arg("../testdata/evil.json")
+        .arg("../testdata/test.dmp")
+        .arg("../testdata/symbols/")
+        .stdout(Stdio::piped())
+        .stderr(Stdio::piped())
+        .output()
+        .unwrap();
+
+    let stdout = String::from_utf8(output.stdout).unwrap();
+    let stderr = String::from_utf8(output.stderr).unwrap();
+
+    assert!(output.status.success());
+    assert_eq!(stderr, "");
+
+    // Hey insta, did the result change?
+    insta::assert_snapshot!("json-pretty-evil-symbols", stdout);
+}
+```
+
+Part of the motivation for this is to ensure we don't break the the JSON output, which has [very detailed schema document](https://github.com/luser/rust-minidump/blob/40c3390f5705890f932f78b7db4fc02866e012b8/minidump-processor/json-schema.md), which we're trying to keep stable so people can actually rely on it while the actual implementation details are still in flux.
+
+Yes, [minidump-stackwalk](https://github.com/luser/rust-minidump/tree/master/minidump-stackwalk) is supposed to be stable and reasonable to use in production!
+
+Oh also if **test_minidump_stackwalk__markdown-help.snap changes, use the new contents to update minidump-stackwalk's README.md**.
+
+
+
+
+## cargo fuzz: Fuzzing The Project
+
+Many of the crates have fuzzing tests. These tests aren't run in CI, but CI does check that they continue to compile. As of this writing, the fuzzer used does not work on Windows (sorry!).
+
+You can run each fuzzer with `cargo fuzz`, but sadly this command is not workspace aware, so you will have to `cd` into the particular project you want to fuzz.
+
+See [the Rust Fuzz Book](https://rust-fuzz.github.io/book/introduction.html) for more details on using `cargo fuzz`.
+
+(Not much to say otherwise, this stuff Just Works, but has to run overnight to get interesting results.)
+
+
+
+
+
+## socc-pair: A Dedicated Production Diffing, Simulating, and Debugging Tool
+
+This tooling is optimized for testing how changes affect Mozilla's crash-reporting infras, but you might find it useful too.
+
+Because minidumps are so horribly fractal and corner-casey, we spent *a lot* of time terrified of subtle issues that would become huge disasters if we ever actually tried to deploy to production, so we made [socc-pair](https://github.com/Gankra/socc-pair/) which takes the id of a crash report from Mozilla's [crash reporting system](https://crash-stats.mozilla.org/) and pulls down the minidump, the old breakpad-based implementation's output, and extra metadata.
+
+It then runs a local rust-minidump (minidump-stackwalk) implementation on the minidump and does a domain-specific diff over the two inputs. The most substantial part of this is a fuzzy diff on the stackwalks that tries to better handle situations like when one implementation adds an extra frame but the two otherwise agree. It also uses the reported techniques each implementation used to try to identify whose output is more trustworthy when they totally diverge.
+
+It also includes a bunch of mocking and benchmarking functionality.
+
+The tool *can* be made to work without mozilla's servers, **but that workflow needs more work**.
+
+It also enables the [really detailed trace-logging for the stackwalker](https://github.com/luser/rust-minidump/tree/master/minidump-stackwalk#debugging-stackwalking), making it easier to do a post-mortem debug on the stackwalk and the decisions it made.
+
+Here's a trimmed down version of the kind of report socc-pair would produce:
+
+```diff
+comparing json...
+
+ : {
+   crash_info: {
+     address: 0x7fff1760aca0
+     crashing_thread: 8
+     type: EXCEPTION_BREAKPOINT
+   }
+   crashing_thread: {
+     frames: [
+       0: {
+         file: hg:hg.mozilla.org/mozilla-central:mozglue/static/rust/wrappers.cpp:1750da2d7f9db490b9d15b3ee696e89e6aa68cb7
+         frame: 0
+         function: RustMozCrash(char const*, int, char const*)
+         function_offset: 0x00000010
+-        did not match
++        line: 17
+-        line: 20
+         module: xul.dll
+
+.....
+.....
+.....
+
+   unloaded_modules: [
+     0: {
+       base_addr: 0x7fff48290000
+-      local val was null instead of:
+       code_id: 68798D2F9000
+       end_addr: 0x7fff48299000
+       filename: KBDUS.DLL
+     }
+     1: {
+       base_addr: 0x7fff56020000
+       code_id: DFD6E84B14000
+       end_addr: 0x7fff56034000
+       filename: resourcepolicyclient.dll
+     }
+   ]
+~  ignoring field write_combine_size: "0"
+ }
+ 
+ - Total errors: 288, warnings: 39
+
+benchmark results (ms):
+  2388, 1986, 2268, 1989, 2353, 
+average runtime: 00m:02s:196ms (2196ms)
+median runtime: 00m:02s:268ms (2268ms)
+min runtime: 00m:01s:986ms (1986ms)
+max runtime: 00m:02s:388ms (2388ms)
+
+max memory (rss) results (bytes):
+  267755520, 261152768, 272441344, 276131840, 279134208, 
+average max-memory: 258MB (271323136 bytes)
+median max-memory: 259MB (272441344 bytes)
+min max-memory: 249MB (261152768 bytes)
+max max-memory: 266MB (279134208 bytes)
+
+Output Files: 
+  * (download) Minidump: C:\Users\gankra\AppData\Local\Temp\socc-pair\dumps\b4f58e9f-49be-4ba5-a203-8ef160211027.dmp
+  * (download) Socorro Processed Crash: C:\Users\gankra\AppData\Local\Temp\socc-pair\dumps\b4f58e9f-49be-4ba5-a203-8ef160211027.json
+  * (download) Raw JSON: C:\Users\gankra\AppData\Local\Temp\socc-pair\dumps\b4f58e9f-49be-4ba5-a203-8ef160211027.raw.json
+  * Local minidump-stackwalk --json Output: C:\Users\gankra\AppData\Local\Temp\socc-pair\dumps\b4f58e9f-49be-4ba5-a203-8ef160211027.local.json
+  * Local minidump-stackwalk Logs: C:\Users\gankra\AppData\Local\Temp\socc-pair\dumps\b4f58e9f-49be-4ba5-a203-8ef160211027.log.txt
+
+``` 
+
diff --git a/Cargo.lock b/Cargo.lock
index 4395b6a..7cf1a4c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -19,9 +19,9 @@ dependencies = [
 
 [[package]]
 name = "arbitrary"
-version = "1.0.3"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "510c76ecefdceada737ea728f4f9a84bd2e1ef29f1ba555e560940fe279954de"
+checksum = "c38b6b6b79f671c25e1a3e785b7b82d7562ffc9cd3efdc98627e5668a2472490"
 dependencies = [
  "derive_arbitrary",
 ]
@@ -201,9 +201,9 @@ dependencies = [
 
 [[package]]
 name = "debugid"
-version = "0.7.2"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f91cf5a8c2f2097e2a32627123508635d47ce10563d999ec1a95addf08b502ba"
+checksum = "d6ee87af31d84ef885378aebca32be3d682b0e0dc119d5b4860a2c5bb5046730"
 dependencies = [
  "uuid",
 ]
@@ -632,9 +632,9 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
 
 [[package]]
 name = "memmap2"
-version = "0.5.2"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe3179b85e1fd8b14447cbebadb75e45a1002f541b925f0bfec366d56a81c56d"
+checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f"
 dependencies = [
  "libc",
 ]
@@ -1099,9 +1099,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.78"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
+checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
 dependencies = [
  "itoa 1.0.1",
  "ryu",
diff --git a/README.md b/README.md
index 304f768..25e0693 100644
--- a/README.md
+++ b/README.md
@@ -236,3 +236,8 @@ This software is provided under the MIT license. See [LICENSE](LICENSE).
 # Release Notes
 
 See [RELEASES.md](RELEASES.md) for release notes, commits, and details on the upcoming release.
+
+
+# Contributing
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) for what is expected of patches, and detailed discussion of testing/documenting rust-minidump.
\ No newline at end of file
diff --git a/RELEASES.md b/RELEASES.md
index 640ab8e..76d73e7 100644
--- a/RELEASES.md
+++ b/RELEASES.md
@@ -142,6 +142,26 @@ We now use the debugid crate and strongly type these ids to better handle the va
 As a result, we should more reliably handle the various types of id for each platform, especially for MacOS.
 
 
+## Improved General Purpose Register Handling
+
+rust-minidump supports many architectures to various levels of usefulness. One very baseline level of support is understanding the CpuContext (CONTEXT_X86, CONTEXT_ARM64, CONTEXT_SPARC, ...). With this release, this baseline support is now significantly improved. CpuContext now includes:
+
+* An associated `REGISTERS` constant, containing the canonical names of a platform's general purpose registers (use memoize_register to canonicalize your register names when there are aliases!).
+
+* A `valid_registers` iterator which yields the value of every known-valid general purpose register.
+
+* A `registers` iterator which yields the value of every geneeral purpose register *regardless of whether they contain unknown garbage*.
+
+In addition, all known `CONTEXT_*` types now implement this trait and are supported by operations which work on contexts. Newly implemented:
+
+* CONTEXT_SPARC
+* CONTEXT_MIPS
+* CONTEXT_PPC
+* CONTEXT_PPC64
+
+MinidumpContext has similarly been extended and had some long-standing `unimplemented!()` paths filled in!
+
+
 ## Input Validation Hardening
 
 As discussed in the top-level notes, allocations are generally bounded by the size of the minidump now, so a rogue list length shouldn't cause you to instantly OOM and die. Allocations and runtime should generally be linearly bounded by the size of the minidump. (But minidump-processor can be made to do a lot more work than this when given a symbol server.)
@@ -211,6 +231,8 @@ feature of the crate.
 
 * Errors now use `thiserror` instead of `failure`, making the interop with std better.
 
+* We no longer panic when the crashing thread has 0 frames (we instead omit it from the output).
+
 * Fatal error messages are now prefixed with the "type" of error.
     * This can be useful for aggregating and correlating failures, and also gives you a starting point if you want to check the docs/code for that error type.
 
diff --git a/breakpad-symbols/Cargo.toml b/breakpad-symbols/Cargo.toml
index 44e3d0e..80846df 100644
--- a/breakpad-symbols/Cargo.toml
+++ b/breakpad-symbols/Cargo.toml
@@ -16,7 +16,7 @@ travis-ci = { repository = "luser/rust-minidump" }
 [dependencies]
 async-trait = "0.1.51"
 circular = "0.3.0"
-debugid = "0.7"
+debugid = "0.7.3"
 log = "0.4.1"
 minidump-common = { version = "0.9.6", path = "../minidump-common" }
 nom = "~1.2.2"
diff --git a/minidump-common/Cargo.toml b/minidump-common/Cargo.toml
index abaf745..0d76ea1 100644
--- a/minidump-common/Cargo.toml
+++ b/minidump-common/Cargo.toml
@@ -15,7 +15,7 @@ travis-ci = { repository = "luser/rust-minidump" }
 [dependencies]
 arbitrary = { version = "1", optional = true, features = ["derive"] }
 bitflags = "1.3.2"
-debugid = "0.7"
+debugid = "0.7.3"
 enum-primitive-derive = "0.2.2"
 log = "0.4.1"
 num-traits = "0.2"
diff --git a/minidump-common/src/format.rs b/minidump-common/src/format.rs
index c10a382..62e7f84 100644
--- a/minidump-common/src/format.rs
+++ b/minidump-common/src/format.rs
@@ -197,19 +197,34 @@ pub enum MINIDUMP_STREAM_TYPE {
     ///
     /// See ['MINIDUMP_THREAD_NAME'].
     ThreadNamesStream = 24,
-    ceStreamNull = 25,
-    ceStreamSystemInfo = 26,
-    ceStreamException = 27,
-    ceStreamModuleList = 28,
-    ceStreamProcessList = 29,
-    ceStreamThreadList = 30,
-    ceStreamThreadContextList = 31,
-    ceStreamThreadCallStackList = 32,
-    ceStreamMemoryVirtualList = 33,
-    ceStreamMemoryPhysicalList = 34,
-    ceStreamBucketParameters = 35,
-    ceStreamProcessModuleMap = 36,
-    ceStreamDiagnosisList = 37,
+    /* Windows CE types, the list  is available here https://docs.microsoft.com/en-us/previous-versions/windows/embedded/ms939649(v=msdn.10) */
+    /// Start of the Windows CE enumerated list, unused.
+    ceStreamNull = 0x00008000,
+    /// System-wide information about the device and operating system where the fault occurred. Windows CE-specific.
+    ceStreamSystemInfo = 0x00008001,
+    /// Exception record and context for the exception that caused the dump file creation. Windows CE-specific.
+    ceStreamException = 0x00008002,
+    /// Modules that were active on the device at the time the dump file was created. Windows CE-specific.
+    ceStreamModuleList = 0x00008003,
+    /// Processes that were active on the device at the time the dump file was created. Windows CE-specific.
+    ceStreamProcessList = 0x00008004,
+    /// Threads that were active on the device at the time the dump file was created. Windows CE-specific.
+    ceStreamThreadList = 0x00008005,
+    /// List of thread context records. Windows CE-specific.
+    ceStreamThreadContextList = 0x00008006,
+    /// List of thread callstack records. Windows CE-specific.
+    ceStreamThreadCallStackList = 0x00008007,
+    /// List of virtual memory dumps. Windows CE-specific.
+    ceStreamMemoryVirtualList = 0x00008008,
+    /// List of physical memory dumps. Windows CE-specific.
+    ceStreamMemoryPhysicalList = 0x00008009,
+    /// Bucketing parameters for Watson server. Windows CE-specific.
+    ceStreamBucketParameters = 0x0000800a,
+    /// Undocumented Windows CE-specific stream.
+    ceStreamProcessModuleMap = 0x0000800b,
+    /// Undocumented Windows CE-specific stream.
+    ceStreamDiagnosisList = 0x0000800c,
+    /// Last stream reserved for use by Windows Operating Systems.
     LastReservedStream = 0x0000ffff,
     /* Breakpad extension types.  0x4767 = "Gg" */
     /// Additional process information (Breakpad extension)
diff --git a/minidump-processor/Cargo.toml b/minidump-processor/Cargo.toml
index 42efaeb..adda3d2 100644
--- a/minidump-processor/Cargo.toml
+++ b/minidump-processor/Cargo.toml
@@ -23,9 +23,9 @@ symbolic-syms = []
 async-trait = "0.1.51"
 breakpad-symbols = { version = "0.9.6", path = "../breakpad-symbols", optional = true }
 clap = "2.34"
-debugid = "0.7"
+debugid = "0.7.3"
 log = "0.4"
-memmap2 = "0.5.2"
+memmap2 = "0.5.3"
 minidump = { version = "0.9.6", path = "../minidump" }
 scroll = "0.10.2"
 serde = { version = "1.0", features = ["derive"] }
diff --git a/minidump-processor/fuzz/Cargo.lock b/minidump-processor/fuzz/Cargo.lock
index e5b8ac6..f800432 100644
--- a/minidump-processor/fuzz/Cargo.lock
+++ b/minidump-processor/fuzz/Cargo.lock
@@ -85,6 +85,7 @@ version = "0.9.6"
 dependencies = [
  "async-trait",
  "circular",
+ "debugid",
  "log",
  "minidump-common",
  "nom",
@@ -170,6 +171,15 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "debugid"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6ee87af31d84ef885378aebca32be3d682b0e0dc119d5b4860a2c5bb5046730"
+dependencies = [
+ "uuid",
+]
+
 [[package]]
 name = "derive_arbitrary"
 version = "1.0.2"
@@ -564,9 +574,9 @@ checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
 
 [[package]]
 name = "memmap2"
-version = "0.5.2"
+version = "0.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fe3179b85e1fd8b14447cbebadb75e45a1002f541b925f0bfec366d56a81c56d"
+checksum = "057a3db23999c867821a7a59feb06a578fcb03685e983dff90daf9e7d24ac08f"
 dependencies = [
  "libc",
 ]
@@ -582,6 +592,7 @@ name = "minidump"
 version = "0.9.6"
 dependencies = [
  "arbitrary",
+ "debugid",
  "encoding",
  "log",
  "memmap2",
@@ -591,6 +602,7 @@ dependencies = [
  "scroll",
  "thiserror",
  "time",
+ "uuid",
 ]
 
 [[package]]
@@ -599,6 +611,7 @@ version = "0.9.6"
 dependencies = [
  "arbitrary",
  "bitflags",
+ "debugid",
  "enum-primitive-derive",
  "log",
  "num-traits 0.2.14",
@@ -614,6 +627,7 @@ dependencies = [
  "async-trait",
  "breakpad-symbols",
  "clap",
+ "debugid",
  "log",
  "memmap2",
  "minidump",
@@ -1207,6 +1221,12 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "uuid"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
+
 [[package]]
 name = "vcpkg"
 version = "0.2.15"
diff --git a/minidump-processor/fuzz/fuzz_targets/process.rs b/minidump-processor/fuzz/fuzz_targets/process.rs
index 8beda8b..88dcaaa 100644
--- a/minidump-processor/fuzz/fuzz_targets/process.rs
+++ b/minidump-processor/fuzz/fuzz_targets/process.rs
@@ -23,8 +23,12 @@ fuzz_target!(|data: (&[u8], &[u8])| {
 
         let provider = minidump_processor::Symbolizer::new(supplier);
 
-        let _: Result<_, _> = minidump_processor_fuzz::fuzzing_block_on(
+        let val: Result<_, _> = minidump_processor_fuzz::fuzzing_block_on(
             minidump_processor::process_minidump(&dump, &provider),
         );
+
+        if let Ok(v) = val {
+            let _: Result<(), _> = v.print_json(&mut std::io::sink(), true);
+        }
     }
 });
diff --git a/minidump-processor/src/process_state.rs b/minidump-processor/src/process_state.rs
index 76b63bc..932c916 100644
--- a/minidump-processor/src/process_state.rs
+++ b/minidump-processor/src/process_state.rs
@@ -58,10 +58,41 @@ pub struct StackFrame {
     ///
     /// On some architectures, the return address as saved on the stack or in
     /// a register is fine for looking up the point of the call. On others, it
-    /// requires adjustment. ReturnAddress returns the address as saved by the
-    /// machine.
+    /// requires adjustment.
     pub instruction: u64,
 
+    /// The instruction address (program counter) that execution of this function
+    /// would resume at, if the callee returns.
+    ///
+    /// This is exactly **the return address of the of the callee**. We use this
+    /// nonstandard terminology because just calling this "return address"
+    /// would be ambiguous and too easy to mix up.
+    ///
+    /// **Note:** you should strongly prefer using [`StackFrame::instruction`][], which should
+    /// be the address of the instruction before this one which called the callee.
+    /// That is the instruction that this function was logically "executing" when the
+    /// program's state was captured, and therefore what people expect from
+    /// backtraces.
+    ///
+    /// This is more than a matter of user expections: **there are situations
+    /// where this value is nonsensical but the [`StackFrame::instruction`][] is valid.**
+    ///
+    /// Specifically, if the callee is "noreturn" then *this function should
+    /// never resume execution*. The compiler has no obligation to emit any
+    /// instructions after such a CALL, but CALL still implicitly pushes the
+    /// instruction after itself to the stack. Such a return address may
+    /// therefore be outside the "bounds" of this function!!!
+    ///
+    /// Yes, compilers *can* just immediately jump into the callee for
+    /// noreturn calls, but it's genuinely very helpful for them to emit a
+    /// CALL because it keeps the stack reasonable for backtraces and
+    /// debuggers, which are more interested in [`StackFrame::instruction`][] anyway!
+    ///
+    /// (If this is the top frame of the call stack, then `resume_address`
+    /// and `instruction` are exactly equal and should reflect the actual
+    /// program counter of this thread.)
+    pub resume_address: u64,
+
     /// The module in which the instruction resides.
     pub module: Option<MinidumpModule>,
 
@@ -233,6 +264,8 @@ impl StackFrame {
     pub fn from_context(context: MinidumpContext, trust: FrameTrust) -> StackFrame {
         StackFrame {
             instruction: context.get_instruction_pointer(),
+            // Initialized the same as `instruction`, but left unmodified during stack walking.
+            resume_address: context.get_instruction_pointer(),
             module: None,
             unloaded_modules: BTreeMap::new(),
             function_name: None,
@@ -245,12 +278,6 @@ impl StackFrame {
             context,
         }
     }
-
-    /// Return the actual return address, as saved on the stack or in a
-    /// register. See the comments for `StackFrame::instruction` for details.
-    pub fn return_address(&self) -> u64 {
-        self.instruction
-    }
 }
 
 impl FrameSymbolizer for StackFrame {
@@ -803,26 +830,30 @@ Unknown streams encountered:
             // of a normal "threads" entry, while the original schema strips
             // many of the fields here. We don't to keep things more uniform.
 
-            let registers = json_registers(&self.threads[requesting_thread].frames[0].context);
-
-            // Yuck, spidering through json...
-            let mut thread =
-                output.get_mut("threads").unwrap().as_array().unwrap()[requesting_thread].clone();
-            let thread_obj = thread.as_object_mut().unwrap();
-            let frames = thread_obj
-                .get_mut("frames")
-                .unwrap()
-                .as_array_mut()
-                .unwrap();
-            let frame = frames[0].as_object_mut().unwrap();
-
-            frame.insert(String::from("registers"), registers);
-            thread_obj.insert(String::from("threads_index"), json!(requesting_thread));
-
-            output
-                .as_object_mut()
-                .unwrap()
-                .insert(String::from("crashing_thread"), thread);
+            // We can't do any of this work if we don't have at least one frame.
+            if let Some(f) = self.threads[requesting_thread].frames.get(0) {
+                let registers = json_registers(&f.context);
+
+                // Yuck, spidering through json...
+                let mut thread = output.get_mut("threads").unwrap().as_array().unwrap()
+                    [requesting_thread]
+                    .clone();
+                let thread_obj = thread.as_object_mut().unwrap();
+                let frames = thread_obj
+                    .get_mut("frames")
+                    .unwrap()
+                    .as_array_mut()
+                    .unwrap();
+                let frame = frames[0].as_object_mut().unwrap();
+
+                frame.insert(String::from("registers"), registers);
+                thread_obj.insert(String::from("threads_index"), json!(requesting_thread));
+
+                output
+                    .as_object_mut()
+                    .unwrap()
+                    .insert(String::from("crashing_thread"), thread);
+            }
         }
 
         if pretty {
diff --git a/minidump-processor/src/processor.rs b/minidump-processor/src/processor.rs
index 5723003..14650e3 100644
--- a/minidump-processor/src/processor.rs
+++ b/minidump-processor/src/processor.rs
@@ -93,17 +93,7 @@ where
         .get_stream::<MinidumpSystemInfo>()
         .or(Err(ProcessError::MissingSystemInfo))?;
 
-    let os_version = format!(
-        "{}.{}.{}",
-        dump_system_info.raw.major_version,
-        dump_system_info.raw.minor_version,
-        dump_system_info.raw.build_number
-    );
-
-    let os_build = dump_system_info
-        .csd_version()
-        .map(|v| v.trim().to_owned())
-        .filter(|v| !v.is_empty());
+    let (os_version, os_build) = dump_system_info.os_parts();
 
     let linux_standard_base = dump.get_stream::<MinidumpLinuxLsbRelease>().ok();
     let linux_cpu_info = dump
diff --git a/minidump-processor/src/stackwalker/amd64.rs b/minidump-processor/src/stackwalker/amd64.rs
index fc9eb66..65eb8b0 100644
--- a/minidump-processor/src/stackwalker/amd64.rs
+++ b/minidump-processor/src/stackwalker/amd64.rs
@@ -281,24 +281,31 @@ where
             // It's unclear why, perhaps the test is buggy, but for now we
             // preserve that behaviour.
             if let Some(last_bp) = last_bp {
-                let address_of_bp = address_of_ip - POINTER_WIDTH;
-                // Can assume this resolves because we already walked over it when
-                // checking address_of_ip values.
-                let bp = stack_memory.get_memory_at_address(address_of_bp as u64)?;
-                if last_bp == address_of_bp
-                    && bp > address_of_ip
-                    && bp - address_of_bp <= MAX_REASONABLE_GAP_BETWEEN_FRAMES
-                {
-                    // Final sanity check that resulting bp is still inside stack memory.
-                    if stack_memory
-                        .get_memory_at_address::<Pointer>(bp as u64)
-                        .is_some()
+                // If we're on the first iteration of the scan, there can't possibly be a
+                // frame pointer, because the entire stack frame is taken up by the return
+                // pointer. And if we're not on the first iteration, then the last iteration
+                // already loaded the location we expect the frame pointer to be in, so we can
+                // unconditionally load it here.
+                if i > 0 {
+                    let address_of_bp = address_of_ip - POINTER_WIDTH;
+                    // Can assume this resolves because we already walked over it when
+                    // checking address_of_ip values.
+                    let bp = stack_memory.get_memory_at_address(address_of_bp as u64)?;
+                    if last_bp == address_of_bp
+                        && bp > address_of_ip
+                        && bp - address_of_bp <= MAX_REASONABLE_GAP_BETWEEN_FRAMES
                     {
-                        caller_bp = Some(bp);
+                        // Final sanity check that resulting bp is still inside stack memory.
+                        if stack_memory
+                            .get_memory_at_address::<Pointer>(bp as u64)
+                            .is_some()
+                        {
+                            caller_bp = Some(bp);
+                        }
+                    } else if last_bp >= caller_sp {
+                        // Don't sanity check that the address is inside the stack? Hmm.
+                        caller_bp = Some(last_bp);
                     }
-                } else if last_bp >= caller_sp {
-                    // Don't sanity check that the address is inside the stack? Hmm.
-                    caller_bp = Some(last_bp);
                 }
             }
 
diff --git a/minidump-processor/src/stackwalker/arm.rs b/minidump-processor/src/stackwalker/arm.rs
index 707517e..3f70f36 100644
--- a/minidump-processor/src/stackwalker/arm.rs
+++ b/minidump-processor/src/stackwalker/arm.rs
@@ -24,7 +24,7 @@ const FRAME_POINTER: &str = Registers::FramePointer.name();
 const STACK_POINTER: &str = Registers::StackPointer.name();
 const PROGRAM_COUNTER: &str = Registers::ProgramCounter.name();
 const LINK_REGISTER: &str = Registers::LinkRegister.name();
-const CALLEE_SAVED_REGS: &[&str] = &["r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11"];
+const CALLEE_SAVED_REGS: &[&str] = &["r4", "r5", "r6", "r7", "r8", "r9", "r10", "fp"];
 
 async fn get_caller_by_cfi<P>(
     ctx: &ArmContext,
diff --git a/minidump-processor/src/stackwalker/arm64.rs b/minidump-processor/src/stackwalker/arm64.rs
index a536c70..1e40eb4 100644
--- a/minidump-processor/src/stackwalker/arm64.rs
+++ b/minidump-processor/src/stackwalker/arm64.rs
@@ -25,7 +25,7 @@ const STACK_POINTER: &str = Registers::StackPointer.name();
 const LINK_REGISTER: &str = Registers::LinkRegister.name();
 const PROGRAM_COUNTER: &str = Registers::ProgramCounter.name();
 const CALLEE_SAVED_REGS: &[&str] = &[
-    "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", "x29",
+    "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", "fp",
 ];
 
 async fn get_caller_by_cfi<P>(
diff --git a/minidump-processor/src/stackwalker/arm64_old.rs b/minidump-processor/src/stackwalker/arm64_old.rs
index 7fdecc2..1f34f99 100644
--- a/minidump-processor/src/stackwalker/arm64_old.rs
+++ b/minidump-processor/src/stackwalker/arm64_old.rs
@@ -25,7 +25,7 @@ const STACK_POINTER: &str = Registers::StackPointer.name();
 const LINK_REGISTER: &str = Registers::LinkRegister.name();
 const PROGRAM_COUNTER: &str = Registers::ProgramCounter.name();
 const CALLEE_SAVED_REGS: &[&str] = &[
-    "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", "x29",
+    "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27", "x28", "fp",
 ];
 
 async fn get_caller_by_cfi<P>(
diff --git a/minidump-processor/src/stackwalker/x86.rs b/minidump-processor/src/stackwalker/x86.rs
index ebecb4e..a88df08 100644
--- a/minidump-processor/src/stackwalker/x86.rs
+++ b/minidump-processor/src/stackwalker/x86.rs
@@ -268,24 +268,31 @@ where
             // smaller than 128 KB.
             const MAX_REASONABLE_GAP_BETWEEN_FRAMES: Pointer = 128 * 1024;
 
-            let address_of_bp = address_of_ip - POINTER_WIDTH;
-            let bp = stack_memory.get_memory_at_address(address_of_bp as u64)?;
-            if bp > address_of_ip && bp - address_of_bp <= MAX_REASONABLE_GAP_BETWEEN_FRAMES {
-                // Sanity check that resulting bp is still inside stack memory.
-                if stack_memory
-                    .get_memory_at_address::<Pointer>(bp as u64)
-                    .is_some()
-                {
-                    caller_bp = Some(bp);
-                }
-            } else if let Some(last_bp) = last_bp {
-                if last_bp >= caller_sp {
+            // If we're on the first iteration of the scan, there can't possibly be a frame pointer,
+            // because the entire stack frame is taken up by the return pointer. And if we're
+            // not on the first iteration, then the last iteration already loaded the location
+            // we expect the frame pointer to be in, so we can unconditionally load it here.
+            if i > 0 {
+                let address_of_bp = address_of_ip - POINTER_WIDTH;
+                let bp = stack_memory.get_memory_at_address(address_of_bp as u64)?;
+
+                if bp > address_of_ip && bp - address_of_bp <= MAX_REASONABLE_GAP_BETWEEN_FRAMES {
                     // Sanity check that resulting bp is still inside stack memory.
                     if stack_memory
-                        .get_memory_at_address::<Pointer>(last_bp as u64)
+                        .get_memory_at_address::<Pointer>(bp as u64)
                         .is_some()
                     {
-                        caller_bp = Some(last_bp);
+                        caller_bp = Some(bp);
+                    }
+                } else if let Some(last_bp) = last_bp {
+                    if last_bp >= caller_sp {
+                        // Sanity check that resulting bp is still inside stack memory.
+                        if stack_memory
+                            .get_memory_at_address::<Pointer>(last_bp as u64)
+                            .is_some()
+                        {
+                            caller_bp = Some(last_bp);
+                        }
                     }
                 }
             }
diff --git a/minidump-processor/src/system_info.rs b/minidump-processor/src/system_info.rs
index 54699b1..51de7e4 100644
--- a/minidump-processor/src/system_info.rs
+++ b/minidump-processor/src/system_info.rs
@@ -12,8 +12,9 @@ pub struct SystemInfo {
     pub os_version: Option<String>,
     /// A string identifying the exact build of the operating system.
     ///
-    /// This may look like "Service Pack 2" or "8L2127", if present. On Windows, this is the CSD
-    /// version, on Linux extended build information.
+    /// This may look like "Service Pack 2" or "8L2127", if present. On Windows,
+    /// this is the CSD version, on Linux, extended build information and macOS,
+    /// the product build version.
     pub os_build: Option<String>,
     /// The CPU on which the dump was produced
     pub cpu: Cpu,
diff --git a/minidump/Cargo.toml b/minidump/Cargo.toml
index 1cb23f9..f93f6ff 100644
--- a/minidump/Cargo.toml
+++ b/minidump/Cargo.toml
@@ -13,10 +13,10 @@ edition = "2018"
 
 [dependencies]
 arbitrary = { version = "1", optional = true, features = ["derive"] }
-debugid = "0.7"
+debugid = "0.7.3"
 encoding = "0.2"
 log = "0.4.1"
-memmap2 = "0.5.2"
+memmap2 = "0.5.3"
 minidump-common = { version = "0.9.6", path = "../minidump-common" }
 num-traits = "0.2"
 range-map = "0.1.5"
diff --git a/minidump/src/context.rs b/minidump/src/context.rs
index ca114c2..4d155c0 100644
--- a/minidump/src/context.rs
+++ b/minidump/src/context.rs
@@ -37,6 +37,9 @@ pub trait CpuContext {
     /// The word size of general-purpose registers in the context.
     type Register: fmt::LowerHex;
 
+    /// General purpose registers in this context type.
+    const REGISTERS: &'static [&'static str];
+
     /// Gets whether the given register is valid
     ///
     /// This is exposed so that the context can map aliases. For instance
@@ -48,6 +51,7 @@ pub trait CpuContext {
             self.memoize_register(reg).is_some()
         }
     }
+
     /// Get a register value if it is valid.
     ///
     /// Get the value of the register named `reg` from this CPU context
@@ -70,7 +74,11 @@ pub trait CpuContext {
     fn set_register(&mut self, reg: &str, val: Self::Register) -> Option<()>;
 
     /// Gets a static version of the given register name, if possible.
-    fn memoize_register(&self, reg: &str) -> Option<&'static str>;
+    ///
+    /// Returns the default name of the register for register name aliases.
+    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
+        default_memoize_register(Self::REGISTERS, reg)
+    }
 
     /// Return a String containing the value of `reg` formatted to its natural width.
     fn format_register(&self, reg: &str) -> String {
@@ -81,15 +89,80 @@ pub trait CpuContext {
         )
     }
 
+    /// An iterator over all registers in this context.
+    ///
+    /// This iterator yields registers and values regardless of whether the register is valid. To
+    /// get valid values, use [`valid_registers`](Self::valid_registers), instead.
+    fn registers(&self) -> CpuRegisters<'_, Self> {
+        self.valid_registers(&MinidumpContextValidity::All)
+    }
+
+    /// An iterator over valid registers in this context.
+    ///
+    /// This iterator yields valid registers and their values.
+    fn valid_registers<'a>(&'a self, valid: &'a MinidumpContextValidity) -> CpuRegisters<'a, Self> {
+        let regs = match valid {
+            MinidumpContextValidity::All => CpuRegistersInner::Slice(Self::REGISTERS.iter()),
+            MinidumpContextValidity::Some(valid) => CpuRegistersInner::Set(valid.iter()),
+        };
+
+        CpuRegisters {
+            regs,
+            context: self,
+        }
+    }
+
     /// Gets the name of the stack pointer register (for use with get_register/set_register).
     fn stack_pointer_register_name(&self) -> &'static str;
+
     /// Gets the name of the instruction pointer register (for use with get_register/set_register).
     fn instruction_pointer_register_name(&self) -> &'static str;
 }
 
+/// Default implementation for `CpuContext::memoize_register`.
+fn default_memoize_register(registers: &[&'static str], reg: &str) -> Option<&'static str> {
+    let idx = registers.iter().position(|val| *val == reg)?;
+    Some(registers[idx])
+}
+
+#[derive(Debug, Clone)]
+enum CpuRegistersInner<'a> {
+    Slice(std::slice::Iter<'a, &'static str>),
+    Set(std::collections::hash_set::Iter<'a, &'static str>),
+}
+
+/// An iterator over registers and values in a [`CpuContext`].
+///
+/// Returned by [`CpuContext::registers`] and [`CpuContext::valid_registers`].
+#[derive(Clone, Debug)]
+pub struct CpuRegisters<'a, T: ?Sized> {
+    regs: CpuRegistersInner<'a>,
+    context: &'a T,
+}
+
+impl<'a, T> Iterator for CpuRegisters<'a, T>
+where
+    T: CpuContext,
+{
+    type Item = (&'static str, T::Register);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let reg = match &mut self.regs {
+            CpuRegistersInner::Slice(iter) => iter.next(),
+            CpuRegistersInner::Set(iter) => iter.next(),
+        }?;
+
+        Some((reg, self.context.get_register_always(reg)))
+    }
+}
+
 impl CpuContext for md::CONTEXT_X86 {
     type Register = u32;
 
+    const REGISTERS: &'static [&'static str] = &[
+        "eip", "esp", "ebp", "ebx", "esi", "edi", "eax", "ecx", "edx", "efl",
+    ];
+
     fn get_register_always(&self, reg: &str) -> u32 {
         match reg {
             "eip" => self.eip,
@@ -102,7 +175,7 @@ impl CpuContext for md::CONTEXT_X86 {
             "ecx" => self.ecx,
             "edx" => self.edx,
             "efl" => self.eflags,
-            _ => unreachable!("Invalid x86 register!"),
+            _ => unreachable!("Invalid x86 register! {}", reg),
         }
     }
 
@@ -123,11 +196,6 @@ impl CpuContext for md::CONTEXT_X86 {
         Some(())
     }
 
-    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
-        let idx = X86_REGS.iter().position(|val| *val == reg)?;
-        Some(X86_REGS[idx])
-    }
-
     fn stack_pointer_register_name(&self) -> &'static str {
         "esp"
     }
@@ -140,6 +208,11 @@ impl CpuContext for md::CONTEXT_X86 {
 impl CpuContext for md::CONTEXT_AMD64 {
     type Register = u64;
 
+    const REGISTERS: &'static [&'static str] = &[
+        "rax", "rdx", "rcx", "rbx", "rsi", "rdi", "rbp", "rsp", "r8", "r9", "r10", "r11", "r12",
+        "r13", "r14", "r15", "rip",
+    ];
+
     fn get_register_always(&self, reg: &str) -> u64 {
         match reg {
             "rax" => self.rax,
@@ -159,7 +232,7 @@ impl CpuContext for md::CONTEXT_AMD64 {
             "r14" => self.r14,
             "r15" => self.r15,
             "rip" => self.rip,
-            _ => unreachable!("Invalid x86-64 register!"),
+            _ => unreachable!("Invalid x86-64 register! {}", reg),
         }
     }
 
@@ -187,11 +260,6 @@ impl CpuContext for md::CONTEXT_AMD64 {
         Some(())
     }
 
-    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
-        let idx = X86_64_REGS.iter().position(|val| *val == reg)?;
-        Some(X86_64_REGS[idx])
-    }
-
     fn stack_pointer_register_name(&self) -> &'static str {
         "rsp"
     }
@@ -204,6 +272,21 @@ impl CpuContext for md::CONTEXT_AMD64 {
 impl CpuContext for md::CONTEXT_ARM {
     type Register = u32;
 
+    const REGISTERS: &'static [&'static str] = &[
+        "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r12", "fp", "sp", "lr",
+        "pc",
+    ];
+
+    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
+        match reg {
+            "r11" => Some("fp"),
+            "r13" => Some("sp"),
+            "r14" => Some("lr"),
+            "r15" => Some("pc"),
+            _ => default_memoize_register(Self::REGISTERS, reg),
+        }
+    }
+
     fn register_is_valid(&self, reg: &str, valid: &MinidumpContextValidity) -> bool {
         if let MinidumpContextValidity::Some(ref which) = valid {
             match reg {
@@ -271,11 +354,6 @@ impl CpuContext for md::CONTEXT_ARM {
         Some(())
     }
 
-    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
-        let idx = ARM_REGS.iter().position(|val| *val == reg)?;
-        Some(ARM_REGS[idx])
-    }
-
     fn stack_pointer_register_name(&self) -> &'static str {
         "sp"
     }
@@ -288,6 +366,20 @@ impl CpuContext for md::CONTEXT_ARM {
 impl CpuContext for md::CONTEXT_ARM64_OLD {
     type Register = u64;
 
+    const REGISTERS: &'static [&'static str] = &[
+        "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x10", "x11", "x12", "x13",
+        "x14", "x15", "x16", "x17", "x18", "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26",
+        "x27", "x28", "fp", "lr", "sp", "pc",
+    ];
+
+    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
+        match reg {
+            "x29" => Some("fp"),
+            "x30" => Some("lr"),
+            _ => default_memoize_register(Self::REGISTERS, reg),
+        }
+    }
+
     fn register_is_valid(&self, reg: &str, valid: &MinidumpContextValidity) -> bool {
         if let MinidumpContextValidity::Some(ref which) = valid {
             match reg {
@@ -383,11 +475,6 @@ impl CpuContext for md::CONTEXT_ARM64_OLD {
         Some(())
     }
 
-    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
-        let idx = ARM64_REGS.iter().position(|val| *val == reg)?;
-        Some(ARM64_REGS[idx])
-    }
-
     fn stack_pointer_register_name(&self) -> &'static str {
         "sp"
     }
@@ -400,6 +487,20 @@ impl CpuContext for md::CONTEXT_ARM64_OLD {
 impl CpuContext for md::CONTEXT_ARM64 {
     type Register = u64;
 
+    const REGISTERS: &'static [&'static str] = &[
+        "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x10", "x11", "x12", "x13",
+        "x14", "x15", "x16", "x17", "x18", "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26",
+        "x27", "x28", "fp", "lr", "sp", "pc",
+    ];
+
+    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
+        match reg {
+            "x29" => Some("fp"),
+            "x30" => Some("lr"),
+            _ => default_memoize_register(Self::REGISTERS, reg),
+        }
+    }
+
     fn register_is_valid(&self, reg: &str, valid: &MinidumpContextValidity) -> bool {
         if let MinidumpContextValidity::Some(ref which) = valid {
             match reg {
@@ -495,9 +596,279 @@ impl CpuContext for md::CONTEXT_ARM64 {
         Some(())
     }
 
-    fn memoize_register(&self, reg: &str) -> Option<&'static str> {
-        let idx = ARM64_REGS.iter().position(|val| *val == reg)?;
-        Some(ARM64_REGS[idx])
+    fn stack_pointer_register_name(&self) -> &'static str {
+        "sp"
+    }
+
+    fn instruction_pointer_register_name(&self) -> &'static str {
+        "pc"
+    }
+}
+
+impl CpuContext for md::CONTEXT_PPC {
+    type Register = u32;
+
+    const REGISTERS: &'static [&'static str] = &[
+        "srr0", "srr1", "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11",
+        "r12", "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23", "r24",
+        "r25", "r26", "r27", "r28", "r29", "r30", "r31", "cr", "xer", "lr", "ctr", "mq", "vrsave",
+    ];
+
+    fn get_register_always(&self, reg: &str) -> Self::Register {
+        match reg {
+            "srr0" => self.srr0,
+            "srr1" => self.srr1,
+            "r0" => self.gpr[0],
+            "r1" => self.gpr[1],
+            "r2" => self.gpr[2],
+            "r3" => self.gpr[3],
+            "r4" => self.gpr[4],
+            "r5" => self.gpr[5],
+            "r6" => self.gpr[6],
+            "r7" => self.gpr[7],
+            "r8" => self.gpr[8],
+            "r9" => self.gpr[9],
+            "r10" => self.gpr[10],
+            "r11" => self.gpr[11],
+            "r12" => self.gpr[12],
+            "r13" => self.gpr[13],
+            "r14" => self.gpr[14],
+            "r15" => self.gpr[15],
+            "r16" => self.gpr[16],
+            "r17" => self.gpr[17],
+            "r18" => self.gpr[18],
+            "r19" => self.gpr[19],
+            "r20" => self.gpr[20],
+            "r21" => self.gpr[21],
+            "r22" => self.gpr[22],
+            "r23" => self.gpr[23],
+            "r24" => self.gpr[24],
+            "r25" => self.gpr[25],
+            "r26" => self.gpr[26],
+            "r27" => self.gpr[27],
+            "r28" => self.gpr[28],
+            "r29" => self.gpr[29],
+            "r30" => self.gpr[30],
+            "r31" => self.gpr[31],
+            "cr" => self.cr,
+            "xer" => self.xer,
+            "lr" => self.lr,
+            "ctr" => self.ctr,
+            "mq" => self.mq,
+            "vrsave" => self.vrsave,
+            _ => unreachable!("Invalid ppc register! {}", reg),
+        }
+    }
+
+    fn set_register(&mut self, reg: &str, val: Self::Register) -> Option<()> {
+        match reg {
+            "srr0" => self.srr0 = val,
+            "srr1" => self.srr1 = val,
+            "r0" => self.gpr[0] = val,
+            "r1" => self.gpr[1] = val,
+            "r2" => self.gpr[2] = val,
+            "r3" => self.gpr[3] = val,
+            "r4" => self.gpr[4] = val,
+            "r5" => self.gpr[5] = val,
+            "r6" => self.gpr[6] = val,
+            "r7" => self.gpr[7] = val,
+            "r8" => self.gpr[8] = val,
+            "r9" => self.gpr[9] = val,
+            "r10" => self.gpr[10] = val,
+            "r11" => self.gpr[11] = val,
+            "r12" => self.gpr[12] = val,
+            "r13" => self.gpr[13] = val,
+            "r14" => self.gpr[14] = val,
+            "r15" => self.gpr[15] = val,
+            "r16" => self.gpr[16] = val,
+            "r17" => self.gpr[17] = val,
+            "r18" => self.gpr[18] = val,
+            "r19" => self.gpr[19] = val,
+            "r20" => self.gpr[20] = val,
+            "r21" => self.gpr[21] = val,
+            "r22" => self.gpr[22] = val,
+            "r23" => self.gpr[23] = val,
+            "r24" => self.gpr[24] = val,
+            "r25" => self.gpr[25] = val,
+            "r26" => self.gpr[26] = val,
+            "r27" => self.gpr[27] = val,
+            "r28" => self.gpr[28] = val,
+            "r29" => self.gpr[29] = val,
+            "r30" => self.gpr[30] = val,
+            "r31" => self.gpr[31] = val,
+            "cr" => self.cr = val,
+            "xer" => self.xer = val,
+            "lr" => self.lr = val,
+            "ctr" => self.ctr = val,
+            "mq" => self.mq = val,
+            "vrsave" => self.vrsave = val,
+            _ => return None,
+        }
+        Some(())
+    }
+
+    fn stack_pointer_register_name(&self) -> &'static str {
+        "r1"
+    }
+
+    fn instruction_pointer_register_name(&self) -> &'static str {
+        "srr0"
+    }
+}
+
+impl CpuContext for md::CONTEXT_PPC64 {
+    type Register = u64;
+
+    const REGISTERS: &'static [&'static str] = &[
+        "srr0", "srr1", "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11",
+        "r12", "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23", "r24",
+        "r25", "r26", "r27", "r28", "r29", "r30", "r31", "cr", "xer", "lr", "ctr", "vrsave",
+    ];
+
+    fn get_register_always(&self, reg: &str) -> Self::Register {
+        match reg {
+            "srr0" => self.srr0,
+            "srr1" => self.srr1,
+            "r0" => self.gpr[0],
+            "r1" => self.gpr[1],
+            "r2" => self.gpr[2],
+            "r3" => self.gpr[3],
+            "r4" => self.gpr[4],
+            "r5" => self.gpr[5],
+            "r6" => self.gpr[6],
+            "r7" => self.gpr[7],
+            "r8" => self.gpr[8],
+            "r9" => self.gpr[9],
+            "r10" => self.gpr[10],
+            "r11" => self.gpr[11],
+            "r12" => self.gpr[12],
+            "r13" => self.gpr[13],
+            "r14" => self.gpr[14],
+            "r15" => self.gpr[15],
+            "r16" => self.gpr[16],
+            "r17" => self.gpr[17],
+            "r18" => self.gpr[18],
+            "r19" => self.gpr[19],
+            "r20" => self.gpr[20],
+            "r21" => self.gpr[21],
+            "r22" => self.gpr[22],
+            "r23" => self.gpr[23],
+            "r24" => self.gpr[24],
+            "r25" => self.gpr[25],
+            "r26" => self.gpr[26],
+            "r27" => self.gpr[27],
+            "r28" => self.gpr[28],
+            "r29" => self.gpr[29],
+            "r30" => self.gpr[30],
+            "r31" => self.gpr[31],
+            "cr" => self.cr,
+            "xer" => self.xer,
+            "lr" => self.lr,
+            "ctr" => self.ctr,
+            "vrsave" => self.vrsave,
+            _ => unreachable!("Invalid ppc64 register! {}", reg),
+        }
+    }
+
+    fn set_register(&mut self, reg: &str, val: Self::Register) -> Option<()> {
+        match reg {
+            "srr0" => self.srr0 = val,
+            "srr1" => self.srr1 = val,
+            "r0" => self.gpr[0] = val,
+            "r1" => self.gpr[1] = val,
+            "r2" => self.gpr[2] = val,
+            "r3" => self.gpr[3] = val,
+            "r4" => self.gpr[4] = val,
+            "r5" => self.gpr[5] = val,
+            "r6" => self.gpr[6] = val,
+            "r7" => self.gpr[7] = val,
+            "r8" => self.gpr[8] = val,
+            "r9" => self.gpr[9] = val,
+            "r10" => self.gpr[10] = val,
+            "r11" => self.gpr[11] = val,
+            "r12" => self.gpr[12] = val,
+            "r13" => self.gpr[13] = val,
+            "r14" => self.gpr[14] = val,
+            "r15" => self.gpr[15] = val,
+            "r16" => self.gpr[16] = val,
+            "r17" => self.gpr[17] = val,
+            "r18" => self.gpr[18] = val,
+            "r19" => self.gpr[19] = val,
+            "r20" => self.gpr[20] = val,
+            "r21" => self.gpr[21] = val,
+            "r22" => self.gpr[22] = val,
+            "r23" => self.gpr[23] = val,
+            "r24" => self.gpr[24] = val,
+            "r25" => self.gpr[25] = val,
+            "r26" => self.gpr[26] = val,
+            "r27" => self.gpr[27] = val,
+            "r28" => self.gpr[28] = val,
+            "r29" => self.gpr[29] = val,
+            "r30" => self.gpr[30] = val,
+            "r31" => self.gpr[31] = val,
+            "cr" => self.cr = val,
+            "xer" => self.xer = val,
+            "lr" => self.lr = val,
+            "ctr" => self.ctr = val,
+            "vrsave" => self.vrsave = val,
+            _ => return None,
+        }
+        Some(())
+    }
+
+    fn stack_pointer_register_name(&self) -> &'static str {
+        "r1"
+    }
+
+    fn instruction_pointer_register_name(&self) -> &'static str {
+        "srr0"
+    }
+}
+
+impl CpuContext for md::CONTEXT_MIPS {
+    type Register = u64;
+
+    const REGISTERS: &'static [&'static str] = &[
+        "gp", "sp", "fp", "ra", "pc", "s0", "s1", "s2", "s3", "s4", "s5", "s6", "s7",
+    ];
+
+    fn get_register_always(&self, reg: &str) -> Self::Register {
+        match reg {
+            "gp" => self.iregs[md::MipsRegisterNumbers::GlobalPointer as usize],
+            "sp" => self.iregs[md::MipsRegisterNumbers::StackPointer as usize],
+            "fp" => self.iregs[md::MipsRegisterNumbers::FramePointer as usize],
+            "ra" => self.iregs[md::MipsRegisterNumbers::ReturnAddress as usize],
+            "pc" => self.epc,
+            "s0" => self.iregs[md::MipsRegisterNumbers::S0 as usize],
+            "s1" => self.iregs[md::MipsRegisterNumbers::S1 as usize],
+            "s2" => self.iregs[md::MipsRegisterNumbers::S2 as usize],
+            "s3" => self.iregs[md::MipsRegisterNumbers::S3 as usize],
+            "s4" => self.iregs[md::MipsRegisterNumbers::S4 as usize],
+            "s5" => self.iregs[md::MipsRegisterNumbers::S5 as usize],
+            "s6" => self.iregs[md::MipsRegisterNumbers::S6 as usize],
+            "s7" => self.iregs[md::MipsRegisterNumbers::S7 as usize],
+            _ => unreachable!("Invalid mips register! {}", reg),
+        }
+    }
+
+    fn set_register(&mut self, reg: &str, val: Self::Register) -> Option<()> {
+        match reg {
+            "gp" => self.iregs[md::MipsRegisterNumbers::GlobalPointer as usize] = val,
+            "sp" => self.iregs[md::MipsRegisterNumbers::StackPointer as usize] = val,
+            "fp" => self.iregs[md::MipsRegisterNumbers::FramePointer as usize] = val,
+            "ra" => self.iregs[md::MipsRegisterNumbers::ReturnAddress as usize] = val,
+            "pc" => self.epc = val,
+            "s0" => self.iregs[md::MipsRegisterNumbers::S0 as usize] = val,
+            "s1" => self.iregs[md::MipsRegisterNumbers::S1 as usize] = val,
+            "s2" => self.iregs[md::MipsRegisterNumbers::S2 as usize] = val,
+            "s3" => self.iregs[md::MipsRegisterNumbers::S3 as usize] = val,
+            "s4" => self.iregs[md::MipsRegisterNumbers::S4 as usize] = val,
+            "s5" => self.iregs[md::MipsRegisterNumbers::S5 as usize] = val,
+            "s6" => self.iregs[md::MipsRegisterNumbers::S6 as usize] = val,
+            "s7" => self.iregs[md::MipsRegisterNumbers::S7 as usize] = val,
+            _ => return None,
+        }
+        Some(())
     }
 
     fn stack_pointer_register_name(&self) -> &'static str {
@@ -509,6 +880,114 @@ impl CpuContext for md::CONTEXT_ARM64 {
     }
 }
 
+impl CpuContext for md::CONTEXT_SPARC {
+    type Register = u64;
+
+    const REGISTERS: &'static [&'static str] = &[
+        "g_r0", "g_r1", "g_r2", "g_r3", "g_r4", "g_r5", "g_r6", "g_r7", "g_r8", "g_r9", "g_r10",
+        "g_r11", "g_r12", "g_r13", "g_r14", "g_r15", "g_r16", "g_r17", "g_r18", "g_r19", "g_r20",
+        "g_r21", "g_r22", "g_r23", "g_r24", "g_r25", "g_r26", "g_r27", "g_r28", "g_r29", "g_r30",
+        "g_r31", "ccr", "pc", "npc", "y", "asi", "fprs",
+    ];
+
+    fn get_register_always(&self, reg: &str) -> Self::Register {
+        match reg {
+            "g_r0" | "g0" => self.g_r[0],
+            "g_r1" | "g1" => self.g_r[1],
+            "g_r2" | "g2" => self.g_r[2],
+            "g_r3" | "g3" => self.g_r[3],
+            "g_r4" | "g4" => self.g_r[4],
+            "g_r5" | "g5" => self.g_r[5],
+            "g_r6" | "g6" => self.g_r[6],
+            "g_r7" | "g7" => self.g_r[7],
+            "g_r8" | "o0" => self.g_r[8],
+            "g_r9" | "o1" => self.g_r[9],
+            "g_r10" | "o2" => self.g_r[10],
+            "g_r11" | "o3" => self.g_r[11],
+            "g_r12" | "o4" => self.g_r[12],
+            "g_r13" | "o5" => self.g_r[13],
+            "g_r14" | "o6" => self.g_r[14],
+            "g_r15" | "o7" => self.g_r[15],
+            "g_r16" | "l0" => self.g_r[16],
+            "g_r17" | "l1" => self.g_r[17],
+            "g_r18" | "l2" => self.g_r[18],
+            "g_r19" | "l3" => self.g_r[19],
+            "g_r20" | "l4" => self.g_r[20],
+            "g_r21" | "l5" => self.g_r[21],
+            "g_r22" | "l6" => self.g_r[22],
+            "g_r23" | "l7" => self.g_r[23],
+            "g_r24" | "i0" => self.g_r[24],
+            "g_r25" | "i1" => self.g_r[25],
+            "g_r26" | "i2" => self.g_r[26],
+            "g_r27" | "i3" => self.g_r[27],
+            "g_r28" | "i4" => self.g_r[28],
+            "g_r29" | "i5" => self.g_r[29],
+            "g_r30" | "i6" => self.g_r[30],
+            "g_r31" | "i7" => self.g_r[31],
+            "ccr" => self.ccr,
+            "pc" => self.pc,
+            "npc" => self.npc,
+            "y" => self.y,
+            "asi" => self.asi,
+            "fprs" => self.fprs,
+            _ => unreachable!("Invalid sparc register! {}", reg),
+        }
+    }
+
+    fn set_register(&mut self, reg: &str, val: Self::Register) -> Option<()> {
+        match reg {
+            "g_r0" | "g0" => self.g_r[0] = val,
+            "g_r1" | "g1" => self.g_r[1] = val,
+            "g_r2" | "g2" => self.g_r[2] = val,
+            "g_r3" | "g3" => self.g_r[3] = val,
+            "g_r4" | "g4" => self.g_r[4] = val,
+            "g_r5" | "g5" => self.g_r[5] = val,
+            "g_r6" | "g6" => self.g_r[6] = val,
+            "g_r7" | "g7" => self.g_r[7] = val,
+            "g_r8" | "o0" => self.g_r[8] = val,
+            "g_r9" | "o1" => self.g_r[9] = val,
+            "g_r10" | "o2" => self.g_r[10] = val,
+            "g_r11" | "o3" => self.g_r[11] = val,
+            "g_r12" | "o4" => self.g_r[12] = val,
+            "g_r13" | "o5" => self.g_r[13] = val,
+            "g_r14" | "o6" => self.g_r[14] = val,
+            "g_r15" | "o7" => self.g_r[15] = val,
+            "g_r16" | "l0" => self.g_r[16] = val,
+            "g_r17" | "l1" => self.g_r[17] = val,
+            "g_r18" | "l2" => self.g_r[18] = val,
+            "g_r19" | "l3" => self.g_r[19] = val,
+            "g_r20" | "l4" => self.g_r[20] = val,
+            "g_r21" | "l5" => self.g_r[21] = val,
+            "g_r22" | "l6" => self.g_r[22] = val,
+            "g_r23" | "l7" => self.g_r[23] = val,
+            "g_r24" | "i0" => self.g_r[24] = val,
+            "g_r25" | "i1" => self.g_r[25] = val,
+            "g_r26" | "i2" => self.g_r[26] = val,
+            "g_r27" | "i3" => self.g_r[27] = val,
+            "g_r28" | "i4" => self.g_r[28] = val,
+            "g_r29" | "i5" => self.g_r[29] = val,
+            "g_r30" | "i6" => self.g_r[30] = val,
+            "g_r31" | "i7" => self.g_r[31] = val,
+            "ccr" => self.ccr = val,
+            "pc" => self.pc = val,
+            "npc" => self.npc = val,
+            "y" => self.y = val,
+            "asi" => self.asi = val,
+            "fprs" => self.fprs = val,
+            _ => return None,
+        }
+        Some(())
+    }
+
+    fn stack_pointer_register_name(&self) -> &'static str {
+        "g_r14" // alias out register o6
+    }
+
+    fn instruction_pointer_register_name(&self) -> &'static str {
+        "pc"
+    }
+}
+
 /// Information about which registers are valid in a `MinidumpContext`.
 #[derive(Clone, Debug, PartialEq)]
 pub enum MinidumpContextValidity {
@@ -547,29 +1026,6 @@ pub enum ContextError {
     UnknownCpuContext,
 }
 
-/// General-purpose registers for x86.
-static X86_REGS: [&str; 10] = [
-    "eip", "esp", "ebp", "ebx", "esi", "edi", "eax", "ecx", "edx", "efl",
-];
-
-/// General-purpose registers for x86-64.
-static X86_64_REGS: [&str; 17] = [
-    "rax", "rdx", "rcx", "rbx", "rsi", "rdi", "rbp", "rsp", "r8", "r9", "r10", "r11", "r12", "r13",
-    "r14", "r15", "rip",
-];
-
-/// General-purpose registers for ARM (32-bit).
-static ARM_REGS: [&str; 20] = [
-    "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "r13", "r14",
-    "r15", "pc", "lr", "fp", "sp",
-];
-
-/// General-purpose registers for aarch64.
-static ARM64_REGS: [&str; 35] = [
-    "x0", "x1", "x2", "x3", "x4", "x5", "x6", "x7", "x8", "x9", "x10", "x11", "x12", "x13", "x14",
-    "x15", "x16", "x17", "x18", "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", "x27",
-    "x28", "x29", "x30", "pc", "lr", "fp", "sp",
-];
 //======================================================
 // Implementations
 
@@ -765,34 +1221,91 @@ impl MinidumpContext {
         }
     }
 
+    pub fn get_register_always(&self, reg: &str) -> u64 {
+        match self.raw {
+            MinidumpRawContext::Amd64(ref ctx) => ctx.get_register_always(reg),
+            MinidumpRawContext::Arm(ref ctx) => ctx.get_register_always(reg).into(),
+            MinidumpRawContext::Arm64(ref ctx) => ctx.get_register_always(reg),
+            MinidumpRawContext::OldArm64(ref ctx) => ctx.get_register_always(reg),
+            MinidumpRawContext::Ppc(ref ctx) => ctx.get_register_always(reg).into(),
+            MinidumpRawContext::Ppc64(ref ctx) => ctx.get_register_always(reg),
+            MinidumpRawContext::Sparc(ref ctx) => ctx.get_register_always(reg),
+            MinidumpRawContext::X86(ref ctx) => ctx.get_register_always(reg).into(),
+            MinidumpRawContext::Mips(ref ctx) => ctx.get_register_always(reg),
+        }
+    }
+
+    pub fn get_register(&self, reg: &str) -> Option<u64> {
+        let valid = match &self.raw {
+            MinidumpRawContext::X86(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Ppc(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Ppc64(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Amd64(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Sparc(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Arm(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Arm64(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::OldArm64(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Mips(ctx) => ctx.register_is_valid(reg, &self.valid),
+        };
+
+        if valid {
+            Some(self.get_register_always(reg))
+        } else {
+            None
+        }
+    }
+
     pub fn format_register(&self, reg: &str) -> String {
         match self.raw {
             MinidumpRawContext::Amd64(ref ctx) => ctx.format_register(reg),
             MinidumpRawContext::Arm(ref ctx) => ctx.format_register(reg),
             MinidumpRawContext::Arm64(ref ctx) => ctx.format_register(reg),
             MinidumpRawContext::OldArm64(ref ctx) => ctx.format_register(reg),
-            MinidumpRawContext::Ppc(_) => unimplemented!(),
-            MinidumpRawContext::Ppc64(_) => unimplemented!(),
-            MinidumpRawContext::Sparc(_) => unimplemented!(),
+            MinidumpRawContext::Ppc(ref ctx) => ctx.format_register(reg),
+            MinidumpRawContext::Ppc64(ref ctx) => ctx.format_register(reg),
+            MinidumpRawContext::Sparc(ref ctx) => ctx.format_register(reg),
             MinidumpRawContext::X86(ref ctx) => ctx.format_register(reg),
-            MinidumpRawContext::Mips(_) => unimplemented!(),
+            MinidumpRawContext::Mips(ref ctx) => ctx.format_register(reg),
         }
     }
 
     pub fn general_purpose_registers(&self) -> &'static [&'static str] {
         match self.raw {
-            MinidumpRawContext::Amd64(_) => &X86_64_REGS[..],
-            MinidumpRawContext::Arm(_) => &ARM_REGS[..],
-            MinidumpRawContext::Arm64(_) => &ARM64_REGS[..],
-            MinidumpRawContext::OldArm64(_) => &ARM64_REGS[..],
-            MinidumpRawContext::Ppc(_) => unimplemented!(),
-            MinidumpRawContext::Ppc64(_) => unimplemented!(),
-            MinidumpRawContext::Sparc(_) => unimplemented!(),
-            MinidumpRawContext::X86(_) => &X86_REGS[..],
-            MinidumpRawContext::Mips(_) => unimplemented!(),
+            MinidumpRawContext::Amd64(_) => md::CONTEXT_AMD64::REGISTERS,
+            MinidumpRawContext::Arm(_) => md::CONTEXT_ARM::REGISTERS,
+            MinidumpRawContext::Arm64(_) => md::CONTEXT_ARM64::REGISTERS,
+            MinidumpRawContext::OldArm64(_) => md::CONTEXT_ARM64::REGISTERS,
+            MinidumpRawContext::Ppc(_) => md::CONTEXT_PPC::REGISTERS,
+            MinidumpRawContext::Ppc64(_) => md::CONTEXT_PPC64::REGISTERS,
+            MinidumpRawContext::Sparc(_) => md::CONTEXT_SPARC::REGISTERS,
+            MinidumpRawContext::X86(_) => md::CONTEXT_X86::REGISTERS,
+            MinidumpRawContext::Mips(_) => md::CONTEXT_MIPS::REGISTERS,
         }
     }
 
+    pub fn registers(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
+        self.general_purpose_registers()
+            .iter()
+            .map(move |&reg| (reg, self.get_register_always(reg)))
+    }
+
+    pub fn valid_registers(&self) -> impl Iterator<Item = (&'static str, u64)> + '_ {
+        // This is suboptimal in theory, as we could iterate over self.valid just like the original
+        // and faster `CpuRegisters` iterator does. However, this complicates code here, and the
+        // minimal gain in performance hasn't been worth the added complexity.
+        self.registers().filter(move |(reg, _)| match &self.raw {
+            MinidumpRawContext::X86(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Ppc(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Ppc64(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Amd64(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Sparc(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Arm(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Arm64(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::OldArm64(ctx) => ctx.register_is_valid(reg, &self.valid),
+            MinidumpRawContext::Mips(ctx) => ctx.register_is_valid(reg, &self.valid),
+        })
+    }
+
     /// Write a human-readable description of this `MinidumpContext` to `f`.
     ///
     /// This is very verbose, it is the format used by `minidump_dump`.
@@ -1052,3 +1565,34 @@ impl MinidumpContext {
         Ok(())
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    /// Smoke test for the default implementation of `memoize_register`.
+    fn test_memoize_amd64() {
+        let context = md::CONTEXT_AMD64::default();
+        assert_eq!(context.memoize_register("rip"), Some("rip"));
+        assert_eq!(context.memoize_register("foo"), None);
+    }
+
+    #[test]
+    /// Test ARM register aliases by example of `fp`.
+    fn test_memoize_arm_alias() {
+        let context = md::CONTEXT_ARM::default();
+        assert_eq!(context.memoize_register("r11"), Some("fp"));
+        assert_eq!(context.memoize_register("fp"), Some("fp"));
+        assert_eq!(context.memoize_register("foo"), None);
+    }
+
+    #[test]
+    /// Test ARM register aliases by example of `fp`.
+    fn test_memoize_arm64_alias() {
+        let context = md::CONTEXT_ARM64::default();
+        assert_eq!(context.memoize_register("x29"), Some("fp"));
+        assert_eq!(context.memoize_register("fp"), Some("fp"));
+        assert_eq!(context.memoize_register("foo"), None);
+    }
+}
diff --git a/minidump/src/minidump.rs b/minidump/src/minidump.rs
index 1105704..564f497 100644
--- a/minidump/src/minidump.rs
+++ b/minidump/src/minidump.rs
@@ -959,11 +959,14 @@ impl Module for MinidumpModule {
                 .ok()?;
                 Some(DebugId::from_parts(uuid, raw.age))
             }
-            Some(CodeView::Pdb20(ref raw)) => {
-                let uuid = Uuid::from_fields(raw.signature, 0, 0, &[0, 0, 0, 0]).ok()?;
-                Some(DebugId::from_parts(uuid, raw.age))
-            }
+            Some(CodeView::Pdb20(ref raw)) => Some(DebugId::from_pdb20(raw.signature, raw.age)),
             Some(CodeView::Elf(ref raw)) => {
+                // For empty or trivial `build_id`s, we don't want to return a `DebugId`.
+                // This can happen for mapped files that aren't executable, like fonts or .jar files.
+                if raw.build_id.iter().all(|byte| *byte == 0) {
+                    return None;
+                }
+
                 // For backwards-compat (Linux minidumps have historically
                 // been written using PDB70 CodeView info), treat build_id
                 // as if the first 16 bytes were a GUID.
@@ -2067,7 +2070,7 @@ impl<'a> MinidumpLinuxMapInfo<'a> {
   base_address          = {:#x}
   final_address         = {:#x}
   kind                  = {:#?}
-  permissions           = 
+  permissions           =\x20
 ",
             self.base_address, self.final_address, self.kind,
         )?;
@@ -2618,6 +2621,10 @@ impl MinidumpSystemInfo {
         Ok(())
     }
 
+    /// If the minidump was generated on:
+    /// - Windows: Returns the the name of the Service Pack.
+    /// - macOS: Returns the product build number.
+    /// - Linux: Returns the contents of `uname -srvmo`.
     pub fn csd_version(&self) -> Option<Cow<str>> {
         self.csd_version.as_deref().map(Cow::Borrowed)
     }
@@ -2626,6 +2633,52 @@ impl MinidumpSystemInfo {
     pub fn cpu_info(&self) -> Option<Cow<str>> {
         self.cpu_info.as_deref().map(Cow::Borrowed)
     }
+
+    /// Strings identifying the version and build number of the operating
+    /// system. Returns a tuple in the format of (version, build number). This
+    /// may be useful to use if the minidump was created on a Linux machine and
+    /// is an producing empty-ish version number (0.0.0).
+    ///
+    /// Tries to parse the version number from the build if it cannot be found
+    /// in the version string. If the stream already contains a valid version
+    /// number or parsing from the build string fails, this will return what's
+    /// directly stored in the stream.
+    pub fn os_parts(&self) -> (String, Option<String>) {
+        let os_version = format!(
+            "{}.{}.{}",
+            self.raw.major_version, self.raw.minor_version, self.raw.build_number
+        );
+
+        let os_build = self
+            .csd_version()
+            .map(|v| v.trim().to_owned())
+            .filter(|v| !v.is_empty());
+
+        if md::PlatformId::from_u32(self.raw.platform_id) != Some(md::PlatformId::Linux)
+            || os_version != "0.0.0"
+        {
+            return (os_version, os_build);
+        }
+
+        // Try to parse the Linux build string. Breakpad and Crashpad run
+        // `uname -srvmo` to generate it. The string follows this structure:
+        // "Linux [version] [build...] [arch] Linux/GNU" where the Linux/GNU
+        // bit may not always be present.
+        let raw_build = self.csd_version().unwrap_or(Cow::Borrowed(""));
+        let mut parts = raw_build.split(' ');
+        let version = parts.nth(1).unwrap_or("0.0.0");
+        let _arch_or_os = parts.next_back().unwrap_or_default();
+        if _arch_or_os == "Linux/GNU" {
+            let _arch = parts.next_back();
+        }
+        let build = parts.collect::<Vec<&str>>().join(" ");
+
+        if version == "0.0.0" {
+            (os_version, os_build)
+        } else {
+            (version.into(), Some(build))
+        }
+    }
 }
 
 // Generates an accessor for a MISC_INFO field with two possible syntaxes:
@@ -4602,7 +4655,8 @@ where
             MINIDUMP_STREAM_TYPE::SystemMemoryInfoStream,
             MINIDUMP_STREAM_TYPE::ProcessVmCountersStream,
             MINIDUMP_STREAM_TYPE::IptTraceStream,
-            // What on earth is all this "ce" stuff?
+            // Windows CE streams, very unlikely to be found in the wild.
+            // Their contents are documented here: https://docs.microsoft.com/en-us/previous-versions/windows/embedded/ms939618(v=msdn.10)
             MINIDUMP_STREAM_TYPE::ceStreamNull,
             MINIDUMP_STREAM_TYPE::ceStreamSystemInfo,
             MINIDUMP_STREAM_TYPE::ceStreamException,
@@ -4869,6 +4923,48 @@ mod test {
         );
     }
 
+    #[test]
+    fn test_module_list_pdb20() {
+        let name = DumpString::new("single module", Endian::Little);
+        let cv_record = Section::with_endian(Endian::Little)
+            .D32(md::CvSignature::Pdb20 as u32) // cv_signature
+            .D32(0x0) // cv_offset
+            .D32(0xabcd1234) // signature
+            .D32(1) // age
+            .append_bytes(b"c:\\foo\\file.pdb\0"); // pdb_file_name
+        let module = SynthModule::new(
+            Endian::Little,
+            0xa90206ca83eb2852,
+            0xada542bd,
+            &name,
+            0xb1054d2a,
+            0x34571371,
+            Some(&STOCK_VERSION_INFO),
+        )
+        .cv_record(&cv_record);
+        let dump = SynthMinidump::with_endian(Endian::Little)
+            .add_module(module)
+            .add(name)
+            .add(cv_record);
+        let dump = read_synth_dump(dump).unwrap();
+        let module_list = dump.get_stream::<MinidumpModuleList>().unwrap();
+        let modules = module_list.iter().collect::<Vec<_>>();
+        assert_eq!(modules.len(), 1);
+        assert_eq!(modules[0].base_address(), 0xa90206ca83eb2852);
+        assert_eq!(modules[0].size(), 0xada542bd);
+        assert_eq!(modules[0].code_file(), "single module");
+        // time_date_stamp and size_of_image concatenated
+        assert_eq!(
+            modules[0].code_identifier(),
+            CodeId::new("B1054D2Aada542bd".to_string())
+        );
+        assert_eq!(modules[0].debug_file().unwrap(), "c:\\foo\\file.pdb");
+        assert_eq!(
+            modules[0].debug_identifier().unwrap(),
+            DebugId::from_pdb20(0xabcd1234, 1)
+        );
+    }
+
     #[test]
     fn test_unloaded_module_list() {
         let name = DumpString::new("single module", Endian::Little);
@@ -4997,7 +5093,7 @@ mod test {
         // Whitespace intentionally wonky to test robustness
         let input = b"
 
- a90206ca83eb2852-b90206ca83eb3852 r-xp  10bac9000 fd:05 1196511 /usr/lib64/libtdb1.so  
+ a90206ca83eb2852-b90206ca83eb3852 r-xp  10bac9000 fd:05 1196511 /usr/lib64/libtdb1.so\x20\x20
 c70206ca83eb2852-de0206ca83eb2852  -w-s  10bac9000 fd:05 1196511 /usr/lib64/libtdb2.so  (deleted)
 
 
@@ -5921,6 +6017,35 @@ c70206ca83eb2852-de0206ca83eb2852  -w-s  10bac9000 fd:05 1196511 /usr/lib64/libt
         assert_eq!(modules[0].debug_file().unwrap(), "helpivecrashed.dylib");
     }
 
+    #[test]
+    fn test_null_id() {
+        // Add a module with an ELF build id of nothing but zeros
+        let name1 = DumpString::new("module 1", Endian::Little);
+        const MODULE1_BUILD_ID: &[u8] = &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
+        let cv_record1 = Section::with_endian(Endian::Little)
+            .D32(md::CvSignature::Elf as u32) // signature
+            .append_bytes(MODULE1_BUILD_ID);
+        let module1 = SynthModule::new(
+            Endian::Little,
+            0x100000000,
+            0x4000,
+            &name1,
+            0xb1054d2a,
+            0x34571371,
+            Some(&STOCK_VERSION_INFO),
+        )
+        .cv_record(&cv_record1);
+        let dump = SynthMinidump::with_endian(Endian::Little)
+            .add_module(module1)
+            .add(name1)
+            .add(cv_record1);
+        let dump = read_synth_dump(dump).unwrap();
+        let module_list = dump.get_stream::<MinidumpModuleList>().unwrap();
+        let modules = module_list.iter().collect::<Vec<_>>();
+
+        assert!(modules[0].debug_identifier().is_none());
+    }
+
     #[test]
     fn test_thread_list_x86() {
         let context = synth_minidump::x86_context(Endian::Little, 0xabcd1234, 0x1010);


Test patch:
diff --git a/minidump-processor/src/stackwalker/amd64_unittest.rs b/minidump-processor/src/stackwalker/amd64_unittest.rs
index 6964eb9..f1791f6 100644
--- a/minidump-processor/src/stackwalker/amd64_unittest.rs
+++ b/minidump-processor/src/stackwalker/amd64_unittest.rs
@@ -663,3 +663,65 @@ async fn test_frame_pointer_barely_no_overflow() {
         }
     }
 }
+
+#[tokio::test]
+async fn test_scan_walk_overflow() {
+    // There's a possible overflow when address_of_ip starts out at 0.
+    //
+    // To avoid this, we only try to recover rbp when we're scanning at least
+    // 1 pointer width away from the start of the stack.
+    let mut f = TestFixture::new();
+    let mut stack = Section::new();
+    let stack_start = 0;
+    stack.start().set_const(stack_start);
+
+    let return_address1 = 0x00007500b0000100_u64;
+
+    let frame1_sp = Label::new();
+    let frame1_rbp = Label::new();
+
+    stack = stack
+        // frame 0
+        .D64(return_address1) // actual return address
+        // frame 1
+        .mark(&frame1_sp)
+        .append_repeated(0, 16) // space
+        .D64(0x00007400b0000000) // more junk
+        .D64(0x00007500d0000000)
+        .mark(&frame1_rbp);
+
+    f.raw.rip = 0x00007400c0000200;
+    f.raw.rbp = frame1_rbp.value().unwrap();
+    f.raw.rsp = stack.start().value().unwrap();
+
+    let s = f.walk_stack(stack).await;
+    assert_eq!(s.frames.len(), 2);
+
+    {
+        // To avoid reusing locals by mistake
+        let f0 = &s.frames[0];
+        assert_eq!(f0.trust, FrameTrust::Context);
+        assert_eq!(f0.context.valid, MinidumpContextValidity::All);
+    }
+
+    {
+        // To avoid reusing locals by mistake
+        let f1 = &s.frames[1];
+        assert_eq!(f1.trust, FrameTrust::Scan);
+        if let MinidumpContextValidity::Some(ref which) = f1.context.valid {
+            assert!(which.contains("rip"));
+            assert!(which.contains("rsp"));
+        } else {
+            unreachable!();
+        }
+
+        if let MinidumpRawContext::Amd64(ctx) = &f1.context.raw {
+            assert_eq!(ctx.rip, return_address1);
+            assert_eq!(ctx.rsp, frame1_sp.value().unwrap());
+            // We were unable to recover rbp, so it defaulted to 0.
+            assert_eq!(ctx.rbp, 0);
+        } else {
+            unreachable!();
+        }
+    }
+}
diff --git a/minidump-processor/src/stackwalker/arm_unittest.rs b/minidump-processor/src/stackwalker/arm_unittest.rs
index c6f12b9..78c3b50 100644
--- a/minidump-processor/src/stackwalker/arm_unittest.rs
+++ b/minidump-processor/src/stackwalker/arm_unittest.rs
@@ -427,7 +427,7 @@ async fn test_frame_pointer_infinite_equality() {
     // Never get to frame 2, alas!
 }
 
-const CALLEE_SAVE_REGS: &[&str] = &["pc", "sp", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11"];
+const CALLEE_SAVE_REGS: &[&str] = &["pc", "sp", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "fp"];
 
 fn init_cfi_state() -> (TestFixture, Section, CONTEXT_ARM, MinidumpContextValidity) {
     let mut f = TestFixture::new();
diff --git a/minidump-processor/src/stackwalker/x86_unittest.rs b/minidump-processor/src/stackwalker/x86_unittest.rs
index e392491..fe13a18 100644
--- a/minidump-processor/src/stackwalker/x86_unittest.rs
+++ b/minidump-processor/src/stackwalker/x86_unittest.rs
@@ -87,7 +87,7 @@ async fn test_traditional() {
         .append_repeated(12, 0) // frame 0: space
         .mark(&frame0_ebp) // frame 0 %ebp points here
         .D32(&frame1_ebp) // frame 0: saved %ebp
-        .D32(0x40008679) // frame 0: return address
+        .D32(0x40008679) // frame 0: resume address
         .append_repeated(8, 0) // frame 1: space
         .mark(&frame1_ebp) // frame 1 %ebp points here
         .D32(0) // frame 1: saved %ebp (stack end)
@@ -102,6 +102,7 @@ async fn test_traditional() {
         assert_eq!(f0.trust, FrameTrust::Context);
         assert_eq!(f0.context.valid, MinidumpContextValidity::All);
         assert_eq!(f0.instruction, 0x4000c7a5);
+        assert_eq!(f0.resume_address, 0x4000c7a5);
         // eip
         // ebp
     }
@@ -110,6 +111,7 @@ async fn test_traditional() {
         assert_eq!(f1.trust, FrameTrust::FramePointer);
         // ContextValidity
         assert_eq!(f1.instruction, 0x40008678);
+        assert_eq!(f1.resume_address, 0x40008679);
         // eip
         // ebp
     }
@@ -154,6 +156,7 @@ async fn test_traditional_scan() {
         assert_eq!(f0.trust, FrameTrust::Context);
         assert_eq!(f0.context.valid, MinidumpContextValidity::All);
         assert_eq!(f0.instruction, 0x4000f49d);
+        assert_eq!(f0.resume_address, 0x4000f49d);
 
         if let MinidumpRawContext::X86(ctx) = &f0.context.raw {
             assert_eq!(ctx.eip, 0x4000f49d);
@@ -176,6 +179,7 @@ async fn test_traditional_scan() {
             unreachable!();
         }
         assert_eq!(f1.instruction + 1, 0x4000129d);
+        assert_eq!(f1.resume_address, 0x4000129d);
 
         if let MinidumpRawContext::X86(ctx) = &f1.context.raw {
             assert_eq!(ctx.eip, 0x4000129d);
diff --git a/minidump-processor/tests/test_processor.rs b/minidump-processor/tests/test_processor.rs
index 10dd9dd..f4a60ee 100644
--- a/minidump-processor/tests/test_processor.rs
+++ b/minidump-processor/tests/test_processor.rs
@@ -240,3 +240,31 @@ async fn test_linux_proc_status() {
     let dump = minimal_minidump().set_linux_proc_status(input);
     let _state = read_synth_dump(dump).await;
 }
+
+#[tokio::test]
+async fn test_no_frames() {
+    let context = synth_minidump::x86_context(Endian::Little, 0, 0);
+
+    let stack = Memory::with_section(Section::with_endian(Endian::Little), 0);
+
+    let thread = Thread::new(Endian::Little, 0x1234, &stack, &context);
+    let system_info = SystemInfo::new(Endian::Little);
+
+    let mut ex = Exception::new(Endian::Little);
+    ex.thread_id = 0x1234;
+
+    let dump = SynthMinidump::with_endian(Endian::Little)
+        .add_thread(thread)
+        .add_exception(ex)
+        .add_system_info(system_info)
+        .add(context)
+        .add_memory(stack);
+
+    let mut state = read_synth_dump(dump).await;
+
+    // I'm not sure if this is really a valid move in a test.
+    // But I can't figure out *how* to get the frames to be clear in a valid dump.
+    state.threads[0].frames.clear();
+
+    state.print_json(&mut std::io::sink(), true).unwrap();
+}
diff --git a/minidump/tests/test_minidump.rs b/minidump/tests/test_minidump.rs
index 3f9d393..58401b8 100644
--- a/minidump/tests/test_minidump.rs
+++ b/minidump/tests/test_minidump.rs
@@ -30,6 +30,11 @@ fn read_test_minidump<'a>() -> Result<Minidump<'a, Mmap>, Error> {
     Minidump::read_path(&path)
 }
 
+fn read_linux_minidump<'a>() -> Result<Minidump<'a, Mmap>, Error> {
+    let path = get_test_minidump_path("linux-mini.dmp");
+    Minidump::read_path(&path)
+}
+
 #[test]
 fn test_minidump_read_path() {
     read_test_minidump().unwrap();
@@ -334,3 +339,15 @@ fn test_record_count_mac_info() {
         }
     }
 }
+
+#[test]
+fn test_linux_os_version() {
+    let dump = read_linux_minidump().unwrap();
+    let system_info = dump.get_stream::<MinidumpSystemInfo>().unwrap();
+
+    assert_eq!(system_info.os_parts().0, "4.9.60-linuxkit-aufs");
+    assert_eq!(
+        system_info.os_parts().1.unwrap(),
+        "#1 SMP Mon Nov 6 16:00:12 UTC 2017",
+    );
+}