use serde::Serialize;
use crate::error::Error;
use crate::gobin::{Arch, GoBinary, SectionKind};
use crate::pclntab::{Function, Pclntab};
use crate::Result;
#[derive(Debug, Clone, Serialize)]
pub struct GoroutineSpawn {
pub call_site: u64,
pub spawner: String,
pub target_addr: Option<u64>,
pub target_name: Option<String>,
pub file: Option<String>,
pub line: Option<u32>,
pub resolution: Resolution,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Resolution {
Resolved,
NoLeaPattern,
FuncvalUnmapped,
}
impl GoroutineSpawn {
pub fn is_resolved(&self) -> bool {
matches!(self.resolution, Resolution::Resolved)
}
}
const SPAWNER_NAMES: &[&str] = &[
"runtime.newproc",
"runtime.deferproc",
"runtime.deferprocStack",
];
pub fn find_spawns(bin: &GoBinary, pcln: &Pclntab<'_>) -> Result<Vec<GoroutineSpawn>> {
if !bin.little_endian {
return Err(Error::Goroutines(
"only little-endian binaries supported today".into(),
));
}
if !matches!(bin.arch, Arch::X86_64 | Arch::Aarch64) {
return Err(Error::Goroutines(format!(
"unsupported arch {:?}; only amd64 and arm64 today",
bin.arch
)));
}
let functions = pcln.functions()?;
let by_addr: std::collections::HashMap<u64, &Function> =
functions.iter().map(|f| (f.address, f)).collect();
let mut spawners: Vec<(u64, &'static str)> = Vec::new();
for name in SPAWNER_NAMES {
if let Some(f) = functions.iter().find(|f| &f.name == name) {
spawners.push((f.address, name));
}
}
if spawners.is_empty() {
return Ok(Vec::new());
}
let spawner_set: std::collections::HashMap<u64, &'static str> =
spawners.iter().copied().collect();
let text = bin
.sections
.iter()
.find(|s| s.kind == SectionKind::Text && s.name.ends_with(".text"))
.or_else(|| {
bin.sections
.iter()
.filter(|s| s.kind == SectionKind::Text)
.max_by_key(|s| s.file_size)
})
.ok_or_else(|| Error::Goroutines("no text section".into()))?;
let text_start_va = text.addr;
let text_bytes = &bin.bytes[text.file_offset..text.file_offset + text.file_size];
let mut out = Vec::new();
match bin.arch {
Arch::X86_64 => scan_amd64(
bin,
text_bytes,
text_start_va,
&spawner_set,
&by_addr,
pcln,
&mut out,
)?,
Arch::Aarch64 => scan_arm64(
bin,
text_bytes,
text_start_va,
&spawner_set,
&by_addr,
pcln,
&mut out,
)?,
_ => unreachable!(),
}
Ok(out)
}
fn scan_amd64(
bin: &GoBinary,
text_bytes: &[u8],
text_start_va: u64,
spawners: &std::collections::HashMap<u64, &'static str>,
by_addr: &std::collections::HashMap<u64, &Function>,
pcln: &Pclntab<'_>,
out: &mut Vec<GoroutineSpawn>,
) -> Result<()> {
let mut i = 0;
while i + 5 <= text_bytes.len() {
if text_bytes[i] == 0xE8 {
let rel = i32::from_le_bytes(text_bytes[i + 1..i + 5].try_into().unwrap());
let call_site_va = text_start_va + i as u64;
let call_next_va = call_site_va + 5;
let target_va = call_next_va.wrapping_add(rel as i64 as u64);
if let Some(&spawner_name) = spawners.get(&target_va) {
let (target_addr, target_name, resolution) =
resolve_amd64_target(bin, text_bytes, text_start_va, i, by_addr);
let (file, line) = pcln
.lookup(call_site_va)
.map(|f| (f.file, f.start_line))
.unwrap_or((None, None));
out.push(GoroutineSpawn {
call_site: call_site_va,
spawner: spawner_name.to_string(),
target_addr,
target_name,
file,
line,
resolution,
});
}
}
i += 1;
}
Ok(())
}
fn resolve_amd64_target(
bin: &GoBinary,
text_bytes: &[u8],
text_start_va: u64,
call_offset: usize,
by_addr: &std::collections::HashMap<u64, &Function>,
) -> (Option<u64>, Option<String>, Resolution) {
let mut saw_pattern = false;
for back in [7usize, 9, 11, 13, 16, 19, 22, 25] {
if call_offset < back {
continue;
}
let pos = call_offset - back;
if text_bytes.len() < pos + 7 {
continue;
}
if text_bytes[pos] != 0x48 || text_bytes[pos + 1] != 0x8d {
continue;
}
let modrm = text_bytes[pos + 2];
if !matches!(modrm, 0x05 | 0x0d | 0x15 | 0x1d | 0x35 | 0x3d) {
continue;
}
saw_pattern = true;
let disp = i32::from_le_bytes(text_bytes[pos + 3..pos + 7].try_into().unwrap());
let lea_next_va = text_start_va + (pos + 7) as u64;
let funcval_va = lea_next_va.wrapping_add(disp as i64 as u64);
if let Some(buf) = bin.read_at_addr(funcval_va, 8) {
let entry_pc = u64::from_le_bytes(buf.try_into().unwrap());
let name = by_addr.get(&entry_pc).map(|f| f.name.clone());
return (Some(entry_pc), name, Resolution::Resolved);
}
}
let reason = if saw_pattern {
Resolution::FuncvalUnmapped
} else {
Resolution::NoLeaPattern
};
(None, None, reason)
}
fn scan_arm64(
bin: &GoBinary,
text_bytes: &[u8],
text_start_va: u64,
spawners: &std::collections::HashMap<u64, &'static str>,
by_addr: &std::collections::HashMap<u64, &Function>,
pcln: &Pclntab<'_>,
out: &mut Vec<GoroutineSpawn>,
) -> Result<()> {
let mut i = 0;
while i + 4 <= text_bytes.len() {
let insn = u32::from_le_bytes(text_bytes[i..i + 4].try_into().unwrap());
if (insn >> 26) == 0x25 {
let imm26 = insn & 0x03ff_ffff;
let signed = if imm26 & 0x0200_0000 != 0 {
(imm26 as i64) | !0x03ff_ffff
} else {
imm26 as i64
};
let call_site_va = text_start_va + i as u64;
let target_va = call_site_va.wrapping_add((signed << 2) as u64);
if let Some(&spawner_name) = spawners.get(&target_va) {
let (target_addr, target_name, resolution) =
resolve_arm64_target(bin, text_bytes, text_start_va, i, by_addr);
let (file, line) = pcln
.lookup(call_site_va)
.map(|f| (f.file, f.start_line))
.unwrap_or((None, None));
out.push(GoroutineSpawn {
call_site: call_site_va,
spawner: spawner_name.to_string(),
target_addr,
target_name,
file,
line,
resolution,
});
}
}
i += 4;
}
Ok(())
}
fn resolve_arm64_target(
bin: &GoBinary,
text_bytes: &[u8],
text_start_va: u64,
call_offset: usize,
by_addr: &std::collections::HashMap<u64, &Function>,
) -> (Option<u64>, Option<String>, Resolution) {
let mut saw_pattern = false;
let mut pos = call_offset;
for _ in 0..8 {
if pos < 8 {
break;
}
pos -= 4;
let candidate_adrp = pos;
if candidate_adrp + 8 > text_bytes.len() {
continue;
}
let adrp = u32::from_le_bytes(
text_bytes[candidate_adrp..candidate_adrp + 4]
.try_into()
.unwrap(),
);
let add = u32::from_le_bytes(
text_bytes[candidate_adrp + 4..candidate_adrp + 8]
.try_into()
.unwrap(),
);
if (adrp >> 31) & 1 != 1 {
continue;
}
if ((adrp >> 24) & 0x1f) != 0x10 {
continue;
}
let rd_adrp = adrp & 0x1f;
if rd_adrp > 2 {
continue; }
let immlo = (adrp >> 29) & 0x3;
let immhi = (adrp >> 5) & 0x7ffff;
let imm = ((immhi << 2) | immlo) as i64;
let imm_signed = if imm & (1 << 20) != 0 {
imm | !0x001f_ffff
} else {
imm
};
let adrp_va = text_start_va + candidate_adrp as u64;
let page = (adrp_va & !0xfff).wrapping_add((imm_signed << 12) as u64);
if (add >> 22) & 0x3ff != 0x244 {
continue;
}
let rd_add = add & 0x1f;
let rn_add = (add >> 5) & 0x1f;
if rd_add != rd_adrp || rn_add != rd_adrp {
continue;
}
let imm12 = (add >> 10) & 0xfff;
let funcval_va = page.wrapping_add(imm12 as u64);
saw_pattern = true;
if let Some(buf) = bin.read_at_addr(funcval_va, 8) {
let entry_pc = u64::from_le_bytes(buf.try_into().unwrap());
let name = by_addr.get(&entry_pc).map(|f| f.name.clone());
return (Some(entry_pc), name, Resolution::Resolved);
}
}
let reason = if saw_pattern {
Resolution::FuncvalUnmapped
} else {
Resolution::NoLeaPattern
};
(None, None, reason)
}