diff options
Diffstat (limited to 'tools/asm-processor/rust')
| -rw-r--r-- | tools/asm-processor/rust/Cargo.lock | 236 | ||||
| -rw-r--r-- | tools/asm-processor/rust/Cargo.toml | 24 | ||||
| -rw-r--r-- | tools/asm-processor/rust/src/main.rs | 518 | ||||
| -rw-r--r-- | tools/asm-processor/rust/src/postprocess.rs | 1312 | ||||
| -rw-r--r-- | tools/asm-processor/rust/src/preprocess.rs | 969 |
5 files changed, 3059 insertions, 0 deletions
diff --git a/tools/asm-processor/rust/Cargo.lock b/tools/asm-processor/rust/Cargo.lock new file mode 100644 index 0000000..46e46e9 --- /dev/null +++ b/tools/asm-processor/rust/Cargo.lock @@ -0,0 +1,236 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "anyhow" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" + +[[package]] +name = "argp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7409aa6f1dd8464eac2e56cf538e1e5f7f79678caa32f198d214a3db8d5075c1" +dependencies = [ + "argp_derive", +] + +[[package]] +name = "argp_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d9b949411282939e3f7d8923127e3f18aa474b46da4e8bb0ddf2cb8c81f963a" +dependencies = [ + "proc-macro2", + "pulldown-cmark", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "array-init" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" + +[[package]] +name = "asm-processor" +version = "1.0.0" +dependencies = [ + "anyhow", + "argp", + "binrw", + "encoding_rs", + "enum-map", + "regex-lite", + "shlex", + "temp-dir", +] + +[[package]] +name = "binrw" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d4bca59c20d6f40c2cc0802afbe1e788b89096f61bdf7aeea6bf00f10c2909b" +dependencies = [ + "array-init", + "binrw_derive", + "bytemuck", +] + +[[package]] +name = "binrw_derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8ba42866ce5bced2645bfa15e97eef2c62d2bdb530510538de8dd3d04efff3c" +dependencies = [ + "either", + "owo-colors", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytemuck" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "temp-dir" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc1ee6eef34f12f765cb94725905c6312b6610ab2b0940889cfe58dae7bc3c72" + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + +[[package]] +name = "unicode-ident" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" diff --git a/tools/asm-processor/rust/Cargo.toml b/tools/asm-processor/rust/Cargo.toml new file mode 100644 index 0000000..3ef4479 --- /dev/null +++ b/tools/asm-processor/rust/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "asm-processor" +version = "1.0.0" +edition = "2021" +description = "Pre-process .c files and post-process .o files to enable embedding MIPS assembly into IDO-compiled C." +license = "Unlicense" +repository = "https://github.com/simonlindholm/asm-processor" +readme = "../README.md" +keywords = ["decompilation", "ido", "mips", "asm"] + +[dependencies] +anyhow = "1.0" +binrw = "0.14.1" +encoding_rs = "0.8.35" +enum-map = "2.7.3" +regex-lite = "0.1.6" +shlex = "1.3.0" +temp-dir = "0.1.14" +argp = "0.4.0" + +# The profile that 'dist' will build with +[profile.dist] +inherits = "release" +lto = "thin" diff --git a/tools/asm-processor/rust/src/main.rs b/tools/asm-processor/rust/src/main.rs new file mode 100644 index 0000000..8e214ee --- /dev/null +++ b/tools/asm-processor/rust/src/main.rs @@ -0,0 +1,518 @@ +mod postprocess; +mod preprocess; + +use std::{ + borrow::Cow, + ffi::OsString, + fmt::Display, + fs::{self, File}, + io::Write, + path::{Path, PathBuf}, + process::{exit, Command}, + str::FromStr, +}; + +use anyhow::Result; +use argp::{EarlyExit, FromArgs, HelpStyle}; +use encoding_rs::EUC_JP; +use enum_map::{Enum, EnumMap}; +use temp_dir::TempDir; + +use postprocess::fixup_objfile; +use preprocess::parse_source; + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum)] +enum OutputSection { + Text, + Data, + Rodata, + Bss, +} + +impl OutputSection { + fn as_str(&self) -> &'static str { + match self { + OutputSection::Text => ".text", + OutputSection::Data => ".data", + OutputSection::Rodata => ".rodata", + OutputSection::Bss => ".bss", + } + } +} + +impl Display for OutputSection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +#[derive(Clone, Debug)] +struct Function { + text_glabels: Vec<String>, + asm_conts: Vec<String>, + late_rodata_dummy_bytes: Vec<[u8; 4]>, + jtbl_rodata_size: usize, + late_rodata_asm_conts: Vec<String>, + fn_desc: String, + data: EnumMap<OutputSection, (Option<String>, usize)>, +} + +#[derive(Clone, Copy, Debug)] +enum Encoding { + Latin1, + Custom(&'static encoding_rs::Encoding), +} + +impl Encoding { + fn encode<'a>(&self, s: &'a str) -> Result<Cow<'a, [u8]>> { + match self { + Encoding::Latin1 => { + if encoding_rs::mem::is_str_latin1(s) { + return Ok(encoding_rs::mem::encode_latin1_lossy(s)); + } + } + Encoding::Custom(enc) => { + if *enc == EUC_JP { + let s = s.replace("〜", "~"); + let (ret, _, failed) = enc.encode(&s); + if !failed { + return Ok(Cow::Owned(ret.into_owned())); + } + } else { + let (ret, _, failed) = enc.encode(s); + if !failed { + return Ok(ret); + } + } + } + } + Err(anyhow::anyhow!("Failed to encode string: {}", s)) + } + + fn decode<'a>(&self, bytes: &'a [u8]) -> Result<Cow<'a, str>> { + match self { + Encoding::Latin1 => Ok(encoding_rs::mem::decode_latin1(bytes)), + Encoding::Custom(enc) => { + let (ret, _, failed) = enc.decode(bytes); + if !failed { + Ok(ret) + } else { + Err(anyhow::anyhow!("Failed to decode string: {}", ret)) + } + } + } + } +} + +impl FromStr for Encoding { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + if s == "latin1" { + Ok(Encoding::Latin1) + } else { + match encoding_rs::Encoding::for_label(s.as_bytes()) { + Some(enc) => Ok(Encoding::Custom(enc)), + None => Err(format!("Unsupported encoding: {}", s)), + } + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +enum ConvertStatics { + No, + Local, + Global, + GlobalWithFilename, +} + +impl FromStr for ConvertStatics { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Ok(match s { + "no" => ConvertStatics::No, + "local" => ConvertStatics::Local, + "global" => ConvertStatics::Global, + "global-with-filename" => ConvertStatics::GlobalWithFilename, + _ => return Err("invalid value for symbol visibility".into()), + }) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum OptLevel { + O0, + O1, + O2, + G, + G3, +} + +/// Pre-process .c files and post-process .o files to enable embedding MIPS assembly into IDO-compiled C. +#[derive(FromArgs)] +struct AsmProcArgs { + /// Path to a file containing a prelude to the assembly file (with .set and .macro directives, e.g.) + #[argp(option, arg_name = "FILE")] + asm_prelude: Option<PathBuf>, + + /// Input encoding (default: latin1) + #[argp( + option, + default = "Encoding::Latin1", + from_str_fn(FromStr::from_str), + arg_name = "ENCODING" + )] + input_enc: Encoding, + + /// Output encoding (default: latin1) + #[argp( + option, + default = "Encoding::Latin1", + from_str_fn(FromStr::from_str), + arg_name = "ENCODING" + )] + output_enc: Encoding, + + /// Drop mdebug and gptab sections + #[argp(switch)] + drop_mdebug_gptab: bool, + + /// Change symbol visibility for static variables. Mode must be one of: + /// no, local, global, global-with-filename (default: local) + #[argp( + option, + default = "ConvertStatics::Local", + from_str_fn(FromStr::from_str), + arg_name = "MODE" + )] + convert_statics: ConvertStatics, + + /// Force processing of files without GLOBAL_ASM blocks + #[argp(switch)] + force: bool, + + /// Emit temporary files to this directory + #[argp(option, arg_name = "DIR")] + keep_preprocessed: Option<PathBuf>, + + /// Replace floats with their encoded hexadecimal representation in CutsceneData data + #[argp(switch)] + encode_cutscene_data_floats: bool, + + /// Don't generate a .d make dependency file + #[argp(switch)] + no_dep_file: bool, + + #[argp(positional, greedy)] + rest: Vec<String>, +} + +struct CompileOpts { + opt: OptLevel, + framepointer: bool, + mips1: bool, + kpic: bool, + pascal: bool, +} + +fn extract_compiler_input_output( + compile_args: &[String], +) -> Result<(PathBuf, PathBuf, Vec<String>), &'static str> { + let mut compile_args: Vec<String> = compile_args.to_vec(); + let out_ind = compile_args + .iter() + .position(|arg| arg == "-o") + .ok_or("missing -o argument")?; + let out_filename = compile_args + .get(out_ind + 1) + .ok_or("missing argument after -o")? + .clone(); + compile_args.remove(out_ind + 1); + compile_args.remove(out_ind); + + let in_file_str = compile_args + .last() + .ok_or("missing input file argument")? + .clone(); + compile_args.pop(); + + let out_file: PathBuf = out_filename.into(); + let in_file: PathBuf = in_file_str.into(); + + Ok((in_file, out_file, compile_args)) +} + +fn parse_compile_args( + compile_args: &[String], + in_file: &Path, +) -> Result<CompileOpts, &'static str> { + let mut opt_flags = vec![]; + for x in compile_args { + opt_flags.push(match x.as_str() { + "-g" => OptLevel::G, + "-O0" => OptLevel::O0, + "-O1" => OptLevel::O1, + "-O2" => OptLevel::O2, + _ => continue, + }); + } + + if opt_flags.len() != 1 { + return Err("exactly one of -g/-O0/-O1/-O2 must be passed"); + } + let mut opt = opt_flags[0]; + + let mips1 = !compile_args.contains(&"-mips2".to_string()); + let framepointer = compile_args.contains(&"-framepointer".to_string()); + let kpic = compile_args.contains(&"-KPIC".to_string()); + if compile_args.contains(&"-g3".to_string()) { + if opt != OptLevel::O2 { + return Err("-g3 is only supported together with -O2"); + } + opt = OptLevel::G3; + } + + if mips1 && (!matches!(opt, OptLevel::O1 | OptLevel::O2) || framepointer) { + return Err("-mips1 is only supported together with -O1 or -O2"); + } + + let in_file_str = in_file.to_string_lossy(); + let pascal = in_file_str.ends_with(".p") + || in_file_str.ends_with(".pas") + || in_file_str.ends_with(".pp"); + + if pascal && !matches!(opt, OptLevel::O1 | OptLevel::O2 | OptLevel::G3) { + return Err("Pascal is only supported together with -O1, -O2 or -O2 -g3"); + } + + Ok(CompileOpts { + opt, + framepointer, + mips1, + kpic, + pascal, + }) +} + +fn parse_rest(rest: &[String]) -> Option<(&[String], &[String], &[String])> { + let mut iter = rest.splitn(3, |x| *x == "--"); + let compiler = iter.next()?; + let assembler = iter.next()?; + let compile_args = iter.next()?; + assert!(iter.next().is_none()); + Some((compiler, assembler, compile_args)) +} + +struct ParsedArgs { + args: AsmProcArgs, + compiler: Vec<String>, + assembler: Vec<String>, + compile_args: Vec<String>, + in_file: PathBuf, + out_file: PathBuf, + opts: CompileOpts, +} + +/// Parse command line arguments while allowing --a=b syntax. +/// +/// This provides backward compatibility with Python argparse. +fn from_args_allow_eq(progname: &[&str], args: &[OsString]) -> Result<AsmProcArgs, EarlyExit> { + let base_res = AsmProcArgs::from_args(progname, args); + if base_res.is_ok() { + return base_res; + } + + // Try splitting up = chars successively, until we get a valid parse. + // This ensures we don't impact the compiler/assembler parts. + // + // Technically this might end up splitting --a --b=c into --a --b c even + // where --a is a flag that takes an argument --b=c, but this seems unlikely + // with our use case. + let mut i = 0; + let mut args = args.to_vec(); + while i < args.len() { + let arg = args[i].as_encoded_bytes(); + if arg.starts_with(b"--") { + if let Some(eq) = arg.iter().position(|&x| x == b'=') { + // SAFETY: splitting on ASCII still results in valid encoded bytes + let before = unsafe { OsString::from_encoded_bytes_unchecked(arg[0..eq].into()) }; + let after = unsafe { OsString::from_encoded_bytes_unchecked(arg[eq + 1..].into()) }; + args.splice(i..i + 1, [before, after]); + let new_res = AsmProcArgs::from_args(progname, &args); + if new_res.is_ok() { + return new_res; + } + i += 1; + } + } + i += 1; + } + + base_res +} + +fn parse_args_or_exit() -> ParsedArgs { + let argv: Vec<_> = std::env::args_os().collect(); + let help_style = HelpStyle { + short_usage: true, + ..HelpStyle::default() + }; + let progname = argv[0].to_string_lossy(); + + let args = from_args_allow_eq(&[&progname], &argv[1..]).unwrap_or_else(|early_exit| { + exit(match early_exit { + EarlyExit::Help(help) => { + println!( + "{}", + help.generate(&help_style).replace( + "[rest...]", + "<compiler...> -- <assembler...> -- <compiler flags...>" + ) + ); + 0 + } + EarlyExit::Err(err) => { + eprintln!("{}\nRun {} --help for more information.", err, progname); + 1 + } + }) + }); + + let Some((compiler, assembler, compile_args)) = parse_rest(&args.rest) else { + eprintln!( + "Usage: {} [options] <compiler...> -- <assembler...> -- <compiler flags...>", + progname + ); + eprintln!("Run {} --help for more information.", progname); + exit(1); + }; + + let (in_file, out_file, compile_args) = extract_compiler_input_output(compile_args) + .unwrap_or_else(|err| { + eprintln!("Failed to parse compiler flags: {}", err); + exit(1); + }); + + let opts = parse_compile_args(&compile_args, &in_file).unwrap_or_else(|err| { + eprintln!("Unsupported compiler flags: {}", err); + exit(1); + }); + + let compiler = compiler.into(); + let assembler = assembler.into(); + + ParsedArgs { + args, + compiler, + assembler, + compile_args, + in_file, + out_file, + opts, + } +} + +fn main() -> Result<()> { + let ParsedArgs { + args, + compiler, + assembler, + compile_args, + in_file, + out_file, + opts, + } = parse_args_or_exit(); + + let assembler_sh = assembler + .iter() + .map(|s| shlex::try_quote(s).unwrap().into_owned()) + .collect::<Vec<String>>() + .join(" "); + + let in_dir = fs::canonicalize(in_file.parent().unwrap().join("."))?; + + let temp_dir = TempDir::with_prefix("asm_processor")?; + let preprocessed_filename = format!( + "preprocessed_{}", + in_file.file_name().unwrap().to_str().unwrap() + ); + let preprocessed_path = temp_dir.path().join(&preprocessed_filename); + let mut preprocessed_file = File::create(&preprocessed_path)?; + + let res = parse_source(&in_file, &args, &opts, true)?; + preprocessed_file.write_all(&res.output)?; + + if let Some(keep_output_dir) = &args.keep_preprocessed { + fs::create_dir_all(keep_output_dir)?; + fs::copy( + &preprocessed_path, + keep_output_dir.join(&preprocessed_filename), + )?; + } + + // Run compiler + let mut compile_command = Command::new(&compiler[0]); + compile_command + .args(&compile_args) + .arg("-I") + .arg(in_dir) + .arg("-o") + .arg(&out_file) + .arg(&preprocessed_path); + + match compile_command.status() { + Ok(status) if status.success() => {} + _ => { + return Err(anyhow::anyhow!( + "Failed to compile file {}. Command line:\n\n{:?}\n", + in_file.display(), + compile_command + )); + } + } + + if !res.functions.is_empty() || args.force { + let prelude_str; + let asm_prelude = match &args.asm_prelude { + Some(prelude) => { + if let Ok(res) = fs::read_to_string(prelude) { + prelude_str = res; + &prelude_str + } else { + return Err(anyhow::anyhow!("Failed to read asm prelude")); + } + } + None => include_str!("../../prelude.inc"), + }; + + fixup_objfile( + &out_file, + &res.functions, + asm_prelude, + &assembler_sh, + &args.output_enc, + args.drop_mdebug_gptab, + args.convert_statics, + )?; + } + + if !res.deps.is_empty() && !args.no_dep_file { + let deps_file = out_file.with_extension("asmproc.d"); + let mut deps_file = File::create(&deps_file)?; + + writeln!( + deps_file, + "{}: {}", + out_file.to_str().unwrap(), + res.deps.join(" \\\n ") + )?; + + for dep in res.deps { + writeln!(deps_file, "\n{dep}:")?; + } + } + + Ok(()) +} diff --git a/tools/asm-processor/rust/src/postprocess.rs b/tools/asm-processor/rust/src/postprocess.rs new file mode 100644 index 0000000..60b782b --- /dev/null +++ b/tools/asm-processor/rust/src/postprocess.rs @@ -0,0 +1,1312 @@ +use anyhow::Result; +use std::{ + cmp::Ordering, + collections::{HashMap, HashSet}, + fs::{self, File}, + io::{self, BufWriter, Cursor, Seek, SeekFrom, Write}, + path::Path, + process::Command, +}; + +use binrw::{binrw, BinRead, BinResult, BinWrite, Endian}; +use enum_map::EnumMap; +use temp_dir::TempDir; + +use crate::{ConvertStatics, Encoding, Function, OutputSection}; + +const EI_NIDENT: usize = 16; +const EI_CLASS: usize = 4; +const EI_DATA: usize = 5; + +const SHN_UNDEF: usize = 0; +const SHN_ABS: usize = 0xfff1; +const SHN_XINDEX: usize = 0xffff; + +const STT_OBJECT: u8 = 1; +const STT_FUNC: u8 = 2; + +const STB_LOCAL: u8 = 0; +const STB_GLOBAL: u8 = 1; + +const STV_DEFAULT: u8 = 0; + +const SHT_NULL: u32 = 0; +const SHT_SYMTAB: u32 = 2; +const SHT_STRTAB: u32 = 3; +const SHT_RELA: u32 = 4; +const SHT_NOBITS: u32 = 8; +const SHT_REL: u32 = 9; +const SHT_MIPS_GPTAB: u32 = 0x70000003; +const SHT_MIPS_DEBUG: u32 = 0x70000005; + +const SHF_LINK_ORDER: u32 = 0x80; + +const MIPS_DEBUG_ST_STATIC: usize = 2; +const MIPS_DEBUG_ST_PROC: usize = 6; +const MIPS_DEBUG_ST_BLOCK: usize = 7; +const MIPS_DEBUG_ST_END: usize = 8; +const MIPS_DEBUG_ST_FILE: usize = 11; +const MIPS_DEBUG_ST_STATIC_PROC: usize = 14; +const MIPS_DEBUG_ST_STRUCT: usize = 26; +const MIPS_DEBUG_ST_UNION: usize = 27; +const MIPS_DEBUG_ST_ENUM: usize = 28; + +#[binrw] +struct ElfHeader { + e_ident: [u8; EI_NIDENT], + e_type: u16, + e_machine: u16, + e_version: u32, + e_entry: u32, + e_phoff: u32, + e_shoff: u32, + e_flags: u32, + e_ehsize: u16, + e_phentsize: u16, + e_phnum: u16, + e_shentsize: u16, + e_shnum: u16, + e_shstrndx: u16, +} + +impl ElfHeader { + const SIZE: usize = 52; + + fn new(data: &[u8], endian: Endian) -> BinResult<Self> { + let mut cursor = Cursor::new(data); + + let header = Self::read_options(&mut cursor, endian, ())?; + + assert_eq!(header.e_ident[EI_CLASS], 1, "ELF must be 32-bit"); + assert_eq!(header.e_type, 1, "ELF must be relocatable"); + assert_eq!(header.e_machine, 8, "ELF must be MIPS 1"); + assert_eq!(header.e_phoff, 0, "ELF must not have program headers"); + assert_ne!(header.e_shoff, 0, "ELF must have section headers"); + assert_ne!( + header.e_shstrndx, SHN_UNDEF as u16, + "ELF must have a section header string table" + ); + + Ok(header) + } + + fn to_bin(&self, endian: Endian) -> [u8; Self::SIZE] { + let mut rv = [0; Self::SIZE]; + let mut cursor = Cursor::new(rv.as_mut_slice()); + + self.write_options(&mut cursor, endian, ()).unwrap(); + rv + } +} + +#[binrw] +struct SymbolData { + st_name: u32, + st_value: u32, + st_size: u32, + st_info: u8, + st_other: u8, + st_shndx: u16, +} + +#[derive(Clone)] +struct Symbol { + st_name: usize, + st_value: usize, + st_size: usize, + st_shndx: usize, + st_type: u8, + st_bind: u8, + st_visibility: u8, + name: Vec<u8>, +} + +impl Symbol { + fn new(data: &[u8], strtab: &Section, endian: Endian) -> BinResult<Self> { + let mut cursor = Cursor::new(data); + + let data = SymbolData::read_options(&mut cursor, endian, ())?; + if data.st_shndx == SHN_XINDEX as u16 { + panic!("too many sections (SHN_XINDEX not supported)"); + } + let st_type = data.st_info & 0xf; + let st_bind = data.st_info >> 4; + let st_visibility = data.st_other & 0x3; + let name = strtab.lookup_str(data.st_name as usize); + + Ok(Self { + st_name: data.st_name as usize, + st_value: data.st_value as usize, + st_size: data.st_size as usize, + st_shndx: data.st_shndx as usize, + st_type, + st_bind, + st_visibility, + name, + }) + } + + fn to_bin(&self) -> Vec<u8> { + let mut rv = vec![]; + let mut cursor = Cursor::new(&mut rv); + + SymbolData { + st_name: self.st_name as u32, + st_value: self.st_value as u32, + st_size: self.st_size as u32, + st_info: self.st_bind << 4 | self.st_type, + st_other: self.st_visibility, + st_shndx: self.st_shndx as u16, + } + .write_options(&mut cursor, Endian::Big, ()) + .unwrap(); + rv + } +} + +#[derive(Clone)] +struct Relocation { + r_offset: usize, + sym_index: usize, + rel_type: u32, + r_addend: Option<u32>, +} + +impl Relocation { + fn new(data: &[u8], sh_type: u32, endian: Endian) -> BinResult<Self> { + let mut cursor = Cursor::new(data); + + let r_offset = u32::read_options(&mut cursor, endian, ())? as usize; + let r_info = u32::read_options(&mut cursor, endian, ())?; + let r_addend = if sh_type == SHT_REL { + None + } else { + Some(u32::read_options(&mut cursor, endian, ())?) + }; + + let sym_index = (r_info >> 8) as usize; + let rel_type = r_info & 0xff; + + Ok(Self { + r_offset, + sym_index, + rel_type, + r_addend, + }) + } + + fn to_bin(&self, endian: Endian) -> Vec<u8> { + let mut rv = vec![]; + let mut cursor = Cursor::new(&mut rv); + let r_offset = self.r_offset as u32; + let r_info = ((self.sym_index as u32) << 8) | self.rel_type; + r_offset.write_options(&mut cursor, endian, ()).unwrap(); + r_info.write_options(&mut cursor, endian, ()).unwrap(); + self.r_addend + .write_options(&mut cursor, endian, ()) + .unwrap(); + rv + } +} + +#[binrw] +struct Hdrr { + magic: u16, + vstamp: u16, + iline_max: u32, + cb_line: u32, + cb_line_offset: u32, + idn_max: u32, + cb_dn_offset: u32, + ipd_max: u32, + cb_pd_offset: u32, + isym_max: u32, + cb_sym_offset: u32, + iopt_max: u32, + cb_opt_offset: u32, + iaux_max: u32, + cb_aux_offset: u32, + iss_max: u32, + cb_ss_offset: u32, + iss_ext_max: u32, + cb_ss_ext_offset: u32, + ifd_max: u32, + cb_fd_offset: u32, + crfd: u32, + cb_rfd_offset: u32, + iext_max: u32, + cb_ext_offset: u32, +} + +impl Hdrr { + const SIZE: usize = 96; +} + +#[binrw] +#[derive(Clone)] +struct SectionHeader { + sh_name: u32, + sh_type: u32, + sh_flags: u32, + sh_addr: u32, + sh_offset: u32, + sh_size: u32, + sh_link: u32, + sh_info: u32, + sh_addralign: u32, + sh_entsize: u32, +} + +impl SectionHeader { + const SIZE: usize = 40; +} + +#[derive(Clone)] +struct Section { + header: SectionHeader, + data: Vec<u8>, + index: usize, + relocated_by: Vec<usize>, + relocations: Vec<Relocation>, + name: String, +} + +impl Section { + fn new(data: &[u8], other_data: &[u8], index: usize, endian: Endian) -> BinResult<Self> { + let mut cursor = Cursor::new(data); + + let header = SectionHeader::read_options(&mut cursor, endian, ())?; + assert!(header.sh_flags & SHF_LINK_ORDER == 0); + if header.sh_entsize != 0 { + assert_eq!(header.sh_size % header.sh_entsize, 0); + } + + let data = if header.sh_type == SHT_NOBITS { + vec![] + } else { + other_data[header.sh_offset as usize..(header.sh_offset + header.sh_size) as usize] + .to_vec() + }; + Ok(Self { + header, + data, + index, + relocated_by: vec![], + relocations: vec![], + name: "".into(), + }) + } + + fn from_parts( + sh_name: u32, + fields: &HeaderFields, + data: &[u8], + index: usize, + endian: Endian, + ) -> Self { + let header = SectionHeader { + sh_name, + sh_type: fields.sh_type, + sh_flags: fields.sh_flags, + sh_addr: 0, + sh_offset: 0, + sh_size: data.len() as u32, + sh_link: fields.sh_link, + sh_info: fields.sh_info, + sh_addralign: fields.sh_addralign, + sh_entsize: fields.sh_entsize, + }; + + let mut rv = [0; SectionHeader::SIZE]; + let mut cursor = Cursor::new(rv.as_mut_slice()); + + header.write_options(&mut cursor, endian, ()).unwrap(); + + Self::new(&rv, data, index, endian).unwrap() + } + + fn lookup_str(&self, index: usize) -> Vec<u8> { + assert_eq!(self.header.sh_type, SHT_STRTAB); + let to = self.data[index..] + .iter() + .position(|&x| x == 0) + .expect("bad strtab index") + + index; + self.data[index..to].to_owned() + } + + fn add_str(&mut self, string: &[u8]) -> u32 { + assert_eq!(self.header.sh_type, SHT_STRTAB); + let index = self.data.len() as u32; + + self.data.extend_from_slice(string); + self.data.push(0); + index + } + + fn is_rel(&self) -> bool { + self.header.sh_type == SHT_REL || self.header.sh_type == SHT_RELA + } + + fn header_to_bin(&mut self, endian: Endian) -> [u8; SectionHeader::SIZE] { + if self.header.sh_type != SHT_NOBITS { + self.header.sh_size = self.data.len() as u32; + } + + let mut rv = [0; SectionHeader::SIZE]; + let mut cursor = Cursor::new(rv.as_mut_slice()); + + self.header.write_options(&mut cursor, endian, ()).unwrap(); + + rv + } + + fn init_relocs(&mut self, endian: Endian) { + assert!(self.is_rel()); + + let mut entries = vec![]; + for i in (0..self.header.sh_size).step_by(self.header.sh_entsize as usize) { + entries.push( + Relocation::new( + &self.data[i as usize..(i + self.header.sh_entsize) as usize], + self.header.sh_type, + endian, + ) + .unwrap(), + ); + } + self.relocations = entries; + } + + fn relocate_mdebug(&mut self, original_offset: u32, endian: Endian) { + assert_eq!(self.header.sh_type, SHT_MIPS_DEBUG); + let shift_by = self.header.sh_offset.wrapping_sub(original_offset); + + let mut hdrr = Hdrr::read_options(&mut Cursor::new(&self.data), endian, ()).unwrap(); + + assert_eq!(hdrr.magic, 0x7009); + + let relocate = |a, b: &mut u32| { + if a != 0 { + *b = b.wrapping_add(shift_by); + } + }; + relocate(hdrr.cb_line, &mut hdrr.cb_line_offset); + relocate(hdrr.idn_max, &mut hdrr.cb_dn_offset); + relocate(hdrr.ipd_max, &mut hdrr.cb_pd_offset); + relocate(hdrr.isym_max, &mut hdrr.cb_sym_offset); + relocate(hdrr.iopt_max, &mut hdrr.cb_opt_offset); + relocate(hdrr.iaux_max, &mut hdrr.cb_aux_offset); + relocate(hdrr.iss_max, &mut hdrr.cb_ss_offset); + relocate(hdrr.iss_ext_max, &mut hdrr.cb_ss_ext_offset); + relocate(hdrr.ifd_max, &mut hdrr.cb_fd_offset); + relocate(hdrr.crfd, &mut hdrr.cb_rfd_offset); + relocate(hdrr.iext_max, &mut hdrr.cb_ext_offset); + + let mut new_data = [0; Hdrr::SIZE]; + let mut cursor = Cursor::new(new_data.as_mut_slice()); + hdrr.write_options(&mut cursor, endian, ()).unwrap(); + + self.data = new_data.to_vec(); + } +} + +struct ElfFile { + data: Vec<u8>, + endian: Endian, + header: ElfHeader, + sections: Vec<Section>, + symbol_entries: Vec<Symbol>, + symtab: usize, + sym_strtab: usize, +} + +struct HeaderFields { + sh_type: u32, + sh_flags: u32, + sh_link: u32, + sh_info: u32, + sh_addralign: u32, + sh_entsize: u32, +} + +impl ElfFile { + fn new(data: &[u8]) -> BinResult<Self> { + let data = data.to_vec(); + assert_eq!(data[..4], [0x7f, b'E', b'L', b'F']); + + let endian: Endian = if data[EI_DATA] == 1 { + Endian::Little + } else if data[5] == 2 { + Endian::Big + } else { + panic!("Invalid ELF endianness"); + }; + let header = ElfHeader::new(&data[..ElfHeader::SIZE], endian).unwrap(); + let offset = header.e_shoff as usize; + let size = header.e_shentsize as usize; + let null_section = Section::new(&data[offset..offset + size], &data, 0, endian).unwrap(); + let num_sections = if header.e_shnum == 0 { + null_section.header.sh_size as usize + } else { + header.e_shnum as usize + }; + let mut sections = vec![null_section]; + + for i in 1..num_sections { + let ind = offset + i * size; + let section = Section::new(&data[ind..ind + size], &data, i, endian).unwrap(); + sections.push(section); + } + + let symtab_index = sections + .iter() + .position(|s| s.header.sh_type == SHT_SYMTAB) + .expect("missing symtab"); + let sym_strtab_index = sections[symtab_index].header.sh_link as usize; + + let symtab = §ions[symtab_index]; + let sym_strtab = §ions[sym_strtab_index]; + let symbol_entries = ElfFile::init_symbols(symtab, sym_strtab, endian); + + let shstr = sections[header.e_shstrndx as usize].clone(); + + for i in 0..sections.len() { + let s = &mut sections[i]; + s.name = String::from_utf8(shstr.lookup_str(s.header.sh_name as usize)).unwrap(); + assert!(s.name.is_ascii()); + + if s.is_rel() { + let target_index = s.header.sh_info as usize; + s.init_relocs(endian); + sections[target_index].relocated_by.push(i); + } + } + + Ok(ElfFile { + data, + endian, + header, + sections, + symbol_entries, + symtab: symtab_index, + sym_strtab: sym_strtab_index, + }) + } + + fn find_section(&self, name: &str) -> Option<&Section> { + self.sections.iter().find(|s| s.name == name) + } + + fn find_section_mut(&mut self, name: &str) -> Option<&mut Section> { + self.sections.iter_mut().find(|s| s.name == name) + } + + fn symtab(&self) -> &Section { + &self.sections[self.symtab] + } + + fn symtab_mut(&mut self) -> &mut Section { + &mut self.sections[self.symtab] + } + + fn sym_strtab(&self) -> &Section { + &self.sections[self.sym_strtab] + } + + fn sym_strtab_mut(&mut self) -> &mut Section { + &mut self.sections[self.sym_strtab] + } + + fn init_symbols(symtab: &Section, strtab: &Section, endian: Endian) -> Vec<Symbol> { + assert_eq!(symtab.header.sh_type, SHT_SYMTAB); + assert_eq!(symtab.header.sh_entsize, 16); + + let mut syms = Vec::new(); + for i in 0..(symtab.data.len() / 16) { + syms.push(Symbol::new(&symtab.data[i * 16..(i + 1) * 16], strtab, endian).unwrap()); + } + syms + } + + fn find_symbol(&self, name: &[u8]) -> Option<(usize, usize)> { + for s in &self.symbol_entries { + if s.name == name { + return Some((s.st_shndx, s.st_value)); + } + } + None + } + + fn find_symbol_in_section(&self, name: &[u8], section: &Section) -> usize { + let Some((st_shndx, st_value)) = self.find_symbol(name) else { + panic!("failed to find symbol: {}", String::from_utf8_lossy(name)); + }; + if st_shndx != section.index { + panic!( + "symbol {} is in wrong section", + String::from_utf8_lossy(name) + ); + } + st_value + } + + fn add_section(&mut self, name: &str, fields: &HeaderFields, data: &[u8], endian: Endian) { + let shstr = self + .sections + .get_mut(self.header.e_shstrndx as usize) + .expect("bad e_shstrndx"); + let sh_name = shstr.add_str(name.as_bytes()); + let mut s = Section::from_parts(sh_name, fields, data, self.sections.len(), endian); + s.name = name.to_string(); + self.sections.push(s); + } + + fn drop_mdebug_gptab(&mut self) { + // We can only drop sections at the end, since otherwise section + // references might be wrong. Luckily, these sections typically are. + while let Some(s) = self.sections.last() { + if s.header.sh_type != SHT_MIPS_DEBUG && s.header.sh_type != SHT_MIPS_GPTAB { + break; + } + self.sections.pop(); + } + } + + fn pad_out(writer: &mut BufWriter<&mut File>, align: usize) -> io::Result<()> { + let pos = writer.stream_position()? as usize; + + if align > 0 && pos % align != 0 { + let pad = align - (pos % align); + for _ in 0..pad { + writer.write_all(&[0])?; + } + } + Ok(()) + } + + fn write(&mut self, writer: &mut BufWriter<&mut File>) -> io::Result<()> { + self.header.e_shnum = self.sections.len() as u16; + writer.write_all(&self.header.to_bin(self.endian))?; + + for s in &mut self.sections { + if s.header.sh_type != SHT_NOBITS && s.header.sh_type != SHT_NULL { + Self::pad_out(writer, s.header.sh_addralign as usize)?; + let old_offset = s.header.sh_offset; + s.header.sh_offset = writer.stream_position()? as u32; + if s.header.sh_type == SHT_MIPS_DEBUG && s.header.sh_offset != old_offset { + // The .mdebug section has moved, relocate offsets + s.relocate_mdebug(old_offset, self.endian); + } + writer.write_all(&s.data)?; + } + } + + Self::pad_out(writer, 4)?; + self.header.e_shoff = writer.stream_position()? as u32; + + for s in &mut self.sections { + writer.write_all(&s.header_to_bin(self.endian))?; + } + + writer.seek(SeekFrom::Start(0))?; + writer.write_all(&self.header.to_bin(self.endian))?; + writer.flush()?; + Ok(()) + } +} + +pub(crate) fn fixup_objfile( + objfile_path: &Path, + functions: &[Function], + asm_prelude: &str, + assembler: &str, + output_enc: &Encoding, + drop_mdebug_gptab: bool, + convert_statics: ConvertStatics, +) -> Result<()> { + const OUTPUT_SECTIONS: [OutputSection; 4] = [ + OutputSection::Data, + OutputSection::Text, + OutputSection::Rodata, + OutputSection::Bss, + ]; + const INPUT_SECTION_NAMES: [&str; 5] = [".data", ".text", ".rodata", ".bss", ".late_rodata"]; + + let objfile_data = fs::read(objfile_path)?; + let mut objfile = ElfFile::new(&objfile_data)?; + let endian = objfile.endian; + + let mut prev_locs: EnumMap<OutputSection, usize> = EnumMap::default(); + + struct ToCopyData { + loc: usize, + size: usize, + temp_name: String, + fn_desc: String, + } + + let mut to_copy: EnumMap<OutputSection, Vec<ToCopyData>> = EnumMap::default(); + + let mut asm: Vec<String> = vec![]; + let mut all_late_rodata_dummy_bytes: Vec<Vec<[u8; 4]>> = vec![]; + let mut all_jtbl_rodata_size: Vec<usize> = vec![]; + let mut late_rodata_asm: Vec<String> = vec![]; + let late_rodata_source_name_start = "_asmpp_late_rodata_start"; + let late_rodata_source_name_end = "_asmpp_late_rodata_end"; + + // Generate an assembly file with all the assembly we need to fill in. For + // simplicity we pad with nops/.space so that addresses match exactly, so we + // don't have to fix up relocations/symbol references. + let mut all_text_glabels: HashSet<Vec<u8>> = HashSet::new(); + let mut func_sizes: HashMap<Vec<u8>, usize> = HashMap::new(); + + for function in functions.iter() { + let text_glabels = function + .text_glabels + .iter() + .map(|x| output_enc.encode(x)) + .collect::<Result<Vec<_>>>()?; + let mut ifdefed = false; + for (sectype, &(ref temp_name, size)) in function.data.iter() { + let Some(temp_name) = temp_name else { continue }; + if size == 0 { + panic!("Size of section {} is 0", sectype.as_str()); + } + let Some((_, loc)) = objfile.find_symbol(temp_name.as_bytes()) else { + ifdefed = true; + break; + }; + let prev_loc = prev_locs[sectype]; + if loc < prev_loc { + // If the dummy C generates too little asm, and we have two + // consecutive GLOBAL_ASM blocks, we detect that error here. + // On the other hand, if it generates too much, we don't have + // a good way of discovering that error: it's indistinguishable + // from a static symbol occurring after the GLOBAL_ASM block. + panic!( + "Wrongly computed size for section {} (diff {}). This is an asm-processor bug!", + sectype, + prev_loc - loc + ); + } + if loc != prev_loc { + asm.push(format!(".section {}", sectype)); + if sectype == OutputSection::Text { + for _ in 0..((loc - prev_loc) / 4) { + asm.push("nop".to_owned()); + } + } else { + asm.push(format!(".space {}", loc - prev_loc)); + } + } + to_copy[sectype].push(ToCopyData { + loc, + size, + temp_name: temp_name.clone(), + fn_desc: function.fn_desc.clone(), + }); + if !text_glabels.is_empty() && sectype == OutputSection::Text { + func_sizes.insert(text_glabels[0].to_vec(), size); + } + prev_locs[sectype] = loc + size; + } + + if !ifdefed { + all_text_glabels.extend(text_glabels.iter().map(|x| x.to_vec())); + all_late_rodata_dummy_bytes.push(function.late_rodata_dummy_bytes.clone()); + all_jtbl_rodata_size.push(function.jtbl_rodata_size); + late_rodata_asm.extend(function.late_rodata_asm_conts.iter().cloned()); + for (sectype, (temp_name, _)) in function.data.iter() { + if let Some(temp_name) = temp_name { + asm.push(format!(".section {}", sectype)); + asm.push(format!("glabel {}_asm_start", temp_name)); + } + } + asm.push(".text".to_owned()); + asm.extend(function.asm_conts.iter().cloned()); + for (sectype, (temp_name, _)) in function.data.iter() { + if let Some(temp_name) = temp_name { + asm.push(format!(".section {}", sectype)); + asm.push(format!("glabel {}_asm_end", temp_name)); + } + } + } + } + + if !late_rodata_asm.is_empty() { + asm.push(".section .late_rodata".to_string()); + // Put some padding at the start to avoid conflating symbols with + // references to the whole section. + asm.push(".word 0, 0".to_string()); + asm.push(format!("glabel {}", late_rodata_source_name_start)); + asm.extend(late_rodata_asm.iter().cloned()); + asm.push(format!("glabel {}", late_rodata_source_name_end)); + } + + let temp_dir = TempDir::with_prefix("asm_processor")?; + + let obj_stem = objfile_path.file_stem().unwrap().to_str().unwrap(); + + let o_file_path = temp_dir + .path() + .join(format!("asm_processor_{}.o", obj_stem)); + let s_file_path = temp_dir + .path() + .join(format!("asm_processor_{}.s", obj_stem)); + { + let mut s_file = File::create(&s_file_path)?; + s_file.write_all(&output_enc.encode(asm_prelude)?)?; + s_file.write_all(&output_enc.encode("\n")?)?; + + for line in asm { + s_file.write_all(&output_enc.encode(&line)?)?; + s_file.write_all(&output_enc.encode("\n")?)?; + } + } + + let status = Command::new("sh") + .arg("-c") + .arg(format!( + "{} {} -o {}", + assembler, + shlex::try_quote(s_file_path.to_str().unwrap()).unwrap(), + shlex::try_quote(o_file_path.to_str().unwrap()).unwrap(), + )) + .status() + .expect("Failed to run shell"); + if !status.success() { + return Err(anyhow::anyhow!("Failed to assemble")); + } + let asm_objfile = ElfFile::new(&fs::read(&o_file_path)?)?; + + // Remove clutter from objdump output for tests, and make the tests + // portable by avoiding absolute paths. Outside of tests .mdebug is + // useful for showing source together with asm, though. + let mdebug_section = objfile.find_section(".mdebug").cloned(); + if drop_mdebug_gptab { + objfile.drop_mdebug_gptab(); + } + + // Unify reginfo sections + if let Some(target_reginfo) = objfile.find_section_mut(".reginfo") { + let source_reginfo = &asm_objfile + .find_section(".reginfo") + .expect("couldn't find source .reginfo"); + for (s, t) in source_reginfo + .data + .iter() + .zip(target_reginfo.data.iter_mut()) + { + *t |= *s; + } + } + + // Move over section contents + let mut modified_text_positions = HashSet::new(); + let mut jtbl_rodata_positions: HashSet<usize> = HashSet::new(); + let mut last_rodata_pos = 0; + for sectype in OUTPUT_SECTIONS { + if to_copy[sectype].is_empty() { + continue; + } + let Some(source) = asm_objfile.find_section(sectype.as_str()) else { + panic!("didn't find source section: {}", sectype); + }; + for &ToCopyData { + loc, + size, + ref temp_name, + ref fn_desc, + } in to_copy[sectype].iter() + { + let loc1 = asm_objfile + .find_symbol_in_section(format!("{}_asm_start", &temp_name).as_bytes(), source); + let loc2 = asm_objfile + .find_symbol_in_section(format!("{}_asm_end", &temp_name).as_bytes(), source); + if loc1 != loc { + panic!( + "assembly and C files don't line up for section {}, {}", + sectype, fn_desc + ); + } + if loc2 - loc1 != size { + return Err(anyhow::anyhow!( + "incorrectly computed size for section {}, {}. If using .double, make sure to provide explicit alignment padding.", + sectype, + fn_desc + )); + } + } + + if sectype == OutputSection::Bss { + continue; + } + + let Some(target) = objfile.find_section_mut(sectype.as_str()) else { + panic!("didn't find target section: {}", sectype); + }; + + for &ToCopyData { loc, size, .. } in to_copy[sectype].iter() { + target.data[loc..loc + size].copy_from_slice(&source.data[loc..loc + size]); + + if sectype == OutputSection::Text { + assert_eq!(size % 4, 0); + assert_eq!(loc % 4, 0); + for j in 0..size / 4 { + modified_text_positions.insert(loc + 4 * j); + } + } else if sectype == OutputSection::Rodata { + last_rodata_pos = loc + size; + } + } + } + + // Move over late rodata. This is heuristic, sadly, since I can't think + // of another way of doing it. + let mut moved_late_rodata: HashMap<usize, usize> = HashMap::new(); + if all_late_rodata_dummy_bytes.iter().any(|b| !b.is_empty()) + || all_jtbl_rodata_size.iter().any(|&s| s > 0) + { + let source = asm_objfile + .find_section(".late_rodata") + .expect(".late_rodata source section should exist"); + let target = objfile + .find_section_mut(".rodata") + .expect(".rodata target section should exist"); + let mut source_pos = + asm_objfile.find_symbol_in_section(late_rodata_source_name_start.as_bytes(), source); + let source_end = + asm_objfile.find_symbol_in_section(late_rodata_source_name_end.as_bytes(), source); + let num_dummies: usize = all_late_rodata_dummy_bytes.iter().map(|x| x.len()).sum(); + let expected_size = num_dummies * 4 + all_jtbl_rodata_size.iter().sum::<usize>(); + + if source_end - source_pos != expected_size { + return Err(anyhow::anyhow!("computed wrong size of .late_rodata")); + } + let mut new_data = target.data.clone(); + + for (dummy_bytes_list, &jtbl_rodata_size) in all_late_rodata_dummy_bytes + .iter_mut() + .zip(all_jtbl_rodata_size.iter()) + { + let dummy_bytes_list_len = dummy_bytes_list.len(); + + for (index, dummy_bytes) in dummy_bytes_list.iter_mut().enumerate() { + if endian == Endian::Little { + dummy_bytes.reverse(); + } + + let mut pos = target.data[last_rodata_pos..] + .windows(4) + .position(|x| x == dummy_bytes) + .expect("failed to find dummy .late_rodata bytes") + + last_rodata_pos; + + if index == 0 + && dummy_bytes_list_len > 1 + && target.data[pos + 4..pos + 8] == *b"\0\0\0\0" + { + // Ugly hack to handle double alignment for non-matching builds. + // We were told by .late_rodata_alignment (or deduced from a .double) + // that a function's late_rodata started out 4 (mod 8), and emitted + // a float and then a double. But it was actually 0 (mod 8), so our + // double was moved by 4 bytes. To make them adjacent to keep jump + // tables correct, move the float by 4 bytes as well. + new_data[pos..pos + 4].copy_from_slice(b"\0\0\0\0"); + pos += 4; + } + new_data[pos..pos + 4].copy_from_slice(&source.data[source_pos..source_pos + 4]); + moved_late_rodata.insert(source_pos, pos); + last_rodata_pos = pos + 4; + source_pos += 4; + } + + if jtbl_rodata_size > 0 { + assert!(!dummy_bytes_list.is_empty()); + let pos = last_rodata_pos; + new_data[pos..pos + jtbl_rodata_size] + .copy_from_slice(&source.data[source_pos..source_pos + jtbl_rodata_size]); + for i in (0..jtbl_rodata_size).step_by(4) { + moved_late_rodata.insert(source_pos + i, pos + i); + jtbl_rodata_positions.insert(pos + i); + } + last_rodata_pos += jtbl_rodata_size; + source_pos += jtbl_rodata_size; + } + } + target.data = new_data; + } + + // Merge strtab data. + let strtab = objfile.sym_strtab_mut(); + let strtab_adj = strtab.data.len(); + strtab.data.extend(&asm_objfile.sym_strtab().data); + + // Find relocated symbols in asm_objfile + let mut relocated_symbols = HashSet::new(); + for sectype in INPUT_SECTION_NAMES.iter() { + if let Some(sec) = asm_objfile.find_section(sectype) { + for reltab_idx in &sec.relocated_by { + let reltab = &asm_objfile.sections[*reltab_idx]; + for rel in &reltab.relocations { + relocated_symbols.insert(rel.sym_index); + } + } + } + } + + enum SymInd { + Obj(usize), + Asm(usize), + } + + // Move over symbols, deleting the temporary function labels. + // Skip over new local symbols that aren't relocated against, to + // avoid conflicts. + let empty_symbol = objfile.symbol_entries[0].clone(); + let mut new_syms: Vec<(Symbol, Vec<SymInd>)> = objfile + .symbol_entries + .iter() + .enumerate() + .map(|(i, x)| (x, vec![SymInd::Obj(i)])) + .skip(1) + .filter(|(x, _)| !x.name.starts_with(b"_asmpp_")) + .map(|(x, inds)| (x.clone(), inds)) + .collect(); + + for (i, s) in asm_objfile.symbol_entries.iter().enumerate() { + let is_local = i < asm_objfile.symtab().header.sh_info as usize; + if is_local && !relocated_symbols.contains(&i) { + continue; + } + if s.name.starts_with(b"_asmpp_") { + assert!(!relocated_symbols.contains(&i)); + continue; + } + let mut s = s.clone(); + if s.st_shndx != SHN_UNDEF && s.st_shndx != SHN_ABS { + let section_name = asm_objfile.sections[s.st_shndx].name.clone(); + let mut target_section_name = section_name.clone(); + if section_name == ".late_rodata" { + target_section_name = ".rodata".to_string(); + } else if !INPUT_SECTION_NAMES.contains(§ion_name.as_str()) { + return Err(anyhow::anyhow!( + "generated assembly .o must only have symbols for .text, .data, .rodata, .late_rodata, ABS and UNDEF, but found {}", + section_name + )); + } + let Some(objfile_section) = objfile.find_section(&target_section_name) else { + return Err(anyhow::anyhow!( + "generated assembly .o has section that real objfile lacks: {}", + target_section_name + )); + }; + s.st_shndx = objfile_section.index; + // glabels aren't marked as functions, making objdump output confusing. Fix that. + if all_text_glabels.contains(&s.name) { + s.st_type = STT_FUNC; + if let Some(&size) = func_sizes.get(&s.name) { + s.st_size = size; + } + } + if section_name == ".late_rodata" { + if s.st_value == 0 { + // This must be a symbol corresponding to the whole .late_rodata + // section, being referred to from a relocation. + // Moving local symbols is tricky, because it requires fixing up + // lo16/hi16 relocation references to .late_rodata+<offset>. + // Just disallow it for now. + return Err(anyhow::anyhow!( + "local symbols in .late_rodata are not allowed" + )); + } + s.st_value = moved_late_rodata[&s.st_value]; + } + } + s.st_name += strtab_adj; + new_syms.push((s, vec![SymInd::Asm(i)])); + } + + // Add static symbols from .mdebug, so they can be referred to from GLOBAL_ASM + if mdebug_section.is_some() && convert_statics != ConvertStatics::No { + let mdebug_section = mdebug_section.unwrap(); + let mut static_name_count: HashMap<Vec<u8>, usize> = HashMap::new(); + let mut strtab_index = objfile.sym_strtab().data.len(); + let mut new_strtab_data = vec![]; + + let read_u32 = |data: &[u8], offset| { + u32::from_be_bytes(data[offset..offset + 4].try_into().unwrap()) as usize + }; + + let ifd_max = read_u32(&mdebug_section.data, 18 * 4); + let cb_fd_offset = read_u32(&mdebug_section.data, 19 * 4); + let cb_sym_offset = read_u32(&mdebug_section.data, 9 * 4); + let cb_ss_offset = read_u32(&mdebug_section.data, 15 * 4); + + for i in 0..ifd_max { + let offset = cb_fd_offset + 18 * 4 * i; + let iss_base = read_u32(&objfile.data, offset + 2 * 4); + let isym_base = read_u32(&objfile.data, offset + 4 * 4); + let csym = read_u32(&objfile.data, offset + 5 * 4); + let mut scope_level = 0; + + for j in 0..csym { + let offset2 = cb_sym_offset + 12 * (isym_base + j); + let iss = read_u32(&objfile.data, offset2); + let value = read_u32(&objfile.data, offset2 + 4); + let st_sc_index = read_u32(&objfile.data, offset2 + 8); + let st = st_sc_index >> 26; + let sc = (st_sc_index >> 21) & 0x1F; + + if st == MIPS_DEBUG_ST_STATIC || st == MIPS_DEBUG_ST_STATIC_PROC { + let symbol_name_offset = cb_ss_offset + iss_base + iss; + let symbol_name_offset_end = objfile_data[symbol_name_offset..] + .iter() + .position(|x| *x == 0) + .expect("bad .mdebug strtab reference") + + symbol_name_offset; + let mut symbol_name = + objfile_data[symbol_name_offset..symbol_name_offset_end].to_owned(); + if scope_level > 1 { + // For in-function statics, append an increasing counter to + // the name, to avoid duplicate conflicting symbols. + let count = static_name_count.get(&symbol_name).unwrap_or(&0) + 1; + static_name_count.insert(symbol_name.clone(), count); + symbol_name.extend(format!(":{}", count).as_bytes()); + } + let mut emitted_symbol_name = symbol_name.clone(); + if convert_statics == ConvertStatics::GlobalWithFilename { + // Change the emitted symbol name to include the filename, + // but don't let that affect deduplication logic (we still + // want to be able to reference statics from GLOBAL_ASM). + let mut new_name = objfile_path.to_string_lossy().into_owned().into_bytes(); + new_name.push(b':'); + new_name.extend(emitted_symbol_name); + emitted_symbol_name = new_name; + }; + let section_name = match sc { + 1 => ".text", + 2 => ".data", + 3 => ".bss", + 15 => ".rodata", + _ => { + return Err(anyhow::anyhow!("unsupported MIPS_DEBUG_SC value: {}", sc)); + } + }; + let Some(section) = objfile.find_section(section_name) else { + panic!( + "couldn't find section referenced from .mdebug: {}", + section_name + ); + }; + let symtype = if sc == 1 { STT_FUNC } else { STT_OBJECT }; + let binding = match convert_statics { + ConvertStatics::Global | ConvertStatics::GlobalWithFilename => STB_GLOBAL, + _ => STB_LOCAL, + }; + let sym = Symbol { + st_name: strtab_index, + st_value: value, + st_size: 0, + st_bind: binding, + st_type: symtype, + st_visibility: STV_DEFAULT, + st_shndx: section.index, + name: symbol_name, + }; + strtab_index += emitted_symbol_name.len() + 1; + new_strtab_data.extend(&emitted_symbol_name); + new_strtab_data.push(b'\0'); + new_syms.push((sym, vec![])); + } + match st { + MIPS_DEBUG_ST_FILE + | MIPS_DEBUG_ST_STRUCT + | MIPS_DEBUG_ST_UNION + | MIPS_DEBUG_ST_ENUM + | MIPS_DEBUG_ST_BLOCK + | MIPS_DEBUG_ST_PROC + | MIPS_DEBUG_ST_STATIC_PROC => { + scope_level += 1; + } + MIPS_DEBUG_ST_END => { + scope_level -= 1; + } + _ => {} + } + } + assert_eq!(scope_level, 0); + } + + objfile.sym_strtab_mut().data.extend(new_strtab_data); + } + + // Get rid of duplicate symbols, favoring ones that are not UNDEF. + // Skip this for unnamed local symbols though. + new_syms.sort_by(|(a, _), (b, _)| { + if a.st_shndx != SHN_UNDEF && b.st_shndx == SHN_UNDEF { + Ordering::Less + } else { + Ordering::Greater + } + }); + + let new_syms_prev = new_syms; + let mut new_syms = vec![]; + let mut name_to_sym = HashMap::new(); + for (mut s, inds) in new_syms_prev { + if s.name == b"_gp_disp" { + s.st_type = STT_OBJECT; + } + if s.st_bind == STB_LOCAL && s.st_shndx == SHN_UNDEF { + return Err(anyhow::anyhow!( + "local symbol \"{}\" is undefined", + String::from_utf8_lossy(&s.name) + )); + } + if s.name.is_empty() { + if s.st_bind != STB_LOCAL { + return Err(anyhow::anyhow!("global symbol with no name")); + } + new_syms.push((s.clone(), inds)); + } else { + match name_to_sym.get(&s.name) { + None => { + name_to_sym.insert(s.name.clone(), new_syms.len()); + new_syms.push((s.clone(), inds)); + } + Some(&existing) => { + let (s2, inds2) = &mut new_syms[existing]; + if s.st_shndx != SHN_UNDEF + && !(s2.st_shndx == s.st_shndx && s2.st_value == s.st_value) + { + return Err(anyhow::anyhow!( + "symbol \"{}\" defined twice", + String::from_utf8_lossy(&s.name) + )); + } + inds2.extend(inds); + } + } + } + } + + // Put local symbols in front, with the initial dummy entry first, and + // _gp_disp at the end if it exists. + new_syms.insert(0, (empty_symbol.clone(), vec![])); + new_syms.sort_by_key(|(a, _)| (a.st_bind != STB_LOCAL, a.name == b"_gp_disp")); + + let mut obj_new_index: HashMap<usize, usize> = HashMap::new(); + let mut asm_new_index: HashMap<usize, usize> = HashMap::new(); + + for (new_index, (_, inds)) in new_syms.iter().enumerate() { + for i in inds { + match i { + SymInd::Obj(i) => obj_new_index.insert(*i, new_index), + SymInd::Asm(i) => asm_new_index.insert(*i, new_index), + }; + } + } + + let new_syms: Vec<_> = new_syms.iter().map(|(s, _)| s).collect(); + let num_local_syms = new_syms.iter().filter(|s| s.st_bind == STB_LOCAL).count(); + let new_sym_data: Vec<u8> = new_syms.iter().flat_map(|s| s.to_bin()).collect(); + + objfile.symtab_mut().data = new_sym_data; + objfile.symtab_mut().header.sh_info = num_local_syms as u32; + + // Fix up relocation symbol references + for sectype in OUTPUT_SECTIONS { + let target = objfile.find_section(sectype.as_str()).cloned(); + + if let Some(target) = target { + // fixup relocation symbol indices, since we butchered them above + for reltab in target.relocated_by.iter() { + let reltab = &mut objfile.sections[*reltab]; + let mut nrels = vec![]; + for rel in reltab.relocations.iter() { + let mut rel = rel.clone(); + if (sectype == OutputSection::Text + && modified_text_positions.contains(&rel.r_offset)) + || (sectype == OutputSection::Rodata + && jtbl_rodata_positions.contains(&rel.r_offset)) + { + // don't include relocations for late_rodata dummy code + continue; + } + rel.sym_index = obj_new_index[&rel.sym_index]; + nrels.push(rel); + } + reltab.data = nrels.iter().flat_map(|x| x.to_bin(endian)).collect(); + reltab.relocations = nrels; + } + } + } + + // Move over relocations + for sectype in INPUT_SECTION_NAMES.iter() { + if let Some(source) = asm_objfile.find_section(sectype) { + if source.data.is_empty() { + continue; + } + + let target_sectype = if *sectype == ".late_rodata" { + ".rodata" + } else { + sectype + }; + let target_index = objfile + .find_section(target_sectype) + .expect("didn't find target section") + .index; + for reltab in &source.relocated_by { + let reltab = &mut asm_objfile.sections[*reltab].clone(); + for rel in &mut reltab.relocations { + rel.sym_index = asm_new_index[&rel.sym_index]; + if *sectype == ".late_rodata" { + rel.r_offset = moved_late_rodata[&rel.r_offset]; + } + } + let new_data: Vec<u8> = reltab + .relocations + .iter() + .flat_map(|x| x.to_bin(endian)) + .collect(); + + let (prefix, sh_entsize) = if reltab.header.sh_type == SHT_REL { + (".rel", 8) + } else { + (".rela", 12) + }; + let rel_section_name = format!("{}{}", prefix, target_sectype); + + if let Some(target_reltab) = objfile.find_section_mut(&rel_section_name) { + target_reltab.data.extend(new_data); + } else { + objfile.add_section( + &rel_section_name, + &HeaderFields { + sh_type: reltab.header.sh_type, + sh_flags: 0, + sh_link: objfile.symtab().index as u32, + sh_info: target_index as u32, + sh_addralign: 4, + sh_entsize, + }, + &new_data, + endian, + ); + } + } + } + } + + let mut file = std::fs::File::create(objfile_path).expect("unable to write to .o file"); + let mut writer = BufWriter::new(&mut file); + objfile.write(&mut writer)?; + + fs::remove_file(s_file_path)?; + fs::remove_file(o_file_path)?; + Ok(()) +} diff --git a/tools/asm-processor/rust/src/preprocess.rs b/tools/asm-processor/rust/src/preprocess.rs new file mode 100644 index 0000000..73af653 --- /dev/null +++ b/tools/asm-processor/rust/src/preprocess.rs @@ -0,0 +1,969 @@ +use std::{fs, io::Write, iter, path::Path, sync::OnceLock}; + +use anyhow::Result; +use enum_map::{Enum, EnumMap}; +use regex_lite::Regex; + +use crate::{AsmProcArgs, CompileOpts, Encoding, Function, OptLevel, OutputSection}; + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Enum)] +enum InputSection { + Text, + Data, + Rodata, + LateRodata, + Bss, +} + +impl InputSection { + fn from_str(name: &str) -> Option<InputSection> { + match name { + ".text" => Some(InputSection::Text), + ".data" => Some(InputSection::Data), + ".rodata" => Some(InputSection::Rodata), + ".late_rodata" => Some(InputSection::LateRodata), + ".bss" => Some(InputSection::Bss), + _ => None, + } + } +} + +#[derive(Clone, Debug)] +struct GlobalState { + late_rodata_hex: u32, + valuectr: usize, + namectr: usize, + min_instr_count: usize, + skip_instr_count: usize, + use_jtbl_for_rodata: bool, + prelude_if_late_rodata: usize, + mips1: bool, + pascal: bool, +} + +impl GlobalState { + fn new( + min_instr_count: usize, + skip_instr_count: usize, + use_jtbl_for_rodata: bool, + prelude_if_late_rodata: usize, + mips1: bool, + pascal: bool, + ) -> Self { + Self { + // A value that hopefully never appears as a 32-bit rodata constant (or we + // miscompile late rodata). Increases by 1 in each step. + late_rodata_hex: 0xE0123456, + valuectr: 0, + namectr: 0, + min_instr_count, + skip_instr_count, + use_jtbl_for_rodata, + prelude_if_late_rodata, + mips1, + pascal, + } + } + + fn next_late_rodata_hex(&mut self) -> [u8; 4] { + let dummy_bytes = self.late_rodata_hex.to_be_bytes(); + if (self.late_rodata_hex & 0xffff) == 0 { + // Avoid lui + self.late_rodata_hex += 1; + } + self.late_rodata_hex += 1; + dummy_bytes + } + + fn make_name(&mut self, cat: &str) -> String { + self.namectr += 1; + format!("_asmpp_{}{}", cat, self.namectr) + } + + fn func_prologue(&self, name: &str) -> String { + if self.pascal { + [ + &format!("procedure {}();", name), + "type", + " pi = ^integer;", + " pf = ^single;", + " pd = ^double;", + "var", + " vi: pi;", + " vf: pf;", + " vd: pd;", + "begin", + " vi := vi;", + " vf := vf;", + " vd := vd;", + ] + .join(" ") + } else { + format!("void {}(void) {{", name) + } + } + + fn func_epilogue(&self) -> String { + if self.pascal { + "end;".to_string() + } else { + '}'.to_string() + } + } + + fn pascal_assignment_float(&mut self, val: f32) -> String { + self.valuectr += 1; + let address = (8 * self.valuectr) & 0x7FFF; + format!("vf := pf({}); vf^ := {:?};", address, val) + } + + fn pascal_assignment_double(&mut self, val: f64) -> String { + self.valuectr += 1; + let address = (8 * self.valuectr) & 0x7FFF; + format!("vd := pd({}); vd^ := {:?};", address, val) + } + + fn pascal_assignment_int(&mut self, val: i32) -> String { + self.valuectr += 1; + let address = (8 * self.valuectr) & 0x7FFF; + format!("vi := pi({}); vi^ := {};", address, val) + } +} + +#[derive(Clone, Debug)] +struct GlobalAsmBlock { + fn_desc: String, + cur_section: InputSection, + asm_conts: Vec<String>, + late_rodata_asm_conts: Vec<String>, + late_rodata_alignment: usize, + late_rodata_alignment_from_context: bool, + text_glabels: Vec<String>, + fn_section_sizes: EnumMap<InputSection, usize>, + fn_ins_inds: Vec<(usize, usize)>, + glued_line: String, + num_lines: usize, +} + +impl GlobalAsmBlock { + fn new(fn_desc: String) -> Self { + Self { + fn_desc, + cur_section: InputSection::Text, + asm_conts: vec![], + late_rodata_asm_conts: vec![], + late_rodata_alignment: 0, + late_rodata_alignment_from_context: false, + text_glabels: vec![], + fn_section_sizes: EnumMap::default(), + fn_ins_inds: vec![], + glued_line: String::new(), + num_lines: 0, + } + } + + fn fail_without_line<T>(&self, msg: &str) -> Result<T> { + Err(anyhow::anyhow!("{}\nwithin {}", msg, self.fn_desc)) + } + + fn fail_at_line<T>(&self, msg: &str, line: &str) -> Result<T> { + Err(anyhow::anyhow!( + "{}\nwithin {} at line {}", + msg, + self.fn_desc, + line + )) + } + + fn count_quoted_size( + &self, + line: &str, + z: bool, + real_line: &str, + output_enc: &Encoding, + ) -> Result<usize> { + let line = output_enc.encode(line)?; + + let mut in_quote = false; + let mut has_comma = true; + let mut num_parts = 0; + let mut ret = 0; + let mut i = 0; + let digits = b"0123456789"; // 0-7 would be more sane, but this matches GNU as + let hexdigits = b"0123456789abcdefABCDEF"; + + while i < line.len() { + let c = line[i]; + i += 1; + if !in_quote { + if c == b'"' { + in_quote = true; + if z && !has_comma { + return self.fail_at_line(".asciiz with glued strings is not supported due to GNU as version diffs", real_line); + } + num_parts += 1; + } else if c == b',' { + has_comma = true; + } + } else { + if c == b'"' { + in_quote = false; + has_comma = false; + continue; + } + ret += 1; + if c != b'\\' { + continue; + } + if i == line.len() { + return self.fail_at_line("backslash at end of line not supported", real_line); + } + let c = line[i]; + i += 1; + // (if c is in "bfnrtv", we have a real escaped literal) + if c == b'x' { + // hex literal, consume any number of hex chars, possibly none + while i < line.len() && hexdigits.contains(&line[i]) { + i += 1; + } + } else if digits.contains(&c) { + // octal literal, consume up to two more digits + let mut it = 0; + while i < line.len() && digits.contains(&line[i]) && it < 2 { + i += 1; + it += 1; + } + } + } + } + + if in_quote { + return self.fail_at_line("unterminated string literal", real_line); + } + if num_parts == 0 { + return self.fail_at_line(".ascii with no string", real_line); + } + Ok(ret + if z { num_parts } else { 0 }) + } + + fn align(&mut self, n: usize) { + let size = &mut self.fn_section_sizes[self.cur_section]; + while *size % n != 0 { + *size += 1; + } + } + + fn add_sized(&mut self, size: isize, line: &str) -> Result<()> { + if (self.cur_section == InputSection::Text || self.cur_section == InputSection::LateRodata) + && size % 4 != 0 + { + return self.fail_at_line("size must be a multiple of 4", line); + } + + if size < 0 { + return self.fail_at_line("size cannot be negative", line); + } + + self.fn_section_sizes[self.cur_section] += size as usize; + + if self.cur_section == InputSection::Text { + if self.text_glabels.is_empty() { + return self.fail_at_line(".text block without an initial glabel", line); + } + self.fn_ins_inds + .push((self.num_lines - 1, size as usize / 4)); + } + + Ok(()) + } + + fn process_line(&mut self, line: &str, output_enc: &Encoding) -> Result<()> { + self.num_lines += 1; + if let Some(stripped) = line.strip_suffix("\\") { + self.glued_line = format!("{}{}", self.glued_line, stripped); + return Ok(()); + } + let mut line = self.glued_line.clone() + line; + self.glued_line = String::new(); + + static CACHE: OnceLock<(Regex, Regex)> = OnceLock::new(); + let (re_comment_or_string, re_label) = CACHE.get_or_init(|| { + ( + Regex::new(r#"#.*|/\*.*?\*/|"(?:\\.|[^\\"])*""#).unwrap(), + Regex::new(r"^[a-zA-Z0-9_]+:\s*").unwrap(), + ) + }); + + fn re_comment_replacer(caps: ®ex_lite::Captures) -> String { + let s = caps[0].to_string(); + if s.starts_with("/") || s.starts_with("#") { + " ".to_owned() + } else { + s + } + } + + let real_line = line.clone(); + line = re_comment_or_string + .replace_all(&line, re_comment_replacer) + .into_owned(); + line = line.trim().to_string(); + line = re_label.replace_all(&line, "").into_owned(); + let mut changed_section = false; + let mut emitting_double = false; + + if (line.starts_with("glabel ") || line.starts_with("jlabel ")) + && self.cur_section == InputSection::Text + { + self.text_glabels + .push(line.split_whitespace().nth(1).unwrap().to_string()); + } + if line.is_empty() { + // empty line + } else if line.starts_with("glabel ") + || line.starts_with("dlabel ") + || line.starts_with("jlabel ") + || line.starts_with("alabel ") + || line.starts_with("endlabel ") + || line.starts_with("enddlabel ") + || line.starts_with("nonmatching ") + || (!line.contains(" ") && line.ends_with(":")) + { + // label + } else if line.starts_with(".section") + || matches!( + line.as_str(), + ".text" | ".data" | ".rdata" | ".rodata" | ".bss" | ".late_rodata" + ) + { + // section change + self.cur_section = if line == ".rdata" { + InputSection::Rodata + } else { + let first_arg = line.split(',').next().unwrap().to_string(); + let name = first_arg.split_whitespace().last().unwrap(); + match InputSection::from_str(name) { + Some(s) => s, + None => { + return self.fail_at_line("unrecognized .section directive", &real_line) + } + } + }; + + changed_section = true; + } else if line.starts_with(".late_rodata_alignment") { + if self.cur_section != InputSection::LateRodata { + return self.fail_at_line( + ".late_rodata_alignment must occur within .late_rodata section", + &real_line, + ); + } + + let value = line.split_whitespace().nth(1).unwrap().parse::<usize>()?; + if value != 4 && value != 8 { + return self + .fail_at_line(".late_rodata_alignment argument must be 4 or 8", &real_line); + } + if self.late_rodata_alignment != 0 && self.late_rodata_alignment != value { + return self.fail_without_line( + ".late_rodata_alignment alignment assumption conflicts with earlier .double directive. Make sure to provide explicit alignment padding." + ); + } + self.late_rodata_alignment = value; + changed_section = true; + } else if line.starts_with(".incbin") { + let size = line + .split(',') + .next_back() + .unwrap() + .trim() + .parse::<isize>()?; + self.add_sized(size, &real_line)?; + } else if line.starts_with(".word") + || line.starts_with(".gpword") + || line.starts_with(".float") + { + self.align(4); + + self.add_sized(4 * line.split(',').count() as isize, &real_line)?; + } else if line.starts_with(".double") { + self.align(4); + + if self.cur_section == InputSection::LateRodata { + let align8 = self.fn_section_sizes[self.cur_section] % 8; + // Automatically set late_rodata_alignment, so the generated C code uses doubles. + // This gives us correct alignment for the transferred doubles even when the + // late_rodata_alignment is wrong, e.g. for non-matching compilation. + if self.late_rodata_alignment == 0 { + self.late_rodata_alignment = 8 - align8; + self.late_rodata_alignment_from_context = true; + } else if self.late_rodata_alignment != 8 - align8 { + if self.late_rodata_alignment_from_context { + return self.fail_at_line( + "found two .double directives with different start addresses mod 8. Make sure to provide explicit alignment padding.", + &real_line + ); + } else { + return self.fail_at_line( + ".double at address that is not 0 mod 8 (based on .late_rodata_alignment assumption). Make sure to provide explicit alignment padding.\n{}", + &real_line + ); + } + } + + self.add_sized(8 * line.split(',').count() as isize, &real_line)?; + emitting_double = true; + } + } else if line.starts_with(".space") { + let size = line.split_whitespace().nth(1).unwrap().parse::<isize>()?; + self.add_sized(size, &real_line)?; + } else if line.starts_with(".balign") { + let align = line.split_whitespace().nth(1).unwrap().parse::<isize>()?; + if align != 4 { + return self.fail_at_line("only .balign 4 is supported", &real_line); + } + self.align(4); + } else if line.starts_with(".align") { + let align = line.split_whitespace().nth(1).unwrap().parse::<isize>()?; + if align != 2 { + return self.fail_at_line("only .align 2 is supported", &real_line); + } + self.align(4); + } else if line.starts_with(".asci") { + let z = line.starts_with(".asciz") || line.starts_with(".asciiz"); + self.add_sized( + self.count_quoted_size(&line, z, &real_line, output_enc)? as isize, + &real_line, + )?; + } else if line.starts_with(".byte") { + self.add_sized(line.split(',').count() as isize, &real_line)?; + } else if line.starts_with(".half") + || line.starts_with(".hword") + || line.starts_with(".short") + { + self.align(2); + self.add_sized(2 * line.split(',').count() as isize, &real_line)?; + } else if line.starts_with(".size") { + } else if line.starts_with('.') { + return self.fail_at_line("asm directive not supported", &real_line); + } else { + // Unfortunately, macros are hard to support for .rodata -- + // we don't know how how space they will expand to before + // running the assembler, but we need that information to + // construct the C code. So if we need that we'll either + // need to run the assembler twice (at least in some rare + // cases), or change how this program is invoked. + // Similarly, we can't currently deal with pseudo-instructions + // that expand to several real instructions. + if self.cur_section != InputSection::Text { + return self.fail_at_line( + "instruction or macro call in non-.text section? not supported", + &real_line, + ); + } + self.add_sized(4, &real_line)?; + } + + if self.cur_section == InputSection::LateRodata { + if !changed_section { + if emitting_double { + self.late_rodata_asm_conts.push(".align 0".to_string()); + } + self.late_rodata_asm_conts.push(real_line.clone()); + if emitting_double { + self.late_rodata_asm_conts.push(".align 2".to_string()); + } + } + } else { + self.asm_conts.push(real_line.clone()); + } + + Ok(()) + } + + const MAX_FN_SIZE: usize = 100; + + fn finish(&self, state: &mut GlobalState) -> Result<(Vec<String>, Function)> { + let mut src = vec!["".to_owned(); self.num_lines + 1]; + let mut late_rodata_dummy_bytes = vec![]; + let mut jtbl_rodata_size = 0; + let mut late_rodata_fn_output = vec![]; + + let num_instr = self.fn_section_sizes[InputSection::Text] / 4; + + if self.fn_section_sizes[InputSection::LateRodata] > 0 { + // Generate late rodata by emitting unique float constants. + // This requires 3 instructions for each 4 bytes of rodata. + // If we know alignment, we can use doubles, which give 3 + // instructions for 8 bytes of rodata. + let size = self.fn_section_sizes[InputSection::LateRodata] / 4; + let mut skip_next = false; + let mut needs_double = self.late_rodata_alignment != 0; + let mut extra_mips1_nop = false; + let (jtbl_size, jtbl_min_rodata_size) = match (state.pascal, state.mips1) { + (true, true) => (9, 2), + (true, false) => (8, 2), + (false, true) => (11, 5), + (false, false) => (9, 5), + }; + + for i in 0..size { + if skip_next { + skip_next = false; + continue; + } + // Jump tables give 9 instructions (11 with -mips1) for >= 5 words of rodata, + // and should be emitted when: + // - -O2 or -O2 -g3 are used, which give the right codegen + // - we have emitted our first .float/.double (to ensure that we find the + // created rodata in the binary) + // - we have emitted our first .double, if any (to ensure alignment of doubles + // in shifted rodata sections) + // - we have at least 5 words of rodata left to emit (otherwise IDO does not + // generate a jump table) + // - we have at least 10 more instructions to go in this function (otherwise our + // function size computation will be wrong since the delay slot goes unused) + if !needs_double + && state.use_jtbl_for_rodata + && i >= 1 + && size - i >= jtbl_min_rodata_size + && num_instr - late_rodata_fn_output.len() >= jtbl_size + 1 + { + let line = if state.pascal { + let cases: String = (0..(size - i)) + .map(|case| format!("{}: ;", case)) + .collect::<Vec<String>>() + .join("\n"); + format!("case 0 of {} otherwise end;", cases) + } else { + let cases: String = (0..(size - i)) + .map(|case| format!("case {}:", case)) + .collect::<Vec<String>>() + .join(" "); + format!("switch (*(volatile int*)0) {{ {} ; }}", cases) + }; + late_rodata_fn_output.push(line); + late_rodata_fn_output.extend(iter::repeat_n("".to_owned(), jtbl_size - 1)); + jtbl_rodata_size = (size - i) * 4; + extra_mips1_nop = i != 2; + break; + } + + let dummy_bytes = state.next_late_rodata_hex(); + late_rodata_dummy_bytes.push(dummy_bytes); + if self.late_rodata_alignment == 4 * ((i + 1) % 2 + 1) && i + 1 < size { + let dummy_bytes2 = state.next_late_rodata_hex(); + late_rodata_dummy_bytes.push(dummy_bytes2); + let combined = [dummy_bytes, dummy_bytes2].concat().try_into().unwrap(); + let fval = f64::from_be_bytes(combined); + let line = if state.pascal { + state.pascal_assignment_double(fval) + } else { + format!("*(volatile double*)0 = {:?};", fval) + }; + late_rodata_fn_output.push(line); + skip_next = true; + needs_double = false; + if state.mips1 { + // mips1 does not have ldc1/sdc1 + late_rodata_fn_output.push("".to_owned()); + late_rodata_fn_output.push("".to_owned()); + } + extra_mips1_nop = false; + } else { + let fval = f32::from_be_bytes(dummy_bytes); + let line = if state.pascal { + state.pascal_assignment_float(fval) + } else { + format!("*(volatile float*)0 = {:?}f;", fval) + }; + late_rodata_fn_output.push(line); + extra_mips1_nop = true; + } + late_rodata_fn_output.push("".to_owned()); + late_rodata_fn_output.push("".to_owned()); + } + + if state.mips1 && extra_mips1_nop { + late_rodata_fn_output.push("".to_owned()); + } + } + + let mut text_name = None; + if self.fn_section_sizes[InputSection::Text] > 0 || !late_rodata_fn_output.is_empty() { + let new_name = state.make_name("func"); + src[0] = state.func_prologue(&new_name); + text_name = Some(new_name); + src[self.num_lines] = state.func_epilogue(); + let instr_count = self.fn_section_sizes[InputSection::Text] / 4; + if instr_count < state.min_instr_count { + return self.fail_without_line("too short .text block"); + } + let mut tot_emitted = 0; + let mut tot_skipped = 0; + let mut fn_emitted = 0; + let mut fn_skipped = 0; + let mut skipping = true; + let mut rodata_stack: Vec<String> = late_rodata_fn_output.clone(); + rodata_stack.reverse(); + + for &(line, count) in &self.fn_ins_inds { + for _ in 0..count { + if fn_emitted > Self::MAX_FN_SIZE + && instr_count - tot_emitted > state.min_instr_count + && (rodata_stack.is_empty() || !rodata_stack.last().unwrap().is_empty()) + { + // Don't let functions become too large. When a function reaches 284 + // instructions, and -O2 -framepointer flags are passed, the IRIX + // compiler decides it is a great idea to start optimizing more. + // Also, Pascal cannot handle too large functions before it runs out + // of unique statements to write. + fn_emitted = 0; + fn_skipped = 0; + skipping = true; + let large_func_name = state.make_name("large_func"); + src[line] += &format!( + " {} {} ", + state.func_epilogue(), + state.func_prologue(&large_func_name) + ); + } + + let skip_for_late_rodata = if !rodata_stack.is_empty() { + state.prelude_if_late_rodata + } else { + 0 + }; + if skipping && fn_skipped < state.skip_instr_count + skip_for_late_rodata { + fn_skipped += 1; + tot_skipped += 1; + } else { + skipping = false; + if let Some(entry) = rodata_stack.pop() { + src[line] += &entry; + } else if state.pascal { + src[line] += &state.pascal_assignment_int(0); + } else { + src[line] += "*(volatile int*)0 = 0;"; + } + } + tot_emitted += 1; + fn_emitted += 1; + } + } + + if !rodata_stack.is_empty() { + let size = late_rodata_fn_output.len() / 3; + let available = instr_count - tot_skipped; + return self.fail_without_line(&format!( + "late rodata to text ratio is too high: {} / {} must be <= 1/3\n + add .late_rodata_alignment (4|8) to the .late_rodata block + to double the allowed ratio.", + size, available + )); + } + } + + let mut rodata_name = None; + if self.fn_section_sizes[InputSection::Rodata] > 0 { + if state.pascal { + return self.fail_without_line(".rodata isn't supported with Pascal for now"); + } + let new_name = state.make_name("rodata"); + src[self.num_lines] += &format!( + " const char {}[{}] = {{1}};", + new_name, + self.fn_section_sizes[InputSection::Rodata] + ); + rodata_name = Some(new_name); + } + + let mut data_name = None; + if self.fn_section_sizes[InputSection::Data] > 0 { + let new_name = state.make_name("data"); + let line = if state.pascal { + format!( + " var {}: packed array[1..{}] of char := [otherwise: 0];", + new_name, + self.fn_section_sizes[InputSection::Data] + ) + } else { + format!( + " char {}[{}] = {{1}};", + new_name, + self.fn_section_sizes[InputSection::Data] + ) + }; + src[self.num_lines] += &line; + data_name = Some(new_name); + } + + let mut bss_name = None; + if self.fn_section_sizes[InputSection::Bss] > 0 { + let new_name = state.make_name("bss"); + if state.pascal { + return self.fail_without_line(".bss isn't supported with Pascal for now"); + } + src[self.num_lines] += &format!( + " char {}[{}];", + new_name, + self.fn_section_sizes[InputSection::Bss] + ); + bss_name = Some(new_name); + } + + let mut data = EnumMap::default(); + data[OutputSection::Text] = (text_name, self.fn_section_sizes[InputSection::Text]); + data[OutputSection::Data] = (data_name, self.fn_section_sizes[InputSection::Data]); + data[OutputSection::Rodata] = (rodata_name, self.fn_section_sizes[InputSection::Rodata]); + data[OutputSection::Bss] = (bss_name, self.fn_section_sizes[InputSection::Bss]); + let ret_fn = Function { + text_glabels: self.text_glabels.clone(), + asm_conts: self.asm_conts.clone(), + late_rodata_dummy_bytes, + jtbl_rodata_size, + late_rodata_asm_conts: self.late_rodata_asm_conts.clone(), + fn_desc: self.fn_desc.clone(), + data, + }; + + Ok((src, ret_fn)) + } +} + +/// Convert a float string to its hexadecimal representation +fn repl_float_hex(cap: ®ex_lite::Captures) -> String { + let float_str = cap[0].trim().trim_end_matches('f'); + let float_val = float_str.parse::<f32>().unwrap(); + let hex_val = f32::to_be_bytes(float_val); + format!("{}", u32::from_be_bytes(hex_val)) +} + +pub(crate) struct ParseSourceResult { + pub functions: Vec<Function>, + pub deps: Vec<String>, + pub output: Vec<u8>, +} + +pub(crate) fn parse_source( + infile_path: &Path, + args: &AsmProcArgs, + opts: &CompileOpts, + encode: bool, +) -> Result<ParseSourceResult> { + let (mut min_instr_count, mut skip_instr_count) = match opts.opt { + OptLevel::O0 => match opts.framepointer { + true => (8, 8), + false => (4, 4), + }, + OptLevel::O1 | OptLevel::O2 => match opts.framepointer { + true => (6, 5), + false => (2, 1), + }, + OptLevel::G => match opts.framepointer { + true => (7, 7), + false => (4, 4), + }, + OptLevel::G3 => match opts.framepointer { + true => (4, 4), + false => (2, 2), + }, + }; + + let mut prelude_if_late_rodata = 0; + if opts.kpic { + // Without optimizations, the PIC prelude always takes up 3 instructions. + // With optimizations, the prelude is optimized out if there's no late rodata. + if matches!(opts.opt, OptLevel::O2 | OptLevel::G3) { + prelude_if_late_rodata = 3; + } else { + min_instr_count += 3; + skip_instr_count += 3; + } + } + + let use_jtbl_for_rodata = + matches!(opts.opt, OptLevel::O2 | OptLevel::G3) && !opts.framepointer && !opts.kpic; + + let mut state = GlobalState::new( + min_instr_count, + skip_instr_count, + use_jtbl_for_rodata, + prelude_if_late_rodata, + opts.mips1, + opts.pascal, + ); + let input_enc = &args.input_enc; + let output_enc = &args.output_enc; + let mut global_asm: Option<(GlobalAsmBlock, usize)> = None; + let mut asm_functions: Vec<Function> = vec![]; + let mut output_lines: Vec<String> = vec![format!("#line 1 \"{}\"", infile_path.display())]; + let mut deps: Vec<String> = vec![]; + + let mut is_cutscene_data = false; + let mut is_early_include = false; + + let cutscene_re = Regex::new(r"CutsceneData (.|\n)*\[\] = \{")?; + let float_re = Regex::new(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?f")?; + + let file_contents = fs::read(infile_path)?; + for (line_no, line) in input_enc.decode(&file_contents)?.lines().enumerate() { + let line_no = line_no + 1; + let mut raw_line = line.trim().to_owned(); + let line = raw_line.trim_start(); + + // Print exactly one output line per source line, to make compiler + // errors have correct line numbers. These will be overridden with + // reasonable content further down. + output_lines.push("".to_owned()); + + if let Some((ref mut gasm, start_index)) = global_asm { + if line.starts_with(')') { + let (mut src, fun) = gasm.finish(&mut state)?; + if state.pascal { + // Pascal has a 1600-character line length limit, so some + // of the lines we emit may be broken up. Correct for that + // using a #line directive. + *src.last_mut().unwrap() += &format!("\n#line {}", line_no + 1); + } + for (i, line2) in src.iter().enumerate() { + output_lines[start_index + i] = line2.clone(); + } + asm_functions.push(fun); + global_asm = None; + } else { + gasm.process_line(&raw_line, output_enc)?; + } + } else if line == "GLOBAL_ASM(" || line == "#pragma GLOBAL_ASM(" { + global_asm = Some(( + GlobalAsmBlock::new(format!("GLOBAL_ASM block at line {}", &line_no.to_string())), + output_lines.len(), + )); + } else if ((line.starts_with("GLOBAL_ASM(\"") || line.starts_with("#pragma GLOBAL_ASM(\"")) + && line.ends_with("\")")) + || ((line.starts_with("INCLUDE_ASM(\"") || line.starts_with("INCLUDE_RODATA(\"")) + && line.contains("\",") + && line.ends_with(");")) + { + let (prologue, fname) = if line.starts_with("INCLUDE_") { + // INCLUDE_ASM("path/to", functionname); + let (before, after) = line.split_once("\",").unwrap(); + let fname = format!( + "{}/{}.s", + before[before.find('(').unwrap() + 2..].to_owned(), + after.trim()[..after.len() - 3].trim() + ); + + if line.starts_with("INCLUDE_RODATA") { + (vec![".section .rodata".to_string()], fname) + } else { + (vec![], fname) + } + } else { + // GLOBAL_ASM("path/to/file.s") + let fname = line[line.find('(').unwrap() + 2..line.len() - 2].to_string(); + (vec![], fname) + }; + + let mut gasm = GlobalAsmBlock::new(fname.clone()); + for line2 in prologue { + gasm.process_line(line2.trim_end(), output_enc)?; + } + + if !Path::new(&fname).exists() { + // The GLOBAL_ASM block might be surrounded by an ifdef, so it's + // not clear whether a missing file actually represents a compile + // error. Pass the responsibility for determining that on to the + // compiler by emitting a bad include directive. (IDO treats + // #error as a warning for some reason.) + let output_lines_len = output_lines.len(); + output_lines[output_lines_len - 1] = format!("#include \"GLOBAL_ASM:{}\"", fname); + continue; + } + + let file_contents = fs::read(&fname)?; + for line2 in input_enc.decode(&file_contents)?.lines() { + gasm.process_line(line2.trim_end(), output_enc)?; + } + + let (mut src, fun) = gasm.finish(&mut state)?; + let output_lines_len = output_lines.len(); + if state.pascal { + // Pascal has a 1600-character line length limit, so avoid putting + // everything on the same line. + src.push(format!("#line {}", line_no + 1)); + output_lines[output_lines_len - 1] = src.join("\n"); + } else { + output_lines[output_lines_len - 1] = src.join(""); + } + asm_functions.push(fun); + deps.push(fname); + } else if line == "#pragma asmproc recurse" { + // C includes qualified as + // #pragma asmproc recurse + // #include "file.c" + // will be processed recursively when encountered + is_early_include = true; + } else if is_early_include { + // Previous line was a #pragma asmproc recurse + is_early_include = false; + if !line.starts_with("#include ") { + return Err(anyhow::anyhow!( + "#pragma asmproc recurse must be followed by an #include " + )); + } + let fpath = infile_path.parent().unwrap(); + let fname = fpath.join(line[line.find(' ').unwrap() + 2..].trim()); + deps.push(fname.to_str().unwrap().to_string()); + let mut res = parse_source(&fname, args, opts, false)?; + deps.append(&mut res.deps); + let res_str = format!( + "{}#line {} \"{}\"", + String::from_utf8(res.output).expect("nested calls generate utf-8"), + line_no + 1, + infile_path.file_name().unwrap().to_str().unwrap() + ); + + let output_lines_len = output_lines.len(); + output_lines[output_lines_len - 1] = res_str; + } else { + if args.encode_cutscene_data_floats { + // This is a hack to replace all floating-point numbers in an array of a particular type + // (in this case CutsceneData) with their corresponding IEEE-754 hexadecimal representation + if cutscene_re.is_match(line) { + is_cutscene_data = true; + } else if line.ends_with("};") { + is_cutscene_data = false; + } + + if is_cutscene_data { + raw_line = float_re.replace_all(&raw_line, repl_float_hex).into_owned(); + } + } + let output_lines_len = output_lines.len(); + output_lines[output_lines_len - 1] = raw_line.to_owned(); + } + } + + let out_data = if encode { + let newline_encoded = output_enc.encode("\n")?; + + let mut data = vec![]; + for line in output_lines { + let line_encoded = output_enc.encode(&line)?; + data.write_all(&line_encoded)?; + data.write_all(&newline_encoded)?; + } + data + } else { + let str = format!("{}\n", output_lines.join("\n")); + + str.as_bytes().to_vec() + }; + + Ok(ParseSourceResult { + functions: asm_functions, + deps, + output: out_data, + }) +} |
