diff --git a/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs b/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs new file mode 100644 index 0000000000..49e6152ac5 --- /dev/null +++ b/decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs @@ -0,0 +1,46 @@ +//! (kuna `libgccsigs`) Compiler-runtime contracts whose integer widths come from +//! machine modes, not the host C data model. Off by default: a caller that +//! returns arithmetic on the locked eight-byte quotient still loses a word. +//! +//! `__divdi3` is `di_int (di_int, di_int)`, with `di_int = int64_t`: +//! https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/builtins/divdi3.c +//! https://github.com/llvm/llvm-project/blob/main/compiler-rt/lib/builtins/int_types.h + +use object::{Architecture, BinaryFormat, Object}; + +use super::{seed_named_prototypes, seed_resolved_prototypes, Sig, Ty, L}; +use crate::pass::{AnalysisCtx, AnalysisOutput, AnalysisPass, Phase}; + +const SIGNATURES: &[(&str, Sig)] = &[ + ("__divdi3", Sig { ret: Ty::Int64, params: &[Ty::Int64, Ty::Int64], vararg: -1 }), +]; + +pub struct LibgccSigsPass; + +impl AnalysisPass for LibgccSigsPass { + fn phase(&self) -> Phase { + Phase::P1 + } + + fn id(&self) -> &'static str { + "libgccsigs" + } + + /// Seed the evidenced SPARC32 ELF runtime ABI without retyping other targets. + fn run(&self, ctx: &AnalysisCtx) -> AnalysisOutput { + let mut out = AnalysisOutput::default(); + if ctx.file.format() != BinaryFormat::Elf + || !matches!(ctx.file.architecture(), Architecture::Sparc | Architecture::Sparc32Plus) + || ctx.arch.get_description() != "sparc:BE:32:default:default" + { + return out; + } + let (imports, reloc_imports) = super::loaded_import_addrs(ctx.file, ctx.bytes, ctx.image); + let names = super::loaded_function_names(ctx.file, ctx.bytes, &reloc_imports, false); + let types = ctx.arch.types(); + let (_, word_size) = ctx.arch.data_org(); + seed_named_prototypes(&mut out, &names, SIGNATURES, types, word_size, L); + seed_resolved_prototypes(&mut out, &imports, SIGNATURES, types, word_size, L); + out + } +} diff --git a/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs b/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs index cd163a30e0..ed2ba66eb9 100644 --- a/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs +++ b/decompiler/crates/kuna-analysis/src/analyzers/protos/mod.rs @@ -38,6 +38,7 @@ use kuna_decomp::fspec::PrototypePieces; use crate::pass::{AnalysisCtx, AnalysisOutput, AnalysisPass, Phase}; +pub mod kuna_libgcc; mod kuna_libcnames; pub mod kuna_libcsigs; pub mod kuna_libctypes; @@ -49,10 +50,9 @@ pub struct LibProtoPass; /// A primitive type slot in a built-in libc signature. /// -/// Every variant is **width-stable**: it is either `void`, exactly 4 bytes, or -/// exactly pointer-width on every ILP32/LP64 target. A C type that is neither -/// (`off_t`, `time_t`, `long long`, `char`/`short` parameters) has no spelling -/// here on purpose — see [`kuna_libcsigs`]. +/// Integer slots are four bytes or pointer-width for libc declarations. Compiler +/// runtime machine-mode contracts can additionally state an exact eight-byte +/// integer; that does not establish the width of libc `long long` parameters. #[derive(Clone, Copy)] enum Ty { /// `void` (return only). @@ -65,6 +65,8 @@ enum Ty { Size, /// `ssize_t` / `long` / `ptrdiff_t` (pointer-width signed). Long, + /// Signed DI-mode integer (exactly eight bytes in the compiler runtime ABI). + Int64, /// `char *`. CharPtr, /// `char **`. @@ -182,6 +184,7 @@ fn build_ty( Ty::UInt => types.get_base(4, type_metatype::TYPE_UINT), Ty::Size => types.get_base(ptr, type_metatype::TYPE_UINT), Ty::Long => types.get_base(ptr, type_metatype::TYPE_INT), + Ty::Int64 => types.get_base(8, type_metatype::TYPE_INT), Ty::CharPtr => { let c = types.get_type_char(types.get_size_of_char())?; types.get_type_pointer(ptr, c, word_size) diff --git a/decompiler/crates/kuna-analysis/src/passes.rs b/decompiler/crates/kuna-analysis/src/passes.rs index 8774d73880..7bbbc3579c 100644 --- a/decompiler/crates/kuna-analysis/src/passes.rs +++ b/decompiler/crates/kuna-analysis/src/passes.rs @@ -88,6 +88,10 @@ pub fn passes_for(compiler: Compiler, format: object::BinaryFormat) -> Vec Vec { + let mut image = write::Object::new(BinaryFormat::Elf, Architecture::Sparc, Endianness::Big); + let text = image.section_id(write::StandardSection::Text); + // save; mov 62,o0; mov i0,o3; sra i0,31,o2; call; sethi 0x200000,o1; + // st o1,[i1]; ret; restore. Independently authored, with the call's delay slot. + let mut words: [u32; 9] = [ + 0x9de3bfa0, 0x9010203e, 0x96100018, 0x953e201f, 0x40000000, 0x13200000, 0xd2264000, + 0x81c7e008, 0x81e80000, + ]; + if !low_word { + words[6] = 0xd0264000; + } + let bytes: Vec = words.into_iter().flat_map(u32::to_be_bytes).collect(); + image.append_section_data(text, &bytes, 4); + image.add_symbol(write::Symbol { + name: b"divide_low".to_vec(), + value: 0, + size: bytes.len() as u64, + kind: SymbolKind::Text, + scope: SymbolScope::Linkage, + weak: false, + section: write::SymbolSection::Section(text), + flags: SymbolFlags::None, + }); + let helper = image.add_symbol(write::Symbol { + name: b"__divdi3".to_vec(), + value: 0, + size: 0, + kind: SymbolKind::Unknown, + scope: SymbolScope::Dynamic, + weak: false, + section: write::SymbolSection::Undefined, + flags: SymbolFlags::None, + }); + image + .add_relocation( + text, + write::Relocation { + offset: 16, + symbol: helper, + addend: 0, + flags: RelocationFlags::Elf { + r_type: object::elf::R_SPARC_WDISP30, + }, + }, + ) + .unwrap(); + image.write().unwrap() +} + +#[test] +fn signed_di_quotient_low_word_survives_an_opaque_call() { + let root = tempfile::tempdir().unwrap(); + let image = root.path().join("divide.o"); + std::fs::write(&image, caller(true)).unwrap(); + let compilers: Vec<_> = ["gcc", "clang"] + .into_iter() + .filter(|cc| Command::new(cc).arg("--version").output().is_ok()) + .collect(); + assert!(!compilers.is_empty(), "a C compiler is required"); + let decompile = |option: Option<&str>| { + let mut args = vec![ + "decompile", + image.to_str().unwrap(), + "divide_low", + "--json", + "--assert", + "prototype divide_low void divide_low(int divisor,unsigned int *result)", + ]; + if let Some(value) = option { + args.extend(["--option", "libgccsigs", value]); + } + let (output, error, status) = common::run_kuna(&args); + assert_eq!(status, 0, "{error}"); + let document: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert!(document["functions"][0]["error"].is_null(), "{output}"); + document["functions"][0]["code"].as_str().unwrap().to_string() + }; + let off = decompile(Some("off")); + assert_eq!(decompile(None), off, "libgccsigs is off by default"); + assert!( + !off.contains("CONCAT44") && off.contains("__divdi3(0x3e,0x80000000,"), + "off keeps the inferred four-word call: {off}" + ); + let body = decompile(Some("on")); + assert!(body.contains("__divdi3(0x3e80000000,"), "{body}"); + let source = root.path().join("quotient.c"); + std::fs::write( + &source, + format!( + r#" +#include +#include +long long __divdi3(long long a,long long b) {{ return a/b; }} +{body} +int main(void) {{ + const int divisors[] = {{1,-1,2,-2,3,-3,7,-7,999,-999,INT_MAX,INT_MIN}}; + for(unsigned i=0;i +#include +unsigned int __divdi3(unsigned int a,unsigned int b,unsigned int c,unsigned int d) {{ return a^b^c^d; }} +{body} +int main(void) {{ + const int inputs[] = {{0,1,-1,37,-37,INT_MAX,INT_MIN}}; + for(unsigned i=0;i 0, "a C compiler is required"); +} + +#[test] +fn declared_wide_results_survive_caller_return_recovery() { + if Command::new("clang").arg("--version").output().is_err() { + return; + } + let root = tempfile::tempdir().unwrap(); + let fixture = root.path().join("returns.c"); + std::fs::write( + &fixture, + r#" +void g(void); +long long q(long long a,long long b){return a/b;} +long long keep(long long a,long long b){long long r=a/b;g();return r;} +long long guard(long long a,long long b){if(b==0)return -1;return a/b;} +long long choose(long long a,long long b,int flag){if(flag)return a/b;return 0x123456789abcdefLL;} +long long mixed(long long a,long long b,int flag){long long r=a/b;if(flag)r=(unsigned int)r;return r;} +int low(long long a,long long b){return (int)(a/b);} +int high(long long a,long long b){return (int)((a/b)>>32);} +"#, + ) + .unwrap(); + for level in ["-O0", "-O2"] { + let image = root.path().join("returns.o"); + common::process::required_output( + Command::new("clang") + .args(["-target", "sparc-linux-gnu", "-integrated-as", level, "-c"]) + .arg(&fixture) + .arg("-o") + .arg(&image), + ); + for explicit in [false, true] { + let mut bodies = String::new(); + for function in ["q", "keep", "guard", "low", "high", "choose", "mixed"] { + let mut args = vec!["decompile", image.to_str().unwrap(), function, "--json"]; + if !explicit { + args.extend(["--option", "libgccsigs", "on"]); + } else { + args.extend([ + "--option", + "libproto", + "off", + "--assert", + "prototype __divdi3 long long __divdi3(long long a,long long b)", + ]); + } + let (output, error, status) = common::run_kuna(&args); + assert_eq!(status, 0, "{error}"); + let document: serde_json::Value = serde_json::from_str(&output).unwrap(); + assert!(document["functions"][0]["error"].is_null(), "{output}"); + bodies.push_str(document["functions"][0]["code"].as_str().unwrap()); + bodies.push('\n'); + } + let source = root.path().join("driver.c"); + std::fs::write(&source, format!(r#" +#include +#include +#include +#define CONCAT44(hi,lo) (((uint64_t)(uint32_t)(hi)<<32)|(uint32_t)(lo)) +static unsigned calls, effects; +long long __divdi3(long long a,long long b) {{ ++calls; return a/b; }} +void g(void) {{ ++effects; }} +{bodies} +#define ARGS(a,b) (uint32_t)((uint64_t)(a)>>32),(uint32_t)(a),(uint32_t)((uint64_t)(b)>>32),(uint32_t)(b) +_Static_assert(sizeof(low(0,0,0,1)) == 4, "the low-word caller returns one word"); +_Static_assert(sizeof(high(0,0,0,1)) == 4, "the high-word caller returns one word"); +_Static_assert(sizeof(choose(0,0,0,1,0)) == 8, "a nontrivial constant exit stays wide"); +_Static_assert(sizeof(mixed(0,0,0,1,0)) == 8, "one changed word does not discard the other"); +int main(void) {{ + const int64_t edges[] = {{0,1,-1,2,-3,INT64_MIN,INT64_MAX,INT64_C(0x123456789abcdef),-INT64_C(0x123456789abcdef)}}; + uint64_t random = UINT64_C(0x79b153c5a7d931e5); + for(unsigned i=0;i<2000;++i) {{ + random ^= random<<13; random ^= random>>7; random ^= random<<17; + int64_t a = i<81 ? edges[i/9] : (int64_t)random; + random ^= random<<13; random ^= random>>7; random ^= random<<17; + int64_t b = i<81 ? edges[i%9] : (int64_t)random; + if(!b || (a==INT64_MIN && b==-1)) continue; + uint64_t expected = (uint64_t)(a/b); + calls = effects = 0; + if(q(ARGS(a,b)) != expected || calls!=1 || effects) return 1; + if(keep(ARGS(a,b)) != expected || calls!=2 || effects!=1) return 2; + if(guard(ARGS(a,b)) != expected || calls!=3 || effects!=1) return 3; + if(low(ARGS(a,b)) != (uint32_t)expected || calls!=4) return 4; + if(high(ARGS(a,b)) != (uint32_t)(expected>>32) || calls!=5) return 5; + if(guard(ARGS(a,0)) != UINT64_MAX || calls!=5 || effects!=1) return 6; + if(choose(ARGS(a,b),1) != expected || calls!=6 || effects!=1) return 7; + if(choose(ARGS(a,b),0) != UINT64_C(0x123456789abcdef) || calls!=6) return 8; + if(mixed(ARGS(a,b),0) != expected || calls!=7) return 9; + if(mixed(ARGS(a,b),1) != (uint32_t)expected || calls!=8) return 10; + }} + return 0; +}} +"#)).unwrap(); + for cc in ["gcc", "clang"] { + if Command::new(cc).arg("--version").output().is_err() { + continue; + } + for opt in ["-O0", "-O2"] { + let executable = root.path().join(format!("run-{cc}")); + let built = Command::new(cc) + .args([ + "-std=c11", + opt, + "-Werror=uninitialized", + "-fsanitize=undefined", + "-fno-sanitize-recover=all", + ]) + .arg(&source) + .arg("-o") + .arg(&executable) + .output() + .unwrap(); + assert!( + built.status.success(), + "{level} explicit={explicit} {cc} {opt}: {}\n{bodies}", + String::from_utf8_lossy(&built.stderr) + ); + let ran = Command::new(&executable).output().unwrap(); + assert!( + ran.status.success(), + "{level} explicit={explicit} {cc} {opt}: {:?}\n{}\n{bodies}", + ran.status.code(), + String::from_utf8_lossy(&ran.stderr) + ); + } + } + } + } +} + +#[test] +fn a_consumed_wide_result_is_not_a_return() { + if Command::new("clang").arg("--version").output().is_err() { + return; + } + let root = tempfile::tempdir().unwrap(); + let fixture = root.path().join("consumed.c"); + std::fs::write( + &fixture, + r#" +long long f(long long a); +long long g64; +long long plain(long long a){return f(a);} +void store(long long a,long long *p){*p=f(a);} +void global(long long a){g64=f(a);} +void twice(long long a,long long *p){p[0]=f(a);p[1]=f(a+1);} +"#, + ) + .unwrap(); + for level in ["-O0", "-O2"] { + let image = root.path().join("consumed.o"); + common::process::required_output( + Command::new("clang") + .args(["-target", "armeb-linux-gnueabi", "-fno-pic", level, "-c"]) + .arg(&fixture) + .arg("-o") + .arg(&image), + ); + for (function, signature) in [ + ("plain", "long long plain("), + ("store", "void store("), + ("global", "void global("), + ("twice", "void twice("), + ] { + let (output, error, status) = common::run_kuna(&[ + "decompile", image.to_str().unwrap(), function, "--json", + "--assert", "prototype f long long f(long long a)", + ]); + assert_eq!(status, 0, "{error}"); + let document: serde_json::Value = serde_json::from_str(&output).unwrap(); + let body = document["functions"][0]["code"].as_str().unwrap(); + assert!(body.starts_with(signature), "{level} {function}: {body}"); + } + } +} diff --git a/decompiler/crates/kuna-cli/tests/ppc64_delayed_constant_switch.rs b/decompiler/crates/kuna-cli/tests/ppc64_delayed_constant_switch.rs new file mode 100644 index 0000000000..5d43edf968 --- /dev/null +++ b/decompiler/crates/kuna-cli/tests/ppc64_delayed_constant_switch.rs @@ -0,0 +1,153 @@ +//! A PPC64 switch whose index a call writes keeps every case of its table. + +use crate::common; + +use object::write::{Object, Symbol, SymbolSection}; +use object::{ + Architecture, BinaryFormat, Endianness, FileFlags, SectionKind, SymbolFlags, SymbolKind, + SymbolScope, +}; +use std::process::Command; + +/// `select` stores 0 to a stack slot, passes its address to `get_state`, which +/// writes it, then reads the slot back, guards it and dispatches through a +/// five-entry relative table (`base + sext(table[i])`, entries 0 and 4 sharing +/// a target). The base is `bcl`/`mflr` plus `addis`/`addi`, a constant expression +/// several ops deep. Each case returns its own constant. +const SELECT: [u32; 45] = [ + 0x7c0802a6, 0xf8010010, 0xf821ff71, 0x39200000, 0x38a10070, 0x91210070, 0x48000091, + 0x2f83000b, 0x419e0074, 0x81210070, 0x2b890004, 0x419d0060, 0x7d2a4b78, 0x429f0005, + 0x7d0802a6, 0x3d080000, 0x39080020, 0x794a1764, 0x7d2852aa, 0x7d294214, 0x7d2903a6, + 0x4e800420, 0x00000014, 0x0000001c, 0x00000024, 0x0000002c, 0x00000014, 0x38600028, + 0x48000028, 0x38600029, 0x48000020, 0x3860002a, 0x48000018, 0x3860002b, 0x48000010, + 0x38600063, 0x48000008, 0x38600001, 0x38210090, 0xe8010010, 0x7c0803a6, 0x4e800020, + 0x90850000, 0x38600000, 0x4e800020, +]; + +/// [`SELECT`] with the slot stored as 6 and the guard `5 <= i && i <= 9` over a +/// ten-entry table whose rows 0 to 4 also hold mapped targets (returning 40 to +/// 44). Those rows never reach the switch, so they must not become cases. +const LOWER_BOUND: [u32; 64] = [ + 0x7c0802a6, 0xf8010010, 0xf821ff71, 0x39200006, 0x38a10070, 0x91210070, 0x480000dd, + 0x2f83000b, 0x419e00c0, 0x81210070, 0x2b890005, 0x419c00ac, 0x2b890009, 0x419d00a4, + 0x7d2a4b78, 0x429f0005, 0x7d0802a6, 0x3d080000, 0x39080020, 0x794a1764, 0x7d2852aa, + 0x7d294214, 0x7d2903a6, 0x4e800420, 0x28, 0x30, 0x38, 0x40, 0x48, 0x50, 0x58, 0x60, 0x68, + 0x70, 0x38600028, 0x48000058, 0x38600029, 0x48000050, 0x3860002a, 0x48000048, 0x3860002b, + 0x48000040, 0x3860002c, 0x48000038, 0x3860002d, 0x48000030, 0x3860002e, 0x48000028, + 0x3860002f, 0x48000020, 0x38600030, 0x48000018, 0x38600031, 0x48000010, 0x38600063, + 0x48000008, 0x38600001, 0x38210090, 0xe8010010, 0x7c0803a6, 0x4e800020, 0x90850000, + 0x38600000, 0x4e800020, +]; + +/// `n` copies of [`SELECT`]'s dispatch in a chain: case 1 of stage `k` runs +/// stage `k + 1`, so each restart reveals one more switch. Stage `k`'s cases +/// return `100 + 10k`, `+ 2`, `+ 3`, and its default `+ 9`. +fn nested(n: usize) -> Vec { + const STAGE: usize = 31; + let out = 3 + n * STAGE + 1; + let get_state = out + 4; + let branch = |op: u32, from: usize, to: usize| op | ((to as i64 - from as i64) as u32 * 4 & 0x03ff_fffc); + let mut code = vec![0x7c0802a6, 0xf8010010, 0xf821ff71]; + for k in 0..n { + let s = code.len(); + let value = |v: usize| 0x38600000 | (100 + 10 * k + v) as u32; + code.extend([0x39200000, 0x38a10070, 0x91210070, branch(0x48000001, s + 3, get_state)]); + code.extend([0x81210070, 0x2b890004, 0x419d0058, 0x7d2a4b78, 0x429f0005, 0x7d0802a6]); + code.extend([0x3d080000, 0x39080020, 0x794a1764, 0x7d2852aa, 0x7d294214, 0x7d2903a6]); + code.extend([0x4e800420, 0x14, 0x34, 0x1c, 0x24, 0x14]); + for (slot, v) in [(22, 0), (24, 2), (26, 3), (28, 9)] { + code.extend([value(v), branch(0x48000000, s + slot + 1, out)]); + } + if k + 1 < n { + code.push(branch(0x48000000, s + 30, s + STAGE)); + } else { + code.extend([0x38600007, branch(0x48000000, s + 31, out)]); + } + } + assert_eq!(code.len(), out); + code.extend([0x38210090, 0xe8010010, 0x7c0803a6, 0x4e800020]); + code.extend([0x90850000, 0x38600000, 0x4e800020]); + code +} + +fn image(endian: Endianness, words: &[u32]) -> Vec { + let mut object = Object::new(BinaryFormat::Elf, Architecture::PowerPc64, endian); + object.flags = FileFlags::Elf { os_abi: 0, abi_version: 0, e_flags: 2 }; + let code: Vec = words + .iter() + .flat_map(|&n| match endian { + Endianness::Big => n.to_be_bytes(), + Endianness::Little => n.to_le_bytes(), + }) + .collect(); + let text = object.add_section(Vec::new(), b".text".to_vec(), SectionKind::Text); + object.append_section_data(text, &code, 4); + let helper = code.len() as u64 - 12; + for (name, value, size) in [("select", 0, helper), ("get_state", helper, 12)] { + object.add_symbol(Symbol { + name: name.as_bytes().to_vec(), + value, + size, + kind: SymbolKind::Text, + scope: SymbolScope::Linkage, + weak: false, + section: SymbolSection::Section(text), + flags: SymbolFlags::None, + }); + } + object.write().unwrap() +} + +/// Decompile `select` from `words` in both byte orders, flattening whitespace. +fn decompile_both(name: &str, words: &[u32]) -> Vec<(Endianness, String, String)> { + let path = common::scratch_file(name, "o"); + let mut results = Vec::new(); + for endian in [Endianness::Big, Endianness::Little] { + std::fs::write(&path, image(endian, words)).unwrap(); + let output = Command::new(env!("CARGO_BIN_EXE_kuna")) + .args(["decompile", path.to_str().unwrap(), "select"]) + .output() + .unwrap(); + assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + let code = String::from_utf8(output.stdout).unwrap(); + let flat = code.split_whitespace().collect::>().join(" "); + results.push((endian, code, flat)); + } + std::fs::remove_file(path).unwrap(); + results +} + +#[test] +fn a_call_written_switch_index_keeps_every_table_case() { + for (endian, code, flat) in decompile_both("ppc64-delayed-constant-switch", &SELECT) { + assert!(!code.contains("does not reproduce the recovered table"), "{endian:?}: {code}"); + for body in [ + "case 0: case 4: return 0x28;", + "case 1: return 0x29;", + "case 2: return 0x2a;", + "case 3: return 0x2b;", + "default: return 99;", + ] { + assert!(flat.contains(body), "{endian:?}: lost `{body}`: {code}"); + } + } +} + +#[test] +fn rows_a_lower_bound_guard_excludes_do_not_become_cases() { + for (endian, code, flat) in decompile_both("ppc64-delayed-lower-bound", &LOWER_BOUND) { + assert!(flat.contains("5 <="), "{endian:?}: dropped the lower bound: {code}"); + for value in 0x28..=0x2c { + assert!(!flat.contains(&format!(": return {value:#x};")), "{endian:?}: row of {value:#x} became a case: {code}"); + } + } +} + +#[test] +fn an_exhausted_restart_budget_keeps_the_analyzed_body() { + for (endian, code, _) in decompile_both("ppc64-delayed-nested-switch", &nested(9)) { + assert!(!code.contains("structuring declined"), "{endian:?}: lost the body: {code}"); + assert!(code.contains("Exceeded maximum restarts with more pending"), "{endian:?}: {code}"); + assert!(code.matches("switch(").count() >= 8, "{endian:?}: {code}"); + } +} diff --git a/decompiler/crates/kuna-console/src/engine.rs b/decompiler/crates/kuna-console/src/engine.rs index 64866765c1..4b2da9c7f8 100644 --- a/decompiler/crates/kuna-console/src/engine.rs +++ b/decompiler/crates/kuna-console/src/engine.rs @@ -3009,6 +3009,7 @@ fn analysis_pass_enabled(arch: &Architecture, pass_id: &str) -> bool { // facts are computed at LOAD and COMMITTED only when this gate is on, so // `off` renders exactly what the base table alone renders. "libcsigs" => arch.analysis_libcsigs, + "libgccsigs" => arch.analysis_libgccsigs, // (kuna) The LOAD-TIME format-string resolver — a Listing consumer whose // per-call-site overrides are computed at the deferred commit point and // applied only when the gate asks for the static half (`static` on x86, diff --git a/decompiler/crates/kuna-console/src/kuna_console.rs b/decompiler/crates/kuna-console/src/kuna_console.rs index 3b18802500..2bc65a06f2 100644 --- a/decompiler/crates/kuna-console/src/kuna_console.rs +++ b/decompiler/crates/kuna-console/src/kuna_console.rs @@ -270,6 +270,7 @@ pub fn kuna_live_value(conf: &Architecture, option: &str) -> Option on_off(conf.analysis_noreturn_known), "libproto" => on_off(conf.analysis_libproto), "libcsigs" => on_off(conf.analysis_libcsigs), + "libgccsigs" => on_off(conf.analysis_libgccsigs), // (kuna `libctypes`) Valued `off|opaque|glibc`, so the live token is the // value, not an on/off word. "libctypes" => match (conf.analysis_libctypes, conf.analysis_libctypes_glibc) { diff --git a/decompiler/crates/kuna-decomp/phases.toml b/decompiler/crates/kuna-decomp/phases.toml index 9dcf9ee349..ad94554eb7 100644 --- a/decompiler/crates/kuna-decomp/phases.toml +++ b/decompiler/crates/kuna-decomp/phases.toml @@ -5971,6 +5971,25 @@ change_kind = "correctness-fix" tier = "analysis" symptoms = "a Windows API call renders with an empty argument list|LoadLibraryExW() or CreateFileW() has no arguments but the pushes are visible above it|a PE function declares dozens of stack locals that only ever hold outgoing call arguments|CALL fall-through addresses are assigned to stack variables|a Win32 call's wide string argument renders as a bare 0x... constant|calleearity and varargstackargs both on and a Windows API call is still argumentless" +[[settable]] +option = "libgccsigs" +values = "on|off" +default = "off" +destructive = false +phase = "P1" +subphase = "external-refinement" +strength = "HARD" +rewind = "P1" +issue = "pr-1252-libgcc-divdi3" +summary = "Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract." +use_when = "Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal and nothing else reads the quotient (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 14 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, a clamp (`if (x > 100) x = 100;`), and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly." +example = "option libgccsigs on" +source_decompiler = "kuna" +inspiration = "libgcc/compiler-rt `__divdi3` (di_int __divdi3(di_int a, di_int b), di_int = int64_t); Ghidra ApplyDataArchiveAnalyzer" +change_kind = "correctness-fix" +tier = "analysis" +symptoms = "a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns|__divdi3 is called with four 32-bit arguments and only %o0 is read back|*p = v1; after a __divdi3 call where v1 is never written" + [[settable]] option = "declaredlibcproto" values = "on|off" diff --git a/decompiler/crates/kuna-decomp/src/infra/architecture.rs b/decompiler/crates/kuna-decomp/src/infra/architecture.rs index c57792f76a..4694051edd 100644 --- a/decompiler/crates/kuna-decomp/src/infra/architecture.rs +++ b/decompiler/crates/kuna-decomp/src/infra/architecture.rs @@ -1694,6 +1694,9 @@ pub struct Architecture { pub analysis_libproto: bool, /// (kuna) Gate the measured libc signature extension (`libcsigs`); default on. pub analysis_libcsigs: bool, + /// (kuna) Gate the libgcc DI-mode `__divdi3` contract on SPARC32 BE ELF + /// (`libgccsigs`); default off. + pub analysis_libgccsigs: bool, /// (kuna) Gate the named libc/POSIX aggregate types in the prototype tables /// (`libctypes`); default on (`opaque`). Catalog-visible twin of the load-time /// [`crate::kuna_libctypes`] **env var** (the named shells are interned at @@ -3161,6 +3164,7 @@ impl Architecture { analysis_peimportcall: false, analysis_libproto: false, analysis_libcsigs: false, + analysis_libgccsigs: false, analysis_libctypes: false, analysis_libctypes_glibc: false, analysis_win32sigs: false, @@ -3571,6 +3575,9 @@ impl Architecture { self.analysis_libproto = true; // (kuna) DIV-65 measured libc signature extension — default-ON. self.analysis_libcsigs = true; + // (kuna) libgcc `__divdi3` contract -- default-OFF: a caller returning + // arithmetic on the quotient still loses a word. + self.analysis_libgccsigs = false; // (kuna) Named libc/POSIX aggregate pointers in the prototype tables -- // default-ON (`opaque`). Real-ELF analysis tier only, so the XML corpora // cannot observe it; the evidence is the corpus sweep in diff --git a/decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs b/decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs index 8d3f3f6a05..f1a4fbe832 100644 --- a/decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs +++ b/decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs @@ -1072,7 +1072,10 @@ fn run_pipeline(arch: &mut Architecture, fd: &mut Funcdata) -> KunaResult return Ok(total); } if reflows >= MAX_REFLOW { - // Exceeded the cross-flow restart budget; keep the last analyzed IR. + // Exceeded the cross-flow restart budget. The last pass's analysis + // is complete; re-following now would clear it and leave nothing + // analyzed to print, so keep it and say more restarts were pending. + fd.warning_header("Exceeded maximum restarts with more pending"); fd.set_restart_pending(false); drain_pipeline_comments(arch, fd); return Ok(total); diff --git a/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_option_dispatch.rs b/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_option_dispatch.rs index 0136fc6214..bbb6662717 100644 --- a/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_option_dispatch.rs +++ b/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_option_dispatch.rs @@ -890,6 +890,7 @@ kuna_options! { self, p1; "peimportcall" => on_off!(analysis_peimportcall, "PE/Mach-O import-slot call binding"), "libproto" => on_off!(analysis_libproto, "Library-prototype analysis pass"), "libcsigs" => on_off!(analysis_libcsigs, "Measured libc signature extension"), + "libgccsigs" => on_off!(analysis_libgccsigs, "libgcc __divdi3 DI-mode contract"), "libctypes" => { use crate::kuna_libctypes::LibcTypesLayout; let layout = match p1.trim().to_ascii_lowercase().as_str() { diff --git a/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_phases/tests.rs b/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_phases/tests.rs index fb2431fd41..7bb3c2097a 100644 --- a/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_phases/tests.rs +++ b/decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_phases/tests.rs @@ -38,13 +38,13 @@ fn surface_count_is_120() { } #[test] -fn settable_count_is_339() { - assert_eq!(kuna_num_settables(), 339); - assert_eq!(SETTABLE_TABLE.len(), 339); +fn settable_count_is_340() { + assert_eq!(kuna_num_settables(), 340); + assert_eq!(SETTABLE_TABLE.len(), 340); } #[test] -fn tier_counts_are_120_core_130_transform_89_analysis() { +fn tier_counts_are_120_core_130_transform_90_analysis() { let mut core = 0; let mut transform = 0; let mut analysis = 0; @@ -56,7 +56,7 @@ fn tier_counts_are_120_core_130_transform_89_analysis() { other => panic!("invalid tier {other:?} on {}", s.option), } } - assert_eq!((core, transform, analysis), (120, 130, 89)); + assert_eq!((core, transform, analysis), (120, 130, 90)); } #[test] @@ -318,6 +318,9 @@ fn option_values_live_value_present_for_154() { // with no codegen live reader (read console-side via kuna_live_value), // same as `libcsigs` above. Default-ON (DIV-141). "win32sigs", + // (kuna) The libgcc `__divdi3` contract -- an analysis-pass gate read + // console-side via kuna_live_value, like `libcsigs`. Default-off. + "libgccsigs", "strings", // (kuna) The 2-byte (UTF-16LE) width of the string-literal pass — an // analysis-pass gate read at the commit boundary (console-side via @@ -1109,7 +1112,7 @@ fn emit_catalog_json_static_form_brackets_and_commas() { // +1 for `goreg`; its P1 row sits beside `goabi`. // +1 for `gobool`; its P4 row sits beside `sourcelang`. // +1 for `swiftdemangle`; its P1 row sits beside `sourcelang`. - assert_eq!(json.matches("},\n").count(), 338); + assert_eq!(json.matches("},\n").count(), 339); } #[test] diff --git a/decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs b/decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs index 7b5c4523cc..5f07ba3e94 100644 --- a/decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs +++ b/decompiler/crates/kuna-decomp/src/p0_knowledge/modes.rs @@ -595,7 +595,11 @@ mod tests { // the web front-end, the benchmark) materialize every target they walk // anyway, so preset membership would buy nothing there and has not been // measured on them. Flip it into the preset with that measurement. - "lazyfuncs"]; + "lazyfuncs", + // `libgccsigs` costs a SPARC32 caller that returns arithmetic on + // the `__divdi3` quotient its low word or its whole return (see its + // catalog row), so the preset must not turn it on. + "libgccsigs"]; /// Default-off options that predate this test and are **not** in the preset, /// i.e. are currently unreachable on the default path. Each is a genuine open diff --git a/decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs b/decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs index 95301733cd..46955a6933 100644 --- a/decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs +++ b/decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs @@ -239,6 +239,9 @@ pub struct Override { protoover: BTreeMap>, /// Addresses of indirect jumps that need multistage recovery. multistagejump: Vec
, + /// (kuna) The targets the late model recovered for a multistage jump, used + /// when the second flow-time stage still recovers a single address. + multistagetargets: BTreeMap>, /// Override the CALL <-> BRANCH interpretation of a branch instruction. flowoverride: BTreeMap, /// `/GS` checker call sites recognized after SSA and replayed on restart. @@ -263,6 +266,7 @@ impl Override { self.indirectover.clear(); self.protoover.clear(); self.multistagejump.clear(); + self.multistagetargets.clear(); self.flowoverride.clear(); self.msvc_cookie_calls.clear(); self.format_calls.clear(); @@ -344,6 +348,17 @@ impl Override { self.multistagejump.push(addr); } + /// (kuna) Record the targets a late model recovered for the multistage jump + /// at `addr`. + pub fn insert_multistage_targets(&mut self, addr: Address, targets: Vec
) { + self.multistagetargets.insert(addr, targets); + } + + /// (kuna) The targets recorded for the multistage jump at `addr`. + pub fn multistage_targets(&self, addr: &Address) -> Option<&[Address]> { + self.multistagetargets.get(addr).map(Vec::as_slice) + } + /// Mark a branch instruction with a different flow type /// (C++ `Override::insertFlowOverride`). pub fn insert_flow_override(&mut self, addr: Address, type_: uint4) { diff --git a/decompiler/crates/kuna-decomp/src/p2_lift/flow.rs b/decompiler/crates/kuna-decomp/src/p2_lift/flow.rs index eb74dd5fb3..a633e5df1f 100644 --- a/decompiler/crates/kuna-decomp/src/p2_lift/flow.rs +++ b/decompiler/crates/kuna-decomp/src/p2_lift/flow.rs @@ -2578,10 +2578,8 @@ truncating the fall-through here" /// caller binds it to the real [`Architecture`]'s action root). For each /// recovered table, `newAddress` re-fills as much flow as possible. /// - /// STUB(W4): `checkContainedCall` / `checkMultistageJumptables` (the PIC / - /// multistage outer loop) need the override table + FuncCallSpecs; the single - /// recovery pass over the current `tablelist` is the load-bearing part for the - /// corpus and runs fully. + /// STUB(W4): `checkContainedCall` (the PIC outer loop) needs FuncCallSpecs; + /// `checkMultistageJumptables` re-queues a table a multistage hint marks. /// /// The outer `do/while` drains `injectlist` on every round (`flow.cc:819`), so a /// registered `` is applied to table-discovered flow exactly as it @@ -2611,10 +2609,8 @@ truncating the fall-through here" } } } - // STUB(W4): checkContainedCall + checkMultistageJumptables (outer - // do/while) — the multistage/inline restart that could re-populate - // tablelist is the W4 override table; the single pass suffices for the - // corpus switches. + // STUB(W4): checkContainedCall (PIC constructions) is not ported. + self.check_multistage_jumptables()?; if self.has_inject() { self.inject_pcode()?; } @@ -2625,6 +2621,27 @@ truncating the fall-through here" Ok(()) } + /// Queue each jump-table that a multistage hint asks to recover again, + /// now that flow has filled in more of the function (C++ + /// `FlowInfo::checkMultistageJumptables`, `flow.cc:1393`). + fn check_multistage_jumptables(&mut self) -> KunaResult<()> { + for i in 0..self.data.num_jump_tables() { + let mut jt = std::mem::replace( + self.data.get_jump_table_mut(i), + crate::jumptable::JumpTable::new(Address::default()), + ); + let res = jt.check_for_multistage(&self.data); + let indop = jt.get_indirect_op(); + *self.data.get_jump_table_mut(i) = jt; + if res? { + if let Some(op) = indop { + self.tablelist.push(op); + } + } + } + Ok(()) + } + /// Generate basic blocks from the raw control-flow (C++ `generateBlocks`, /// `flow.cc:826`). /// diff --git a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs index d9d90c2bd5..e65f11b1b1 100644 --- a/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs +++ b/decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs @@ -5973,7 +5973,7 @@ impl JumpTable { return Ok(()); } if jbasic.rows_rejected { - self.drop_model_for_rows(fd); + self.drop_model_for_rows(fd, true); return Ok(()); } // Try model 2 (default-path). @@ -6165,6 +6165,16 @@ impl JumpTable { Ok(()) } + /// (kuna) A second flow-time stage that again recovered a single address + /// adopts the targets the late model recorded with the multistage hint, so + /// flow follows every case of the table. + pub fn kuna_adopt_multistage_targets(&mut self, targets: Vec
) { + if self.addresstable.len() == 1 && targets.len() > 1 { + self.addresstable = targets; + self.row_loads = None; + } + } + /// Set manual override information on \b this jump-table /// (C++ `JumpTable::setOverride`, `jumptable.cc:2616`). /// @@ -6186,9 +6196,10 @@ impl JumpTable { /// Try to match the JumpTable model to the existing function /// (C++ `JumpTable::matchModel`, `jumptable.cc:2833`). /// - /// The table keeps its flow-recovered addresses; the multistage restart - /// upstream takes on a size mismatch is replaced by a row check that - /// decides which values may label them ([`Self::choose_label_values`]). + /// A one-address flow-time table whose current model has more entries + /// requests upstream's multistage restart ([`Self::request_multistage`]). + /// Any other size mismatch keeps the flow-recovered addresses, and a row + /// check decides which values may label them ([`Self::choose_label_values`]). pub fn match_model(&mut self, fd: &mut Funcdata) -> KunaResult<()> { if !self.is_recovered() { return Err(KunaError::lowlevel( @@ -6212,20 +6223,72 @@ impl JumpTable { .and_then(|m| LabelRows::new(m, &self.addresstable, self.row_loads.as_ref())) .map(|rows| Rc::new(rows.with_default(self.default_addresses()))); self.recover_model(fd, rows.clone())?; // Create a current instance of the model + // (kuna) Unlike upstream, keep labelling this pass: when the drive's + // restart budget is spent, this pass is the one printed, under its own + // restart warning rather than the model-drop one. + let restarting = self.request_multistage(fd); if let Some(rows) = rows { - self.choose_label_values(fd, &rows); + self.choose_label_values(fd, &rows, !restarting); } Ok(()) } + /// The multistage restart of C++ `JumpTable::matchModel` (`jumptable.cc:2709`): + /// a flow-time table of one address whose current model has more entries was + /// not fully recovered during flow, so record the jump for a second recovery + /// stage and request a restart. + /// + /// (kuna) The current model's own targets are recorded with the hint, since + /// the second flow-time stage repeats the first one's view of the function + /// and can recover the same single address. A jump is restarted at most once, + /// and only when every value in the model's range passes the guards on its + /// path (a range wider than the guards, as `switchguardbound` can recover, + /// would turn guarded-out rows into cases) and every target is mapped and + /// within the `0xffff` reach that `sanity_check` allows an intraprocedural + /// table. + fn request_multistage(&self, fd: &mut Funcdata) -> bool { + let Some(model) = self.jmodel.as_ref() else { return false }; + if self.addresstable.len() != 1 + || model.get_table_size() <= 1 + || fd.get_override().query_multistage_jumptable(&self.opaddress) + { + return false; + } + let Some(indirect) = self.indirect else { return false }; + let dispatched = model + .as_any() + .downcast_ref::() + .and_then(|basic| Some(basic.values_pass_guards(fd, basic.jrange.as_ref()?.clone_box(), 0))); + if !dispatched.is_some_and(|rows| !rows.is_empty() && rows.iter().all(|&row| row)) { + return false; + } + let mut targets = Vec::new(); + if model.build_addresses(fd, indirect, &mut targets, None, None).is_err() || targets.len() <= 1 { + return false; + } + let jump = self.opaddress.get_offset(); + if !targets.iter().all(|target| { + let offset = target.get_offset(); + offset != 0 + && offset.abs_diff(jump) <= 0xffff + && fd.get_arch().get_load_image_value(target, 4).is_ok() + }) { + return false; + } + fd.get_override_mut().insert_multistage_jump(self.opaddress.clone()); + fd.get_override_mut().insert_multistage_targets(self.opaddress.clone(), targets); + fd.set_restart_pending(true); + true + } + /// Choose the values that label \b this table after a late re-recovery: /// the flow-time values when the new model reproduces their rows, the new /// model's own values when they rebuild the table, and otherwise none (the /// model is dropped, so the trivial model labels each case by address). /// - /// Stands in for the upstream multistage restart on a table mismatch - /// (`Override::insertMultistageJump`/`setRestartPending`, not ported). - fn choose_label_values(&mut self, fd: &mut Funcdata, rows: &LabelRows) { + /// Stands in for the upstream multistage restart on a table mismatch that + /// [`Self::request_multistage`] does not restart. + fn choose_label_values(&mut self, fd: &mut Funcdata, rows: &LabelRows, warn: bool) { let indop = self.indirect.unwrap(); let Some(model) = self .jmodel @@ -6246,7 +6309,7 @@ impl JumpTable { self.origmodel = None; return; } - self.drop_model_for_rows(fd); + self.drop_model_for_rows(fd, warn); } /// The table addresses whose block is the current default destination. @@ -6260,8 +6323,9 @@ impl JumpTable { /// Drop the model of a table whose flow-time rows no recovered model can /// label, so the trivial model labels each case by address: by the - /// flow-time table values that reach it. - fn drop_model_for_rows(&mut self, fd: &mut Funcdata) { + /// flow-time table values that reach it. `warn` adds the header warning; a + /// pass that has requested a multistage restart is not the final one. + fn drop_model_for_rows(&mut self, fd: &mut Funcdata, warn: bool) { self.case_values = self .block2addr .iter() @@ -6273,9 +6337,11 @@ impl JumpTable { self.case_values.sort(); self.case_values.dedup(); self.jmodel = None; - fd.warning_header( - "Switch model does not reproduce the recovered table; cases are labelled by address", - ); + if warn { + fd.warning_header( + "Switch model does not reproduce the recovered table; cases are labelled by address", + ); + } } /// Recover the case labels for \b this jump-table @@ -6382,12 +6448,8 @@ impl JumpTable { } /// Check if this jump-table requires an additional recovery stage - /// (C++ `JumpTable::checkForMultistage`, `jumptable.cc:3003`). - /// - /// STUB(W4): the override-table query (`fd->getOverride().queryMultistageJumptable`) - /// is W4; the structural guards are faithful and short-circuit before the - /// query whenever possible. - pub fn check_for_multistage(&mut self, _fd: &Funcdata) -> KunaResult { + /// (C++ `JumpTable::checkForMultistage`, `jumptable.cc:2865`). + pub fn check_for_multistage(&mut self, fd: &Funcdata) -> KunaResult { if self.addresstable.len() != 1 { return Ok(false); } @@ -6400,10 +6462,14 @@ impl JumpTable { if self.recover_count > 1 { return Ok(false); } - // fd->getOverride().queryMultistageJumptable(indirect->getAddr()) STUB(W4) - Err(KunaError::lowlevel( - "JumpTable::checkForMultistage: Override::queryMultistageJumptable is W4", - )) + let Some(addr) = self.indirect.and_then(|op| fd.obank().get(op)).map(|op| op.get_addr().clone()) else { + return Ok(false); + }; + if fd.get_override().query_multistage_jumptable(&addr) { + self.partial_table = true; // Mark that we need additional recovery + return Ok(true); + } + Ok(false) } /// Perform a sanity check on recovered address targets diff --git a/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs b/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs index 077f0cf2aa..bce3a3b1c7 100644 --- a/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs +++ b/decompiler/crates/kuna-decomp/src/p3_dataflow/heritage.rs @@ -2758,29 +2758,63 @@ impl Heritage { } } - /// Guard RETURN ops when the heritaged range *contains* the output storage, - /// truncating to the recovered output (C++ `Heritage::guardReturnsOverlapping`, - /// `heritage.cc:1610`). Registers the truncated output trial and inserts a - /// SUBPIECE before each RETURN, appending the truncated value as a new input. + /// Guard the biggest model output contained by a wider heritaged range. On a + /// model whose four-byte pair returns the high word first, also guard every + /// other contained output: a locked register-pair call result covers both + /// words, and [`crate::kuna_retcallhalf::whole_pair`] may need the second one. fn guard_returns_overlapping( &mut self, fd: &mut crate::funcdata::Funcdata, addr: &Address, size: int4, + ) { + let split = fd.get_func_proto().has_model() + && crate::kuna_bejoin::wide_pair(fd.get_func_proto().model(), fd.get_arch().manage()) + .is_some_and(|pair| pair.hi.1 == 4 && pair.lo.1 == 4); + self.guard_returns_in_range(fd, addr, size, addr, size, true, split); + } + + fn guard_returns_in_range( + &mut self, + fd: &mut crate::funcdata::Funcdata, + addr: &Address, + size: int4, + range: &Address, + range_size: int4, + primary: bool, + split: bool, ) { use kuna_num::opcodes::OpCode; use kuna_num::pcoderaw::VarnodeData; let mut vdata = VarnodeData::default(); - if !fd.get_func_proto().get_biggest_contained_output(addr, size, &mut vdata) { + if !fd.get_func_proto().get_biggest_contained_output(range, range_size, &mut vdata) { return; } let vspace = match vdata.space.clone() { Some(s) => s, None => return, }; + let Some(before) = vdata.offset.checked_sub(range.get_offset()) + .and_then(|n| int4::try_from(n).ok()) + else { + return; + }; + let piece_size = vdata.size as int4; + if before > range_size || piece_size <= 0 || piece_size > range_size - before { + return; + } + if split && before > 0 { + self.guard_returns_in_range(fd, addr, size, range, before, false, true); + } let trunc_addr = Address::new(Rc::clone(&vspace), vdata.offset); if let Some(active) = fd.get_active_output_mut() { active.register_trial(&trunc_addr, vdata.size as int4); + if !primary { + // The wide range alone is no evidence that the caller returns + // this extra word. Activate it only after a whole-pair proof. + let index = active.get_num_trials() - 1; + active.get_trial_mut(index).mark_inactive(); + } } // offset = least-significant bytes to truncate (endian-adjusted). let mut offset = (vdata.offset.wrapping_sub(addr.get_offset())) as i64 as int4; @@ -2815,6 +2849,10 @@ impl Heritage { .set_active_heritage(); let _ = fd.op_insert_input(op, ret_val, n); } + let after = range_size - before - piece_size; + if split && after > 0 { + self.guard_returns_in_range(fd, addr, size, &(&trunc_addr + piece_size as i64), after, false, true); + } } /// (kuna `stackstoreguard`) May this pass guard the frame of `addr`'s diff --git a/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4.rs b/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4.rs index b6708b3847..8e5b1fdc13 100644 --- a/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4.rs +++ b/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4.rs @@ -155,15 +155,39 @@ impl RuleLoadVarnode { data: &Funcdata, vn: VarnodeId, spc: &Rc, + depth: u32, ) -> Option<(Rc, uintb)> { if let Some(retspace) = RuleLoadVarnode::correct_spacebase(data, vn, spc) { return Some((retspace, 0)); } + if depth == 8 { + return None; + } let v = data.vbank().get(vn)?; if !v.is_written() { return None; } let op = v.get_def()?; + if code(data, op) == OpCode::CPUI_INT_OR { + let a = in_vn(data, op, 0); + let b = in_vn(data, op, 1); + let (base, constant) = if is_constant(data, b) { + (a, offset_of(data, b)) + } else if is_constant(data, a) { + (b, offset_of(data, a)) + } else { + return None; + }; + // Clang can address a word inside an aligned frame slot as + // (spacebase + offset) | word_offset. It is addition only when + // known-zero bits prove that no set bit can overlap. + if data.vbank().get(base)?.get_nz_mask() & constant != 0 { + return None; + } + let (space, offset) = Self::vn_spacebase(data, base, spc, depth + 1)?; + let offset = space.wrap_offset(offset.wrapping_add(constant)); + return Some((space, offset)); + } if code(data, op) != OpCode::CPUI_INT_ADD { return None; } @@ -205,7 +229,7 @@ impl RuleLoadVarnode { // Check for a literal constant offset into `loadspace`. return Some((loadspace, offset_of(data, offvn))); } - RuleLoadVarnode::vn_spacebase(data, offvn, &loadspace) + RuleLoadVarnode::vn_spacebase(data, offvn, &loadspace, 0) } } diff --git a/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4/tests.rs b/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4/tests.rs index f3cab57b15..72628b8ae4 100644 --- a/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4/tests.rs +++ b/decompiler/crates/kuna-decomp/src/p3_dataflow/ruleaction_4/tests.rs @@ -1432,6 +1432,47 @@ fn concatcommute_or_lower() { assert_eq!(fd.vbank().get(cc).unwrap().get_offset(), 0x00ff); } +#[test] +fn loadvarnode_promotes_only_proven_disjoint_or_offsets() { + for (offset, mask, expected) in [ + (0x120, 0x120, Some(0x124)), + (0x124, 0x124, None), + (0x120, u64::MAX, None), + ] { + for commuted in [false, true] { + let mut fd = build_fd(); + let bl = mk_block(&mut fd); + // A constant pseudo-spacebase is the existing global-space form. + // The CLI regression exercises the same rewrite on real frame SSA. + let base = fd.new_constant(8, 0); + fd.vbank_mut().get_mut(base).unwrap().set_spacebase(); + let displacement = fd.new_constant(8, offset); + let (_, address) = mk_def( + &mut fd, bl, OpCode::CPUI_INT_ADD, 8, 0x20, &[base, displacement], + ); + fd.vbank_mut().get_mut(address).unwrap().set_nz_mask(mask); + let word = fd.new_constant(8, 4); + let inputs = if commuted { [word, address] } else { [address, word] }; + let (_, pointer) = mk_def(&mut fd, bl, OpCode::CPUI_INT_OR, 8, 0x30, &inputs); + let space = fd.new_constant(8, ram_space(&fd).get_index() as u64); + let (load, _) = mk_def(&mut fd, bl, OpCode::CPUI_LOAD, 4, 0x40, &[space, pointer]); + let changed = RuleLoadVarnode::new().apply_op(load, &mut fd); + if let Some(expected) = expected { + assert_eq!(changed, 1); + assert_eq!(fd.obank().get(load).unwrap().code(), OpCode::CPUI_COPY); + let input = fd.obank().get(load).unwrap().get_in(0).unwrap(); + let value = fd.vbank().get(input).unwrap(); + assert_eq!(value.get_offset(), expected); + assert_eq!(value.get_size(), 4); + assert_eq!(value.get_space().get_name(), "ram"); + } else { + assert_eq!(changed, 0); + assert_eq!(fd.obank().get(load).unwrap().code(), OpCode::CPUI_LOAD); + } + } + } +} + // --------------------------------------------------------------------------- // Stub-affected rules: assert the documented early-out // --------------------------------------------------------------------------- diff --git a/decompiler/crates/kuna-decomp/src/p4_calls/coreaction_protos.rs b/decompiler/crates/kuna-decomp/src/p4_calls/coreaction_protos.rs index d036cb8dcf..0cf1231489 100644 --- a/decompiler/crates/kuna-decomp/src/p4_calls/coreaction_protos.rs +++ b/decompiler/crates/kuna-decomp/src/p4_calls/coreaction_protos.rs @@ -1728,6 +1728,10 @@ impl Action for ActionReturnRecovery { let mut cond_failed = Vec::new(); let return_ops: Vec = data.obank().iter_code(OpCode::CPUI_RETURN).collect(); + if crate::kuna_retcallhalf::awaiting_stack(data, &active, &return_ops) { + data.restore_active_output(active); + return 0; + } for &op in &return_ops { let o = match data.obank().get(op) { Some(o) => o, @@ -1818,11 +1822,21 @@ impl Action for ActionReturnRecovery { crate::p4_calls::kuna_x87emptyreturn::drop_empty(data, &mut active, &return_ops); let own_input = crate::kuna_retcallhalf::accept(data, &mut active, &return_ops); crate::p4_calls::kuna_callwide::retain_pair(data, &mut active, &return_ops); + let whole_pair = crate::kuna_retcallhalf::whole_pair(data, &active, &return_ops); + if let Some(indices) = whole_pair { + for index in indices { + active.get_trial_mut(index).mark_active(); + } + } let manager_rc = data.get_arch().manage.clone(); let _ = data.get_func_proto().derive_output_map(&mut active, &manager_rc); crate::p4_calls::kuna_voidret::whole_or_none(data, &mut active); crate::kuna_retinputhalf::note_moved_back_returns(data, &active, &return_ops, own_input); - let order = crate::kuna_bejoin::join_order(&active, data, &return_ops); + let order = if whole_pair.is_some() && crate::kuna_bejoin::live(data) { + active.join_pair_order() + } else { + crate::kuna_bejoin::join_order(&active, data, &return_ops) + }; data.kuna_set_pairs_first_low(crate::kuna_bejoin::joins_first_low(&active, order)); let return_single = data.get_arch().return_single; for &op in &return_ops { diff --git a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_bejoin.rs b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_bejoin.rs index af3817896f..cf0af1b02f 100644 --- a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_bejoin.rs +++ b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_bejoin.rs @@ -2265,7 +2265,7 @@ fn ones(size: int4) -> u64 { /// The value of `vn` when it is a constant or integer arithmetic and logic on /// constants alone, within `depth` operations, truncated to its size. -fn literal_value(data: &Funcdata, vn: VarnodeId, depth: u32) -> Option { +pub(crate) fn literal_value(data: &Funcdata, vn: VarnodeId, depth: u32) -> Option { let v = data.vbank().get(vn)?; let size = v.get_size(); let mask = |x: u64, sz: int4| if sz >= 8 { x } else { x & ((1u64 << (8 * sz)) - 1) }; diff --git a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs index df1b3c0f25..94d8517e50 100644 --- a/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs +++ b/decompiler/crates/kuna-decomp/src/p4_calls/kuna_retcallhalf.rs @@ -61,8 +61,9 @@ //! is not changed: `bl g; orr r0,r0,#255` is byte for byte `int f(void) { //! return (int)g() | 255; }` as well as a 64-bit return. Nor is one whose //! second register feeds the first (`asr r1,r0,#2; add r0,r1,r0,lsr #31` is -//! an `int` division by a constant). And a big-endian pair is left alone, -//! because a pair is still joined in little-endian order there. +//! an `int` division by a constant). These heuristics leave high-first pairs +//! alone; [`whole_pair`] separately retains both halves of one declared +//! integer call result, using the model's word order. //! //! # A pair the function names only in part //! @@ -101,6 +102,348 @@ const MAX_DEPTH: u32 = 8; /// How many defining ops the zero test follows. const ZERO_DEPTH: u32 = 8; +/// Preserve pending return reads until scheduled frame SSA can resolve spills. +/// A LOAD is not itself proof: it may reload unused incoming arguments. +pub(crate) fn awaiting_stack( + data: &Funcdata, + active: &ParamActive, + returns: &[OpId], +) -> bool { + let Some(stack) = data.get_arch().manage().get_stack_space() else { return false }; + let Ok(passes) = data.num_heritage_passes(stack) else { return false }; + if !data.get_func_proto().has_model() || active.get_num_trials() < 2 { + return false; + } + if !(0..data.num_calls()).any(|i| wide_integer_call(data, data.get_call_specs(i))) { + return false; + } + if !crate::kuna_bejoin::wide_pair(data.get_func_proto().model(), data.get_arch().manage()) + .is_some_and(|pair| pair.hi.1 == 4 && pair.lo.1 == 4) + { + return false; + } + returns.iter().any(|&ret| { + data.obank().get(ret).is_some_and(|op| { + !op.is_dead() && op.get_halt_type() == 0 + && !crate::kuna_bejoin::never_reached(data, ret) + && (0..active.get_num_trials()).any(|i| { + op.get_in(active.get_trial(i).get_slot()).is_some_and(|vn| { + pending_stack_word(data, (vn, 0), stack.get_index(), passes <= 1, MAX_DEPTH) + }) + }) + }) + }) +} + +/// A frame read which the next simplification pool can materialize for heritage. +/// In particular, a narrow read underneath a phi or extension can survive the +/// first stack SSA pass even after the final eight-byte reload was resolved. +fn pending_stack_word( + data: &Funcdata, + word: (VarnodeId, u64), + stack: i32, + early: bool, + depth: u32, +) -> bool { + if depth == 0 { + return false; + } + let Some((vn, offset)) = word_source(data, word) else { return false }; + let Some(value) = data.vbank().get(vn) else { return false }; + let Some(id) = value.get_def() else { return false }; + let Some(op) = data.obank().get(id) else { return false }; + match op.code() { + OpCode::CPUI_LOAD => { + // Before spacebase propagation, preserve the original wide reload. + // Later, wait only for a proven frame access that RuleLoadVarnode + // can rewrite, not an arbitrary load or an extra fixed pass count. + early && value.get_size() == 8 + || crate::ruleaction_4::RuleLoadVarnode::check_spacebase(data, id) + .is_some_and(|(space, _)| space.get_index() == stack) + } + OpCode::CPUI_MULTIEQUAL => (0..op.num_input()).any(|slot| { + op.get_in(slot).is_some_and(|input| { + pending_stack_word(data, (input, offset), stack, early, depth - 1) + }) + }), + _ => false, + } +} + +/// A high-first pair with at least one complete declared integer call result. +/// Other paths may replace either word with a literal, including before the +/// simplification pool folds it, but may not introduce unrelated live values. +pub(crate) fn whole_pair( + data: &Funcdata, + active: &ParamActive, + returns: &[OpId], +) -> Option<[i32; 2]> { + let proto = data.get_func_proto(); + if proto.is_output_locked() || !proto.has_model() || active.get_num_trials() < 2 { + return None; + } + // No word walk can establish a complete declared result without such a call. + if !(0..data.num_calls()).any(|i| wide_integer_call(data, data.get_call_specs(i))) { + return None; + } + let pair = crate::kuna_bejoin::wide_pair(proto.model(), data.get_arch().manage())?; + if pair.hi.1 != 4 || pair.lo.1 != 4 { + return None; + } + let mut indices = [0; 2]; + for (slot, (address, size)) in [&pair.hi, &pair.lo].into_iter().enumerate() { + indices[slot] = (0..active.get_num_trials()).find(|&i| { + let trial = active.get_trial(i); + trial.get_address() == address && trial.get_size() == *size && !trial.is_unref() + })?; + } + if (0..active.get_num_trials()) + .any(|i| !indices.contains(&i) && active.get_trial(i).is_active()) + { + return None; + } + let mut wide = false; + for &ret in returns { + let operation = data.obank().get(ret)?; + if operation.is_dead() || operation.get_halt_type() != 0 + || crate::kuna_bejoin::never_reached(data, ret) + { + continue; + } + let hi = operation.get_in(active.get_trial(indices[0]).get_slot())?; + let lo = operation.get_in(active.get_trial(indices[1]).get_slot())?; + wide |= whole_words(data, (hi, 0), (lo, 0), MAX_DEPTH)?; + } + wide.then_some(indices) +} + +/// Follow only representation-preserving moves and exact four-byte slices. +fn word_source(data: &Funcdata, mut word: (VarnodeId, u64)) -> Option<(VarnodeId, u64)> { + // Moves cannot cycle in SSA without a phi, where this walk stops. + // Use the graph's own size as the bound, not an arbitrary copy-chain depth. + for _ in 0..data.vbank().num_varnodes() { + let value = data.vbank().get(word.0)?; + if word.1.checked_add(4)? > value.get_size() as u64 { + return None; + } + let Some(op) = value.get_def().and_then(|id| data.obank().get(id)) else { + return Some(word); + }; + match op.code() { + OpCode::CPUI_COPY => word.0 = op.get_in(0)?, + OpCode::CPUI_INT_ZEXT | OpCode::CPUI_INT_SEXT => { + let input = op.get_in(0)?; + if word.1.checked_add(4)? > data.vbank().get(input)?.get_size() as u64 { + return Some(word); + } + word.0 = input; + } + OpCode::CPUI_INDIRECT if !op.is_indirect_creation() => word.0 = op.get_in(0)?, + OpCode::CPUI_INT_OR if op.get_in(0) == op.get_in(1) => word.0 = op.get_in(0)?, + OpCode::CPUI_INT_ADD | OpCode::CPUI_INT_OR | OpCode::CPUI_INT_XOR => { + let a = op.get_in(0)?; + let b = op.get_in(1)?; + word.0 = if is_zero(data, a, ZERO_DEPTH) { + b + } else if is_zero(data, b, ZERO_DEPTH) { + a + } else { + return Some(word); + }; + } + OpCode::CPUI_SUBPIECE => { + let offset = data.vbank().get(op.get_in(1)?)?; + if !offset.is_constant() { + return None; + } + word = (op.get_in(0)?, word.1.checked_add(offset.get_offset())?); + } + OpCode::CPUI_PIECE => { + let low = op.get_in(1)?; + let low_size = data.vbank().get(low)?.get_size() as u64; + if word.1.checked_add(4)? <= low_size { + word.0 = low; + } else if word.1 >= low_size { + word = (op.get_in(0)?, word.1 - low_size); + } else { + return None; + } + } + _ => return Some(word), + } + } + None +} + +fn whole_words( + data: &Funcdata, + hi: (VarnodeId, u64), + lo: (VarnodeId, u64), + depth: u32, +) -> Option { + if depth == 0 { + return None; + } + let hi = word_source(data, hi)?; + let lo = word_source(data, lo)?; + let literal = |word: (VarnodeId, u64)| { + crate::kuna_bejoin::literal_value(data, word.0, ZERO_DEPTH).is_some() + // A word above a zero-extension's input is literal zero even when + // the complete extended value depends on the call's low word. + || data.vbank().get(word.0) + .and_then(|value| value.get_def()) + .and_then(|id| data.obank().get(id)) + .filter(|op| op.code() == OpCode::CPUI_INT_ZEXT) + .and_then(|op| op.get_in(0)) + .and_then(|id| data.vbank().get(id)) + .is_some_and(|input| word.1 >= input.get_size() as u64) + }; + let hi_literal = literal(hi); + let lo_literal = literal(lo); + if hi_literal && lo_literal { + return Some(false); + } + let phi = |vn: VarnodeId| { + data.vbank().get(vn)?.get_def().and_then(|id| data.obank().get(id)) + .filter(|op| op.code() == OpCode::CPUI_MULTIEQUAL) + }; + match (phi(hi.0), phi(lo.0)) { + (Some(hop), Some(lop)) + if hop.get_parent() == lop.get_parent() && hop.num_input() == lop.num_input() => + { + let mut wide = false; + for slot in 0..hop.num_input() { + wide |= whole_words(data, (hop.get_in(slot)?, hi.1), (lop.get_in(slot)?, lo.1), depth - 1)?; + } + return Some(wide); + } + (Some(hop), None) => { + let mut wide = false; + for slot in 0..hop.num_input() { + wide |= whole_words(data, (hop.get_in(slot)?, hi.1), lo, depth - 1)?; + } + return Some(wide); + } + (None, Some(lop)) => { + let mut wide = false; + for slot in 0..lop.num_input() { + wide |= whole_words(data, hi, (lop.get_in(slot)?, lo.1), depth - 1)?; + } + return Some(wide); + } + _ => {} + } + match (call_word(data, hi, 4), call_word(data, lo, 0)) { + (Some(h), Some(l)) if h == l && only_returned(data, h) => Some(true), + (Some(_), None) if lo_literal => Some(false), + (None, Some(_)) if hi_literal => Some(false), + _ => None, + } +} + +/// Every reader of a call result, through moves, slices and phis, is a RETURN. +/// A caller that stores, passes or tests the result consumed it, so the words +/// left in the return registers are no evidence that it returns them. +fn only_returned(data: &Funcdata, result: VarnodeId) -> bool { + let mut seen = std::collections::BTreeSet::new(); + let mut work = vec![result]; + while let Some(cur) = work.pop() { + if !seen.insert(cur) { + continue; + } + if seen.len() > 64 { + return false; + } + let Some(v) = data.vbank().get(cur) else { return false }; + if v.is_persist() { + return false; + } + for reader in v.descend_iter() { + let Some(op) = data.obank().get(reader) else { return false }; + let moves = match op.code() { + OpCode::CPUI_RETURN => continue, + OpCode::CPUI_CALL | OpCode::CPUI_CALLIND => { + if pending_argument(data, reader, op.get_slot(cur)) { + continue; + } + false + } + OpCode::CPUI_COPY + | OpCode::CPUI_SUBPIECE + | OpCode::CPUI_PIECE + | OpCode::CPUI_MULTIEQUAL + | OpCode::CPUI_INT_ZEXT + | OpCode::CPUI_INT_SEXT => true, + OpCode::CPUI_INDIRECT => op.get_in(0) == Some(cur), + OpCode::CPUI_INT_OR if op.get_in(0) == op.get_in(1) => true, + OpCode::CPUI_INT_ADD | OpCode::CPUI_INT_OR | OpCode::CPUI_INT_XOR => (0..2).any(|slot| { + op.get_in(slot) + .is_some_and(|other| other != cur && is_zero(data, other, ZERO_DEPTH)) + }), + _ => false, + }; + match op.get_out() { + Some(out) if moves => work.push(out), + _ => return false, + } + } + } + true +} + +/// An input of a later call whose parameter recovery has not made it an +/// argument, as upstream's `checkCallDoubleUse` treats it. +fn pending_argument(data: &Funcdata, call: OpId, slot: i32) -> bool { + let Some(index) = data.get_call_specs_index(call) else { return false }; + let spec = data.get_call_specs(index); + let placeholder = spec.get_stack_placeholder_slot(); + if slot <= 0 || slot == placeholder || !spec.is_input_active() { + return false; + } + let active = spec.active_input(); + let trial = slot - if placeholder < 0 || slot < placeholder { 1 } else { 2 }; + if trial < 0 || trial >= active.get_num_trials() { + return false; + } + let trial = active.get_trial(trial); + !(trial.is_checked() && trial.is_active()) +} + +/// A word at its ABI position in one locked integer call result. +fn call_word(data: &Funcdata, word: (VarnodeId, u64), offset: u64) -> Option { + let value = data.vbank().get(word.0)?; + if word.1 != offset || value.get_size() != 8 { + return None; + } + let op = data.obank().get(value.get_def()?)?; + let call = match op.code() { + OpCode::CPUI_CALL | OpCode::CPUI_CALLIND => value.get_def()?, + OpCode::CPUI_INDIRECT if op.is_indirect_creation() => { + let iop = data.vbank().get(op.get_in(1)?)?; + OpId::from(slotmap::KeyData::from_ffi(iop.get_offset())) + } + _ => return None, + }; + let spec = data.get_call_specs(data.get_call_specs_index(call)?); + if !wide_integer_call(data, spec) + || op.code() == OpCode::CPUI_INDIRECT && spec.proto().get_output().get_address() != *value.get_addr() + { + return None; + } + Some(word.0) +} + +fn wide_integer_call(data: &Funcdata, spec: &crate::fspec::FuncCallSpecs) -> bool { + spec.proto().is_output_locked() + && spec.proto().has_matching_model(data.get_func_proto().model()) + && spec.proto().get_output().get_type().is_some_and(|ty| { + ty.get_size() == 8 && matches!( + ty.get_metatype(), + crate::dtype::type_metatype::TYPE_INT | crate::dtype::type_metatype::TYPE_UINT + ) + }) +} + /// What the first register of a pair holds at one RETURN, when upstream's /// scoring refused it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/decompiler/crates/kuna-decomp/src/substrate/funcdata_block.rs b/decompiler/crates/kuna-decomp/src/substrate/funcdata_block.rs index 8c7721ec1e..bfa239d27b 100644 --- a/decompiler/crates/kuna-decomp/src/substrate/funcdata_block.rs +++ b/decompiler/crates/kuna-decomp/src/substrate/funcdata_block.rs @@ -2604,7 +2604,12 @@ impl Funcdata { let addr2 = partial.obank().get(partop).unwrap().get_addr().clone(); jt.set_indirect_op_addr(partop, addr2); let res = if jt.is_partial() { - jt.recover_multistage(partial) + let targets = self.get_override().multistage_targets(&addr).map(<[Address]>::to_vec); + jt.recover_multistage(partial).map(|()| { + if let Some(targets) = targets { + jt.kuna_adopt_multistage_targets(targets); + } + }) } else { jt.recover_addresses(partial) }; diff --git a/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs b/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs index d33041d029..afba7e9689 100644 --- a/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs +++ b/decompiler/crates/kuna-decomp/tests/catalog_bytecompat.rs @@ -76,14 +76,14 @@ fn fixture_has_no_current_field() { } #[test] -fn fixture_has_all_339_settables() { +fn fixture_has_all_340_settables() { // One `"option":` per settable row; the authoritative per-option list is // phases.toml settableTable (counts asserted in kuna_phases/tests.rs). - assert_eq!(FIXTURE.matches("\"option\": ").count(), 339); + assert_eq!(FIXTURE.matches("\"option\": ").count(), 340); // Every row carries the tier field appended after change_kind. - assert_eq!(FIXTURE.matches("\"tier\": ").count(), 339); + assert_eq!(FIXTURE.matches("\"tier\": ").count(), 340); // ... and the symptoms array appended after tier (C3). - assert_eq!(FIXTURE.matches("\"symptoms\": ").count(), 339); + assert_eq!(FIXTURE.matches("\"symptoms\": ").count(), 340); } #[test] diff --git a/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json b/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json index 69c1bfafe1..320ede63d9 100644 --- a/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json +++ b/decompiler/crates/kuna-decomp/tests/fixtures/phase_catalog.json @@ -216,6 +216,7 @@ {"option": "libcsigs", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-libcsigs", "summary": "Seed the MEASURED extension of the built-in libc prototype table: the 539 further signatures the 28-entry libproto table does not carry (exit, gettext, dcgettext, error, strerror, strdup, strcasecmp, close, open, read, write, stat, getenv, syslog, the FORTIFY __printf_chk/__fprintf_chk/__memcpy_chk family, __errno_location, __stack_chk_fail, the *at family, the SELinux, account, xattr, socket and pthread surfaces, ...). Two mechanical selection rules built it, both over the frozen decbench C corpus and neither one a judgement about which names matter: the first 206 were ranked by PLT call-site frequency (>= 100 call sites, or >= 3 of the 1587 cases where IDA scores a perfect type_match and kuna does not); the 333 that widened it are every name that is an UNDEFINED FUNC symbol in at least 3 of the corpus's 665 dynamically linked binaries and is not already carried. A name in either rule is admitted only if its platform declaration reduces WHOLE to the width-stable Ty vocabulary -- a slot with no honest spelling (off_t, time_t, long long, char) rejects the declaration rather than approximating it, which is why lseek, time, strtoll, mmap, signal and qsort are deliberately absent. Every signature is reduced by script from gcc -aux-info over the platform headers (_GNU_SOURCE + _FORTIFY_SOURCE=2), never written from memory. Applied ONLY to names the image imports and does not itself define, so a program's own function that happens to share a libc spelling is never retyped.", "use_when": "On (default) types the arguments of every common libc call, so a caller whose parameter only flows into one gets a concrete type (char *path) instead of the inferred unsigned long, and the callee's return type is known. Flip OFF when a binary links a private library that reuses libc spellings with different signatures and is NOT statically linked (the pass already skips any name the image defines), or to ablate this table's contribution to a type-recovery difference; off declares exactly the names the 28-entry base table carries and nothing more. It is NOT an ablation of everything a declared prototype does to a call site: the stack placeholder that a call carries until its stack space is heritaged is stripped unconditionally, not by this option, so a base-table name renders with exactly its declared arity in both arms. A declared arity is also an assertion about the CALL SITE, not only about the types: a name this table carries renders with exactly the arguments the signature states, where an unsignatured name renders with whatever argument recovery found. That is the point where a wrong row would show, so the derivation, the rejected set and the whole-corpus diff of the widening live in docs/features/libcwiden/.", "example": "option libcsigs off", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt library archives); IDA Pro's til type libraries; measured on decbench C corpus (1587 ida-perfect/kuna-imperfect type_match cases)", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a caller's parameter is unsigned long where it is only ever passed to a libc function that takes a char */int", "a libc call renders its string argument as a bare 0x... constant instead of a literal", "a wrapper around a libc function loses its return value or return type", "__printf_chk / __fprintf_chk arguments are shifted by the fortify flag", "an exit or __stack_chk_fail call shows phantom arguments"]}, {"option": "libctypes", "values": ["off", "opaque", "glibc"], "default": "opaque", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-libctypes", "summary": "Spell the libc/POSIX aggregate pointers in the built-in prototype tables by NAME instead of void *: FILE *, DIR *, stat *, dirent *, passwd *, group *, tm *, option *, timespec *, timeval *, sigaction *, sigset_t *, mbstate_t *, termios *, sockaddr *, pthread_mutex_t *, obstack *, spwd *, utmpx *, utmp *, re_pattern_buffer *, lconv *, statfs *. The Ty vocabulary of those tables is width-stable, so every aggregate pointer in them is a void * today - honest about the width and silent about the pointee, which is the one thing a declaration actually knows. The retarget is enumerated slot by slot (a blanket void *->FILE * would type the va_list of vasprintf/vsnprintf/__vfprintf_chk/verr as a stream), and it brings the stdio names neither shipped table carries: __uflow, fgetc, rewind, freopen, popen, pclose, getdelim, flockfile, funlockfile. A second round of slots was mined from the benchmark corpus's own debug twins - which pointer-to-named-struct variables ground truth holds, and which libc slot each could be reached from - and adds the rest of the stream surface (fseeko, ftello, fread_unlocked, fputc_unlocked, feof_unlocked, fgets_unlocked, setbuf, vfprintf, __getdelim), the record-at-a-time and reentrant halves of the three account databases (getpwent/fgetpwent/putpwent/getpwnam_r and their group and spwd twins), more tm/timespec/termios slots, the regex entry points, localeconv and statfs/fstatfs. Two kinds of slot are left out because naming them splits the caller's frame object: the two-element timespec/timeval arrays of utimensat/futimens/utimes/futimesat, and further sigset_t slots, which a caller hands &sa.sa_mask of a struct sigaction local. Those names are new to both shipped tables, so each supplies an ARITY where there was none as well as a pointee. One table is matched against a name the image DEFINES rather than imports: the five _obstack_* entry points, which are the implementation-reserved half of obstack.h and can only be glibc's or gnulib's copy of the same file. That is what makes obstack reachable at all - gnulib links its copy in and the linker exports the symbols, so a stripped grep, tar or coreutils image carries _obstack_newchunk in .dynsym and nothing else in it says what the first argument addresses; obstack is the widest aggregate in the measured ground truth after FILE. Each named type is interned as a shell carrying its real glibc x86-64 width (FILE 216, stat 144, dirent 280, ...), never width 0 - a zero-width pointee keeps a PTRSUB alive through RulePtrsubUndo and the printer then emits the functional PTRSUB(p,0x28) form - and is left INCOMPLETE so the exported .h declares it opaque rather than as a struct with a width and no members. On an image that carries debug info the platform's own definition is used instead, fields and all: the pass runs after the DWARF importer and adopts an aggregate of the declared width already held under the name (or under glibc's _IO_FILE spelling of FILE) - complete or still being populated - and declines the signature rather than touch a name held by anything else, a different width included. The third value, `glibc`, fills in the nine aggregates whose layout the platform PUBLISHES and an application is meant to read - FILE, stat, timespec, timeval, tm, passwd, group, option, dirent - with their real x86-64 members, so an access renders under its own name (`f->_IO_read_ptr`, `st->st_size`, `opt[i].name`) instead of `f->field_0x8`, and the loaded value takes the FIELD's type rather than whatever the cast said - `st_size` is a signed long, so a wrapper that returns -1 on failure stops printing its sentinel as 0xffffffffffffffff. Every offset and width is measured with offsetof/sizeof against the installed headers, not restated from memory (docs/features/libctypes/glibc.md). Two claims the tables refuse to make: no field is spelled as a pointer to the struct being filled (_IO_FILE::_chain is void * - completing a struct re-keys it into a new Rc, so a self-pointer taken mid-completion would strand the program on two stream types), and no layout is installed unless it is TRUE of the image - x86-64 ELF whose .dynstr names glibc (the libc.so.6 soname or a GLIBC_2.x symbol version). musl, another libc and a statically linked image all keep the opaque shells, which for a static image costs nothing because these tables are matched against imported names only. The WIDTHS are a claim too, and about the caller's frame, not only about offsets inside the shell: a named pointer handed a frame object's address grows that object to the declared width, which is right on x86-64 against glibc (`char v1 [12]` becomes `termios v1` where the program touched twelve bytes of the struct `tcgetattr` fills) and on i386 folded the caller's neighbouring locals into an 8-byte timespec declared as 16 (`v1._12_4_ = a0 + 2`). So nothing at all is named - no slot, no stream symbol, no shell - unless the image is an x86-64 ELF not dynamically linked against another C library (no .dynstr, or one naming glibc); i386, ARM, AArch64, MIPS, PE and Mach-O get the shipped void * tables, byte for byte what `off` gives, and a name declared there by hand does too. A named vote is also held to what the caller does through the pointer. A libc aggregate is often the FIRST member of a larger object (grep's kwset begins with its obstack, an rsyslog instance record with its mutex), whose address is then the one the call reads; where the caller reads that argument past the aggregate's end or before its start, forms an address at or past the end, or steps it by a constant at least as large as the aggregate that is not exactly its size (coreutils wc walks &fstatus[i].st and reads `failed` 8 bytes before it), that call's vote falls back to void * (`kuna_libcfit`) and the declared type decides only the call's cast; type propagation likewise refuses to carry the named pointer onto a value read past the end, so `kwsincr` stops printing every kwset member as an element of an obstack array (`&v1[0x1b].field_0x38`). DIR, sigaction, sigset_t, mbstate_t, termios, sockaddr and pthread_mutex_t stay opaque under `glibc` too: glibc publishes no layout for DIR, and the rest are reserved words no caller reads by name. The same tables also type the stdio STREAMS themselves, which no prototype slot can reach: `stdin`/`stdout`/`stderr` are storage, not an argument, so a function that never calls a typed stdio function sees only an `undefined8` word, and on a shared object the slot has no name at all. Under any enabled value the pass emits a typed DATA symbol for each stream slot, read off the DYNAMIC RELOCATION that binds it - which is also the evidence that the name is the C library's and not a global the program spells `stdout` itself. A COPY relocation names a .bss word the run-time loader fills with libc's own `FILE *stdout`, so that slot is `FILE *` under the stream's own name; a GLOB_DAT on an UNDEFINED symbol names a GOT word holding the stream's ADDRESS, so it is `FILE **` under the minted name `stdout_ptr` - calling that one `stdout` would make the emitted `*stdout_ptr` read as an indirection the program does not perform. Because `stdout_ptr` is kuna's coinage and not the image's, the GLOB_DAT arm declines when either symbol table (.symtab as much as .dynsym) already spells it, rather than printing two addresses under one identifier. Linked ELF only, under the same width gate as the rest of the table, and a spelling that occurs twice in .dynsym is declined outright. The slots point at the same `FILE` the rest of the table uses, so `opaque` gives `stdout->field_0x28` and `glibc` `stdout->_IO_write_ptr`, and an image whose own debug info holds a different `FILE` contributes no stream symbol rather than a contradictory one.", "use_when": "On by default (opaque): a pointer the callee's own libc declaration names carries that name - get_prefix(FILE *f) instead of sub_2f30(long a0) on a stripped -O2 coreutils reader, fopen/fdopen/popen results as FILE *, stat/lstat/fstat filling a stat *, getopt_long reading an option * table, readdir returning a dirent *, localtime a tm *, an obstack handle as obstack * even where gnulib linked the obstack code into the program. Flip OFF to ablate the table: to check whether a pointee name came from this declaration rather than from inference, when a target statically links a private function under a libc spelling that the imports-only restriction did not catch (it would then be typed against libc's aggregate, not its own), or when a decompile-project export has to stay as close to compilable as it gets - the shells are deliberately INCOMPLETE, so the exported .c reads fields out of a type its own .h declares opaque (measured +58 cc errors on an ls export - 51 of them from reading fields out of the incomplete shells, the rest from a type name that collides with a function name of its own in the exported body; the .h itself is 0 errors either way). Off is byte-identical to the shipped void * tables, because with the gate off the pass does not run and not one named shell is interned. No datatest loads a file, so the 675 assertions cannot see this row either way; the stage corpus can, and tests/stages/kuna-libctypes.xml pins it in both arms. The flip's evidence is the corpus sweep in docs/features/libctypes/record.json. Flip to `glibc` for the field NAMES: on an x86-64 ELF against glibc it installs the published layouts of FILE/stat/timespec/timeval/tm/passwd/group/option/dirent, turning `*(unsigned char **)&f->field_0x8` into `f->_IO_read_ptr` and `*(unsigned long *)&st->field_0x30` into `st->st_size`, and giving the loaded value the field's type. Prefer it when reading stdio-heavy or stat-heavy code; stay on `opaque` when a decompile-project export must not declare a type the toolchain's own headers also declare. On a target that is not x86-64 against glibc every value is inert: no slot is named at all, because the widths are not that ABI's. The default also NAMES and TYPES the stream slots: `*(long *)(stdout + 0x28)` becomes `*(long *)&stdout->field_0x28` (`stdout->_IO_write_ptr` under `glibc`), and a shared object's `*(long *)(*dat_3fe0 + 0x28)` becomes `*(long *)&(*stdout_ptr)->field_0x28`. That reach is the function that makes no typed stdio call and the shared object whose stream never had a name - over twelve whole binaries 45 lines move, 38 of them a GOT slot gaining its name, and eight of the twelve are byte-identical because inference had already typed those streams from a typed stdio call in the same function. Its cost is local merging: a slot that takes `FILE *` stops merging with the unrelated values a scalar local had absorbed, which adds a declaration and renumbers the locals after it. Naming a data object stays `datasyms`' call - with that option off the stream slot goes back to `dat_`, since the type rides on the symbol - so ablate the stream half with `datasyms off` and the whole table with `libctypes off`.", "example": "option libctypes off", "source_decompiler": "ghidra", "inspiration": "Ghidra ApplyDataArchiveAnalyzer applying generic_clib_64.gdt, whose FILE */struct stat * slots carry the header's own pointee; signatures reduced from gcc -aux-info over the platform headers (_GNU_SOURCE + _FORTIFY_SOURCE=2), widths from sizeof/_Alignof", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["a function that only ever passes its argument to a libc stdio call still declares it long or unsigned long", "a fopen or fdopen result is a void * that nothing can name", "a stat or lstat call fills an untyped void * buffer", "getopt_long takes a bare (void *)0x... long-option table", "readdir returns a void * whose fields are casts and offsets", "a stream parameter is void * where the ground truth is FILE *", "a FILE * or stat * renders its members as field_0x8 / field_0x30 instead of _IO_read_ptr / st_size", "a value loaded out of a named libc struct keeps the cast's type instead of the field's", "stdin/stdout/stderr render as an untyped word and their fields as raw offsets ((long)stdout + 0x28)", "a shared object's stream slot has no name at all (*(long *)(*dat_3fe0 + 0x28))", "two functions in one binary disagree about what stdout is, one calling it FILE * and the other a long"]}, {"option": "win32sigs", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "repipe-resource-loader-c-retains", "summary": "Seed built-in Win32 API prototypes onto a PE's imported API names, keyed by ENTRY ADDRESS. kuna carried libc signatures (libproto, libcsigs) and not one Windows API signature, so on a PE every LoadLibraryExW/CreateFileW/WriteFile call reached ActionDefaultParams with an empty prototype and its arguments had to be recovered from the call site alone; where that recovery loses -- the ordinary case for an image that writes its outgoing slots well before the call -- the call renders `LoadLibraryExW()` and the argument stores survive as mapped stack locals, along with the return-address push the call itself makes. The table is ~130 WINAPI exports ranked by an import histogram over the 54 PE images of the RE arena corpus (admitted at >= 5 images), plus the resource/loader family (FindResourceW, LoadResource, SizeofResource, EnumResourceTypesW/NamesW) below that bar. Each signature is reduced to the width-stable Ty vocabulary and a declaration with a slot that has no honest spelling (a by-value LARGE_INTEGER, a DWORD64) is rejected rather than approximated. Address is the key because a PE import is TWO FunctionSymbols -- the size-0 IAT slot the engine constant-folds through and the `FF 25` thunk veneer a direct `call` targets -- and the global by-name query used by libproto answers with the slot, which is not the address ActionDefaultParams asks about. Imported names only, so a program's own function sharing a Win32 spelling is never retyped; no CRT (__cdecl) spelling is admitted, because on x86 the PE default model is __stdcall and a locked arity is also the callee's stack pop.", "use_when": "On by default (DIV-141). On, a Windows PE renders `LoadLibraryExW(v1,0,8)` / `WriteFile(h,buf,n,&written,0)` instead of `LoadLibraryExW()` with the pushed values left behind as `v41 = 0x401baf;`-style stack stores. Flip OFF to ablate the table's contribution -- to check whether a Win32 call's arguments came from the parked signature rather than from recovery, or if a target statically links its own function under a Win32 spelling that the imports-only restriction did not catch. Every non-PE target is byte-identical either way.", "example": "option win32sigs off", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt library archives); IDA Pro's til type libraries; import histogram over the 54 PE images of the RE arena corpus", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a Windows API call renders with an empty argument list", "LoadLibraryExW() or CreateFileW() has no arguments but the pushes are visible above it", "a PE function declares dozens of stack locals that only ever hold outgoing call arguments", "CALL fall-through addresses are assigned to stack variables", "a Win32 call's wide string argument renders as a bare 0x... constant", "calleearity and varargstackargs both on and a Windows API call is still argumentless"]}, + {"option": "libgccsigs", "values": ["on", "off"], "default": "off", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "pr-1252-libgcc-divdi3", "summary": "Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract.", "use_when": "Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal and nothing else reads the quotient (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 14 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, a clamp (`if (x > 100) x = 100;`), and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly.", "example": "option libgccsigs on", "source_decompiler": "kuna", "inspiration": "libgcc/compiler-rt `__divdi3` (di_int __divdi3(di_int a, di_int b), di_int = int64_t); Ghidra ApplyDataArchiveAnalyzer", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns", "__divdi3 is called with four 32-bit arguments and only %o0 is read back", "*p = v1; after a __divdi3 call where v1 is never written"]}, {"option": "declaredlibcproto", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "external-refinement", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-declaredlibcproto", "summary": "Answer a DECLARED function name out of the built-in libc signature tables, so naming a callee also gives it a prototype. `libproto` and `libcsigs` match a name the IMAGE carries -- its FUNC symbols, its imports -- which is exactly the evidence a stripped, statically linked target does not have. There the name exists only because the operator supplied it (`--define-function 0x8048968=ptrace`, `--assert 'function 0x8048968 ptrace'`), and until now that named the entry and nothing more: the call sites still rendered `ptrace()` with no arguments, because the callee's arity has to come from somewhere and a glibc varargs wrapper (`long ptrace(enum __ptrace_request, ...)`, the rest fetched with `va_arg`) cannot yield it to any amount of body analysis. With this on, a declared name is looked up in both tables and the matching signature is parked on the DECLARED ENTRY ADDRESS -- the key `ActionDefaultParams` reads back per call site, and the only key that survives two symbols sharing a name. The imports-only restriction the `libcsigs` table carries is deliberately lifted here: it exists so a coincidental spelling cannot retype a function the image defines itself, and that is a judgement about evidence, which is different once a human or an agent has identified the entry outright.", "use_when": "On by default: naming a callee is the one thing an RE agent always does, and a name that buys no prototype buys almost nothing. The shape that needs it is a stripped or packed image where argument recovery has nothing to work from -- the tell is a call rendered `f()` with the pushed argument slots visible as raw stores on the lines above it (`*(int *)(esp - 4) = ...` four times, then `v += ptrace();`). Structurally inert unless a run declares a function name (`--define-function START=NAME`, `--assert function`, console `function bounds ... as NAME`), so it is byte-identical over both parity corpora, and inert again unless that name is one of the ~230 the built-in tables carry. Flip OFF when a target defines its own function that merely shares a libc spelling and the declaration is about that function, not the library one -- or to check whether the parked signature, rather than recovery, is what moved a call's arguments. An explicit `--assert prototype` on the same function still wins: it is applied after the declaration.", "example": "kuna decompile ./crackme sub_80483b0 --define-function 0x8048968=ptrace", "source_decompiler": "kuna", "inspiration": "Ghidra ApplyDataArchiveAnalyzer (.gdt archives applied on rename); IDA Pro's til type libraries, which retype a function the moment it is given a library name", "change_kind": "correctness-fix", "tier": "analysis", "symptoms": ["a call to a function I named with --define-function still renders with no arguments", "the argument stores are emitted as raw stack writes on the lines above an argumentless call", "naming a callee changes the spelling of the call and nothing else", "calleearity and varargstackargs both on and a stripped statically linked libc call is still argumentless", "a declared ptrace/read/write/open call loses every argument"]}, {"option": "strings", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "code-data-partition", "strength": "HARD", "rewind": "P1", "issue": "kuna-analysis-strings", "summary": "Run the string-literal analysis pass: detect NUL-terminated ASCII strings (minimum length 4) and plant a typelocked char[N] data symbol at each. The four-character threshold keeps command and mode literals such as quit, show, bind, fork, and dup2 visible while the separate UTF-16 arm retains Ghidra's length-5 default.", "use_when": "On (default) lays char[N] data at detected strings; off leaves those addresses undefined.", "example": "option strings off", "source_decompiler": "kuna", "inspiration": "Ghidra StringsAnalyzer; pwn.college CSCI 4250 module 5 game servers, where four-character commands and libc operation names rendered as addresses", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["string constants render as raw addresses or unnamed data instead of quoted char[N] literals", "no data symbols at ascii runs in rodata", "four-character commands such as quit, show, bind, or fork render as hexadecimal addresses"]}, {"option": "widestrings", "values": ["on", "off"], "default": "on", "destructive_as_default": false, "phase": "P1", "subphase": "code-data-partition", "strength": "HARD", "rewind": "P1", "issue": "re-wide-api-string-args", "summary": "Scan the loaded image at 2-byte (UTF-16LE) width as well as 1-byte, and plant a typelocked wchar2[N] data symbol at each wide literal -- the allCharWidths arm of Ghidra's StringsAnalyzer that kuna's 1-byte port left as a documented seam. Read at 1-byte width a UTF-16LE literal is a ONE-CHARACTER string (`L\"ntdll.dll\"` is 6e 00 74 00 ..., whose first NUL closes the run after `n`), so nothing is marked up there, the constant keeps whatever char * the type lattice gave it, and the printer reads to the same NUL: every wide Windows-API argument rendered as its own first character -- LoadLibraryW(\"n\"), FindWindowW(\"O\",0). The widened matcher is the 1-byte MinLengthCharSequenceMatcher over 2-byte little-endian code units and nothing else: the same printable-ASCII recognizer over each unit's low byte, the same require-NUL-end rule, the same minimum length of 5, over exactly the address set the 1-byte pass scans, on even addresses only. The two widths cannot both claim a run -- a wide unit demands a zero high byte, so five consecutive 1-byte-charset bytes never occur inside a wide run -- and the 1-byte facts are committed first regardless, so the established width owns any address both could reach. With the symbol planted the existing printer path does the rest: the char type's size 2 is what emits the L prefix and reads the bytes two at a time.", "use_when": "On by default. A wide (UTF-16) string argument renders as a one-character literal -- LoadLibraryW(\"n\") where the binary says L\"ntdll.dll\", FindWindowW(\"O\",0) where it says L\"OllyDbg - [CPU]\" -- which is most of what a Windows anti-debugging or anti-VM check contains. `kuna strings --encoding utf16` already reported those literals in full while the decompiled C did not, and the difference between the two surfaces is the tell. Flip OFF to restore markup that is exactly the 1-byte pass's -- e.g. on an image whose 16-bit data tables read as plausible wide text, or to ablate this width's contribution.", "example": "option widestrings off", "source_decompiler": "ghidra-upstream", "inspiration": "Ghidra StringsAnalyzer's allCharWidths setting (the UTF-16 arm of MinLengthCharSequenceMatcher); measured on crackmes.one 65acadf3eef082e477ff5ede (ObfuscationFiesta.exe, PE32+), where sub_140002530's LoadLibraryW / FindWindowW anti-debug checks rendered as one-character strings", "change_kind": "analysis-enablement", "tier": "analysis", "symptoms": ["a wide Windows API argument renders as a one-character string literal", "LoadLibraryW(\"n\") or FindWindowW(\"O\",0) in the emitted C", "kuna strings --encoding utf16 reports a literal the decompiled C truncates to its first character", "a UTF-16 string address carries no data symbol while the ascii ones do", "an anti-debugging check names no DLL or window class"]}, diff --git a/decompiler/crates/kuna-slacomp/tests/golden/README.md b/decompiler/crates/kuna-slacomp/tests/golden/README.md index 83809f03d4..02c734f0dc 100644 --- a/decompiler/crates/kuna-slacomp/tests/golden/README.md +++ b/decompiler/crates/kuna-slacomp/tests/golden/README.md @@ -19,7 +19,11 @@ The SPARC entries include kuna's destination corrections for `fdtoi`, `fqtoi`, `fstox` and `fqtox`. The first three were regenerated with Ghidra's C++ compiler on the corrected sources, which reproduces the previous hashes on the unmodified sources; the `fqtox` correction was regenerated with `slacomp`, -which reproduces those C++ digests. +which reproduces those C++ digests. The current digests also cover the branch +conditions sampled before a delay slot (`icc`/`xcc` `cs`, `neg` and `vs`, and +the non-annulled register branches). They were regenerated with the recorded +C++ compiler, which reproduces the previous SPARC digests on the previous +sources; `slacomp` produces the same digests. The x86 entries include the width-specific x87 integer-conversion operations, the control-word writes of `FXRSTOR` and `XRSTOR`/`XRSTORS`, and the signed diff --git a/decompiler/crates/kuna-slacomp/tests/golden/compiler.sha256 b/decompiler/crates/kuna-slacomp/tests/golden/compiler.sha256 index fa2a8a0174..8fa385ccb0 100644 --- a/decompiler/crates/kuna-slacomp/tests/golden/compiler.sha256 +++ b/decompiler/crates/kuna-slacomp/tests/golden/compiler.sha256 @@ -25,9 +25,9 @@ db22e1cd2be9d4c96c1fd32a93d87e562cca50faabe5807463e1cdeca55b83b9 specs/Ghidra/P ecea205201328f704b229f7ac810f800fb9c8805f5de912fc308dbb4d35334ab specs/Ghidra/Processors/MC6800/data/languages/H6309.slaspec d4e2cfa83ada5e9e19abcecb5fb360adf45c4b5b7b15761c1dec16d72540e82e specs/Ghidra/Processors/NDS32/data/languages/nds32be.slaspec 66abba4eb5f35b3e85687c43c5dd6202cc2bce04959524c49aca52a2ac2668ec specs/Ghidra/Processors/NDS32/data/languages/nds32le.slaspec -# SPARC fdtoi/fqtoi/fstox/fqtox destination corrections. -e33bf3dd3d7cd90f20461915f51981e757af41e177ae9178ce95f05e865999db specs/Ghidra/Processors/Sparc/data/languages/SparcV9_32.slaspec -de8de7baf9348c29f6c2ca3dcfe07266a6651d8b1398ec646c1f9872b2a73107 specs/Ghidra/Processors/Sparc/data/languages/SparcV9_64.slaspec +# SPARC fdtoi/fqtoi/fstox/fqtox destination corrections and delay-slot branch-condition snapshots. +087e1846df849cbe332f44093bed30b1e78a66db75c085b1ff9d3c989108390a specs/Ghidra/Processors/Sparc/data/languages/SparcV9_32.slaspec +a724b2694d40000b078e3824f24c02d7a7b8d558bc1a5064b254bc5a5030a51a specs/Ghidra/Processors/Sparc/data/languages/SparcV9_64.slaspec 21f6f960e061de299c8266872af3ec621f539fed8b748f5137128b19d8e87e28 specs/Ghidra/Processors/SuperH/data/languages/sh-1.slaspec ec462b829a1469bb1c7e4a4d54dea350c9642290de5bf5fa5476bd0233204d40 specs/Ghidra/Processors/SuperH4/data/languages/SuperH4_be.slaspec a1aa90d64bbaa0aac2e8208c4e31e4e295b2d997b1973b6c6100704f87d3c4b0 specs/Ghidra/Processors/SuperH4/data/languages/SuperH4_le.slaspec diff --git a/decompiler/crates/kuna-sleigh/tests/integration.rs b/decompiler/crates/kuna-sleigh/tests/integration.rs index 5dd2ce53a3..9c1359f009 100644 --- a/decompiler/crates/kuna-sleigh/tests/integration.rs +++ b/decompiler/crates/kuna-sleigh/tests/integration.rs @@ -29,6 +29,9 @@ mod ppc_ldx_ldarx_encoding; #[path = "ppc_nested_operands.rs"] mod ppc_nested_operands; +#[path = "sparc_delay_slot_condition.rs"] +mod sparc_delay_slot_condition; + #[path = "sparc_float_to_integer.rs"] mod sparc_float_to_integer; diff --git a/decompiler/crates/kuna-sleigh/tests/sparc_delay_slot_condition.rs b/decompiler/crates/kuna-sleigh/tests/sparc_delay_slot_condition.rs new file mode 100644 index 0000000000..b72ff6471b --- /dev/null +++ b/decompiler/crates/kuna-sleigh/tests/sparc_delay_slot_condition.rs @@ -0,0 +1,187 @@ +//! SPARC branches sample their condition before a delay slot that rewrites it. +use std::{cell::RefCell, path::PathBuf, rc::Rc}; + +use kuna_base::{address::Address, error::KunaResult, space::AddrSpace}; +use kuna_sleigh::{ + emulate::{BreakTableCallBack, Emulate, EmulatePcodeCache}, + globalcontext::ContextInternal, + loadimage::LoadImage, + memstate::{MemoryHashOverlay, MemoryState}, + sleigh::Sleigh, + translate::Translate, +}; + +struct Bytes(Vec); +impl LoadImage for Bytes { + fn get_file_name(&self) -> &str { + "synthetic-sparc-delay-condition" + } + fn get_arch_type(&self) -> Vec { + Vec::new() + } + fn adjust_vma(&mut self, _: i64) {} + fn load_fill(&mut self, out: &mut [u8], addr: &Address) -> KunaResult<()> { + for (i, byte) in out.iter_mut().enumerate() { + *byte = addr + .get_offset() + .checked_sub(0x1000) + .and_then(|offset| self.0.get(offset as usize + i)) + .copied() + .unwrap_or(0); + } + Ok(()) + } +} + +struct Machine { + emulator: EmulatePcodeCache, + state: Rc>, + ram: Rc, +} + +impl Machine { + fn new(bits: u32, branch: u32, slot: u32) -> Self { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.."); + let sla = std::fs::read(root.join(format!( + "specs/Ghidra/Processors/Sparc/data/languages/SparcV9_{bits}.sla" + ))) + .unwrap(); + let bytes: Vec<_> = [branch, slot, 0x01000000, 0x01000000, 0x01000000] + .into_iter() + .flat_map(u32::to_be_bytes) + .collect(); + let mut sleigh = Sleigh::new(Box::new(Bytes(bytes)), Box::new(ContextInternal::new())); + sleigh.initialize_from_sla(&sla).unwrap(); + let manager = sleigh.base().manager_rc(); + let ram = Rc::clone(manager.get_space_by_name("ram").unwrap()); + let trans: Rc = Rc::new(sleigh); + let mut state = MemoryState::new(Rc::clone(&trans)); + for name in ["ram", "register", "unique"] { + let space = Rc::clone(manager.get_space_by_name(name).unwrap()); + state.set_memory_bank(Rc::new(RefCell::new(MemoryHashOverlay::new( + space, 8, 64, 64, None, + )))); + } + let state = Rc::new(RefCell::new(state)); + let breaks = Rc::new(RefCell::new(BreakTableCallBack::new(Rc::clone(&trans)))); + let emulator = EmulatePcodeCache::new(trans, manager, Rc::clone(&state), breaks); + Machine { + emulator, + state, + ram, + } + } + + fn step(&mut self, registers: &[(&str, u64)]) -> u64 { + { + let mut state = self.state.borrow_mut(); + for (name, value) in registers { + state.set_value_by_name(name, *value).unwrap(); + } + } + self.emulator + .set_execute_address(&Address::new(Rc::clone(&self.ram), 0x1000)) + .unwrap(); + self.emulator.execute_instruction().unwrap(); + self.emulator.get_execute_address().get_offset() + } + + fn read(&self, name: &str) -> u64 { + self.state.borrow().get_value_by_name(name).unwrap() + } +} + +fn check_branch(bits: u32, predicted: bool, extended: bool, annul: bool, cond: u32, flag: &str) { + let word = (u32::from(annul) << 29) + | (cond << 25) + | (if predicted { 1 << 22 } else { 2 << 22 }) + | (if extended { 2 << 20 } else { 0 }) + | 4; + let mut machine = Machine::new(bits, word, 0x80a04002); + for incoming in [0, 1] { + for slot_flag in [0, 1] { + let (left, right) = if slot_flag == 0 { + (0, 0) + } else if cond == 7 { + (if extended { 1u64 << 63 } else { 1u64 << 31 }, 1) + } else { + (0, 1) + }; + let mut registers: Vec<(&str, u64)> = ["i_cf", "i_nf", "i_vf", "x_cf", "x_nf", "x_vf"] + .into_iter() + .map(|name| (name, 0)) + .collect(); + registers.extend([(flag, incoming), ("g1", left), ("g2", right)]); + let next = machine.step(®isters); + let context = format!("SPARC{bits} predicted={predicted} extended={extended} annul={annul} {flag}={incoming} slot={slot_flag}"); + assert_eq!( + next, + if incoming == 1 { 0x1010 } else { 0x1008 }, + "{context}" + ); + assert_eq!( + machine.read(flag), + if annul && incoming == 0 { + incoming + } else { + slot_flag + }, + "{context}" + ); + } + } +} + +#[test] +fn integer_branches_use_incoming_flags_and_publish_delay_slot_flags() { + for bits in [32, 64] { + for annul in [false, true] { + for (cond, flag) in [(5, "cf"), (6, "nf"), (7, "vf")] { + check_branch(bits, false, false, annul, cond, &format!("i_{flag}")); + check_branch(bits, true, false, annul, cond, &format!("i_{flag}")); + if bits == 64 { + check_branch(bits, true, true, annul, cond, &format!("x_{flag}")); + } + } + } + } +} + +#[test] +fn register_branches_test_the_register_before_the_delay_slot_rewrites_it() { + let rconds: [(u32, fn(i64) -> bool); 6] = [ + (1, |v| v == 0), + (2, |v| v <= 0), + (3, |v| v < 0), + (5, |v| v != 0), + (6, |v| v > 0), + (7, |v| v >= 0), + ]; + for bits in [32u32, 64] { + let mask = if bits == 32 { + u64::from(u32::MAX) + } else { + u64::MAX + }; + for annul in [false, true] { + for (rcond, holds) in rconds { + for slot in [-1i64, 0, 1] { + let branch = + (u32::from(annul) << 29) | (rcond << 25) | (3 << 22) | (1 << 14) | 4; + let mov_g1 = 0x82102000 | (slot as u32 & 0x1fff); + let mut machine = Machine::new(bits, branch, mov_g1); + for incoming in [-1i64, 0, 1] { + let taken = holds(incoming); + let next = machine.step(&[("g1", incoming as u64 & mask)]); + let context = format!( + "SPARC{bits} rcond={rcond} annul={annul} g1={incoming} slot={slot}" + ); + assert_eq!(next, if taken { 0x1010 } else { 0x1008 }, "{context}"); + let g1 = if annul && !taken { incoming } else { slot }; + assert_eq!(machine.read("g1"), g1 as u64 & mask, "{context}"); + } + } + } + } + } +} diff --git a/docs/baseline-stages.json b/docs/baseline-stages.json index f1c4ee0b65..7c710f808d 100644 --- a/docs/baseline-stages.json +++ b/docs/baseline-stages.json @@ -1,7 +1,7 @@ { "data_footer": [ - 3228, - 3228 + 3233, + 3233 ], "passing": [ "data:64-bit logical shift retains unsigned destination", @@ -973,7 +973,7 @@ "data:KUNA-CATALOG #12: scalar ARM inputs are a default-off opt-in", "data:KUNA-CATALOG #13: validated A32 inventory frames are the default", "data:KUNA-CATALOG #14: full ARM frames are explicitly enabled", - "data:KUNA-CATALOG #15: 339 registry rows plus six individual queries", + "data:KUNA-CATALOG #15: 340 registry rows plus six individual queries", "data:KUNA-CATALOG #16: standard C byte-array rendering is explicitly opt-in", "data:KUNA-CATALOG #17: standard C byte-array rendering exposes its live state", "data:KUNA-CATALOG #18: bounded local views are default-on and live", @@ -1588,6 +1588,11 @@ "data:SIGNEDREAD #6: the unsigned compare keeps its unsigned operand", "data:SIGNEDREAD-A64 #1: the word index sign-extends sig - 1", "data:SIGNEDREAD-A64 #2: no zero extension of sig - 1", + "data:SPARC delay condition: current-value comparison remains live", + "data:SPARC delay condition: guarded clear remains a byte store", + "data:SPARC delay condition: previous-value comparison remains live", + "data:SPARC delay condition: register branch keeps its nonzero path", + "data:SPARC delay condition: register branch keeps its zero path", "data:SPARC-EXTERNAL-TAIL #1: off still follows the jump into missing extern bytes", "data:SPARC-EXTERNAL-TAIL #2: default recovers the named tail call and delay-slot argument", "data:SPARC-REGISTER-PAIR #1: the loop index in %l4 steps by itself at the default", diff --git a/docs/modes.md b/docs/modes.md index 78e6e4ae7a..7761aa96b8 100644 --- a/docs/modes.md +++ b/docs/modes.md @@ -196,9 +196,16 @@ not, and the invariant test's `EXCLUDED_ON_PURPOSE` list is the authority: the preset would make a near-neutral judgment call the default output under 500 KiB. +- **`libgccsigs`** — the SPARC32 `long long __divdi3(long long, long long)` + contract fixes a caller that stores only the quotient's low word, but a caller + that returns arithmetic on the quotient (`a / b + 1`, `-(a / b)`, + `f ? a / b : c`) then keeps only the high word or returns nothing, where the + default returns every word. + These therefore stay manual per-run opt-ins (`--option v850indirectbranch on`, `--option dwarf_lines on`, `--option formatstring full`, `--option ifuncfpret on`, -`--option msvcstrappend on`, `--option bejoin on`, `--option regsigned on`) +`--option msvcstrappend on`, `--option bejoin on`, `--option regsigned on`, +`--option libgccsigs on`) even under `--mode aggressive`; a named `--option` still wins over the preset by last-write precedence. For `formatstring` only the `full` loop above is excluded: its `static` default reads the same format constants out of the image at load, so diff --git a/docs/options.md b/docs/options.md index b2b288ec60..b3ceefba42 100644 --- a/docs/options.md +++ b/docs/options.md @@ -509,6 +509,9 @@ Three tiers: | CALL fall-through addresses are assigned to stack variables | [`win32sigs`](#win32sigs) | | a Win32 call's wide string argument renders as a bare 0x... constant | [`win32sigs`](#win32sigs) | | calleearity and varargstackargs both on and a Windows API call is still argumentless | [`win32sigs`](#win32sigs) | +| a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns | [`libgccsigs`](#libgccsigs) | +| __divdi3 is called with four 32-bit arguments and only %o0 is read back | [`libgccsigs`](#libgccsigs) | +| *p = v1; after a __divdi3 call where v1 is never written | [`libgccsigs`](#libgccsigs) | | a call to a function I named with --define-function still renders with no arguments | [`declaredlibcproto`](#declaredlibcproto) | | the argument stores are emitted as raw stack writes on the lines above an argumentless call | [`declaredlibcproto`](#declaredlibcproto) | | naming a callee changes the spelling of the call and nothing else | [`declaredlibcproto`](#declaredlibcproto) | @@ -2411,6 +2414,14 @@ Program-prep enablement: what is discovered, decoded, and named before any funct - **Where / provenance:** P1/external-refinement · kuna · correctness-fix · repipe-resource-loader-c-retains - **Example:** `option win32sigs off` +### `libgccsigs` -- on | off, default `off` + +- **Symptoms:** a SPARC32 caller stores the low word of a 64-bit quotient from a local nothing assigns; __divdi3 is called with four 32-bit arguments and only %o0 is read back; *p = v1; after a __divdi3 call where v1 is never written. +- **What it does:** Seed the libgcc DI-mode contract `long long __divdi3(long long, long long)` onto a SPARC32 big-endian ELF (`sparc:BE:32:default:default`) that names `__divdi3`, defined or imported. The helper's widths come from GCC machine modes, not the C data model: it takes two eight-byte operands in %o0:%o1 and %o2:%o3 and returns the eight-byte quotient in %o0:%o1, high word first. Without the contract the caller sees an unknown callee that writes only %o0, so a caller that stores only the low word (`*p = (int)(a / b);`, the `st %o1` after the call) stores a local nothing assigns. With it the call prints `__divdi3(CONCAT44(a0,a1),CONCAT44(a2,a3))` and the stored word is `(int)__divdi3(..)`. A DWARF or explicit `--assert prototype` declaration keeps precedence, a stripped image names no helper and is unchanged, and every other target and helper (`__moddi3`, `__udivdi3`, `__umoddi3`) keeps its inferred contract. +- **When to flip:** Off by default: the locked eight-byte result still costs a caller that RETURNS arithmetic on the quotient. Return recovery keeps a caller's two return words only when each is a bare slice of the call result or a literal and nothing else reads the quotient (`return a / b;`, a quotient kept across a call, a guarded `return -1`), so `return a / b + 1;` prints `int inc(..)` with only the high word, `return -(a / b);` prints `int neg(..)`, and `return f ? a / b : c;` prints `void sel(..)`, dropping both values; with the option off all three return every word (in the first-register-low join `bejoin` leaves without evidence). Of 21 authored caller shapes at clang -O2, 14 lose words that way: +1, +c, negation, abs, `>> 3`, an unsigned `>> 32`, ^c, |c, *c, a sum of two quotients, an accumulation loop, a quotient an argument replaces on one path, a clamp (`if (x > 100) x = 100;`), and the `sel` phi. Turn on for a SPARC32 binary whose callers store or pass the quotient (`*p = (int)(a / b);`, `g(a / b)`) or return it unchanged, where the off output reads an unassigned local; check any `int` or `void` caller that should return a `long long` against the disassembly. +- **Where / provenance:** P1/external-refinement · kuna · correctness-fix · pr-1252-libgcc-divdi3 +- **Example:** `option libgccsigs on` + ### `declaredlibcproto` -- on | off, default `on` - **Symptoms:** a call to a function I named with --define-function still renders with no arguments; the argument stores are emitted as raw stack writes on the lines above an argumentless call; naming a callee changes the spelling of the call and nothing else; calleearity and varargstackargs both on and a stripped statically linked libc call is still argumentless; a declared ptrace/read/write/open call loses every argument. diff --git a/docs/spec/00-overview.md b/docs/spec/00-overview.md index 9cf1ea2124..9ec15b2c6a 100644 --- a/docs/spec/00-overview.md +++ b/docs/spec/00-overview.md @@ -3484,15 +3484,14 @@ study summarized in `docs/history.md`; every row below is re-verified against th | (kuna/angr) P7/P8 structuring fallback | degraded re-run | the region structurer cannot collapse the graph to a single root | nothing; `sblocks` is re-seeded clean | `decompiler/crates/kuna-decomp/src/p8_structure/blockaction.rs (ActionBlockStructure)` falls back to `CollapseStructure` after `decompiler/crates/kuna-decomp/src/p8_structure/region_structurer.rs (run_region_structurer)` declines | | P0 → everything, the outer loop | knowledge-store re-run | an operator/agent writes an assertion (`option`, `kassert`, override) and re-decompiles | the entire P0 store | `decompiler/crates/kuna-decomp/src/p0_knowledge/kuna_assert.rs (Dispatch)`; the console rebuilds the IR per `decompile`, re-seeding stashed facts — `decompiler/crates/kuna-decomp/src/infra/decompile_drive.rs (decompile_func_full_with_override_dyn)` (§0.8 is when the rebuild is skipped) | -**Not implemented in kuna** (theory-only, kept for the record): the upstream -jump-table *size-mismatch* restart — `matchModel` finding the recovered model's -size differs from the flow-recovered address table would persist -`Override::insertMultistageJump` and restart. In kuna the mismatch keeps the -flow-recovered addresses and does not restart -(`decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs (JumpTable::match_model)`, -a documented stub); the Override store already carries the hint surface -(`decompiler/crates/kuna-decomp/src/p0_knowledge/overrides.rs -(Override::insert_multistage_jump)`) with no live producer. +**Partly implemented in kuna**: the upstream jump-table *size-mismatch* +restart. When `matchModel` finds a flow-time table of one address whose late +model has more entries, kuna persists `Override::insert_multistage_jump` (with +the late model's targets) and restarts, at most once per jump +(`decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs (JumpTable::request_multistage)`; +§2.3 *Multistage recovery* has the guard and reach checks). Every other size +mismatch keeps the flow-recovered addresses and does not restart +(`decompiler/crates/kuna-decomp/src/p2_lift/jumptable.rs (JumpTable::match_model)`). Mechanisms are mutually disabling by design: no restart, and no dead-code-delay bump, fires inside the jump-table sub-decompilation — the sub-query must answer diff --git a/docs/spec/01-program-prep.md b/docs/spec/01-program-prep.md index c05cc77443..9889d88629 100644 --- a/docs/spec/01-program-prep.md +++ b/docs/spec/01-program-prep.md @@ -1849,6 +1849,24 @@ moves. (stale i386 `-O0` outgoing stack slots, the AArch64 `x8` result register), and in an i386 PIC object, whose `GOTOFF` addresses the loader leaves unresolved, a `char *` parameter renders such a garbage address as a string literal. +- **(kuna) libgcc DI-mode contracts** (`libgccsigs`, off by default, + `decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libgcc.rs (LibgccSigsPass)`): + seeds the reserved compiler-runtime name `__divdi3` on SPARC32 big-endian ELF + under `sparc:BE:32:default:default`. Its DI-mode contract is a signed + eight-byte quotient of two signed eight-byte operands, independent of the + target's four-byte C `long`. The normal compiler model assigns the argument + and return register pairs. Thus a caller storing only `%o1` stores the low + word of the computed quotient instead of reading an unassigned local. Other + names and targets retain their existing inference. Existing import-address + and name-ambiguity rules apply, and DWARF or explicit prototype assertions + retain precedence over the built-in signature. The pass's facts are computed + at load and committed only when the option is on. It ships off because the + locked eight-byte result still costs a caller that returns arithmetic on the + quotient: §4's whole-pair proof keeps both return words only when each is a + bare slice of the call result or a literal and nothing else reads the + quotient, so `return a / b + 1;`, + `return -(a / b);` and `return f ? a / b : c;` keep only the high word or + return nothing, where the off output returns every word. - **(kuna) Measured libc signatures** (`libcsigs`, `decompiler/crates/kuna-analysis/src/analyzers/protos/kuna_libcsigs.rs (LibcSigsPass)`): the second, larger half of the same table, closing most of the LOSS above. Which diff --git a/docs/spec/02-lift-and-flow.md b/docs/spec/02-lift-and-flow.md index fe882fe3bd..8077320c09 100644 --- a/docs/spec/02-lift-and-flow.md +++ b/docs/spec/02-lift-and-flow.md @@ -37,6 +37,17 @@ the upper register bank. The rules apply in both language variants. See the [SPARC V9 instruction mapping](https://docs.oracle.com/cd/E19963-01/html/821-1607/sparcv9-32853.html). This is a SLEIGH correctness repair, without an inference option. +SPARC integer branch conditions snapshot the ICC/XCC carry, negative, and +overflow flags before expanding the delay slot. A flag-writing slot still +publishes its new flags to subsequent instructions, but cannot change the +current branch's decision. Ordinary and predicted branches share these +condition operands; annulled branches retain their conditional slot execution. +The non-annulled register branches (`brz`, `brlez`, `brlz`, `brnz`, `brgz`, +`brgez`) likewise test `rs1` before the delay slot, so a slot that rewrites +that register keeps its new value without changing the branch. Annulled forms +already tested the register first. This is a SLEIGH correctness repair, +without an inference option. + PowerPC `lwarx` sets the reservation (`RESERVE`, its length and its address) before loading the word, as the ISA constructor does. `stwcx.` and `stdcx.` store only while `RESERVE` is set, so a load-reserve/store-conditional retry @@ -1652,11 +1663,35 @@ Without the option the branch is truncated to a `CALLIND` carrying (the `jumpassist` user-op family) and the manual `JumpBasicOverride` model are unported shells: `jumptable.rs (JumpTable::set_override)` and the `` arm of `jumptable.rs (JumpTable::decode)` return errors, and -`recover_model` walks only JumpBasic/JumpBasic2 and, when `option constselectjump` is on, the constant-destination model above (Trivial exists only as the label-time fallback). Likewise upstream's -multistage *restart* accounting — persisting a table whose size disagrees at -`matchModel` time and restarting the whole function — is a recorded loss: kuna -keeps the flow-recovered addresses instead and lets the row check below decide -which values label them (`jumptable.rs (JumpTable::match_model)`). +`recover_model` walks only JumpBasic/JumpBasic2 and, when `option constselectjump` is on, the constant-destination model above (Trivial exists only as the label-time fallback). + +**Multistage recovery.** A flow-time table can come out with a single address +when the partial clone folds its index to a constant. One case is a stack slot +stored before a call, whose address is passed to that call and read back as the +switch index. The partial does not yet know the call's inputs, so it collapses +the INDIRECT the call casts over the slot and reads the stored value. As +upstream does at `matchModel` time, a table of one address whose late model has +more entries records a multistage hint for its jump and restarts the function +(`jumptable.rs (JumpTable::match_model)`). On the next flow, +`flow.rs (FlowInfo::check_multistage_jumptables)` queues the hinted table again +and recovers it through `jumptable.rs (JumpTable::recover_multistage)`. (kuna) The +second stage runs on the same kind of partial, so it can fold the index again. +The hint therefore also carries the targets the late model built +(`overrides.rs (Override::multistage_targets)`). A second stage that again finds +one address adopts them (`jumptable.rs (JumpTable::kuna_adopt_multistage_targets)`), +so flow follows every case and the late check below labels them. Each jump is +restarted at most once. A jump is not restarted unless every value in the +late model's range passes the guards on its path +(`jumptable.rs (JumpBasicModel::values_pass_guards)`) and every target is +mapped and within the `0xffff` reach of the jump. A range wider than its guards, +as `switchguardbound` can recover under a lower-bound guard, would otherwise +turn rows the code never dispatches into cases. The restarting pass still +labels its table, without the model-drop warning, because it is the pass +printed when the drive's restart budget runs out. On that last pass +`decompile_drive.rs (run_pipeline)` keeps the analysis instead of re-following +flow, and adds `Exceeded maximum restarts with more pending` to the header. +This is a correctness repair, without an option: before it, such a switch kept +one case, and its other indexes fell through to the code after the switch. ### The late check: labels, normalization folding, guard folding diff --git a/docs/spec/04-calls-and-prototypes.md b/docs/spec/04-calls-and-prototypes.md index 09e795622c..b70e843445 100644 --- a/docs/spec/04-calls-and-prototypes.md +++ b/docs/spec/04-calls-and-prototypes.md @@ -2077,6 +2077,66 @@ output sits at the constructed join address (falling back to the first piece if no join can be built); more pieces chain PIECEs over contiguous trials. The (kuna) `returnpair` gate intercepts this join — §4.4. +A wider heritaged register range can contain several separate output entries. +On a model whose four-byte register pair returns the high word first +(`kuna_bejoin`'s `wide_pair`: 32-bit SPARC, PowerPC, MIPS and ARM big-endian), +`Heritage::guard_returns_overlapping` keeps the extra entries as inactive +candidates instead of dropping their bytes or treating the range alone as +evidence that the caller returns every word. Every other model keeps +upstream's single biggest contained entry: there `whole_pair` never applies, +and an inactive leftover word would make `voidret`'s whole-or-none rule drop +an ARM `d0` or `r0:r1` return the caller still reads. With a declared wide callee, +return scoring lets frame slots materialized by spacebase simplification +receive stack SSA before finalizing. A wide load alone is not proof, since it +can reload unused incoming arguments. +This also preserves a narrow frame read beneath a phi or extension until +the standard spacebase-load rule can materialize it for the following +heritage pass; resolving only the final eight-byte reload is insufficient. +The spacebase-load consumer also recognizes a constant OR displacement, such +as `(spacebase - 32) | 4`, only when existing nonzero masks prove its set bits +disjoint from the base. Overlapping or unknown bits do not establish an +additive frame offset. +Before deriving the output map, `kuna_retcallhalf::whole_pair` can retain a +high-first pair when at least one path returns corresponding slices of one +locked, same-convention eight-byte integer call result. Corresponding phi +inputs are checked together; a phi in only one word is checked against the +other word on every incoming edge. Other paths may replace either word with +a literal, including constant-only expressions not yet folded by the +simplification pool and the upper zero word of a zero-extension. A lower word +inside an extension keeps its source provenance. Unrelated live values do not +establish a pair, and at least one path must return a whole wide value. +Every reader of that call result, through moves, slices and phis, must be a +RETURN or an input that a later call's parameter recovery has not made an +argument: a caller that stores, passes or tests the result consumed it, so a +void `*p = f(x);` or `G = f(x);` that leaves the words in the return +registers is not a return. The ABI supplies the join order. Explicit +caller outputs and competing output classes are unchanged. Provenance rejects +single-word truncations that move a different word into the return storage, +including the SPARC register-window controls in the regression. + +This does not prove an unknown caller's C return width from its callee's width. +On a high-first ABI, returning the whole result and returning only its high +word can have identical machine instructions: authored PowerPC32 callers +compiled with sibling-call optimization disabled exhibit exactly that case. +Recovery then retains the wide result; an explicit scalar caller prototype +is needed to state the otherwise indistinguishable narrow contract. + +The pair proof does not follow arithmetic. A caller that returns a value +computed from the call's words (`a / b + 1`, `-(a / b)`, an absolute value, a +shift) or a phi of the call's words and an incoming argument +(`f ? a / b : c`) leaves the second word inactive, so its return keeps only the +high word or none. That is why the SPARC32 `__divdi3` contract (`libgccsigs`, +§1) is off by default: without it the helper is an unknown callee, and such a +caller returns every word in the first-register-low join. With a declared wide +callee from DWARF or `--assert prototype`, the same limit applies. + +The authored `kuna-cli/tests/libgcc_divdi3.rs` round trips cover direct, +saved-across-call, guarded, nontrivial-constant and one-word-replaced quotient +returns at compiler `-O0` and `-O2`, +both with `libgccsigs` on and with an explicit wide prototype while +`libproto` is off. They also check single-word truncations, call effects, and +that the default (`libgccsigs` off) leaves a low-word store as before. + After native return trials are finalized, an otherwise unknown ten-byte scalar result in the full x87 ST0 register receives an extended floating type when the active convention explicitly classifies that exact output storage as floating. diff --git a/specs/Ghidra/Processors/Sparc/data/languages/SparcV9.sinc b/specs/Ghidra/Processors/Sparc/data/languages/SparcV9.sinc index 664b25bf90..80426818a9 100644 --- a/specs/Ghidra/Processors/Sparc/data/languages/SparcV9.sinc +++ b/specs/Ghidra/Processors/Sparc/data/languages/SparcV9.sinc @@ -741,11 +741,11 @@ icc: "l" is cond=0x3 { tmp:1=(i_nf ^^ i_vf); export tmp; } icc: "gu" is cond=0xc { tmp:1=!(i_cf || i_zf); export tmp; } icc: "leu" is cond=0x4 { tmp:1=(i_cf || i_zf); export tmp; } icc: "cc" is cond=0xd { tmp:1=!(i_cf); export tmp; } -icc: "cs" is cond=0x5 { export i_cf; } +icc: "cs" is cond=0x5 { tmp:1=i_cf; export tmp; } icc: "pos" is cond=0xe { tmp:1=!(i_nf); export tmp; } -icc: "neg" is cond=0x6 { export i_nf; } +icc: "neg" is cond=0x6 { tmp:1=i_nf; export tmp; } icc: "vc" is cond=0xf { tmp:1=!(i_vf); export tmp; } -icc: "vs" is cond=0x7 { export i_vf; } +icc: "vs" is cond=0x7 { tmp:1=i_vf; export tmp; } xcc: "a" is cond=0x8 { tmp:1=1; export tmp; } xcc: "ne" is cond=0x9 { tmp:1=!x_zf; export tmp; } @@ -757,11 +757,11 @@ xcc: "l" is cond=0x3 { tmp:1=(x_nf ^^ x_vf); export tmp; } xcc: "gu" is cond=0xc { tmp:1=!(x_cf || x_zf); export tmp; } xcc: "leu" is cond=0x4 { tmp:1=(x_cf || x_zf); export tmp; } xcc: "cc" is cond=0xd { tmp:1=!(x_cf); export tmp; } -xcc: "cs" is cond=0x5 { export x_cf; } +xcc: "cs" is cond=0x5 { tmp:1=x_cf; export tmp; } xcc: "pos" is cond=0xe { tmp:1=!(x_nf); export tmp; } -xcc: "neg" is cond=0x6 { export x_nf; } +xcc: "neg" is cond=0x6 { tmp:1=x_nf; export tmp; } xcc: "vc" is cond=0xf { tmp:1=!(x_vf); export tmp; } -xcc: "vs" is cond=0x7 { export x_vf; } +xcc: "vs" is cond=0x7 { tmp:1=x_vf; export tmp; } cc: icc is cc1=0 & cc0=0 & icc { export icc; } cc: xcc is cc1=1 & cc0=0 & xcc { export xcc; } @@ -771,12 +771,12 @@ d16off: reloc is d16hi & d16lo [reloc = inst_start+4*((d16hi<<14) | d16lo);] { predict: ",pt" is p=1 { } predict: ",pn" is p=0 { } -:brz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x1 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 == 0) goto d16off;} -:brlez^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x2 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 s<= 0) goto d16off;} -:brlz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x3 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 s< 0) goto d16off;} -:brnz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x5 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 != 0) goto d16off;} -:brgz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x6 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 s> 0) goto d16off;} -:brgez^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x7 & op2=0x3 & RS1 & d16off & predict { delayslot(1); if (RS1 s>= 0) goto d16off;} +:brz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x1 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 == 0); delayslot(1); if (taken) goto d16off;} +:brlez^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x2 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 s<= 0); delayslot(1); if (taken) goto d16off;} +:brlz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x3 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 s< 0); delayslot(1); if (taken) goto d16off;} +:brnz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x5 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 != 0); delayslot(1); if (taken) goto d16off;} +:brgz^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x6 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 s> 0); delayslot(1); if (taken) goto d16off;} +:brgez^predict RS1,d16off is op=0 & a=0 & bit28=0 & rcond2=0x7 & op2=0x3 & RS1 & d16off & predict { local taken:1 = (RS1 s>= 0); delayslot(1); if (taken) goto d16off;} :brz^",a"^predict RS1,d16off is op=0 & a=1 & bit28=0 & rcond2=0x1 & op2=0x3 & RS1 & d16off & predict { if (RS1 != 0) goto inst_next; delayslot(1); goto d16off;} :brlez^",a"^predict RS1,d16off is op=0 & a=1 & bit28=0 & rcond2=0x2 & op2=0x3 & RS1 & d16off & predict { if (RS1 s> 0) goto inst_next; delayslot(1); goto d16off;} diff --git a/tests/golden/vectors/lift/README.md b/tests/golden/vectors/lift/README.md index afd26f961f..d5555a811d 100644 --- a/tests/golden/vectors/lift/README.md +++ b/tests/golden/vectors/lift/README.md @@ -26,6 +26,12 @@ operand loads. Thirteen lines change only unique-space offsets by `0xc00`. The refreshed runtime output uses the x86 specs independently matched against the pinned C++ compiler; both focused fixture replays pass. +`gh6990-returnpair.txt` was refreshed for the SPARC delay-slot branch-condition +correction. The SPARC specs it replays match the pinned C++ compiler's digests. +The function contains no `cs`, `neg`, `vs` or register branch, so its +instructions, operations and temporary aliasing are unchanged; 150 lines change +only unique-space offsets by `0xc00`. + ## Regeneration ```bash diff --git a/tests/golden/vectors/lift/gh6990-returnpair.txt b/tests/golden/vectors/lift/gh6990-returnpair.txt index cedb35e524..2f9100681b 100644 --- a/tests/golden/vectors/lift/gh6990-returnpair.txt +++ b/tests/golden/vectors/lift/gh6990-returnpair.txt @@ -21,7 +21,7 @@ space join index=7 size=4 wordsize=1 bigendian=1 space stack index=8 size=4 wordsize=1 bigendian=1 # golden lift ram:0x32148 80 (kuna_goldengen.cc; see tests/golden/vectors/lift/README.md) insn ram:0x32148 4 - INT_ADD (unique,0x28e00,4) (register,0x38,4) (const,0xffffff48,4) + INT_ADD (unique,0x29a00,4) (register,0x38,4) (const,0xffffff48,4) CBRANCH - (const,0x59,4) (register,0x5020,1) INT_MULT (unique,0x0,4) (register,0x6054,4) (const,0x10,4) INT_ADD (unique,0x100,4) (unique,0x0,4) (const,0x0,4) @@ -128,7 +128,7 @@ insn ram:0x32148 4 COPY (register,0x58,4) (const,0x0,4) COPY (register,0x5c,4) (const,0x0,4) INT_ADD (register,0x6054,4) (register,0x6054,4) (const,0x1,4) - COPY (register,0x38,4) (unique,0x28e00,4) + COPY (register,0x38,4) (unique,0x29a00,4) insn ram:0x3214c 4 insn ram:0x32150 4 INT_SUB (register,0x6054,4) (register,0x6054,4) (const,0x1,4) @@ -234,7 +234,7 @@ insn ram:0x32154 8 INT_ADD (unique,0xb100,4) (register,0x3c,4) (const,0x8,4) RETURN - (unique,0xb100,4) insn ram:0x3215c 4 - INT_ADD (unique,0x28e00,4) (register,0x38,4) (const,0xffffffa0,4) + INT_ADD (unique,0x29a00,4) (register,0x38,4) (const,0xffffffa0,4) CBRANCH - (const,0x59,4) (register,0x5020,1) INT_MULT (unique,0x0,4) (register,0x6054,4) (const,0x10,4) INT_ADD (unique,0x100,4) (unique,0x0,4) (const,0x0,4) @@ -341,7 +341,7 @@ insn ram:0x3215c 4 COPY (register,0x58,4) (const,0x0,4) COPY (register,0x5c,4) (const,0x0,4) INT_ADD (register,0x6054,4) (register,0x6054,4) (const,0x1,4) - COPY (register,0x38,4) (unique,0x28e00,4) + COPY (register,0x38,4) (unique,0x29a00,4) insn ram:0x32160 4 insn ram:0x32164 4 INT_SUB (register,0x6054,4) (register,0x6054,4) (const,0x1,4) @@ -444,221 +444,221 @@ insn ram:0x32164 4 LOAD (register,0x5c,4) (const,register,8) (unique,0xa000,4) COPY (register,0x5010,1) (const,0x1,1) insn ram:0x32168 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3216c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32170 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32174 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32178 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3217c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32180 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32184 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32188 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3218c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32190 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32194 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32198 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3219c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321a0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321a4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321a8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321ac 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321b0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321b4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321b8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321bc 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321c0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321c4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321c8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321cc 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321d0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321d4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321d8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321dc 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321e0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321e4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321e8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321ec 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321f0 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321f4 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321f8 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x321fc 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32200 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32204 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32208 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3220c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32210 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32214 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32218 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3221c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32220 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32224 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32228 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3222c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32230 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32234 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32238 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3223c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32240 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32244 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32248 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3224c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32250 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32254 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32258 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3225c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32260 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32264 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32268 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3226c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32270 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32274 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32278 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x3227c 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32280 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32284 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) insn ram:0x32288 4 - CALLOTHER (unique,0x39300,4) (const,0x3,4) (const,0x0,4) - BRANCHIND - (unique,0x39300,4) + CALLOTHER (unique,0x39f00,4) (const,0x3,4) (const,0x0,4) + BRANCHIND - (unique,0x39f00,4) diff --git a/tests/stages/kuna-catalog.xml b/tests/stages/kuna-catalog.xml index 53b181d265..73258f1090 100644 --- a/tests/stages/kuna-catalog.xml +++ b/tests/stages/kuna-catalog.xml @@ -53,7 +53,7 @@ "option": "armfloatargs", "values": \["on", "off"\], "default": "off", "current": "off" "option": "armframes", "values": \["inventory", "on", "off"\], "default": "inventory", "current": "inventory" "option": "armframes", "values": \["inventory", "on", "off"\], "default": "inventory", "current": "on" -"option": +"option": "option": "globalarrayanchor", "values": \["on", "off"\], "default": "off", "current": "off" "option": "bytearrayc", "values": \["on", "off"\], "default": "off", "current": "off" "option": "bytearrayc", "values": \["on", "off"\], "default": "off", "current": "on" diff --git a/tests/stages/kuna-sparc-delay-condition.xml b/tests/stages/kuna-sparc-delay-condition.xml new file mode 100644 index 0000000000..f507280763 --- /dev/null +++ b/tests/stages/kuna-sparc-delay-condition.xml @@ -0,0 +1,57 @@ + + + + +80a2000a0a80000580a2400a81c3e0080100000001000000 +2abffffdc02ac00081c3e00801000000 +02ca00049010200581c3e0080100000081c3e00890022001 + + + + + +(previous < limit|limit <= previous) +(current < limit|limit <= current) +\*out = 0; +return 6; +return 5; +