Repository: gimli-rs/addr2line
Pull request #260 (2023-01-15 18:28:08)

Support split DWARF
This is pretty invasive. It also depends on gimli-rs/gimli#637

To make split DWARF work, we'll ultimately need to be able to create the correct `gimli::Dwarf` and `gimli::Unit`s. The former is a blend of sections from the main (at a minimum, .debug_addr) and dwo files. The latter is parsed from the dwo and then has `copy_relocated_attributes` called with the skeleton unit from the main file.

We also don't want to force the loading of all split DWARF data up front for performance reasons.

This means we need a mechanism for lazy loading of split DWARF data (enter the new `SplitDwarfLoader` trait) that can be provided by the addr2line consumer. Once the consumer loads the correct `gimli::Dwarf` and returns it to addr2line, we can produce the correct `gimli::Unit`.

We then have to pass the correct `gimli::Dwarf` and `gimli::Unit` down into the machinery that actually does the work (largely function parsing).  To make that possible I've split (heh) `ResDwarf` into a parsed component that is append only (through the various `LazyCell`s) and a raw component which can be cheaply cloned and can have its `gimli::Dwarf` replaced when necessary. The single instance of the parsed component is passed around by reference as necessary. The raw component (along with the correct `gimli::Unit` to use) is computed by `ResUnit::dwarf_and_unit_dwo` as necessary.

A simple `SplitDwarfLoader` implementation is included in the tests. I have tested this with `-Csplit-debuginfo=unpacked` and `-Csplit-debuginfo=packed`, both with and without `-Zsplit-dwarf-kind=single`, and together with various outstanding cargo and rustc PRs I have all addr2line tests pass.

khuey: I suppose I should add a `SplitDwarfLoader` implementation to the example too.
mstange: I haven't looked at the patch in detail yet, but it looks like it requires synchronous loading of the dwo file. In my use of addr2line I cannot load files synchronously, so for my purposes I would need a different API.

It would be great if a lookup could return some kind of "need extra file" token, so that the caller can load the file on their own terms (for example asynchronously) and then call back into some kind of "lookup with extra file" API.

Here's how I handled the analogous macOS case in the API for [wholesym](https://docs.rs/wholesym/): The result from an address lookup is an [enum `FramesLookupResult`](https://docs.rs/wholesym/latest/wholesym/enum.FramesLookupResult.html), which is either `Available`, `External`, or `Unavailable`. If it's `External`, the caller can load that extra file and then call another method to finish the lookup.
khuey: > it looks like it requires synchronous loading of the dwo file

Yes that's right.

We do that kind of continuation based value injection for evaluating DWARF expressions in gimli. It's possible to do here but it would be  even more invasive than the existing patch.
philipc: It's okay to have invasive changes, and asynchronous loading is something we should support, even if it is not initially implemented in this PR. With that in view, maybe it would be better if the loader was passed in each time, instead of storing it in `Context`.
bjorn3: Split DWARF is already supported through `Context::new_with_sup`, right? This PR merely helps with finding the split debuginfo file as I understand it.
philipc: No, `Context::new_with_sup` is for supplementary object files, such as those created by running `dwz` after linking.
bjorn3: I see
khuey: Here's a prototype implementation of what things would look at in a continuation style. This is not high enough quality to merge at this point but it'll give you an idea of what the API would have to look like. In particular, look at the documentation of `LookupContinuation`, the function `builtin_split_dwarf_loader::SplitDwarfLoader::run`, and the example and correctness test.
mstange: Thank you! But why is it so complicated? What are the cases where we need to load multiple extra files? Is it because we don't know for sure which unit an address is located in until we've "looked inside" that unit?
khuey: basically compiler bugs

https://github.com/gimli-rs/addr2line/blob/3a2dbaf84551a06a429f26e9c96071bb409b371f/src/lib.rs#L204
mstange: I see. So we could, in theory, say "we don't support that workaround in the split dwarf case" and only allow one unit to be in a different file. Or we could return information about all potential units upfront, and require them all to be loaded in the follow-up call, hoping that the case of multiple potential units is not worth optimizing for.
khuey: In theory, maybe. I think we'd need to understand rust-lang/backtrace-rs#327 more before we did that.

The way I've written this is the way you'd write it with generators (`LookupContinuation<Output, R>` is essentially a manually implemented `Generator<R = Option<Arc<gimli::Dwarf<R>>, Yield = SplitDwarfLookup<R>, Return = Output>`) so it'd be easy to switch if generators are ever stabilized. We could get cute by gathering a `Vec<SplitDwarfLookup<R>>` up front. I doubt it's worth it, driving it in a loop is straightforward.
philipc: > Weak symbols have shown up in the wild which cause this to happen

I think this comment is talking about comdat symbols. Not sure if this will be a problem here.

> this can happen if the CU has non-contiguous functions but only reports a single range

I think this has only ever happened on macOS, and that is unlikely to need support for split DWARF. Or if it does have split DWARF, it will be with a compiler that generates correct CU ranges.


khuey: fwiw I see identical code ranges across multiple units with `RUSTFLAGS=--Clink-args=-Wl,--icf=all` on programs as trivial as gimli's dwarfdump example, including at least one case where the CU claims to cover the range but there's no DW_TAG_subprogram that does inside the CU. So I don't think handling this is optional.
khuey: (That's on Linux, rustc 1.66.1, lld 10)
mstange: Ah, that is unfortunate. Thanks for checking!

Okay, if we need to be prepared to check multiple files anyway, I would still prefer a solution where we know about all potentially-needed files upfront (right after the lookup-address call), so that my wrapper API in wholesym can stay simple and only require one .await call.
khuey: Trivial functions like `drop_in_place` could be ICFed over a very large number of CUs, so the set of "potentially needed" files might be very large.
philipc: Instead of hiding the control flow, can we return an iterator over the units that may need loading, plus a map function, and the caller can decide whether to interleave the iteration and map calls, or to do some or all of the iteration first.
khuey: Is it possible? Yes. I don't think it's a great API design though.

I think it'd be better to offer a function that "preloads" all potentially relevant units for an address. So it'd look something like this:

```rust
pub struct PreloadInfo<R> {
    indices: Vec<usize>,
    pub loads: Vec<SplitDwarfInfo<R>>,
}

pub struct CompletedPreload<R> {
    indices: Vec<usize>,
    results: Vec<Option<gimli::Dwarf<R>>>,
}

impl<R> PreloadInfo<R> {
    fn complete(self, results: Vec<Option<gimli::Dwarf<R>>>) -> CompletedPreload<R> {
        CompletedPreload {
            indices: self.indices,
            results,
        }
    }
}

impl<R: gimli::Reader> Context<R> {
    fn start_preload(&self, addr: u64) -> PreloadInfo<R>;
    fn complete_preload(&self, preload_info: PreloadInfo<R>) {
        for (index, dwarf) in preload_info.indices.into_iter().zip(preload_info.results.into_iter()) {
            let unit = &self.parsed_dwarf.units[index];
            unit.dwo = do_the_thing(dwarf);
        }
    }
}

fn consumer_api(addr: u64) -> FramesIter {
    let mut preload = ctx.start_preload(addr);
    let loads_to_do = mem::take(preload.loads());
    let results = /* API consumer does the loads */;
    ctx.complete_preload(preload.complete(results));
    let continuation = ctx.find_frames(addr);
    match continuation.state() {
        ControlFlow::Break(v) => v,
        ControlFlow::Continue(_) => unreachable!("addr2line's API promises this won't happen!!!"),
    }
}
```
philipc: Some sort of preload API is fine too. I'd prefer either `Iterator<Item=PreloadInfo>` or `Vec<PreloadInfo>` instead of having `Vec` internal to `PreloadInfo` and `CompletedPreload`, but that's a minor difference.
khuey: The tricky thing there is that the context needs to be able to match the `gimli::Dwarf`'s it gets back with the correct units.
philipc: That's done via the index though, which we can still have:

```rust
pub struct PreloadInfo<R> {
    index: usize,
    pub load: SplitDwarfInfo<R>,
}

pub struct CompletedPreload<R> {
    index: usize,
    result: Option<gimli::Dwarf<R>>,
}

impl<R> PreloadInfo<R> {
    fn complete(self, result: Option<gimli::Dwarf<R>>) -> CompletedPreload<R> {
        CompletedPreload {
            index: self.index,
            result,
        }
    }
}
```
khuey: Oh, I see. I didn't understand what you meant the first time but yeah, that would work.
khuey: If you're otherwise happy with the API I'll clean things up to a reviewable state this weekend.
khuey: Also one question I did have in the back of mind while writing this: is code in addr2line allowed to panic? Especially since its presumably running in the panic handler machinery ...
philipc: I'm happy with the API if it meets @mstange's needs.

I think it's the standard library's responsibility to handle what happens if this code panics in the panic handler machinery, but we still avoid panics. So only use `debug_assert` and only do an unwrap if it is logically impossible to fail.
khuey: The draft commits have unwraps that can be triggered by API misuse. Is that unacceptable?
philipc: Ideally we would rework the API so that they are impossible, but it won't block landing this.
khuey: Ok, I have an idea of how we might accomplish that.
khuey: Ok I found time sooner than expected. What do you think of that?
mstange: > Trivial functions like `drop_in_place` could be ICFed over a very large number of CUs, so the set of "potentially needed" files might be very large.

In this case, does every potential file contain the debug info for `drop_in_place`? So would the first lookup in practice be successful in this case?

All this makes me thankful for the mach-O OSO stab symbol implementation, where the linked file tells you per function which original object file the function came from.
philipc: That looks good assuming it's possible for the consumer to easily store the callbacks for use after the asynchronous load (it's probably okay, just not something I'm sure about).
khuey: > In this case, does every potential file contain the debug info for `drop_in_place`? So would the first lookup in practice be successful in this case?

I saw at least one file that did not actually contain debug info. So we're not guaranteed that the first lookup would be successful, though in practice I think we would find one pretty quickly.
philipc: > All this makes me thankful for the mach-O OSO stab symbol implementation, where the linked file tells you per function which original object file the function came from.

That wouldn't help here, because the function came from multiple original object files. There is no single true source, they are all valid, and so you must look at the caller to disambiguate. http://wiki.dwarfstd.org/index.php?title=ICF describes some DWARF extensions to handle this, but I haven't looked to see the status of these, or to see if there are alternative solutions.
khuey: I'm pretty confident that that particular proposal never went anywhere.
khuey: > That looks good assuming it's possible for the consumer to easily store the callbacks for use after the asynchronous load (it's probably okay, just not something I'm sure about).

This compiles as expected

```rust
async fn do_loads<R>(loads: Vec<SplitDwarfLoad<R>>) -> Vec<Option<Arc<gimli::Dwarf<R>>>> {
    unimplemented!();
}

#[tokio::main]
async fn main() {
    let map = find_debuginfo();
    let file = &object::File::parse(&*map).unwrap();
    let ctx = Context::new(file).unwrap();
    let (loads, continuations) = ctx.preload_units(0xdeadbeef).fold((Vec::new(), Vec::new()), |(mut vec1, mut vec2), (thing1, thing2)| {
        vec1.push(thing1);
        vec2.push(thing2);
        (vec1, vec2)
    });
    let load_results = do_loads(loads).await;
    //mem::drop(ctx); // E0505 can't move out of ctx because it's borrowed (by continuations)
    continuations.into_iter().zip(load_results.into_iter()).try_for_each(|(callback, result)| callback(result)).unwrap();
}
```
khuey: I think in theory it might be possible to use the DWARF 5 call site information to pick the correct ICFed subprogram but neither gdb nor lldb do this and it would require a different addr2line API anyways (because you'd need to infer which subprogram you're looking at based off its caller).
khuey: (This will need a gimli release for me to point the PR at before CI will pass, if that's not clear)
philipc: I've released gimli 0.27.1
khuey: Ok, the final issue remaining is the MSRV test. You're testing for an MSRV of 1.42. The `ControlFlow` enum this PR is using was introduced in 1.55. So we either need to bump the MSRV or replace the enum with our own copy. I don't know how or why the MSRV was chosen so I don't know whether or not we can bump the MSRV.
philipc: Bumping the MSRV is ok if it's not too recent. We don't make any promises about it yet. 1.55 is well over a year old, and `object` is already using 1.52, so 1.55 seems fine.
khuey: Alright I think we're good to go here.

We can't actually test split DWARF support in CI until rust-lang/cargo#11572, rust-lang/rust#106811, and rust-lang/rust#106904 are merged and percolate into releases but I have tested this myself and it works.

If you're happy with everything I'll squash it down into one commit with a sensible commit message.
philipc: This looks fine but I haven't done a detailed review yet. I'll try to soon.
khuey: Coverage is busted again but everything else is green.
khuey: Do you want me to squash everything?
philipc: Yes I'll wait for you to squash.
khuey: Squashed.

Patch:
diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml
index d8ffee2..b1513a1 100644
--- a/.github/workflows/rust.yml
+++ b/.github/workflows/rust.yml
@@ -67,7 +67,7 @@ jobs:
     steps:
       - uses: actions/checkout@v2
       - name: Install rust
-        run: rustup update 1.42.0 && rustup default 1.42.0
+        run: rustup update 1.55.0 && rustup default 1.55.0
       - name: Build
         run: cargo build
 
diff --git a/Cargo.toml b/Cargo.toml
index 76d97d5..b76e3f1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,8 +11,9 @@ readme = "./README.md"
 repository = "https://github.com/gimli-rs/addr2line"
 
 [dependencies]
-gimli = { version = "0.27.0", default-features = false, features = ["read"] }
+gimli = { version = "0.27.1", default-features = false, features = ["read"] }
 fallible-iterator = { version = "0.2", default-features = false, optional = true }
+memmap2 = { version = "0.5.5", optional = true }
 object = { version = "0.30.0", default-features = false, features = ["read"], optional = true }
 smallvec = { version = "1", default-features = false, optional = true }
 rustc-demangle = { version = "0.1", optional = true }
@@ -25,7 +26,6 @@ alloc = { version = '1.0.0', optional = true, package = 'rustc-std-workspace-all
 compiler_builtins = { version = '0.1.2', optional = true }
 
 [dev-dependencies]
-memmap2 = "0.5.5"
 clap = "3.1.6"
 backtrace = "0.3.13"
 findshlibs = "0.10"
@@ -41,7 +41,7 @@ debug = true
 codegen-units = 1
 
 [features]
-default = ["rustc-demangle", "cpp_demangle", "std-object", "fallible-iterator", "smallvec"]
+default = ["rustc-demangle", "cpp_demangle", "std-object", "fallible-iterator", "smallvec", "memmap2"]
 std = ["gimli/std"]
 std-object = ["std", "object", "object/std", "object/compression", "gimli/endian-reader"]
 
@@ -52,7 +52,7 @@ rustc-dep-of-std = ['core', 'alloc', 'compiler_builtins', 'gimli/rustc-dep-of-st
 [[test]]
 name = "output_equivalence"
 harness = false
-required-features = ["std-object"]
+required-features = ["default"]
 
 [[test]]
 name = "correctness"
@@ -64,4 +64,4 @@ required-features = ["std-object"]
 
 [[example]]
 name = "addr2line"
-required-features = ["std-object"]
+required-features = ["default"]
diff --git a/benches/bench.rs b/benches/bench.rs
index 0ac7ac5..64ce5f4 100644
--- a/benches/bench.rs
+++ b/benches/bench.rs
@@ -10,6 +10,7 @@ use std::env;
 use std::fs::File;
 use std::path::{self, PathBuf};
 
+use addr2line::LookupResultExt;
 use object::{Object, ObjectSection, ObjectSymbol};
 
 fn release_fixture_path() -> PathBuf {
@@ -224,7 +225,7 @@ fn context_query_with_functions_rc(b: &mut test::Bencher) {
         let ctx = addr2line::Context::new(file).unwrap();
         // Ensure nothing is lazily loaded.
         for addr in &addresses {
-            let mut frames = ctx.find_frames(*addr).unwrap();
+            let mut frames = ctx.find_frames(*addr).skip_all_loads().unwrap();
             while let Ok(Some(ref frame)) = frames.next() {
                 test::black_box(frame);
             }
@@ -232,7 +233,7 @@ fn context_query_with_functions_rc(b: &mut test::Bencher) {
 
         b.iter(|| {
             for addr in &addresses {
-                let mut frames = ctx.find_frames(*addr).unwrap();
+                let mut frames = ctx.find_frames(*addr).skip_all_loads().unwrap();
                 while let Ok(Some(ref frame)) = frames.next() {
                     test::black_box(frame);
                 }
@@ -253,7 +254,7 @@ fn context_query_with_functions_slice(b: &mut test::Bencher) {
         let ctx = addr2line::Context::from_dwarf(dwarf).unwrap();
         // Ensure nothing is lazily loaded.
         for addr in &addresses {
-            let mut frames = ctx.find_frames(*addr).unwrap();
+            let mut frames = ctx.find_frames(*addr).skip_all_loads().unwrap();
             while let Ok(Some(ref frame)) = frames.next() {
                 test::black_box(frame);
             }
@@ -261,7 +262,7 @@ fn context_query_with_functions_slice(b: &mut test::Bencher) {
 
         b.iter(|| {
             for addr in &addresses {
-                let mut frames = ctx.find_frames(*addr).unwrap();
+                let mut frames = ctx.find_frames(*addr).skip_all_loads().unwrap();
                 while let Ok(Some(ref frame)) = frames.next() {
                     test::black_box(frame);
                 }
@@ -314,7 +315,7 @@ fn context_new_and_query_with_functions_rc(b: &mut test::Bencher) {
         b.iter(|| {
             let ctx = addr2line::Context::new(file).unwrap();
             for addr in addresses.iter().take(100) {
-                let mut frames = ctx.find_frames(*addr).unwrap();
+                let mut frames = ctx.find_frames(*addr).skip_all_loads().unwrap();
                 while let Ok(Some(ref frame)) = frames.next() {
                     test::black_box(frame);
                 }
@@ -334,7 +335,7 @@ fn context_new_and_query_with_functions_slice(b: &mut test::Bencher) {
             let dwarf = dwarf_borrow(&dwarf);
             let ctx = addr2line::Context::from_dwarf(dwarf).unwrap();
             for addr in addresses.iter().take(100) {
-                let mut frames = ctx.find_frames(*addr).unwrap();
+                let mut frames = ctx.find_frames(*addr).skip_all_loads().unwrap();
                 while let Ok(Some(ref frame)) = frames.next() {
                     test::black_box(frame);
                 }
diff --git a/examples/addr2line.rs b/examples/addr2line.rs
index fa4d8e4..2b1d83a 100644
--- a/examples/addr2line.rs
+++ b/examples/addr2line.rs
@@ -16,6 +16,8 @@ use fallible_iterator::FallibleIterator;
 use object::{Object, ObjectSection, SymbolMap, SymbolMapName};
 use typed_arena::Arena;
 
+#[cfg(not(unix))]
+use addr2line::LookupResultExt;
 use addr2line::{Context, Location};
 
 fn parse_uint_from_hex_string(string: &str) -> Option<u64> {
@@ -201,6 +203,13 @@ fn main() {
         dwarf.load_sup(&mut load_sup_section).unwrap();
     }
 
+    #[cfg(unix)]
+    let mut split_dwarf_loader = addr2line::builtin_split_dwarf_loader::SplitDwarfLoader::new(
+        |data, endian| {
+            gimli::EndianSlice::new(arena_data.alloc(Cow::Owned(data.into_owned())), endian)
+        },
+        Some(std::path::PathBuf::from(path)),
+    );
     let ctx = Context::from_dwarf(dwarf).unwrap();
 
     let stdin = std::io::stdin();
@@ -227,7 +236,12 @@ fn main() {
         if do_functions || do_inlines {
             let mut printed_anything = false;
             if let Some(probe) = probe {
-                let mut frames = ctx.find_frames(probe).unwrap().enumerate();
+                let frames = ctx.find_frames(probe);
+                #[cfg(unix)]
+                let frames = split_dwarf_loader.run(frames).unwrap();
+                #[cfg(not(unix))]
+                let frames = frames.skip_all_loads().unwrap();
+                let mut frames = frames.enumerate();
                 while let Some((i, frame)) = frames.next().unwrap() {
                     if pretty && i != 0 {
                         print!(" (inlined by) ");
diff --git a/src/builtin_split_dwarf_loader.rs b/src/builtin_split_dwarf_loader.rs
new file mode 100644
index 0000000..d912afc
--- /dev/null
+++ b/src/builtin_split_dwarf_loader.rs
@@ -0,0 +1,155 @@
+use alloc::borrow::Cow;
+use alloc::sync::Arc;
+use std::fs::File;
+use std::path::PathBuf;
+
+use object::Object;
+
+use crate::{LookupContinuation, LookupResult};
+
+#[cfg(unix)]
+fn convert_path<R: gimli::Reader<Endian = gimli::RunTimeEndian>>(
+    r: &R,
+) -> Result<PathBuf, gimli::Error> {
+    use std::ffi::OsStr;
+    use std::os::unix::ffi::OsStrExt;
+    let bytes = r.to_slice()?;
+    let s = OsStr::from_bytes(&*bytes);
+    Ok(PathBuf::from(s))
+}
+
+fn load_section<'data: 'file, 'file, O, R, F>(
+    id: gimli::SectionId,
+    file: &'file O,
+    endian: R::Endian,
+    loader: &mut F,
+) -> Result<R, gimli::Error>
+where
+    O: object::Object<'data, 'file>,
+    R: gimli::Reader<Endian = gimli::RunTimeEndian>,
+    F: FnMut(Cow<'data, [u8]>, R::Endian) -> R,
+{
+    use object::ObjectSection;
+
+    let data = id
+        .dwo_name()
+        .and_then(|dwo_name| {
+            file.section_by_name(dwo_name)
+                .and_then(|section| section.uncompressed_data().ok())
+        })
+        .unwrap_or(Cow::Borrowed(&[]));
+    Ok(loader(data, endian))
+}
+
+/// A simple builtin split DWARF loader.
+pub struct SplitDwarfLoader<R, F>
+where
+    R: gimli::Reader<Endian = gimli::RunTimeEndian>,
+    F: FnMut(Cow<[u8]>, R::Endian) -> R,
+{
+    loader: F,
+    dwarf_package: Option<gimli::DwarfPackage<R>>,
+}
+
+impl<R, F> SplitDwarfLoader<R, F>
+where
+    R: gimli::Reader<Endian = gimli::RunTimeEndian>,
+    F: FnMut(Cow<[u8]>, R::Endian) -> R,
+{
+    fn load_dwarf_package(loader: &mut F, path: Option<PathBuf>) -> Option<gimli::DwarfPackage<R>> {
+        let mut path = path.map(Ok).unwrap_or_else(std::env::current_exe).ok()?;
+        let dwp_extension = path
+            .extension()
+            .map(|previous_extension| {
+                let mut previous_extension = previous_extension.to_os_string();
+                previous_extension.push(".dwp");
+                previous_extension
+            })
+            .unwrap_or_else(|| "dwp".into());
+        path.set_extension(dwp_extension);
+        let file = File::open(&path).ok()?;
+        let map = unsafe { memmap2::Mmap::map(&file).ok()? };
+        let dwp = object::File::parse(&*map).ok()?;
+
+        let endian = if dwp.is_little_endian() {
+            gimli::RunTimeEndian::Little
+        } else {
+            gimli::RunTimeEndian::Big
+        };
+
+        let empty = loader(Cow::Borrowed(&[]), endian);
+        gimli::DwarfPackage::load(
+            |section_id| load_section(section_id, &dwp, endian, loader),
+            empty,
+        )
+        .ok()
+    }
+
+    /// Create a new split DWARF loader.
+    pub fn new(mut loader: F, path: Option<PathBuf>) -> SplitDwarfLoader<R, F> {
+        let dwarf_package = SplitDwarfLoader::load_dwarf_package(&mut loader, path);
+        SplitDwarfLoader {
+            loader,
+            dwarf_package,
+        }
+    }
+
+    /// Run the provided `LookupResult` to completion, loading any necessary
+    /// split DWARF along the way.
+    pub fn run<L>(&mut self, mut l: LookupResult<L>) -> L::Output
+    where
+        L: LookupContinuation<Buf = R>,
+    {
+        loop {
+            let (load, continuation) = match l {
+                LookupResult::Break(output) => break output,
+                LookupResult::Continue(load) => load,
+            };
+
+            let mut r: Option<Arc<gimli::Dwarf<_>>> = None;
+            if let Some(dwp) = self.dwarf_package.as_ref() {
+                if let Ok(Some(cu)) = dwp.find_cu(load.dwo_id, &load.parent) {
+                    r = Some(Arc::new(cu));
+                }
+            }
+
+            if r.is_none() {
+                let mut path = PathBuf::new();
+                if let Some(p) = load.comp_dir.as_ref() {
+                    if let Ok(p) = convert_path(p) {
+                        path.push(p);
+                    }
+                }
+
+                if let Some(p) = load.path.as_ref() {
+                    if let Ok(p) = convert_path(p) {
+                        path.push(p);
+                    }
+                }
+
+                if let Ok(file) = File::open(&path) {
+                    if let Ok(map) = unsafe { memmap2::Mmap::map(&file) } {
+                        if let Ok(file) = object::File::parse(&*map) {
+                            let endian = if file.is_little_endian() {
+                                gimli::RunTimeEndian::Little
+                            } else {
+                                gimli::RunTimeEndian::Big
+                            };
+
+                            r = gimli::Dwarf::load(|id| {
+                                load_section(id, &file, endian, &mut self.loader)
+                            })
+                            .ok()
+                            .map(|mut dwo_dwarf| {
+                                dwo_dwarf.make_dwo(&load.parent);
+                                Arc::new(dwo_dwarf)
+                            });
+                        }
+                    }
+                }
+            }
+
+            l = continuation.resume(r);
+        }
+    }
+}
diff --git a/src/function.rs b/src/function.rs
index 44dc73f..8264e8a 100644
--- a/src/function.rs
+++ b/src/function.rs
@@ -5,7 +5,7 @@ use core::iter;
 
 use crate::lazy::LazyCell;
 use crate::maybe_small;
-use crate::{Error, RangeAttributes, ResDwarf};
+use crate::{DebugFile, Error, ParsedDwarf, RangeAttributes, RawDwarf};
 
 pub(crate) struct Functions<R: gimli::Reader> {
     /// List of all `DW_TAG_subprogram` details in the unit.
@@ -55,7 +55,7 @@ pub(crate) struct InlinedFunction<R: gimli::Reader> {
 }
 
 impl<R: gimli::Reader> Functions<R> {
-    pub(crate) fn parse(unit: &gimli::Unit<R>, dwarf: &ResDwarf<R>) -> Result<Functions<R>, Error> {
+    pub(crate) fn parse(unit: &gimli::Unit<R>, raw: &RawDwarf<R>) -> Result<Functions<R>, Error> {
         let mut functions = Vec::new();
         let mut addresses = Vec::new();
         let mut entries = unit.entries_raw(None)?;
@@ -74,7 +74,7 @@ impl<R: gimli::Reader> Functions<R> {
                                         }
                                         gimli::AttributeValue::DebugAddrIndex(index) => {
                                             ranges.low_pc =
-                                                Some(dwarf.sections.address(unit, index)?);
+                                                Some(raw.sections.address(unit, index)?);
                                         }
                                         _ => {}
                                     },
@@ -84,7 +84,7 @@ impl<R: gimli::Reader> Functions<R> {
                                         }
                                         gimli::AttributeValue::DebugAddrIndex(index) => {
                                             ranges.high_pc =
-                                                Some(dwarf.sections.address(unit, index)?);
+                                                Some(raw.sections.address(unit, index)?);
                                         }
                                         gimli::AttributeValue::Udata(val) => {
                                             ranges.size = Some(val)
@@ -92,9 +92,8 @@ impl<R: gimli::Reader> Functions<R> {
                                         _ => {}
                                     },
                                     gimli::DW_AT_ranges => {
-                                        ranges.ranges_offset = dwarf
-                                            .sections
-                                            .attr_ranges_offset(unit, attr.value())?;
+                                        ranges.ranges_offset =
+                                            raw.sections.attr_ranges_offset(unit, attr.value())?;
                                     }
                                     _ => {}
                                 };
@@ -104,7 +103,7 @@ impl<R: gimli::Reader> Functions<R> {
                     }
 
                     let function_index = functions.len();
-                    if ranges.for_each_range(&dwarf.sections, unit, |range| {
+                    if ranges.for_each_range(&raw.sections, unit, |range| {
                         addresses.push(FunctionAddress {
                             range,
                             function: function_index,
@@ -151,12 +150,13 @@ impl<R: gimli::Reader> Functions<R> {
     pub(crate) fn parse_inlined_functions(
         &self,
         unit: &gimli::Unit<R>,
-        dwarf: &ResDwarf<R>,
+        parsed: &ParsedDwarf<R>,
+        raw: &RawDwarf<R>,
     ) -> Result<(), Error> {
         for function in &*self.functions {
             function
                 .1
-                .borrow_with(|| Function::parse(function.0, unit, dwarf))
+                .borrow_with(|| Function::parse(function.0, unit, parsed, raw))
                 .as_ref()
                 .map_err(Error::clone)?;
         }
@@ -168,7 +168,8 @@ impl<R: gimli::Reader> Function<R> {
     pub(crate) fn parse(
         dw_die_offset: gimli::UnitOffset<R::Offset>,
         unit: &gimli::Unit<R>,
-        dwarf: &ResDwarf<R>,
+        parsed: &ParsedDwarf<R>,
+        raw: &RawDwarf<R>,
     ) -> Result<Self, Error> {
         let mut entries = unit.entries_raw(Some(dw_die_offset))?;
         let depth = entries.next_depth();
@@ -181,18 +182,25 @@ impl<R: gimli::Reader> Function<R> {
                 Ok(ref attr) => {
                     match attr.name() {
                         gimli::DW_AT_linkage_name | gimli::DW_AT_MIPS_linkage_name => {
-                            if let Ok(val) = dwarf.sections.attr_string(unit, attr.value()) {
+                            if let Ok(val) = raw.sections.attr_string(unit, attr.value()) {
                                 name = Some(val);
                             }
                         }
                         gimli::DW_AT_name => {
                             if name.is_none() {
-                                name = dwarf.sections.attr_string(unit, attr.value()).ok();
+                                name = raw.sections.attr_string(unit, attr.value()).ok();
                             }
                         }
                         gimli::DW_AT_abstract_origin | gimli::DW_AT_specification => {
                             if name.is_none() {
-                                name = name_attr(attr.value(), unit, dwarf, 16)?;
+                                name = name_attr(
+                                    attr.value(),
+                                    DebugFile::Primary,
+                                    unit,
+                                    parsed,
+                                    raw,
+                                    16,
+                                )?;
                             }
                         }
                         _ => {}
@@ -208,7 +216,8 @@ impl<R: gimli::Reader> Function<R> {
             &mut entries,
             depth,
             unit,
-            dwarf,
+            parsed,
+            raw,
             &mut inlined_functions,
             &mut inlined_addresses,
             0,
@@ -250,7 +259,8 @@ impl<R: gimli::Reader> Function<R> {
         entries: &mut gimli::EntriesRaw<R>,
         depth: isize,
         unit: &gimli::Unit<R>,
-        dwarf: &ResDwarf<R>,
+        parsed: &ParsedDwarf<R>,
+        raw: &RawDwarf<R>,
         inlined_functions: &mut Vec<InlinedFunction<R>>,
         inlined_addresses: &mut Vec<InlinedFunctionAddress>,
         inlined_depth: usize,
@@ -273,7 +283,8 @@ impl<R: gimli::Reader> Function<R> {
                             abbrev,
                             next_depth,
                             unit,
-                            dwarf,
+                            parsed,
+                            raw,
                             inlined_functions,
                             inlined_addresses,
                             inlined_depth,
@@ -348,7 +359,8 @@ impl<R: gimli::Reader> InlinedFunction<R> {
         abbrev: &gimli::Abbreviation,
         depth: isize,
         unit: &gimli::Unit<R>,
-        dwarf: &ResDwarf<R>,
+        parsed: &ParsedDwarf<R>,
+        raw: &RawDwarf<R>,
         inlined_functions: &mut Vec<InlinedFunction<R>>,
         inlined_addresses: &mut Vec<InlinedFunctionAddress>,
         inlined_depth: usize,
@@ -364,35 +376,36 @@ impl<R: gimli::Reader> InlinedFunction<R> {
                     gimli::DW_AT_low_pc => match attr.value() {
                         gimli::AttributeValue::Addr(val) => ranges.low_pc = Some(val),
                         gimli::AttributeValue::DebugAddrIndex(index) => {
-                            ranges.low_pc = Some(dwarf.sections.address(unit, index)?);
+                            ranges.low_pc = Some(raw.sections.address(unit, index)?);
                         }
                         _ => {}
                     },
                     gimli::DW_AT_high_pc => match attr.value() {
                         gimli::AttributeValue::Addr(val) => ranges.high_pc = Some(val),
                         gimli::AttributeValue::DebugAddrIndex(index) => {
-                            ranges.high_pc = Some(dwarf.sections.address(unit, index)?);
+                            ranges.high_pc = Some(raw.sections.address(unit, index)?);
                         }
                         gimli::AttributeValue::Udata(val) => ranges.size = Some(val),
                         _ => {}
                     },
                     gimli::DW_AT_ranges => {
                         ranges.ranges_offset =
-                            dwarf.sections.attr_ranges_offset(unit, attr.value())?;
+                            raw.sections.attr_ranges_offset(unit, attr.value())?;
                     }
                     gimli::DW_AT_linkage_name | gimli::DW_AT_MIPS_linkage_name => {
-                        if let Ok(val) = dwarf.sections.attr_string(unit, attr.value()) {
+                        if let Ok(val) = raw.sections.attr_string(unit, attr.value()) {
                             name = Some(val);
                         }
                     }
                     gimli::DW_AT_name => {
                         if name.is_none() {
-                            name = dwarf.sections.attr_string(unit, attr.value()).ok();
+                            name = raw.sections.attr_string(unit, attr.value()).ok();
                         }
                     }
                     gimli::DW_AT_abstract_origin | gimli::DW_AT_specification => {
                         if name.is_none() {
-                            name = name_attr(attr.value(), unit, dwarf, 16)?;
+                            name =
+                                name_attr(attr.value(), DebugFile::Primary, unit, parsed, raw, 16)?;
                         }
                     }
                     gimli::DW_AT_call_file => {
@@ -421,7 +434,7 @@ impl<R: gimli::Reader> InlinedFunction<R> {
             call_column,
         });
 
-        ranges.for_each_range(&dwarf.sections, unit, |range| {
+        ranges.for_each_range(&raw.sections, unit, |range| {
             inlined_addresses.push(InlinedFunctionAddress {
                 range,
                 call_depth: inlined_depth,
@@ -433,7 +446,8 @@ impl<R: gimli::Reader> InlinedFunction<R> {
             entries,
             depth,
             unit,
-            dwarf,
+            parsed,
+            raw,
             inlined_functions,
             inlined_addresses,
             inlined_depth + 1,
@@ -443,8 +457,10 @@ impl<R: gimli::Reader> InlinedFunction<R> {
 
 fn name_attr<R>(
     attr: gimli::AttributeValue<R>,
+    mut file: DebugFile,
     unit: &gimli::Unit<R>,
-    dwarf: &ResDwarf<R>,
+    parsed: &ParsedDwarf<R>,
+    raw: &RawDwarf<R>,
     recursion_limit: usize,
 ) -> Result<Option<R>, Error>
 where
@@ -455,27 +471,34 @@ where
     }
 
     match attr {
-        gimli::AttributeValue::UnitRef(offset) => name_entry(unit, offset, dwarf, recursion_limit),
+        gimli::AttributeValue::UnitRef(offset) => {
+            name_entry(file, unit, offset, parsed, raw, recursion_limit)
+        }
         gimli::AttributeValue::DebugInfoRef(dr) => {
-            let res_unit = dwarf.find_unit(dr)?;
+            let res_unit = parsed.find_unit(dr, file)?;
             name_entry(
+                file,
                 &res_unit.dw_unit,
                 gimli::UnitOffset(dr.0 - res_unit.offset.0),
-                dwarf,
+                parsed,
+                raw,
                 recursion_limit,
             )
         }
         gimli::AttributeValue::DebugInfoRefSup(dr) => {
-            if let Some(sup_dwarf) = dwarf.sup.as_ref() {
-                let res_unit = sup_dwarf.find_unit(dr)?;
+            if file == DebugFile::Supplementary || raw.sup_sections.is_none() {
+                Ok(None)
+            } else {
+                file = DebugFile::Supplementary;
+                let res_unit = parsed.find_unit(dr, file)?;
                 name_entry(
+                    file,
                     &res_unit.dw_unit,
                     gimli::UnitOffset(dr.0 - res_unit.offset.0),
-                    sup_dwarf,
+                    parsed,
+                    raw,
                     recursion_limit,
                 )
-            } else {
-                Ok(None)
             }
         }
         _ => Ok(None),
@@ -483,9 +506,11 @@ where
 }
 
 fn name_entry<R>(
+    file: DebugFile,
     unit: &gimli::Unit<R>,
     offset: gimli::UnitOffset<R::Offset>,
-    dwarf: &ResDwarf<R>,
+    parsed: &ParsedDwarf<R>,
+    raw: &RawDwarf<R>,
     recursion_limit: usize,
 ) -> Result<Option<R>, Error>
 where
@@ -497,6 +522,11 @@ where
     } else {
         return Err(gimli::Error::NoEntryAtGivenOffset);
     };
+    let sections = if let Some(sections) = raw.sections_for_file(file) {
+        sections
+    } else {
+        return Ok(None);
+    };
 
     let mut name = None;
     let mut next = None;
@@ -504,12 +534,12 @@ where
         match entries.read_attribute(*spec) {
             Ok(ref attr) => match attr.name() {
                 gimli::DW_AT_linkage_name | gimli::DW_AT_MIPS_linkage_name => {
-                    if let Ok(val) = dwarf.sections.attr_string(unit, attr.value()) {
+                    if let Ok(val) = sections.attr_string(unit, attr.value()) {
                         return Ok(Some(val));
                     }
                 }
                 gimli::DW_AT_name => {
-                    if let Ok(val) = dwarf.sections.attr_string(unit, attr.value()) {
+                    if let Ok(val) = sections.attr_string(unit, attr.value()) {
                         name = Some(val);
                     }
                 }
@@ -527,7 +557,7 @@ where
     }
 
     if let Some(next) = next {
-        return name_attr(next, unit, dwarf, recursion_limit - 1);
+        return name_attr(next, file, unit, parsed, raw, recursion_limit - 1);
     }
 
     Ok(None)
diff --git a/src/lazy.rs b/src/lazy.rs
index a34ed17..2df2ed6 100644
--- a/src/lazy.rs
+++ b/src/lazy.rs
@@ -10,6 +10,10 @@ impl<T> LazyCell<T> {
         }
     }
 
+    pub fn borrow(&self) -> Option<&T> {
+        unsafe { &*self.contents.get() }.as_ref()
+    }
+
     pub fn borrow_with(&self, closure: impl FnOnce() -> T) -> &T {
         // First check if we're already initialized...
         let ptr = self.contents.get();
diff --git a/src/lib.rs b/src/lib.rs
index 3afa37f..12c20ba 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -25,6 +25,9 @@
 #![deny(missing_docs)]
 #![no_std]
 
+#[cfg(feature = "std")]
+extern crate std;
+
 #[allow(unused_imports)]
 #[macro_use]
 extern crate alloc;
@@ -49,8 +52,10 @@ use alloc::vec::Vec;
 
 use core::cmp::{self, Ordering};
 use core::iter;
+use core::marker::PhantomData;
 use core::mem;
 use core::num::NonZeroU64;
+use core::ops::ControlFlow;
 use core::u64;
 
 use crate::function::{Function, Functions, InlinedFunction};
@@ -67,17 +72,126 @@ mod maybe_small {
     pub type IntoIter<T> = alloc::vec::IntoIter<T>;
 }
 
+#[cfg(all(unix, feature = "std", feature = "object", feature = "memmap2"))]
+/// A simple builtin split DWARF loader.
+pub mod builtin_split_dwarf_loader;
 mod function;
 mod lazy;
 
 type Error = gimli::Error;
 
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum DebugFile {
+    Primary,
+    Supplementary,
+}
+
+/// Operations that consult debug information may require additional files
+/// to be loaded if split DWARF is being used. This enum returns the result
+/// of the operation in the `Break` variant, or information about the split
+/// DWARF that is required and a continuation to invoke once it is available
+/// in the `Continue` variant.
+///
+/// This enum is intended to be used in a loop like so:
+/// ```no_run
+///   # use addr2line::*;
+///   # use std::sync::Arc;
+///   # let ctx: Context<gimli::EndianRcSlice<gimli::RunTimeEndian>> = todo!();
+///   # let do_split_dwarf_load = |load: SplitDwarfLoad<gimli::EndianRcSlice<gimli::RunTimeEndian>>| -> Option<Arc<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>>> { None };
+///   const ADDRESS: u64 = 0xdeadbeef;
+///   let mut r = ctx.find_frames(ADDRESS);
+///   let result = loop {
+///     match r {
+///       LookupResult::Break(result) => break result,
+///       LookupResult::Continue((load, continuation)) => {
+///         let dwo = do_split_dwarf_load(load);
+///         r = continuation.resume(dwo);
+///       }
+///     }
+///   };
+/// ```
+pub type LookupResult<L> = ControlFlow<
+    <L as LookupContinuation>::Output,
+    (SplitDwarfLoad<<L as LookupContinuation>::Buf>, L),
+>;
+
+/// This trait represents a partially complete operation that can be resumed
+/// once a load of needed split DWARF data is completed or abandoned by the
+/// API consumer.
+pub trait LookupContinuation: Sized {
+    /// The final output of this operation.
+    type Output;
+    /// The type of reader used.
+    type Buf: gimli::Reader;
+
+    /// Resumes the operation with the provided data.
+    ///
+    /// After the caller loads the split DWARF data required, call this
+    /// method to resume the operation. The return value of this method
+    /// indicates if the computation has completed or if further data is
+    /// required.
+    ///
+    /// If the additional data cannot be located, or the caller does not
+    /// support split DWARF, `resume(None)` can be used to continue the
+    /// operation with the data that is available.
+    fn resume(self, input: Option<Arc<gimli::Dwarf<Self::Buf>>>) -> LookupResult<Self>;
+}
+
+/// This trait is used to extend `LookupResult`.
+pub trait LookupResultExt<L: LookupContinuation> {
+    /// Callers that do not handle split DWARF can call `skip_all_loads`
+    /// to fast-forward to the end result. This result is produced with
+    /// the data that is available and may be less accurate than the
+    /// the results that would be produced if the caller did properly
+    /// support split DWARF.
+    fn skip_all_loads(self) -> L::Output;
+}
+
+impl<L: LookupContinuation> LookupResultExt<L> for LookupResult<L> {
+    fn skip_all_loads(mut self) -> L::Output {
+        loop {
+            self = match self {
+                ControlFlow::Break(t) => return t,
+                ControlFlow::Continue((_, continuation)) => continuation.resume(None),
+            };
+        }
+    }
+}
+
+trait LookupResultExtInternal<L: LookupContinuation> {
+    fn map<T, F: FnOnce(L::Output) -> T>(self, f: F) -> LookupResult<MappedLookup<T, L, F>>;
+    fn unwrap(self) -> L::Output;
+}
+
+impl<L: LookupContinuation> LookupResultExtInternal<L> for LookupResult<L> {
+    fn map<T, F: FnOnce(L::Output) -> T>(self, f: F) -> LookupResult<MappedLookup<T, L, F>> {
+        match self {
+            ControlFlow::Break(t) => ControlFlow::Break(f(t)),
+            ControlFlow::Continue((load, continuation)) => ControlFlow::Continue((
+                load,
+                MappedLookup {
+                    original: continuation,
+                    mutator: f,
+                },
+            )),
+        }
+    }
+
+    fn unwrap(self) -> L::Output {
+        match self {
+            ControlFlow::Break(t) => t,
+            ControlFlow::Continue(_) => unreachable!("Internal API misuse"),
+        }
+    }
+}
+
 /// The state necessary to perform address to line translation.
 ///
 /// Constructing a `Context` is somewhat costly, so users should aim to reuse `Context`s
 /// when performing lookups for many addresses in the same executable.
 pub struct Context<R: gimli::Reader> {
-    dwarf: ResDwarf<R>,
+    parsed_dwarf: ParsedDwarf<R>,
+    raw_dwarf: RawDwarf<R>,
 }
 
 /// The type of `Context` that supports the `new` method.
@@ -109,7 +223,7 @@ impl Context<gimli::EndianRcSlice<gimli::RunTimeEndian>> {
     /// This means it is not thread safe, has no lifetime constraints (since it copies
     /// the input data), and works for any endianity.
     ///
-    /// Performance sensitive applications may want to use `Context::from_dwarf_with_sup`
+    /// Performance sensitive applications may want to use `Context::from_dwarf`
     /// with a more specialised `gimli::Reader` implementation.
     pub fn new_with_sup<'data: 'file, 'file, O: object::Object<'data, 'file>>(
         file: &'file O,
@@ -187,18 +301,21 @@ impl<R: gimli::Reader> Context<R> {
 
     /// Construct a new `Context` from an existing [`gimli::Dwarf`] object.
     #[inline]
-    pub fn from_dwarf(sections: gimli::Dwarf<R>) -> Result<Self, Error> {
-        let mut dwarf = ResDwarf::parse(Arc::new(sections))?;
-        dwarf.sup = match dwarf.sections.sup.clone() {
-            Some(sup_sections) => Some(Box::new(ResDwarf::parse(sup_sections)?)),
-            None => None,
+    pub fn from_dwarf(sections: gimli::Dwarf<R>) -> Result<Context<R>, Error> {
+        let sup_sections = sections.sup.clone();
+        let raw_dwarf = RawDwarf {
+            sections: Arc::new(sections),
+            sup_sections,
         };
-        Ok(Context { dwarf })
-    }
-
-    /// The dwarf sections associated with this `Context`.
-    pub fn dwarf(&self) -> &gimli::Dwarf<R> {
-        &self.dwarf.sections
+        let mut parsed_dwarf = ParsedDwarf::parse(&raw_dwarf.sections)?;
+        if let Some(sup) = raw_dwarf.sup_sections.as_ref() {
+            let sup_dwarf = ParsedDwarf::parse(sup)?;
+            parsed_dwarf.sup_units = Some(sup_dwarf.units);
+        }
+        Ok(Context {
+            parsed_dwarf,
+            raw_dwarf,
+        })
     }
 
     /// Finds the CUs for the function address given.
@@ -229,7 +346,7 @@ impl<R: gimli::Reader> Context<R> {
         // First up find the position in the array which could have our function
         // address.
         let pos = match self
-            .dwarf
+            .parsed_dwarf
             .unit_ranges
             .binary_search_by_key(&probe_high, |i| i.range.begin)
         {
@@ -244,7 +361,7 @@ impl<R: gimli::Reader> Context<R> {
 
         // Once we have our index we iterate backwards from that position
         // looking for a matching CU.
-        self.dwarf.unit_ranges[..pos]
+        self.parsed_dwarf.unit_ranges[..pos]
             .iter()
             .rev()
             .take_while(move |i| {
@@ -265,25 +382,51 @@ impl<R: gimli::Reader> Context<R> {
                 if probe_low >= i.range.end || probe_high <= i.range.begin {
                     return None;
                 }
-                Some((&self.dwarf.units[i.unit_id], &i.range))
+                Some((&self.parsed_dwarf.units[i.unit_id], &i.range))
             })
     }
 
     /// Find the DWARF unit corresponding to the given virtual memory address.
-    pub fn find_dwarf_unit(&self, probe: u64) -> Option<&gimli::Unit<R>> {
-        for unit in self.find_units(probe) {
-            match unit.find_function_or_location(probe, &self.dwarf) {
-                Ok((Some(_), _)) | Ok((_, Some(_))) => return Some(&unit.dw_unit),
-                _ => {}
-            }
+    pub fn find_dwarf_and_unit(
+        &self,
+        probe: u64,
+    ) -> LookupResult<
+        impl LookupContinuation<Output = Option<(&gimli::Dwarf<R>, &gimli::Unit<R>)>, Buf = R>,
+    > {
+        let mut units_iter = self.find_units(probe);
+        if let Some(unit) = units_iter.next() {
+            return LoopingLookup::new_lookup(
+                unit.find_function_or_location(probe, self),
+                move |r| {
+                    ControlFlow::Break(match r {
+                        Ok((Some(_), _)) | Ok((_, Some(_))) => {
+                            let (dwarf, unit) = unit
+                                .dwarf_and_unit_dwo(self)
+                                // We've already been through both error cases here to get to this point.
+                                .unwrap()
+                                .unwrap();
+                            Some((&*dwarf.sections, unit))
+                        }
+                        _ => match units_iter.next() {
+                            Some(next_unit) => {
+                                return ControlFlow::Continue(
+                                    next_unit.find_function_or_location(probe, self),
+                                );
+                            }
+                            None => None,
+                        },
+                    })
+                },
+            );
         }
-        None
+
+        LoopingLookup::new_complete(None)
     }
 
     /// Find the source file and line corresponding to the given virtual memory address.
     pub fn find_location(&self, probe: u64) -> Result<Option<Location<'_>>, Error> {
         for unit in self.find_units(probe) {
-            if let Some(location) = unit.find_location(probe, &self.dwarf.sections)? {
+            if let Some(location) = unit.find_location(probe, &self.raw_dwarf.sections)? {
                 return Ok(Some(location));
             }
         }
@@ -309,33 +452,94 @@ impl<R: gimli::Reader> Context<R> {
     /// If the probe address is for an inline function then the first frame corresponds
     /// to the innermost inline function.  Subsequent frames contain the caller and call
     /// location, until an non-inline caller is reached.
-    pub fn find_frames(&self, probe: u64) -> Result<FrameIter<R>, Error> {
-        for unit in self.find_units(probe) {
-            match unit.find_function_or_location(probe, &self.dwarf)? {
-                (Some(function), location) => {
-                    let inlined_functions = function.find_inlined_functions(probe);
-                    return Ok(FrameIter(FrameIterState::Frames(FrameIterFrames {
-                        unit,
-                        sections: &self.dwarf.sections,
-                        function,
-                        inlined_functions,
-                        next: location,
-                    })));
-                }
-                (None, Some(location)) => {
-                    return Ok(FrameIter(FrameIterState::Location(Some(location))));
-                }
-                _ => {}
-            }
+    pub fn find_frames(
+        &self,
+        probe: u64,
+    ) -> LookupResult<impl LookupContinuation<Output = Result<FrameIter<R>, Error>, Buf = R>> {
+        let mut units_iter = self.find_units(probe);
+        if let Some(unit) = units_iter.next() {
+            LoopingLookup::new_lookup(unit.find_function_or_location(probe, self), move |r| {
+                ControlFlow::Break(match r {
+                    Err(e) => Err(e),
+                    Ok((Some(function), location)) => {
+                        let inlined_functions = function.find_inlined_functions(probe);
+                        Ok(FrameIter(FrameIterState::Frames(FrameIterFrames {
+                            unit,
+                            sections: &self.raw_dwarf.sections,
+                            function,
+                            inlined_functions,
+                            next: location,
+                        })))
+                    }
+                    Ok((None, Some(location))) => {
+                        Ok(FrameIter(FrameIterState::Location(Some(location))))
+                    }
+                    Ok((None, None)) => match units_iter.next() {
+                        Some(next_unit) => {
+                            return ControlFlow::Continue(
+                                next_unit.find_function_or_location(probe, self),
+                            );
+                        }
+                        None => Ok(FrameIter(FrameIterState::Empty)),
+                    },
+                })
+            })
+        } else {
+            LoopingLookup::new_complete(Ok(FrameIter(FrameIterState::Empty)))
         }
-        Ok(FrameIter(FrameIterState::Empty))
+    }
+
+    /// Preload units for `probe`.
+    ///
+    /// The iterator returns pairs of `SplitDwarfLoad`s containing the
+    /// information needed to locate and load split DWARF for `probe` and
+    /// a matching callback to invoke once that data is available.
+    ///
+    /// If this method is called, and all of the returned closures are invoked,
+    /// addr2line guarantees that any future API call for the address `probe`
+    /// will not require the loading of any split DWARF.
+    ///
+    /// ```no_run
+    ///   # use addr2line::*;
+    ///   # use std::sync::Arc;
+    ///   # let ctx: Context<gimli::EndianRcSlice<gimli::RunTimeEndian>> = todo!();
+    ///   # let do_split_dwarf_load = |load: SplitDwarfLoad<gimli::EndianRcSlice<gimli::RunTimeEndian>>| -> Option<Arc<gimli::Dwarf<gimli::EndianRcSlice<gimli::RunTimeEndian>>>> { None };
+    ///   const ADDRESS: u64 = 0xdeadbeef;
+    ///   ctx.preload_units(ADDRESS).for_each(|(load, callback)| {
+    ///     let dwo = do_split_dwarf_load(load);
+    ///     callback(dwo);
+    ///   });
+    ///
+    ///   let frames_iter = match ctx.find_frames(ADDRESS) {
+    ///     LookupResult::Break(result) => result,
+    ///     LookupResult::Continue(_) => unreachable!("addr2line promised we wouldn't get here"),
+    ///   };
+    ///
+    ///   // ...
+    /// ```
+    pub fn preload_units(
+        &'_ self,
+        probe: u64,
+    ) -> impl Iterator<
+        Item = (
+            SplitDwarfLoad<R>,
+            impl FnOnce(Option<Arc<gimli::Dwarf<R>>>) -> Result<(), gimli::Error> + '_,
+        ),
+    > {
+        self.find_units(probe)
+            .filter_map(move |unit| match unit.dwarf_and_unit_dwo(self) {
+                LookupResult::Break(_) => None,
+                LookupResult::Continue((load, continuation)) => Some((load, |result| {
+                    continuation.resume(result).unwrap().map(|_| ())
+                })),
+            })
     }
 
     /// Initialize all line data structures. This is used for benchmarks.
     #[doc(hidden)]
     pub fn parse_lines(&self) -> Result<(), Error> {
-        for unit in &self.dwarf.units {
-            unit.parse_lines(&self.dwarf.sections)?;
+        for unit in &self.parsed_dwarf.units {
+            unit.parse_lines(&self.raw_dwarf.sections)?;
         }
         Ok(())
     }
@@ -343,8 +547,8 @@ impl<R: gimli::Reader> Context<R> {
     /// Initialize all function data structures. This is used for benchmarks.
     #[doc(hidden)]
     pub fn parse_functions(&self) -> Result<(), Error> {
-        for unit in &self.dwarf.units {
-            unit.parse_functions(&self.dwarf)?;
+        for unit in &self.parsed_dwarf.units {
+            unit.parse_functions(self).skip_all_loads()?;
         }
         Ok(())
     }
@@ -352,8 +556,8 @@ impl<R: gimli::Reader> Context<R> {
     /// Initialize all inlined function data structures. This is used for benchmarks.
     #[doc(hidden)]
     pub fn parse_inlined_functions(&self) -> Result<(), Error> {
-        for unit in &self.dwarf.units {
-            unit.parse_inlined_functions(&self.dwarf)?;
+        for unit in &self.parsed_dwarf.units {
+            unit.parse_inlined_functions(self).skip_all_loads()?;
         }
         Ok(())
     }
@@ -365,15 +569,19 @@ struct UnitRange {
     range: gimli::Range,
 }
 
-struct ResDwarf<R: gimli::Reader> {
+struct ParsedDwarf<R: gimli::Reader> {
     unit_ranges: Vec<UnitRange>,
     units: Vec<ResUnit<R>>,
+    sup_units: Option<Vec<ResUnit<R>>>,
+}
+
+struct RawDwarf<R: gimli::Reader> {
     sections: Arc<gimli::Dwarf<R>>,
-    sup: Option<Box<ResDwarf<R>>>,
+    sup_sections: Option<Arc<gimli::Dwarf<R>>>,
 }
 
-impl<R: gimli::Reader> ResDwarf<R> {
-    fn parse(sections: Arc<gimli::Dwarf<R>>) -> Result<Self, Error> {
+impl<R: gimli::Reader> ParsedDwarf<R> {
+    fn parse(sections: &gimli::Dwarf<R>) -> Result<Self, Error> {
         // Find all the references to compilation units in .debug_aranges.
         // Note that we always also iterate through all of .debug_info to
         // find compilation units, because .debug_aranges may be missing some.
@@ -484,7 +692,7 @@ impl<R: gimli::Reader> ResDwarf<R> {
                         }
                     }
                 } else {
-                    have_unit_range |= ranges.for_each_range(&sections, &dw_unit, |range| {
+                    have_unit_range |= ranges.for_each_range(sections, &dw_unit, |range| {
                         unit_ranges.push(UnitRange {
                             range,
                             unit_id,
@@ -500,7 +708,7 @@ impl<R: gimli::Reader> ResDwarf<R> {
                 // Try to get some ranges from the line program sequences.
                 if let Some(ref ilnp) = dw_unit.line_program {
                     if let Ok(lines) = lines
-                        .borrow_with(|| Lines::parse(&dw_unit, ilnp.clone(), &*sections))
+                        .borrow_with(|| Lines::parse(&dw_unit, ilnp.clone(), sections))
                         .as_ref()
                     {
                         for sequence in lines.sequences.iter() {
@@ -523,6 +731,7 @@ impl<R: gimli::Reader> ResDwarf<R> {
                 lang,
                 lines,
                 funcs: LazyCell::new(),
+                dwo: LazyCell::new(),
             });
         }
 
@@ -537,22 +746,46 @@ impl<R: gimli::Reader> ResDwarf<R> {
             i.max_end = max;
         }
 
-        Ok(ResDwarf {
+        Ok(ParsedDwarf {
             units: res_units,
             unit_ranges,
-            sections,
-            sup: None,
+            sup_units: None,
         })
     }
 
-    fn find_unit(&self, offset: gimli::DebugInfoOffset<R::Offset>) -> Result<&ResUnit<R>, Error> {
-        match self
-            .units
-            .binary_search_by_key(&offset.0, |unit| unit.offset.0)
-        {
+    fn find_unit(
+        &self,
+        offset: gimli::DebugInfoOffset<R::Offset>,
+        file: DebugFile,
+    ) -> Result<&ResUnit<R>, Error> {
+        let units = match file {
+            DebugFile::Primary => &self.units,
+            DebugFile::Supplementary => self
+                .sup_units
+                .as_ref()
+                .ok_or(gimli::Error::NoEntryAtGivenOffset)?,
+        };
+
+        match units.binary_search_by_key(&offset.0, |unit| unit.offset.0) {
             // There is never a DIE at the unit offset or before the first unit.
             Ok(_) | Err(0) => Err(gimli::Error::NoEntryAtGivenOffset),
-            Err(i) => Ok(&self.units[i - 1]),
+            Err(i) => Ok(&units[i - 1]),
+        }
+    }
+}
+
+impl<R: gimli::Reader> RawDwarf<R> {
+    fn dwo(&self, dwo_sections: Arc<gimli::Dwarf<R>>) -> RawDwarf<R> {
+        RawDwarf {
+            sections: dwo_sections,
+            sup_sections: self.sup_sections.clone(),
+        }
+    }
+
+    fn sections_for_file(&self, file: DebugFile) -> Option<&gimli::Dwarf<R>> {
+        match file {
+            DebugFile::Primary => Some(&self.sections),
+            DebugFile::Supplementary => self.sup_sections.as_deref(),
         }
     }
 }
@@ -686,10 +919,252 @@ struct ResUnit<R: gimli::Reader> {
     lang: Option<gimli::DwLang>,
     lines: LazyCell<Result<Lines, Error>>,
     funcs: LazyCell<Result<Functions<R>, Error>>,
+    dwo: LazyCell<Result<Option<Box<(RawDwarf<R>, gimli::Unit<R>)>>, Error>>,
+}
+
+/// This struct contains the information needed to find split DWARF data
+/// and to produce a `gimli::Dwarf<R>` for it.
+pub struct SplitDwarfLoad<R> {
+    /// The dwo id, for looking up in a DWARF package, or for
+    /// verifying an unpacked dwo found on the file system
+    pub dwo_id: gimli::DwoId,
+    /// The compilation directory `path` is relative to.
+    pub comp_dir: Option<R>,
+    /// A path on the filesystem, relative to `comp_dir` to find this dwo.
+    pub path: Option<R>,
+    /// Once the split DWARF data is loaded, the loader is expected
+    /// to call [make_dwo(parent)](gimli::read::Dwarf::make_dwo) before
+    /// returning the data.
+    pub parent: Arc<gimli::Dwarf<R>>,
+}
+
+struct SimpleLookup<T, R, F>
+where
+    F: FnOnce(Option<Arc<gimli::Dwarf<R>>>) -> T,
+    R: gimli::Reader,
+{
+    f: F,
+    phantom: PhantomData<(T, R)>,
+}
+
+impl<T, R, F> SimpleLookup<T, R, F>
+where
+    F: FnOnce(Option<Arc<gimli::Dwarf<R>>>) -> T,
+    R: gimli::Reader,
+{
+    fn new_complete(t: F::Output) -> LookupResult<SimpleLookup<T, R, F>> {
+        ControlFlow::Break(t)
+    }
+
+    fn new_needs_load(lookup: SplitDwarfLoad<R>, f: F) -> LookupResult<SimpleLookup<T, R, F>> {
+        ControlFlow::Continue((
+            lookup,
+            SimpleLookup {
+                f,
+                phantom: PhantomData,
+            },
+        ))
+    }
+}
+
+impl<T, R, F> LookupContinuation for SimpleLookup<T, R, F>
+where
+    F: FnOnce(Option<Arc<gimli::Dwarf<R>>>) -> T,
+    R: gimli::Reader,
+{
+    type Output = T;
+    type Buf = R;
+
+    fn resume(self, v: Option<Arc<gimli::Dwarf<Self::Buf>>>) -> LookupResult<Self> {
+        ControlFlow::Break((self.f)(v))
+    }
+}
+
+struct MappedLookup<T, L, F>
+where
+    L: LookupContinuation,
+    F: FnOnce(L::Output) -> T,
+{
+    original: L,
+    mutator: F,
+}
+
+impl<T, L, F> LookupContinuation for MappedLookup<T, L, F>
+where
+    L: LookupContinuation,
+    F: FnOnce(L::Output) -> T,
+{
+    type Output = T;
+    type Buf = L::Buf;
+
+    fn resume(self, v: Option<Arc<gimli::Dwarf<Self::Buf>>>) -> LookupResult<Self> {
+        match self.original.resume(v) {
+            ControlFlow::Break(t) => ControlFlow::Break((self.mutator)(t)),
+            ControlFlow::Continue((load, continuation)) => ControlFlow::Continue((
+                load,
+                MappedLookup {
+                    original: continuation,
+                    mutator: self.mutator,
+                },
+            )),
+        }
+    }
+}
+
+/// Some functions (e.g. `find_frames`) require considering multiple
+/// compilation units, each of which might require their own split DWARF
+/// lookup (and thus produce a continuation).
+///
+/// We store the underlying continuation here as well as a mutator function
+/// that will either a) decide that the result of this continuation is
+/// what is needed and mutate it to the final result or b) produce another
+/// `LookupResult`. `new_lookup` will in turn eagerly drive any non-continuation
+/// `LookupResult` with successive invocations of the mutator, until a new
+/// continuation or a final result is produced. And finally, the impl of
+/// `LookupContinuation::resume` will call `new_lookup` each time the
+/// computation is resumed.
+struct LoopingLookup<T, L, F>
+where
+    L: LookupContinuation,
+    F: FnMut(L::Output) -> ControlFlow<T, LookupResult<L>>,
+{
+    continuation: L,
+    mutator: F,
+}
+
+impl<T, L, F> LoopingLookup<T, L, F>
+where
+    L: LookupContinuation,
+    F: FnMut(L::Output) -> ControlFlow<T, LookupResult<L>>,
+{
+    fn new_complete(t: T) -> LookupResult<Self> {
+        ControlFlow::Break(t)
+    }
+
+    fn new_lookup(mut r: LookupResult<L>, mut mutator: F) -> LookupResult<Self> {
+        // Drive the loop eagerly so that we only ever have to represent one state
+        // (the r == ControlFlow::Continue state) in LoopingLookup.
+        let (load, continuation) = loop {
+            match r {
+                ControlFlow::Break(l) => match mutator(l) {
+                    ControlFlow::Break(t) => return ControlFlow::Break(t),
+                    ControlFlow::Continue(r2) => {
+                        r = r2;
+                    }
+                },
+                ControlFlow::Continue((load, continuation)) => break (load, continuation),
+            }
+        };
+
+        ControlFlow::Continue((
+            load,
+            LoopingLookup {
+                continuation,
+                mutator,
+            },
+        ))
+    }
+}
+
+impl<T, L, F> LookupContinuation for LoopingLookup<T, L, F>
+where
+    L: LookupContinuation,
+    F: FnMut(L::Output) -> ControlFlow<T, LookupResult<L>>,
+{
+    type Output = T;
+    type Buf = L::Buf;
+
+    fn resume(self, v: Option<Arc<gimli::Dwarf<Self::Buf>>>) -> LookupResult<Self> {
+        let r = self.continuation.resume(v);
+        LoopingLookup::new_lookup(r, self.mutator)
+    }
 }
 
 impl<R: gimli::Reader> ResUnit<R> {
+    fn dwarf_and_unit_dwo<'unit, 'ctx: 'unit>(
+        &'unit self,
+        ctx: &'ctx Context<R>,
+    ) -> LookupResult<
+        SimpleLookup<
+            Result<(&'unit RawDwarf<R>, &'unit gimli::Unit<R>), Error>,
+            R,
+            impl FnOnce(
+                Option<Arc<gimli::Dwarf<R>>>,
+            ) -> Result<(&'unit RawDwarf<R>, &'unit gimli::Unit<R>), Error>,
+        >,
+    > {
+        loop {
+            break SimpleLookup::new_complete(match self.dwo.borrow() {
+                Some(Ok(Some(v))) => Ok((&v.0, &v.1)),
+                Some(Ok(None)) => Ok((&ctx.raw_dwarf, &self.dw_unit)),
+                Some(Err(e)) => Err(*e),
+                None => {
+                    let dwo_id = match self.dw_unit.dwo_id {
+                        None => {
+                            self.dwo.borrow_with(|| Ok(None));
+                            continue;
+                        }
+                        Some(dwo_id) => dwo_id,
+                    };
+
+                    let dwo_path_f = || {
+                        let dwo_comp_dir = self.dw_unit.comp_dir.clone();
+
+                        let dwo_path = self.dw_unit.dwo_name().and_then(|s| {
+                            if let Some(s) = s {
+                                Ok(Some(ctx.raw_dwarf.sections.attr_string(&self.dw_unit, s)?))
+                            } else {
+                                Ok(None)
+                            }
+                        })?;
+                        Ok((dwo_comp_dir, dwo_path))
+                    };
+
+                    let (comp_dir, path) = match dwo_path_f() {
+                        Ok(v) => v,
+                        Err(e) => {
+                            self.dwo.borrow_with(|| Err(e));
+                            continue;
+                        }
+                    };
+
+                    let process_dwo = move |dwo_dwarf: Option<Arc<gimli::Dwarf<R>>>| {
+                        let dwo_dwarf = match dwo_dwarf {
+                            None => return Ok(None),
+                            Some(dwo_dwarf) => dwo_dwarf,
+                        };
+                        let mut dwo_units = dwo_dwarf.units();
+                        let dwo_header = match dwo_units.next()? {
+                            Some(dwo_header) => dwo_header,
+                            None => return Ok(None),
+                        };
+
+                        let mut dwo_unit = dwo_dwarf.unit(dwo_header)?;
+                        dwo_unit.copy_relocated_attributes(&self.dw_unit);
+                        Ok(Some(Box::new((ctx.raw_dwarf.dwo(dwo_dwarf), dwo_unit))))
+                    };
+
+                    return SimpleLookup::new_needs_load(
+                        SplitDwarfLoad {
+                            dwo_id,
+                            comp_dir,
+                            path,
+                            parent: ctx.raw_dwarf.sections.clone(),
+                        },
+                        move |dwo_dwarf| match self.dwo.borrow_with(|| process_dwo(dwo_dwarf)) {
+                            Ok(Some(v)) => Ok((&v.0, &v.1)),
+                            Ok(None) => Ok((&ctx.raw_dwarf, &self.dw_unit)),
+                            Err(e) => Err(*e),
+                        },
+                    );
+                }
+            });
+        }
+    }
+
     fn parse_lines(&self, sections: &gimli::Dwarf<R>) -> Result<Option<&Lines>, Error> {
+        // NB: line information is always stored in the main debug file so this does not need
+        // to handle DWOs.
         let ilnp = match self.dw_unit.line_program {
             Some(ref ilnp) => ilnp,
             None => return Ok(None),
@@ -701,19 +1176,39 @@ impl<R: gimli::Reader> ResUnit<R> {
             .map_err(Error::clone)
     }
 
-    fn parse_functions(&self, dwarf: &ResDwarf<R>) -> Result<&Functions<R>, Error> {
+    fn parse_functions_dwarf_and_unit(
+        &self,
+        unit: &gimli::Unit<R>,
+        raw_dwarf: &RawDwarf<R>,
+    ) -> Result<&Functions<R>, Error> {
         self.funcs
-            .borrow_with(|| Functions::parse(&self.dw_unit, dwarf))
+            .borrow_with(|| Functions::parse(unit, raw_dwarf))
             .as_ref()
             .map_err(Error::clone)
     }
 
-    fn parse_inlined_functions(&self, dwarf: &ResDwarf<R>) -> Result<(), Error> {
-        self.funcs
-            .borrow_with(|| Functions::parse(&self.dw_unit, dwarf))
-            .as_ref()
-            .map_err(Error::clone)?
-            .parse_inlined_functions(&self.dw_unit, dwarf)
+    fn parse_functions<'unit, 'ctx: 'unit>(
+        &'unit self,
+        ctx: &'ctx Context<R>,
+    ) -> LookupResult<impl LookupContinuation<Output = Result<&'unit Functions<R>, Error>, Buf = R>>
+    {
+        self.dwarf_and_unit_dwo(ctx).map(move |r| {
+            let (raw_dwarf, unit) = r?;
+            self.parse_functions_dwarf_and_unit(unit, raw_dwarf)
+        })
+    }
+    fn parse_inlined_functions<'unit, 'ctx: 'unit>(
+        &'unit self,
+        ctx: &'ctx Context<R>,
+    ) -> LookupResult<impl LookupContinuation<Output = Result<(), Error>, Buf = R> + 'unit> {
+        self.dwarf_and_unit_dwo(ctx).map(move |r| {
+            let (raw_dwarf, unit) = r?;
+            self.funcs
+                .borrow_with(|| Functions::parse(unit, raw_dwarf))
+                .as_ref()
+                .map_err(Error::clone)?
+                .parse_inlined_functions(unit, &ctx.parsed_dwarf, raw_dwarf)
+        })
     }
 
     fn find_location(
@@ -741,27 +1236,37 @@ impl<R: gimli::Reader> ResUnit<R> {
         LocationRangeUnitIter::new(self, sections, probe_low, probe_high)
     }
 
-    fn find_function_or_location(
-        &self,
+    fn find_function_or_location<'unit, 'ctx: 'unit>(
+        &'unit self,
         probe: u64,
-        dwarf: &ResDwarf<R>,
-    ) -> Result<(Option<&Function<R>>, Option<Location<'_>>), Error> {
-        let functions = self.parse_functions(dwarf)?;
-        let function = match functions.find_address(probe) {
-            Some(address) => {
-                let function_index = functions.addresses[address].function;
-                let (offset, ref function) = functions.functions[function_index];
-                Some(
-                    function
-                        .borrow_with(|| Function::parse(offset, &self.dw_unit, dwarf))
-                        .as_ref()
-                        .map_err(Error::clone)?,
-                )
-            }
-            None => None,
-        };
-        let location = self.find_location(probe, &dwarf.sections)?;
-        Ok((function, location))
+        ctx: &'ctx Context<R>,
+    ) -> LookupResult<
+        impl LookupContinuation<
+            Output = Result<(Option<&'unit Function<R>>, Option<Location<'unit>>), Error>,
+            Buf = R,
+        >,
+    > {
+        self.dwarf_and_unit_dwo(ctx).map(move |r| {
+            let (raw_dwarf, unit) = r?;
+            let functions = self.parse_functions_dwarf_and_unit(unit, raw_dwarf)?;
+            let function = match functions.find_address(probe) {
+                Some(address) => {
+                    let function_index = functions.addresses[address].function;
+                    let (offset, ref function) = functions.functions[function_index];
+                    Some(
+                        function
+                            .borrow_with(|| {
+                                Function::parse(offset, unit, &ctx.parsed_dwarf, raw_dwarf)
+                            })
+                            .as_ref()
+                            .map_err(Error::clone)?,
+                    )
+                }
+                None => None,
+            };
+            let location = self.find_location(probe, &raw_dwarf.sections)?;
+            Ok((function, location))
+        })
     }
 }
 
@@ -778,7 +1283,7 @@ pub struct LocationRangeIter<'ctx, R: gimli::Reader> {
 impl<'ctx, R: gimli::Reader> LocationRangeIter<'ctx, R> {
     #[inline]
     fn new(ctx: &'ctx Context<R>, probe_low: u64, probe_high: u64) -> Result<Self, Error> {
-        let sections = &ctx.dwarf.sections;
+        let sections = &ctx.raw_dwarf.sections;
         let unit_iter = ctx.find_units_range(probe_low, probe_high);
         Ok(Self {
             unit_iter: Box::new(unit_iter),
@@ -879,12 +1384,11 @@ impl<'ctx> LocationRangeUnitIter<'ctx> {
 
             let row_idx = if let Some(seq) = lines.sequences.get(seq_idx) {
                 let idx = seq.rows.binary_search_by(|row| row.address.cmp(&probe_low));
-                let idx = match idx {
+                match idx {
                     Ok(x) => x,
                     Err(0) => 0, // probe below sequence, but range could overlap
                     Err(x) => x - 1,
-                };
-                idx
+                }
             } else {
                 0
             };
@@ -906,12 +1410,7 @@ impl<'ctx> Iterator for LocationRangeUnitIter<'ctx> {
     type Item = (u64, u64, Location<'ctx>);
 
     fn next(&mut self) -> Option<(u64, u64, Location<'ctx>)> {
-        loop {
-            let seq = match self.seqs.get(self.seq_idx) {
-                Some(seq) => seq,
-                None => break,
-            };
-
+        while let Some(seq) = self.seqs.get(self.seq_idx) {
             if seq.start >= self.probe_high {
                 break;
             }


Test patch:
diff --git a/tests/correctness.rs b/tests/correctness.rs
index 955e2b8..66caf39 100644
--- a/tests/correctness.rs
+++ b/tests/correctness.rs
@@ -5,11 +5,13 @@ extern crate gimli;
 extern crate memmap2;
 extern crate object;
 
-use addr2line::Context;
+use addr2line::{Context, LookupResultExt};
 use fallible_iterator::FallibleIterator;
 use findshlibs::{IterationControl, SharedLibrary, TargetSharedLibrary};
 use object::Object;
+use std::borrow::Cow;
 use std::fs::File;
+use std::sync::Arc;
 
 fn find_debuginfo() -> memmap2::Mmap {
     let path = std::env::current_exe().unwrap();
@@ -42,7 +44,38 @@ fn correctness() {
     let map = find_debuginfo();
     let file = &object::File::parse(&*map).unwrap();
     let module_base = file.relative_address_base();
-    let ctx = Context::new(file).unwrap();
+
+    let endian = if file.is_little_endian() {
+        gimli::RunTimeEndian::Little
+    } else {
+        gimli::RunTimeEndian::Big
+    };
+
+    fn load_section<'data: 'file, 'file, O, Endian>(
+        id: gimli::SectionId,
+        file: &'file O,
+        endian: Endian,
+    ) -> Result<gimli::EndianArcSlice<Endian>, gimli::Error>
+    where
+        O: object::Object<'data, 'file>,
+        Endian: gimli::Endianity,
+    {
+        use object::ObjectSection;
+
+        let data = file
+            .section_by_name(id.name())
+            .and_then(|section| section.uncompressed_data().ok())
+            .unwrap_or(Cow::Borrowed(&[]));
+        Ok(gimli::EndianArcSlice::new(Arc::from(&*data), endian))
+    }
+
+    let dwarf = gimli::Dwarf::load(|id| load_section(id, file, endian)).unwrap();
+    let ctx = Context::from_dwarf(dwarf).unwrap();
+    #[cfg(unix)]
+    let mut split_dwarf_loader = addr2line::builtin_split_dwarf_loader::SplitDwarfLoader::new(
+        |data, endian| gimli::EndianArcSlice::new(Arc::from(&*data), endian),
+        None,
+    );
 
     let mut bias = None;
     TargetSharedLibrary::each(|lib| {
@@ -50,10 +83,15 @@ fn correctness() {
         IterationControl::Break
     });
 
-    let test = |sym: u64, expected_prefix: &str| {
+    #[allow(unused_mut)]
+    let mut test = |sym: u64, expected_prefix: &str| {
         let ip = sym.wrapping_sub(bias.unwrap());
 
-        let frames = ctx.find_frames(ip).unwrap();
+        let frames = ctx.find_frames(ip);
+        #[cfg(unix)]
+        let frames = split_dwarf_loader.run(frames).unwrap();
+        #[cfg(not(unix))]
+        let frames = frames.skip_all_loads().unwrap();
         let frame = frames.last().unwrap().unwrap();
         let name = frame.function.as_ref().unwrap().demangle().unwrap();
         // Old rust versions generate DWARF with wrong linkage name,
@@ -87,6 +125,13 @@ fn zero_function() {
     let file = &object::File::parse(&*map).unwrap();
     let ctx = Context::new(file).unwrap();
     for probe in 0..10 {
-        assert!(ctx.find_frames(probe).unwrap().count().unwrap() < 10);
+        assert!(
+            ctx.find_frames(probe)
+                .skip_all_loads()
+                .unwrap()
+                .count()
+                .unwrap()
+                < 10
+        );
     }
 }