1#![allow(unsafe_code)] use std::ops::Range;
26
27#[cfg(any(target_os = "linux", target_os = "macos"))]
28const MAX_MODULE_IMAGE_BYTES: u64 = 512 * 1024 * 1024;
29
30#[cfg(windows)]
31use std::io;
32#[cfg(windows)]
33use winapi::shared::minwindef::{DWORD, HMODULE};
34#[cfg(windows)]
35use winapi::um::processthreadsapi::GetCurrentProcess;
36#[cfg(windows)]
37use winapi::um::psapi::{
38 EnumProcessModules, GetModuleFileNameExW, GetModuleInformation, MODULEINFO,
39};
40#[cfg(windows)]
41use winapi::um::winnt::{
42 IMAGE_DOS_HEADER, IMAGE_DOS_SIGNATURE, IMAGE_NT_HEADERS64, IMAGE_NT_SIGNATURE,
43 IMAGE_SECTION_HEADER,
44};
45
46fn hex_bytes(bytes: &[u8]) -> String {
47 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
48}
49
50#[cfg(target_os = "linux")]
51struct LoadedElfIdentity {
52 load_bias: u64,
53 mapped_ranges: Vec<Range<u64>>,
54 debug_id: String,
55}
56
57#[cfg(target_os = "linux")]
58fn loaded_elf_identities() -> Vec<LoadedElfIdentity> {
59 unsafe extern "C" fn visit(
60 info: *mut libc::dl_phdr_info,
61 _size: libc::size_t,
62 data: *mut libc::c_void,
63 ) -> libc::c_int {
64 const MAX_NOTE_BYTES: usize = 1024 * 1024;
65 let info = unsafe { &*info };
66 let out = unsafe { &mut *data.cast::<Vec<LoadedElfIdentity>>() };
67 if info.dlpi_phdr.is_null() || info.dlpi_phnum == 0 {
68 return 0;
69 }
70 #[allow(clippy::unnecessary_cast)]
73 let load_bias = info.dlpi_addr as u64;
74 let headers =
75 unsafe { std::slice::from_raw_parts(info.dlpi_phdr, usize::from(info.dlpi_phnum)) };
76 let mapped_ranges = headers
77 .iter()
78 .filter(|header| header.p_type == libc::PT_LOAD && header.p_memsz > 0)
79 .filter_map(|header| {
80 let start = load_bias.checked_add(header.p_vaddr)?;
81 let end = start.checked_add(header.p_memsz)?;
82 Some(start..end)
83 })
84 .collect::<Vec<_>>();
85 for header in headers {
86 if header.p_type != libc::PT_NOTE {
87 continue;
88 }
89 let Ok(length) = usize::try_from(header.p_memsz) else {
90 continue;
91 };
92 if length == 0 || length > MAX_NOTE_BYTES {
93 continue;
94 }
95 let Some(address) = load_bias.checked_add(header.p_vaddr) else {
96 continue;
97 };
98 let Some(note_end) = address.checked_add(length as u64) else {
99 continue;
100 };
101 let is_mapped = headers.iter().any(|load| {
102 if load.p_type != libc::PT_LOAD || load.p_flags & libc::PF_R == 0 {
103 return false;
104 }
105 let Some(start) = load_bias.checked_add(load.p_vaddr) else {
106 return false;
107 };
108 let Some(end) = start.checked_add(load.p_memsz) else {
109 return false;
110 };
111 address >= start && note_end <= end
112 });
113 if address == 0 || !is_mapped {
114 continue;
115 }
116 let notes = unsafe { std::slice::from_raw_parts(address as *const u8, length) };
117 if let Some(build_id) = gnu_build_id_from_notes(notes) {
118 out.push(LoadedElfIdentity {
119 load_bias,
120 mapped_ranges,
121 debug_id: format!("elf:{}", hex_bytes(build_id)),
122 });
123 break;
124 }
125 }
126 0
127 }
128
129 let mut out = Vec::new();
130 unsafe {
131 libc::dl_iterate_phdr(
132 Some(visit),
133 (&mut out as *mut Vec<LoadedElfIdentity>).cast::<libc::c_void>(),
134 );
135 }
136 out
137}
138
139#[cfg(target_os = "linux")]
140fn gnu_build_id_from_notes(mut notes: &[u8]) -> Option<&[u8]> {
141 fn aligned(value: usize) -> Option<usize> {
142 value.checked_add(3).map(|value| value & !3)
143 }
144
145 while notes.len() >= 12 {
146 let name_len = usize::try_from(u32::from_ne_bytes(notes[0..4].try_into().ok()?)).ok()?;
147 let desc_len = usize::try_from(u32::from_ne_bytes(notes[4..8].try_into().ok()?)).ok()?;
148 let kind = u32::from_ne_bytes(notes[8..12].try_into().ok()?);
149 let name_end = 12usize.checked_add(name_len)?;
150 let desc_start = 12usize.checked_add(aligned(name_len)?)?;
151 let desc_end = desc_start.checked_add(desc_len)?;
152 let next = desc_start.checked_add(aligned(desc_len)?)?;
153 if next > notes.len() || name_end > notes.len() || desc_end > notes.len() {
154 return None;
155 }
156 if kind == 3 && notes.get(12..name_end)?.starts_with(b"GNU") && desc_len > 0 {
157 return notes.get(desc_start..desc_end);
158 }
159 notes = ¬es[next..];
160 }
161 None
162}
163
164#[cfg(windows)]
165unsafe fn loaded_pe_debug_info(base: u64, image_size: u64) -> Option<(String, String)> {
166 const DEBUG_DIRECTORY_INDEX: usize = 6;
167 const IMAGE_DEBUG_TYPE_CODEVIEW: u32 = 2;
168 const DEBUG_DIRECTORY_SIZE: usize = 28;
169
170 let dos = unsafe { &*(base as *const IMAGE_DOS_HEADER) };
171 if dos.e_magic != IMAGE_DOS_SIGNATURE {
172 return None;
173 }
174 let nt_offset = usize::try_from(dos.e_lfanew).ok()?;
175 if nt_offset.checked_add(std::mem::size_of::<IMAGE_NT_HEADERS64>())?
176 > usize::try_from(image_size).ok()?
177 {
178 return None;
179 }
180 let nt_address = (base as usize).checked_add(nt_offset)?;
181 let nt = unsafe { &*(nt_address as *const IMAGE_NT_HEADERS64) };
182 if nt.Signature != IMAGE_NT_SIGNATURE {
183 return None;
184 }
185 let directory = nt.OptionalHeader.DataDirectory[DEBUG_DIRECTORY_INDEX];
186 let directory_start = u64::from(directory.VirtualAddress);
187 let directory_size = usize::try_from(directory.Size).ok()?;
188 if directory_start
189 .checked_add(directory_size as u64)?
190 .gt(&image_size)
191 {
192 return None;
193 }
194 let directory_address = base.checked_add(directory_start)?;
195 let bytes =
196 unsafe { std::slice::from_raw_parts(directory_address as *const u8, directory_size) };
197 for entry in bytes.chunks_exact(DEBUG_DIRECTORY_SIZE) {
198 let kind = u32::from_le_bytes(entry[12..16].try_into().ok()?);
199 if kind != IMAGE_DEBUG_TYPE_CODEVIEW {
200 continue;
201 }
202 let size = usize::try_from(u32::from_le_bytes(entry[16..20].try_into().ok()?)).ok()?;
203 let rva = u64::from(u32::from_le_bytes(entry[20..24].try_into().ok()?));
204 if size < 24 || rva.checked_add(size as u64)?.gt(&image_size) {
205 continue;
206 }
207 let record_address = base.checked_add(rva)?;
208 let record = unsafe { std::slice::from_raw_parts(record_address as *const u8, size) };
209 if record.get(..4) != Some(b"RSDS") {
210 continue;
211 }
212 let mut guid: [u8; 16] = record.get(4..20)?.try_into().ok()?;
213 guid[0..4].reverse();
214 guid[4..6].reverse();
215 guid[6..8].reverse();
216 let age = u32::from_le_bytes(record.get(20..24)?.try_into().ok()?);
217 let path_bytes = record.get(24..)?;
218 let path_end = path_bytes
219 .iter()
220 .position(|byte| *byte == 0)
221 .unwrap_or(path_bytes.len());
222 let recorded = String::from_utf8_lossy(&path_bytes[..path_end]);
223 let pdb_name = recorded
224 .rsplit(['/', '\\'])
225 .find(|part| !part.is_empty())?
226 .to_owned();
227 if pdb_name == "."
228 || pdb_name == ".."
229 || pdb_name.chars().any(|character| {
230 matches!(
231 character,
232 '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*'
233 )
234 })
235 {
236 return None;
237 }
238 return Some((format!("pdb:{}-{age}", hex_bytes(&guid)), pdb_name));
239 }
240 None
241}
242
243#[cfg(target_os = "macos")]
244unsafe fn loaded_macho_uuid(header: *const u8) -> Option<[u8; 16]> {
245 const MH_MAGIC_64: u32 = 0xfeed_facf;
246 const LC_UUID: u32 = 0x1b;
247 const MACH_HEADER_64_SIZE: usize = 32;
248 const MAX_LOAD_COMMAND_BYTES: usize = 1024 * 1024;
249 const MAX_LOAD_COMMANDS: u32 = 4096;
250
251 let magic = unsafe { (header.cast::<u32>()).read_unaligned() };
252 if magic != MH_MAGIC_64 {
253 return None;
254 }
255 let ncmds = unsafe { header.add(16).cast::<u32>().read_unaligned() };
256 let sizeofcmds =
257 usize::try_from(unsafe { header.add(20).cast::<u32>().read_unaligned() }).ok()?;
258 if ncmds > MAX_LOAD_COMMANDS || sizeofcmds > MAX_LOAD_COMMAND_BYTES {
259 return None;
260 }
261 let commands =
262 unsafe { std::slice::from_raw_parts(header.add(MACH_HEADER_64_SIZE), sizeofcmds) };
263 let mut offset = 0usize;
264 for _ in 0..ncmds {
265 let prefix = commands.get(offset..offset.checked_add(8)?)?;
266 let command = u32::from_le_bytes(prefix[0..4].try_into().ok()?);
267 let size = usize::try_from(u32::from_le_bytes(prefix[4..8].try_into().ok()?)).ok()?;
268 if size < 8 || offset.checked_add(size)? > commands.len() {
269 return None;
270 }
271 if command == LC_UUID && size >= 24 {
272 return commands.get(offset + 8..offset + 24)?.try_into().ok();
273 }
274 offset += size;
275 }
276 None
277}
278
279#[derive(Clone, Debug, PartialEq, Eq)]
281pub struct Section {
282 pub name: String,
287 pub range: Range<u64>,
289}
290
291#[derive(Clone, Debug)]
293pub struct LoadedModule {
294 pub base: u64,
296 pub size: u64,
298 pub(crate) mapped_ranges: Vec<Range<u64>>,
300 #[cfg_attr(any(windows, not(target_arch = "x86_64")), allow(dead_code))]
302 pub(crate) executable_ranges: Vec<Range<u64>>,
303 pub path: Option<String>,
310 pub debug_id: Option<String>,
316 pub debug_file: Option<String>,
318 pub sections: Vec<Section>,
320}
321
322impl LoadedModule {
323 pub fn range(&self) -> Range<u64> {
325 match (self.mapped_ranges.first(), self.mapped_ranges.last()) {
326 (Some(first), Some(last)) => first.start..last.end,
327 _ => self.base..self.base + self.size,
328 }
329 }
330
331 pub fn contains(&self, address: u64) -> bool {
333 if self.mapped_ranges.is_empty() {
334 self.range().contains(&address)
335 } else {
336 self.mapped_ranges
337 .iter()
338 .any(|range| range.contains(&address))
339 }
340 }
341
342 #[cfg_attr(any(windows, not(target_arch = "x86_64")), allow(dead_code))]
344 pub(crate) fn contains_executable(&self, address: u64) -> bool {
345 self.executable_ranges
346 .iter()
347 .any(|range| range.contains(&address))
348 }
349
350 pub fn section(&self, name: &str) -> Option<&Section> {
352 self.sections.iter().find(|s| s.name == name)
353 }
354}
355
356#[cfg(windows)]
364unsafe fn read_sections(base: u64) -> Option<Vec<Section>> {
365 let dos = base as *const IMAGE_DOS_HEADER;
366 if (*dos).e_magic != IMAGE_DOS_SIGNATURE {
367 return None;
368 }
369
370 let lfanew = (*dos).e_lfanew;
372 if lfanew < 0 {
373 return None;
374 }
375 let nt = (base + lfanew as u64) as *const IMAGE_NT_HEADERS64;
376 if (*nt).Signature != IMAGE_NT_SIGNATURE {
377 return None;
378 }
379
380 let section_count = (*nt).FileHeader.NumberOfSections as usize;
381 let opt_size = (*nt).FileHeader.SizeOfOptionalHeader as u64;
385 let opt_start = base + lfanew as u64 + 4 + 20 ;
386 let table = (opt_start + opt_size) as *const IMAGE_SECTION_HEADER;
387
388 let mut sections = Vec::with_capacity(section_count);
389 for i in 0..section_count {
390 let header = &*table.add(i);
391
392 let raw = &header.Name;
396 let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
397 let name = String::from_utf8_lossy(&raw[..end]).into_owned();
398
399 let start = base + u64::from(header.VirtualAddress);
400 let size = u64::from(unsafe { *header.Misc.VirtualSize() });
403
404 sections.push(Section {
405 name,
406 range: start..start + size,
407 });
408 }
409 Some(sections)
410}
411
412#[cfg(windows)]
414pub fn enumerate_modules() -> io::Result<Vec<LoadedModule>> {
415 let process = unsafe { GetCurrentProcess() };
416
417 let mut needed: DWORD = 0;
420 let ok = unsafe { EnumProcessModules(process, std::ptr::null_mut(), 0, &mut needed) };
421 if ok == 0 {
422 return Err(io::Error::last_os_error());
423 }
424
425 let count = needed as usize / std::mem::size_of::<HMODULE>();
426 let mut handles: Vec<HMODULE> = vec![std::ptr::null_mut(); count];
427 let mut needed2: DWORD = 0;
428 let ok = unsafe {
429 EnumProcessModules(
430 process,
431 handles.as_mut_ptr(),
432 (handles.len() * std::mem::size_of::<HMODULE>()) as DWORD,
433 &mut needed2,
434 )
435 };
436 if ok == 0 {
437 return Err(io::Error::last_os_error());
438 }
439 let usable = (needed2 as usize / std::mem::size_of::<HMODULE>()).min(handles.len());
441
442 let mut modules = Vec::with_capacity(usable);
443 for handle in handles.into_iter().take(usable) {
444 let mut info: MODULEINFO = unsafe { std::mem::zeroed() };
445 let ok = unsafe {
446 GetModuleInformation(
447 process,
448 handle,
449 &mut info,
450 std::mem::size_of::<MODULEINFO>() as DWORD,
451 )
452 };
453 if ok == 0 {
454 continue;
457 }
458
459 let base = info.lpBaseOfDll as u64;
460 let sections = match unsafe { read_sections(base) } {
461 Some(s) => s,
462 None => continue,
463 };
464 let (debug_id, debug_file) = unsafe {
465 loaded_pe_debug_info(base, u64::from(info.SizeOfImage))
466 .map(|(identity, file)| (Some(identity), Some(file)))
467 .unwrap_or((None, None))
468 };
469
470 modules.push(LoadedModule {
471 base,
472 size: u64::from(info.SizeOfImage),
473 mapped_ranges: Vec::new(),
474 executable_ranges: Vec::new(),
475 path: unsafe { module_path(process, handle) },
476 debug_id,
477 debug_file,
478 sections,
479 });
480 }
481
482 Ok(modules)
483}
484
485#[cfg(windows)]
491unsafe fn module_path(process: winapi::um::winnt::HANDLE, handle: HMODULE) -> Option<String> {
492 let mut buffer = [0u16; 32768];
493 let len = GetModuleFileNameExW(process, handle, buffer.as_mut_ptr(), buffer.len() as DWORD);
494 if len == 0 {
495 return None;
496 }
497 Some(String::from_utf16_lossy(&buffer[..len as usize]))
498}
499
500pub fn module_for_address(modules: &[LoadedModule], address: u64) -> Option<&LoadedModule> {
502 modules.iter().find(|m| m.contains(address))
503}
504
505#[cfg(target_os = "linux")]
506fn next_maps_field(input: &str) -> Option<(&str, &str)> {
507 let input = input.trim_start();
508 let end = input.find(char::is_whitespace).unwrap_or(input.len());
509 (!input.is_empty()).then_some((&input[..end], &input[end..]))
510}
511
512#[cfg(target_os = "linux")]
513struct LinuxImage {
514 mapped_ranges: Vec<Range<u64>>,
515 executable_ranges: Vec<Range<u64>>,
516 path: String,
517 device_major: u64,
518 device_minor: u64,
519 inode: String,
520}
521
522#[cfg(target_os = "linux")]
523type LinuxImageKey = (String, String, String, u64);
524
525#[cfg(target_os = "linux")]
526struct LinuxMapping {
527 range: Range<u64>,
528 executable: bool,
529}
530
531#[cfg(target_os = "linux")]
532fn linux_images() -> std::io::Result<Vec<LinuxImage>> {
533 use std::collections::BTreeMap;
534
535 let mut images: BTreeMap<LinuxImageKey, Vec<LinuxMapping>> = BTreeMap::new();
537 for line in std::fs::read_to_string("/proc/self/maps")?.lines() {
538 let Some((range, rest)) = next_maps_field(line) else {
539 continue;
540 };
541 let Some((perms, rest)) = next_maps_field(rest) else {
542 continue;
543 };
544 let Some((offset, rest)) = next_maps_field(rest) else {
545 continue;
546 };
547 let Some((dev, rest)) = next_maps_field(rest) else {
548 continue;
549 };
550 let Some((inode, rest)) = next_maps_field(rest) else {
551 continue;
552 };
553 let path = rest.trim_start();
554 if !path.starts_with('/') {
555 continue;
556 }
557 if path.ends_with(" (deleted)") {
558 continue;
562 }
563 let path = path.to_owned();
564 let Some((start, end)) = range.split_once('-') else {
565 continue;
566 };
567 let (Ok(start), Ok(end), Ok(offset)) = (
568 u64::from_str_radix(start, 16),
569 u64::from_str_radix(end, 16),
570 u64::from_str_radix(offset, 16),
571 ) else {
572 continue;
573 };
574 let candidate_base = start.saturating_sub(offset);
575 images
576 .entry((path, dev.to_owned(), inode.to_owned(), candidate_base))
577 .or_default()
578 .push(LinuxMapping {
579 range: start..end,
580 executable: perms.as_bytes().get(2) == Some(&b'x'),
581 });
582 }
583
584 Ok(images
585 .into_iter()
586 .filter_map(|((path, device, inode, _load_bias), mut mappings)| {
587 let (major, minor) = device.split_once(':')?;
588 let device_major = u64::from_str_radix(major, 16).ok()?;
589 let device_minor = u64::from_str_radix(minor, 16).ok()?;
590 mappings.sort_by_key(|mapping| mapping.range.start);
591 Some(LinuxImage {
592 mapped_ranges: mappings
593 .iter()
594 .map(|mapping| mapping.range.clone())
595 .collect(),
596 executable_ranges: mappings
597 .into_iter()
598 .filter_map(|mapping| mapping.executable.then_some(mapping.range))
599 .collect(),
600 path,
601 device_major,
602 device_minor,
603 inode,
604 })
605 })
606 .collect())
607}
608
609#[cfg(target_os = "linux")]
610pub fn enumerate_modules() -> std::io::Result<Vec<LoadedModule>> {
612 use object::{Object, ObjectKind, ObjectSection};
613 use std::io::Read as _;
614
615 let mut modules = Vec::new();
616 let loaded_identities = loaded_elf_identities();
617 for LinuxImage {
618 mapped_ranges,
619 executable_ranges,
620 path,
621 device_major,
622 device_minor,
623 inode,
624 } in linux_images()?
625 {
626 let Some(mapped_start) = mapped_ranges.first().map(|range| range.start) else {
627 continue;
628 };
629 let Some(mapped_end) = mapped_ranges.last().map(|range| range.end) else {
630 continue;
631 };
632 use std::os::unix::fs::MetadataExt as _;
633
634 let Ok(file_handle) = std::fs::File::open(&path) else {
635 continue;
636 };
637 let Ok(metadata) = file_handle.metadata() else {
638 continue;
639 };
640 if u64::from(libc::major(metadata.dev())) != device_major
641 || u64::from(libc::minor(metadata.dev())) != device_minor
642 || metadata.ino().to_string() != inode
643 {
644 continue;
646 }
647 if metadata.len() > MAX_MODULE_IMAGE_BYTES {
648 continue;
649 }
650 let mut data = Vec::new();
651 if file_handle
652 .take(MAX_MODULE_IMAGE_BYTES + 1)
653 .read_to_end(&mut data)
654 .is_err()
655 || data.len() as u64 > MAX_MODULE_IMAGE_BYTES
656 {
657 continue;
661 }
662 let Ok(file) = object::File::parse(data.as_slice()) else {
663 continue;
664 };
665 let Some(loaded_identity) = loaded_identities.iter().find(|identity| {
666 identity.mapped_ranges.iter().any(|loaded| {
667 mapped_ranges
668 .iter()
669 .any(|mapped| loaded.start < mapped.end && mapped.start < loaded.end)
670 })
671 }) else {
672 continue;
673 };
674 let base = if file.kind() == ObjectKind::Executable {
675 0
676 } else {
677 loaded_identity.load_bias
678 };
679 let debug_id = loaded_identity.debug_id.clone();
680 let file_debug_id = file
681 .build_id()
682 .ok()
683 .flatten()
684 .map(|build_id| format!("elf:{}", hex_bytes(build_id)));
685 if file_debug_id.as_deref() != Some(debug_id.as_str()) {
686 continue;
690 }
691 let sections = file
692 .sections()
693 .filter_map(|section| {
694 let name = section.name().ok()?.to_owned();
695 let start = base.checked_add(section.address())?;
696 let end = start.checked_add(section.size())?;
697 Some(Section {
698 name,
699 range: start..end,
700 })
701 })
702 .collect();
703
704 modules.push(LoadedModule {
705 base,
706 size: mapped_end.saturating_sub(mapped_start),
707 mapped_ranges,
708 executable_ranges,
709 path: Some(path),
710 debug_id: Some(debug_id),
711 debug_file: None,
712 sections,
713 });
714 }
715 modules.sort_by_key(|module| module.base);
716 Ok(modules)
717}
718
719#[cfg(target_os = "macos")]
720unsafe extern "C" {
721 fn _dyld_image_count() -> u32;
722 fn _dyld_get_image_header(image_index: u32) -> *const libc::c_void;
723 fn _dyld_get_image_vmaddr_slide(image_index: u32) -> isize;
724 fn _dyld_get_image_name(image_index: u32) -> *const libc::c_char;
725}
726
727#[cfg(target_os = "macos")]
728fn add_slide(address: u64, slide: isize) -> Option<u64> {
729 if slide >= 0 {
730 address.checked_add(slide as u64)
731 } else {
732 address.checked_sub(slide.unsigned_abs() as u64)
733 }
734}
735
736#[cfg(target_os = "macos")]
737pub fn enumerate_modules() -> std::io::Result<Vec<LoadedModule>> {
739 use object::{Object, ObjectSection, ObjectSegment};
740 use std::ffi::CStr;
741 use std::io::Read as _;
742
743 let count = unsafe { _dyld_image_count() };
744 let mut modules = Vec::with_capacity(count as usize);
745 for index in 0..count {
746 let name = unsafe { _dyld_get_image_name(index) };
747 let header = unsafe { _dyld_get_image_header(index) };
748 if name.is_null() || header.is_null() {
749 continue;
750 }
751 let path = unsafe { CStr::from_ptr(name) }
752 .to_string_lossy()
753 .into_owned();
754 let Some(loaded_uuid) = (unsafe { loaded_macho_uuid(header.cast()) }) else {
755 continue;
756 };
757 let Ok(file_handle) = std::fs::File::open(&path) else {
758 continue;
762 };
763 let mut data = Vec::new();
764 if file_handle
765 .take(MAX_MODULE_IMAGE_BYTES + 1)
766 .read_to_end(&mut data)
767 .is_err()
768 || data.len() as u64 > MAX_MODULE_IMAGE_BYTES
769 {
770 continue;
771 }
772 let Ok(file) = object::File::parse(data.as_slice()) else {
773 continue;
774 };
775 if file.mach_uuid().ok().flatten() != Some(loaded_uuid) {
776 continue;
779 }
780 let slide = unsafe { _dyld_get_image_vmaddr_slide(index) };
781 let debug_id = Some(format!("macho:{}", hex_bytes(&loaded_uuid)));
782 let base_svma = file.relative_address_base();
783 let base = add_slide(base_svma, slide).unwrap_or(header as u64);
784
785 let mut mapped_start = u64::MAX;
786 let mut mapped_end = 0u64;
787 let mut executable_ranges = Vec::new();
788 for segment in file.segments() {
789 if segment.name().ok().flatten() == Some("__PAGEZERO") {
790 continue;
791 }
792 let Some(start) = add_slide(segment.address(), slide) else {
793 continue;
794 };
795 let Some(end) = start.checked_add(segment.size()) else {
796 continue;
797 };
798 mapped_start = mapped_start.min(start);
799 mapped_end = mapped_end.max(end);
800 if matches!(
801 segment.flags(),
802 object::SegmentFlags::MachO { initprot, .. }
803 if initprot & object::macho::VM_PROT_EXECUTE != 0
804 ) {
805 executable_ranges.push(start..end);
806 }
807 }
808 if mapped_start == u64::MAX || mapped_end <= base {
809 continue;
810 }
811
812 let sections = file
813 .sections()
814 .filter_map(|section| {
815 let name = section.name().ok()?.to_owned();
816 let start = add_slide(section.address(), slide)?;
817 let end = start.checked_add(section.size())?;
818 Some(Section {
819 name,
820 range: start..end,
821 })
822 })
823 .collect();
824 let _ = mapped_start;
825 modules.push(LoadedModule {
826 base,
827 size: mapped_end - base,
828 mapped_ranges: Vec::new(),
829 executable_ranges,
830 path: Some(path),
831 debug_id,
832 debug_file: None,
833 sections,
834 });
835 }
836 modules.sort_by_key(|module| module.base);
837 Ok(modules)
838}
839
840#[cfg(all(test, windows))]
841mod tests {
842 use super::*;
843
844 #[inline(never)]
846 fn landmark() -> u64 {
847 std::hint::black_box(0xD1A6_0057_u64)
850 }
851
852 #[test]
853 fn enumeration_finds_at_least_the_executable_and_some_dlls() {
854 let modules = enumerate_modules().expect("enumerate");
855 assert!(
856 modules.len() >= 2,
857 "expected the exe plus at least one DLL, got {}",
858 modules.len()
859 );
860 }
861
862 #[test]
863 fn every_module_reports_a_nonempty_range_and_sections() {
864 for m in enumerate_modules().expect("enumerate") {
865 assert!(m.base != 0, "module with null base");
866 assert!(m.size > 0, "module with zero size at {:#x}", m.base);
867 assert!(
868 !m.sections.is_empty(),
869 "module at {:#x} parsed no sections",
870 m.base
871 );
872 }
873 }
874
875 #[test]
882 fn text_section_contains_a_known_function_address() {
883 let addr = (landmark as fn() -> u64) as usize as u64;
884 let modules = enumerate_modules().expect("enumerate");
885
886 let owner = module_for_address(&modules, addr)
887 .unwrap_or_else(|| panic!("no module contains {addr:#x}"));
888
889 let text = owner
890 .section(".text")
891 .unwrap_or_else(|| panic!("module at {:#x} has no .text", owner.base));
892
893 assert!(
894 text.range.contains(&addr),
895 "function at {addr:#x} is outside its module's .text ({:#x}..{:#x})",
896 text.range.start,
897 text.range.end
898 );
899 assert_eq!(landmark(), 0xD1A6_0057_u64);
901 }
902
903 #[test]
904 fn loaded_pe_owner_carries_its_codeview_identity() {
905 let addr = (landmark as fn() -> u64) as usize as u64;
906 let modules = enumerate_modules().expect("enumerate");
907 let owner = module_for_address(&modules, addr).expect("owning module");
908 assert!(
909 owner
910 .debug_id
911 .as_deref()
912 .is_some_and(|identity| identity.starts_with("pdb:")),
913 "loaded PE did not expose its mapped CodeView GUID+age: {:?}",
914 owner.debug_id
915 );
916 }
917
918 #[test]
919 fn module_lookup_rejects_an_address_outside_every_module() {
920 let modules = enumerate_modules().expect("enumerate");
921 assert!(module_for_address(&modules, 0x1).is_none());
923 }
924
925 #[test]
928 fn own_module_exposes_unwind_sections() {
929 let addr = (landmark as fn() -> u64) as usize as u64;
930 let modules = enumerate_modules().expect("enumerate");
931 let owner = module_for_address(&modules, addr).expect("owning module");
932
933 assert!(
934 owner.section(".pdata").is_some(),
935 "x86_64 PE modules carry .pdata unwind tables; sections found: {:?}",
936 owner.sections.iter().map(|s| &s.name).collect::<Vec<_>>()
937 );
938 }
939
940 #[test]
941 fn sections_do_not_extend_past_their_module() {
942 for m in enumerate_modules().expect("enumerate") {
943 let module_end = m.base + m.size;
944 for s in &m.sections {
945 assert!(
946 s.range.start >= m.base && s.range.start <= module_end,
947 "section {} at {:#x} lies outside module {:#x}..{:#x}",
948 s.name,
949 s.range.start,
950 m.base,
951 module_end
952 );
953 }
954 }
955 }
956}
957
958#[cfg(all(test, target_os = "linux"))]
959mod linux_tests {
960 use super::*;
961
962 #[inline(never)]
963 fn landmark() {}
964
965 #[test]
966 fn maps_path_preserves_spaces_and_deleted_suffix() {
967 let line = "1000-2000 r-xp 00000000 08:01 42 /tmp/a file (deleted)";
968 let (_, rest) = next_maps_field(line).unwrap();
969 let (_, rest) = next_maps_field(rest).unwrap();
970 let (_, rest) = next_maps_field(rest).unwrap();
971 let (_, rest) = next_maps_field(rest).unwrap();
972 let (_, rest) = next_maps_field(rest).unwrap();
973 assert_eq!(rest.trim_start(), "/tmp/a file (deleted)");
974 assert!(rest.trim_start().ends_with(" (deleted)"));
975 }
976
977 #[test]
978 fn elf_load_bias_does_not_expand_mapped_coverage_to_zero() {
979 let module = LoadedModule {
980 base: 0,
981 size: 0x2000,
982 mapped_ranges: vec![0x400000..0x401000, 0x402000..0x403000],
983 executable_ranges: std::iter::once(0x400000..0x401000).collect(),
984 path: Some("/tmp/non-pie".into()),
985 debug_id: None,
986 debug_file: None,
987 sections: Vec::new(),
988 };
989 assert!(module.contains(0x400100));
990 assert!(module.contains(0x402100));
991 assert!(!module.contains(0x401100));
992 assert!(!module.contains(1));
993 assert!(module.contains_executable(0x400100));
994 assert!(!module.contains_executable(0x402100));
995 }
996
997 #[test]
998 fn loaded_elf_owner_carries_its_pt_note_build_id() {
999 let address = landmark as fn() as usize as u64;
1000 let modules = enumerate_modules().expect("enumerate");
1001 let owner = module_for_address(&modules, address).expect("owning module");
1002 assert!(
1003 owner
1004 .debug_id
1005 .as_deref()
1006 .is_some_and(|identity| identity.starts_with("elf:")),
1007 "loaded ELF did not expose its PT_NOTE build-id: {:?}",
1008 owner.debug_id
1009 );
1010 }
1011
1012 #[test]
1013 fn gnu_note_parser_extracts_the_build_id() {
1014 let mut note = Vec::new();
1015 note.extend_from_slice(&4u32.to_ne_bytes());
1016 note.extend_from_slice(&4u32.to_ne_bytes());
1017 note.extend_from_slice(&3u32.to_ne_bytes());
1018 note.extend_from_slice(b"GNU\0");
1019 note.extend_from_slice(&[0xaa, 0xbb, 0xcc, 0xdd]);
1020 assert_eq!(
1021 gnu_build_id_from_notes(¬e),
1022 Some(&[0xaa, 0xbb, 0xcc, 0xdd][..])
1023 );
1024 }
1025}
1026
1027#[cfg(all(test, target_os = "macos"))]
1028mod macos_tests {
1029 use super::*;
1030
1031 #[inline(never)]
1032 fn landmark() {}
1033
1034 #[test]
1035 fn loaded_macho_owner_carries_its_lc_uuid() {
1036 let address = landmark as fn() as usize as u64;
1037 let modules = enumerate_modules().expect("enumerate");
1038 let owner = module_for_address(&modules, address).expect("owning module");
1039 assert!(
1040 owner
1041 .debug_id
1042 .as_deref()
1043 .is_some_and(|identity| identity.starts_with("macho:")),
1044 "loaded Mach-O did not expose its LC_UUID: {:?}",
1045 owner.debug_id
1046 );
1047 }
1048}