📝 24 Feb 2023
Emulating Arm64 Machine Code in Unicorn
Unicorn is a lightweight CPU Emulator Framework based on QEMU.
(Programmable with C, Rust, Python and many other languages)
We’re porting a new operating system Apache NuttX RTOS to Pine64 PinePhone. And I wondered…
To make PinePhone testing easier… Can we emulate Arm64 PinePhone with Unicorn Emulator?
Let’s find out! In this article we’ll call Unicorn Emulator to…
Emulate Arm64 Machine Code
Attach Hooks to intercept Memory Access and Code Execution
Boot Apache NuttX RTOS in the emulator
Simulate the UART Controller for PinePhone
Track an Exception in Arm64 Memory Management
We’ll do all this in basic Rust, instead of classic C.
(That’s because I’m too old to write meticulous C… But I’m OK to get nagged by Rust Compiler if I miss something!)
We begin by emulating some machine code…
Suppose we wish to emulate this Arm64 Machine Code…
// Start Address: 0x10000
// str w11, [x13], #0
AB 05 00 B8
// ldrb w15, [x13], #0
AF 05 40 38
// End Address: 0x10008
With these Arm64 Register Values…
Register | Value |
---|---|
X11 | 0x12345678 |
X13 | 0x10008 |
X15 | 0x33 |
Which means…
Store X11
(value 0x12345678
)
Into the address referenced by X13
(Address 0x10008
)
Load X15
as a Single Byte
From the address referenced by X13
(Address 0x10008
)
Which sets X15
to 0x78
(Because 0x10008
contains byte 0x78
)
This is how we call Unicorn Emulator to emulate the Arm64 Machine Code: main.rs
use unicorn_engine::{Unicorn, RegisterARM64};
use unicorn_engine::unicorn_const::{Arch, Mode, Permission};
fn main() {
// Arm64 Memory Address where emulation starts
const ADDRESS: u64 = 0x10000;
// Arm64 Machine Code for the above address
let arm64_code: Vec<u8> = vec![
0xab, 0x05, 0x00, 0xb8, // str w11, [x13], #0
0xaf, 0x05, 0x40, 0x38, // ldrb w15, [x13], #0
];
We begin by defining the Arm64 Machine Code.
Then we initialise the emulator…
// Init Emulator in Arm64 mode
let mut unicorn = Unicorn::new(
Arch::ARM64,
Mode::LITTLE_ENDIAN
).expect("failed to init Unicorn");
// Magical horse mutates to bird
let emu = &mut unicorn;
Unicorn needs some Emulated Memory to run our code.
We map 2MB of Executable Memory…
// Map 2MB of Executable Memory at 0x10000
// for Arm64 Machine Code
emu.mem_map(
ADDRESS, // Address is 0x10000
2 * 1024 * 1024, // Size is 2MB
Permission::ALL // Read, Write and Execute Access
).expect("failed to map code page");
And we populate the Executable Memory with our Arm64 Machine Code…
// Write Arm64 Machine Code to emulated Executable Memory
emu.mem_write(
ADDRESS, // Address is 0x10000
&arm64_code // Arm64 Machine Code
).expect("failed to write instructions");
We set the Arm64 Registers: X11, X13 and X15…
// Register Values
const X11: u64 = 0x12345678; // X11 value
const X13: u64 = ADDRESS + 0x8; // X13 value
const X15: u64 = 0x33; // X15 value
// Set the Arm64 Registers
emu.reg_write(RegisterARM64::X11, X11)
.expect("failed to set X11");
emu.reg_write(RegisterARM64::X13, X13)
.expect("failed to set X13");
emu.reg_write(RegisterARM64::X15, X15)
.expect("failed to set X15");
We start the emulator…
// Emulate Arm64 Machine Code
let err = emu.emu_start(
ADDRESS, // Begin Address is 0x10000
ADDRESS + arm64_code.len() as u64, // End Address is 0x10008
0, // No Timeout
0 // Unlimited number of instructions
);
// Print the Emulator Error
println!("err={:?}", err);
Finally we read Register X15 and verify the result…
// Read the X15 Register
assert_eq!(
emu.reg_read(RegisterARM64::X15), // Register X15
Ok(0x78) // Expected Result
);
}
And we’re done!
Remember to add unicorn-engine to the dependencies: Cargo.toml
[dependencies]
unicorn-engine = "2.0.0"
When we run our Rust Program…
→ cargo run --verbose
Fresh cc v1.0.79
Fresh cmake v0.1.49
Fresh pkg-config v0.3.26
Fresh bitflags v1.3.2
Fresh libc v0.2.139
Fresh unicorn-engine v2.0.1
Fresh pinephone-emulator v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.08s
Running `target/debug/pinephone-emulator`
err=Ok(())
Unicorn is hunky dory!
Let’s talk about Memory-Mapped Input / Output…
Memory Access Hook for Arm64 Emulation
To emulate our gadget (like PinePhone), we need to handle Memory-Mapped Input / Output.
(Like for printing to the Serial or UART Port)
We do this in Unicorn Emulator with a Memory Access Hook that will be called to intercept every Memory Access.
Here’s a sample Hook Function that will be called to intercept every Arm64 Read / Write Access: main.rs
// Hook Function for Memory Access.
// Called once for every Arm64 Memory Access.
fn hook_memory(
_: &mut Unicorn<()>, // Emulator
mem_type: MemType, // Read or Write Access
address: u64, // Accessed Address
size: usize, // Number of bytes accessed
value: i64 // Write Value
) -> bool { // Always return true
// TODO: Emulate Memory-Mapped Input/Output (UART Controller)
println!("hook_memory: mem_type={:?}, address={:#x}, size={:?}, value={:#x}", mem_type, address, size, value);
// Always return true, value is unused by caller
// https://github.com/unicorn-engine/unicorn/blob/dev/docs/FAQ.md#i-cant-recover-from-unmapped-readwrite-even-i-return-true-in-the-hook-why
true
}
Our Hook Function prints every Read / Write Access to the Emulated Arm64 Memory.
This is how we attach the Memory Hook Function to Unicorn Emulator: main.rs
// Add Hook for Arm64 Memory Access
let _ = emu.add_mem_hook(
HookType::MEM_ALL, // Intercept Read and Write Access
0, // Begin Address
u64::MAX, // End Address
hook_memory // Hook Function
).expect("failed to add memory hook");
When we run this, we see the Read and Write Memory Accesses made by our Emulated Arm64 Code…
hook_memory:
mem_type=WRITE,
address=0x10008,
size=4,
value=0x12345678
hook_memory:
mem_type=READ,
address=0x10008,
size=1,
value=0x0
(Value is not relevant for Memory Reads)
Later we’ll implement UART Output with a Memory Access Hook. But first we intercept some code…
Code Execution Hook for Arm64 Emulation
Can we intercept every Arm64 Instruction that will be emulated?
Yep we can attach a Code Execution Hook to Unicorn Emulator.
Here’s a sample Hook Function that will be called for every Arm64 Instruction emulated: main.rs
// Hook Function for Code Emulation.
// Called once for each Arm64 Instruction.
fn hook_code(
_: &mut Unicorn<()>, // Emulator
address: u64, // Instruction Address
size: u32 // Instruction Size
) {
// TODO: Handle special Arm64 Instructions
println!("hook_code: address={:#x}, size={:?}", address, size);
}
And this is how we call Unicorn Emulator to attach the Code Hook Function: main.rs
// Add Hook for emulating each Arm64 Instruction
let _ = emu.add_code_hook(
ADDRESS, // Begin Address
ADDRESS + arm64_code.len() as u64, // End Address
hook_code // Hook Function for Code Emulation
).expect("failed to add code hook");
When we run this with our Arm64 Machine Code, we see the Address of every Arm64 Instruction emulated (and its size)…
hook_code:
address=0x10000,
size=4
hook_code:
address=0x10004,
size=4
We might use this to emulate Special Arm64 Instructions.
If we don’t need to intercept every single instruction, try the Block Execution Hook…
Is there something that works like a Code Execution Hook…
But doesn’t stop at every single Arm64 Instruction?
Yep Unicorn Emulator supports Block Execution Hooks.
This Hook Function will be called once when executing a Block of Arm64 Instructions: main.rs
// Hook Function for Block Emulation.
// Called once for each Basic Block of Arm64 Instructions.
fn hook_block(
_: &mut Unicorn<()>, // Emulator
address: u64, // Block Address
size: u32 // Block Size
) {
// TODO: Trace the flow of emulated code
println!("hook_block: address={:#x}, size={:?}", address, size);
}
This is how we attach the Block Execution Hook: main.rs
// Add Hook for emulating each Basic Block of Arm64 Instructions
let _ = emu.add_block_hook(hook_block)
.expect("failed to add block hook");
Block Execution Hooks are less granular (called less often) than Code Execution Hooks…
hook_block: address=0x10000, size=8
hook_code: address=0x10000, size=4
hook_code: address=0x10004, size=4
Which means that Unicorn Emulator calls our Hook Function only once for the entire Block of two Arm64 Instructions.
(What’s a Block of Arm64 Instructions?)
How is this useful?
The Block Execution Hook is super helpful for monitoring the Execution Flow of our emulated code. We can…
Read the ELF Debug Symbols (from the NuttX Image)
Match with the Block Execution Addresses
And print the Name of the Function that’s being emulated
This is how we do it…
What happens when Unicorn Emulator tries to access memory that isn’t mapped?
Unicorn Emulator will call our Memory Access Hook with mem_type set to READ_UNMAPPED…
hook_memory:
address=0x01c28014,
size=2,
mem_type=READ_UNMAPPED,
value=0x0
The log says that our Arm64 Machine Code attempted to read address 01C2
8014
, which is unmapped.
This is how we map the memory: rust.rs
// Map 16 MB at 0x0100 0000 for Memory-Mapped I/O by Allwinner A64 Peripherals
// https://github.com/apache/nuttx/blob/master/arch/arm64/src/a64/hardware/a64_memorymap.h#L33-L51
emu.mem_map(
0x0100_0000, // Address
16 * 1024 * 1024, // Size
Permission::READ | Permission::WRITE // Read and Write Access
).expect("failed to map memory mapped I/O");
We’ll see this later when we handle Memory-Mapped Input / Output.
Can we map Memory Regions during emulation?
Yep we may use a Memory Access Hook to map memory regions on the fly.
Running Apache NuttX RTOS in Unicorn
We’re ready to run Apache NuttX RTOS in Unicorn Emulator!
We’ve compiled Apache NuttX RTOS for PinePhone into an Arm64 Binary Image: nuttx.bin
This is how we load the NuttX Binary Image into Unicorn: main.rs
// Arm64 Memory Address where emulation starts
const ADDRESS: u64 = 0x4008_0000;
// Arm64 Machine Code for the above address
let arm64_code = include_bytes!("../nuttx/nuttx.bin");
We initialise the emulator the same way…
// Init Emulator in Arm64 mode
let mut unicorn = Unicorn::new(
Arch::ARM64,
Mode::LITTLE_ENDIAN
).expect("failed to init Unicorn");
// Magical horse mutates to bird
let emu = &mut unicorn;
Based on the NuttX Memory Map for PinePhone, we map two Memory Regions for NuttX…
Executable Memory (128 MB) at 4000
0000
(For Arm64 Machine Code, Data and BSS)
Read / Write Memory (512 MB) at 0000
0000
(For Memory-Mapped I/O by Allwinner A64 Peripherals)
// Map 128 MB Executable Memory at 0x4000 0000 for Arm64 Machine Code
// https://github.com/apache/nuttx/blob/master/arch/arm64/include/a64/chip.h#L44-L52
emu.mem_map(
0x4000_0000, // Address
128 * 1024 * 1024, // Size
Permission::ALL // Read, Write and Execute Access
).expect("failed to map code page");
// Map 512 MB Read/Write Memory at 0x0000 0000 for
// Memory-Mapped I/O by Allwinner A64 Peripherals
// https://github.com/apache/nuttx/blob/master/arch/arm64/include/a64/chip.h#L44-L52
emu.mem_map(
0x0000_0000, // Address
512 * 1024 * 1024, // Size
Permission::READ | Permission::WRITE // Read and Write Access
).expect("failed to map memory mapped I/O");
We load the NuttX Machine Code into Emulated Memory…
// Write Arm64 Machine Code to emulated Executable Memory
emu.mem_write(
ADDRESS, // Address is 4008 0000
arm64_code // NuttX Binary Image
).expect("failed to write instructions");
And we run NuttX RTOS!
// Omitted: Attach Code, Block and Memory Hooks
...
// Emulate Arm64 Machine Code
let err = emu.emu_start(
ADDRESS, // Begin Address
ADDRESS + arm64_code.len() as u64, // End Address
0, // No Timeout
0 // Unlimited number of instructions
);
Unicorn happily boots Nuttx RTOS (yay!)…
→ cargo run
hook_block: address=0x40080000, size=8
hook_block: address=0x40080040, size=4
hook_block: address=0x40080044, size=12
hook_block: address=0x40080118, size=16
...
But our legendary creature gets stuck in mud. Let’s find out why…
Unicorn gets stuck in a curious loop while booting NuttX RTOS…
hook_code: address=0x400801f8, size=4
hook_code: address=0x400801fc, size=4
hook_block: address=0x400801f4, size=12
hook_code: address=0x400801f4, size=4
hook_memory: address=0x01c28014, size=2, mem_type=READ, value=0x0
hook_code: address=0x400801f8, size=4
hook_code: address=0x400801fc, size=4
hook_block: address=0x400801f4, size=12
hook_code: address=0x400801f4, size=4
hook_memory: address=0x01c28014, size=2, mem_type=READ, value=0x0
...
See the pattern? Unicorn Emulator loops forever at address 4008
01F4
…
While reading the data from address 01C2
8014
.
What’s at 4008 01F4?
Here’s the NuttX Arm64 Disassembly at address 4008
01F4
: nuttx.S
SECTION_FUNC(text, up_lowputc)
ldr x15, =UART0_BASE_ADDRESS
400801f0: 580000cf ldr x15, 40080208 <up_lowputc+0x18>
nuttx/arch/arm64/src/chip/a64_lowputc.S:89
early_uart_ready x15, w2
400801f4: 794029e2 ldrh w2, [x15, #20]
400801f8: 721b005f tst w2, #0x20
400801fc: 54ffffc0 b.eq 400801f4 <up_lowputc+0x4> // b.none
nuttx/arch/arm64/src/chip/a64_lowputc.S:90
early_uart_transmit x15, w0
40080200: 390001e0 strb w0, [x15]
nuttx/arch/arm64/src/chip/a64_lowputc.S:91
ret
40080204: d65f03c0 ret
Which comes from this NuttX Source Code: a64_lowputc.S
/* Wait for A64 UART to be ready to transmit
* xb: Register that contains the UART Base Address
* wt: Scratch register number
*/
.macro early_uart_ready xb, wt
1:
ldrh \wt, [\xb, #0x14] /* UART_LSR (Line Status Register) */
tst \wt, #0x20 /* Check THRE (TX Holding Register Empty) */
b.eq 1b /* Wait for the UART to be ready (THRE=1) */
.endm
NuttX is printing something to the UART Port?
Yep! NuttX prints Debug Messages to the (Serial) UART Port when it boots…
And it’s waiting for the UART Controller to be ready, before printing!
What’s at 01C2 8014?
01C2
8014
is the UART Line Status Register (UART_LSR) for the Allwinner A64 UART Controller inside PinePhone.
Bit 5 needs to be set to 1 to indicate that the UART Transmit FIFO is ready. Or NuttX will wait forever!
How to fix the UART Ready Bit at 01C2 8014?
This is how we emulate the UART Ready Bit with our Input / Output Memory: main.rs
// Allwinner A64 UART Line Status Register (UART_LSR) at Offset 0x14.
// To indicate that the UART Transmit FIFO is ready:
// Set Bit 5 to 1.
// https://lupyuen.codeberg.page/articles/serial#wait-to-transmit
emu.mem_write(
0x01c2_8014, // UART Register Address
&[0b10_0000] // UART Register Value
).expect("failed to set UART_LSR");
And Unicorn Emulator stops looping!
Unicorn continues booting NuttX, which fills the BSS Section with 0…
hook_block: address=0x40089328, size=8
hook_memory: address=0x400b6a52, size=1, mem_type=WRITE, value=0x0
hook_block: address=0x40089328, size=8
hook_memory: address=0x400b6a53, size=1, mem_type=WRITE, value=0x0
...
But we don’t see any NuttX Boot Messages. Let’s print the UART Output…
Emulating UART Output in Unicorn
We expect to see Boot Messages from NuttX…
How do we print the UART Output?
NuttX RTOS will write the UART Output to Allwinner A64’s UART Transmit Holding Register (THR) at 01C2
8000
…
To emulate the UART Output, we use Unicorn’s Memory Access Hook…
In our Memory Access Hook, we intercept all writes to 01C2
8000
and dump the bytes written: main.rs
// Hook Function for Memory Access.
// Called once for every Arm64 Memory Access.
fn hook_memory(
_: &mut Unicorn<()>, // Emulator
mem_type: MemType, // Read or Write Access
address: u64, // Accessed Address
size: usize, // Number of bytes accessed
value: i64 // Write Value
) -> bool { // Always return true
// If writing to UART Transmit Holding Register (THR):
// Print the output
// https://lupyuen.codeberg.page/articles/serial#transmit-uart
if address == 0x01c2_8000 {
println!("uart output: {:?}", value as u8 as char);
}
// Always return true, value is unused by caller
// https://github.com/unicorn-engine/unicorn/blob/dev/docs/FAQ.md#i-cant-recover-from-unmapped-readwrite-even-i-return-true-in-the-hook-why
true
}
When we run this, we see a long chain of UART Output…
→ cargo run | grep uart
uart output: '-'
uart output: ' '
uart output: 'R'
uart output: 'e'
uart output: 'a'
uart output: 'd'
uart output: 'y'
...
Which reads as…
- Ready to Boot CPU
- Boot from EL2
- Boot from EL1
- Boot to C runtime for OS Initialize
Yep NuttX RTOS is booting on Unicorn Emulator! But we have a problem…
NuttX RTOS boots OK on Unicorn Emulator?
Not quite. Unicorn Emulator halts with an Arm64 Exception at address 4008
0EF8
…
hook_block: address=0x40080eec, size=16
hook_code: address=0x40080eec, size=4
hook_code: address=0x40080ef0, size=4
hook_code: address=0x40080ef4, size=4
hook_code: address=0x40080ef8, size=4
err=Err(EXCEPTION)
Here’s the NuttX Arm64 Disassembly at address 4008
0EF8
: nuttx.S
nuttx/arch/arm64/src/common/arm64_mmu.c:544
write_sysreg((value | SCTLR_M_BIT | SCTLR_C_BIT), sctlr_el1);
40080ef0: d28000a1 mov x1, #0x5 // #5
40080ef4: aa010000 orr x0, x0, x1
40080ef8: d5181000 msr sctlr_el1, x0
Which comes from this NuttX Source Code: arm64_mmu.c
// Enable the MMU and data cache:
// Read from System Control Register EL1
value = read_sysreg(sctlr_el1);
// Write to System Control Register EL1
write_sysreg( // Write to System Register...
value | SCTLR_M_BIT | SCTLR_C_BIT, // Enable Address Translation and Caching
sctlr_el1 // System Control Register EL1
);
The code above sets these flags in Arm64 System Control Register EL1 (SCTLR_EL1)…
SCTLR_M_BIT (Bit 0): Enable Address Translation for EL0 and EL1 Stage 1
SCTLR_C_BIT (Bit 2): Enable Caching for EL0 and EL1 Stage 1
Thus the Address Translation (or Caching) has failed in our Emulated Arm64 Memory Management Unit, inside enable_mmu_el1…
We won’t chase the Unicorn into the Rabbit Hole, but the details are covered here…
Apache NuttX RTOS on PinePhone
So are we happy with Unicorn Emulator?
Yep! Unicorn Emulator is sufficient for Automated Daily Build and Test for NuttX on PinePhone. (Via GitHub Actions)
Which will be similar to this BL602 setup, except that we’ll boot the Daily Build on Unicorn Emulator (instead of Real Hardware)…
Also Unicorn Emulator has produced a Call Graph for NuttX on PinePhone, which is extremely valuable for troubleshooting…
But our PinePhone Emulator doesn’t handle Console Input…
Yeah we’ll do that later. We have a long wishlist of features to build: Interrupts, Memory Protection, Multiple CPUs, Cortex A53, GIC v2, …
NuttX runs graphical apps on PinePhone right?
Yep someday we’ll render the NuttX Graphics Framebuffers in Unicorn.
(Maybe with a Rust GUI Library. Or Unicorn.js and WebAssembly)
What about emulating other operating systems: Linux / macOS / Windows / Android?
Check out the Qiling Binary Emulation Framework…
How about other hardware platforms: STM32 Blue Pill and ESP32?
Check out QEMU Emulator…
Check out the next article…
This has been a fun educational exercise. Now we have a way to run Automated Daily Tests for Apache NuttX RTOS on PinePhone… Kudos to the Maintainers of Unicorn Emulator!
Many Thanks to my GitHub Sponsors for supporting my work! This article wouldn’t have been possible without your support.
Got a question, comment or suggestion? Create an Issue or submit a Pull Request here…
lupyuen.github.io/src/unicorn.md
Check out the new article…
Our Block Execution Hook now prints the Function Name and the Filename…
hook_block:
address=0x40080eb0,
size=12,
setup_page_tables,
arch/arm64/src/common/arm64_mmu.c:516:25
hook_block:
address=0x40080eec,
size=16,
enable_mmu_el1,
arch/arm64/src/common/arm64_mmu.c:543:11
err=Err(EXCEPTION)
Our Hook Function looks up the Address in the DWARF Debug Symbols of the NuttX ELF File, like so: main.rs
/// Hook Function for Block Emulation.
/// Called once for each Basic Block of Arm64 Instructions.
fn hook_block(
_: &mut Unicorn<()>, // Emulator
address: u64, // Block Address
size: u32 // Block Size
) {
print!("hook_block: address={:#010x}, size={:02}", address, size);
// Print the Function Name
let function = map_address_to_function(address);
if let Some(ref name) = function {
print!(", {}", name);
}
// Print the Source Filename
let loc = map_address_to_location(address);
if let Some((ref file, line, col)) = loc {
let file = file.clone().unwrap_or("".to_string());
let line = line.unwrap_or(0);
let col = col.unwrap_or(0);
print!(", {}:{}:{}", file, line, col);
}
println!();
}
We map the Block Address to Function Name and Source File in map_address_to_function and map_address_to_location: main.rs
/// Map the Arm64 Code Address to the Function Name by looking up the ELF Context
fn map_address_to_function(
address: u64 // Code Address
) -> Option<String> { // Function Name
// Lookup the Arm64 Code Address in the ELF Context
let context = ELF_CONTEXT.context.borrow();
let mut frames = context.find_frames(address)
.expect("failed to find frames");
// Return the Function Name
if let Some(frame) = frames.next().unwrap() {
if let Some(func) = frame.function {
if let Ok(name) = func.raw_name() {
let s = String::from(name);
return Some(s);
}
}
}
None
}
/// Map the Arm64 Code Address to the Source Filename, Line and Column
fn map_address_to_location(
address: u64 // Code Address
) -> Option<( // Returns...
Option<String>, // Filename
Option<u32>, // Line
Option<u32> // Column
)> {
// Lookup the Arm64 Code Address in the ELF Context
let context = ELF_CONTEXT.context.borrow();
let loc = context.find_location(address)
.expect("failed to find location");
// Return the Filename, Line and Column
if let Some(loc) = loc {
if let Some(file) = loc.file {
let s = String::from(file)
.replace("/private/tmp/nuttx/nuttx/", "")
.replace("arch/arm64/src/chip", "arch/arm64/src/a64"); // TODO: Handle other chips
Some((Some(s), loc.line, loc.column))
} else {
Some((None, loc.line, loc.column))
}
} else {
None
}
}
To run this, we need the addr2line, gimli and once_cell crates: Cargo.toml
[dependencies]
addr2line = "0.19.0"
gimli = "0.27.2"
once_cell = "1.17.1"
unicorn-engine = "2.0.0"
At startup, we load the NuttX ELF File into ELF_CONTEXT as a Lazy Static: main.rs
use std::rc::Rc;
use std::cell::RefCell;
use once_cell::sync::Lazy;
/// ELF File for mapping Addresses to Function Names and Filenames
const ELF_FILENAME: &str = "nuttx/nuttx";
/// ELF Context for mapping Addresses to Function Names and Filenames
static ELF_CONTEXT: Lazy<ElfContext> = Lazy::new(|| {
// Open the ELF File
let path = std::path::PathBuf::from(ELF_FILENAME);
let file_data = std::fs::read(path)
.expect("failed to read ELF");
let slice = file_data.as_slice();
// Parse the ELF File
let obj = addr2line::object::read::File::parse(slice)
.expect("failed to parse ELF");
let context = addr2line::Context::new(&obj)
.expect("failed to parse debug info");
// Set the ELF Context
ElfContext {
context: RefCell::new(context),
}
});
/// Wrapper for ELF Context. Needed for `Lazy`
struct ElfContext {
context: RefCell<
addr2line::Context<
gimli::EndianReader<
gimli::RunTimeEndian,
Rc<[u8]> // Doesn't implement Send / Sync
>
>
>
}
/// Send and Sync for ELF Context. Needed for `Lazy`
unsafe impl Send for ElfContext {}
unsafe impl Sync for ElfContext {}