Making stinkarm stink less, or more?

xnacly1 pts0 comments

Making stinkarm stink way less, or more? | xnacly - blogMaking stinkarm stink way less, or more?<br>Aug 16, 2026<br>2321 Words<br>11 Minute read

Tags:<br>Arm<br>Rust

Its been a while, but about half a year ago I wrote an article about<br>implementing a userspace armv7 emulator from scratch, meaning I implemented:<br>ARMASM<br>1 .section .rodata<br>2msg:<br>3 .asciz "Hello, world!\n"<br>5 .section .text<br>6 .global _start<br>7_start:<br>8 ldr r0, =1<br>9 ldr r1, =msg<br>10 mov r2, #14<br>11 mov r7, #4<br>12 svc #0<br>13<br>14 mov r0, #0<br>15 mov r7, #1<br>16 svc #0<br>Or as a list:<br>elf(32) parsing, validation and interpretation<br>decoding of a very small subset of armv7 instructions (only 3)<br>executing said instructions, even conditional ones 🤓<br>translating memory access from the guest into the host<br>syscall forwarding (from armv7 to x86)<br>syscall sandboxing (only a restricted syscall subset) and denying syscall execution<br>Do read Building a Minimal Viable Armv7 Emulator from<br>Scratch, since this<br>post doesnt go as deep into detail as the previous one (It&rsquo;s my first article<br>in 3 months I had enough motivation for writing :O). This is partially an<br>update, partially my toughts on decoding and emulating armv7-a and also a bit<br>of a devlog.<br>Overly complex host to guest mem translation<br>On the first article, ~aengelke on<br>lobste.rs,<br>had some comments, the one resonating the most was:<br>[&mldr;]<br>The Mem indirection seems pretty inefficient. When emulating 32-bit platforms<br>on a 64-bit system, just mmap a 4 GiB region, the translation then becomes a<br>single addition. Otherwise, having a small hash table of recently translated<br>address regions can avoid more expensive searches – memory accesses have a<br>very high locality. The number of mappings is usually small, so binary search<br>over a sorted array is simpler than a B-tree.<br>[&mldr;]

So now i figured, why not improve on my implementation a bit, first with<br>replacing the complex allocation region based tracking with just allocating a<br>4gig slab in memory for the guest, mapping the process regions there and<br>handing out pointers into that region to the guest.<br>So, previously the memory translation worked as follows:<br>One takes a binary tree map of a guest starting addr to its host segment<br>RUST<br>1struct MappedSegment {<br>2 host_ptr: *mut u8,<br>3 len: u32,<br>4}<br>6pub struct Mem {<br>7 maps: BTreeMapu32, MappedSegment>,<br>8}

On ask for a region handout, specifically on mapping ELF segments with a<br>starting addr, map_region is called:<br>RUST<br>1// in stinkarm::elf::pheader::Pheader::map:<br>3// record mapping in guest memory table, so CPU can translate guest vaddr to host pointer<br>4guest_mem.map_region(self.vaddr, len, segment_ptr);<br>6// in stinkarm::mem::Mem:<br>8pub fn map_region(&mut self, guest_addr: u32, len: u32, host_ptr: *mut u8) {<br>9 self.maps<br>10 .insert(guest_addr, MappedSegment { host_ptr, len });<br>11}

Since the cpu needs to fetch an instruction, there is read_u32, calling translate:<br>RUST<br>1/// translate a guest addr to a host addr we can write and read from<br>2pub fn translate(&self, guest_addr: u32) -> Optionmut u8> {<br>3 // Find the greatest key 4 let (&base, seg) = self.maps.range(..=guest_addr).next_back()?;<br>5 if guest_addr base.wrapping_add(seg.len) {<br>6 let offset = guest_addr.wrapping_sub(base);<br>7 Some(unsafe { seg.host_ptr.add(offset as usize) })<br>8 } else {<br>9 None<br>10 }<br>11}<br>12<br>13pub fn read_u32(&self, guest_addr: u32) -> Optionu32> {<br>14 let ptr = self.translate(guest_addr)?;<br>15 unsafe { Some(u32::from_le(*(ptr as *const u32))) }<br>16}<br>17<br>18<br>19// in stinkarm::cpu::Cpu:<br>20<br>21pub fn step(&mut self) -> Resultbool, err::Err> {<br>22 let Some(word) = self.mem.read_u32(self.pc()) else {<br>23 return Ok(false);<br>24 };<br>25<br>26 // [...]<br>27}

Of course this totally unnecessary work, we dont need to keep track of every<br>mapping/allocation/region by walking their ranges, we only need to make sure<br>the R/W interaction request is within bounds. Thus the new implementation is:<br>One takes a pointer and a size:<br>RUST<br>1pub struct Mem {<br>2 ptr: NonNullu8>,<br>3 len: usize,<br>4}

When asked to map ELF segments, stinkarm::mem::Mem::map_region is called:<br>RUST<br>1// in stinkarm::elf::pheader::Pheader::map:<br>2guest_mem.map_region(self.vaddr, file_slice)?;<br>4// in stinkarm::mem::Mem:<br>6pub fn map_region(&mut self, guest_addr: u32, data: &[u8]) -> Result(), String> {<br>7 let dst = self<br>8 .get_slice_mut(guest_addr, data.len())<br>9 .ok_or_else(|| format!("guest region out of bounds at {guest_addr:#010x}"))?;<br>10 dst.copy_from_slice(data);<br>11 Ok(())<br>12}

When cpu requests a dword for decoding, it does so by invoking<br>stinkarm::mem::Mem::read32, just as before, only this time with<br>bounds checks:<br>RUST<br>1pub fn read_u32(&self, guest_addr: u32) -> Optionu32> {<br>2 let bytes = self.get_slice(guest_addr, 4)?;<br>3 Some(u32::from_le_bytes(bytes.try_into().unwrap()))<br>4}<br>6fn get_slice(&self, guest_addr: u32, len: usize) -> Option[u8]> {<br>7 if !self.in_bounds(guest_addr, len) {<br>8 return None;<br>9 }<br>10<br>11 Some(unsafe { std::slice::from_raw_parts(self.ptr.as_ptr().add(guest_addr as usize), len) })<br>12}

Hardening the existing implementation<br>I...

self guest_addr stinkarm guest rust region

Related Articles