Skip to main content

remu/
main.rs

1//! The CLI builds one guest process and keeps runtime policy outside the JIT library.
2//!
3//! RISC-V instruction lengths: https://docs.riscv.org/reference/isa/unpriv/intro.html#base-instruction-length-encoding
4//! RISC-V register names: https://riscv-non-isa.github.io/riscv-elf-psabi-doc/#_integer_register_convention
5
6use chrono::Local;
7use clap::Parser;
8
9use jit::{ExitReason, GuestMemory, GuestState, JitRuntime, ProgramImage, RemuConfig};
10use lifter::{MemoryView, RiscVArch, lift_block};
11
12use std::env;
13use std::fs;
14use std::io::{self, Write as _};
15use std::path::{Path, PathBuf};
16use std::sync::atomic::Ordering;
17
18#[derive(Parser, Debug)]
19#[command(name = "remu")]
20// Keep all host and guest inputs explicit so a run can be reproduced from its command line.
21struct Cli {
22    // A path type preserves host path syntax and avoids a later text-to-path conversion.
23    input_file: PathBuf,
24
25    #[arg(long)]
26    // Print the address of one ELF symbol for entry-point diagnostics.
27    entry_symbol: Option<String>,
28
29    #[arg(long)]
30    // Skip early compilation so all regions use the lazy JIT path.
31    no_aot: bool,
32
33    #[arg(long)]
34    // Supply the guest root for absolute paths and the ELF interpreter.
35    sysroot: Option<PathBuf>,
36
37    #[arg(long)]
38    // Supply the writable guest directory for relative paths.
39    workdir: Option<PathBuf>,
40
41    #[arg(short, long)]
42    // Show compiler and cache activity that normal guest output does not need.
43    verbose: bool,
44
45    #[arg(long)]
46    // Stop before execution and enable block-level debugger commands.
47    debug: bool,
48
49    #[arg(
50        long,
51        default_value_t = 2,
52        value_parser = clap::value_parser!(u8).range(0..=2)
53    )]
54    // The range matches the three optimization levels that this JIT runtime supports.
55    // The default value 2 gives generated code the highest enabled optimization level.
56    jit_opt_level: u8,
57
58    #[arg(long)]
59    // Use per-run artifacts only, which prevents reuse of old compiled regions.
60    no_jit_cache: bool,
61
62    // todo -- wip
63    #[arg(long)]
64    // Use the Linux x86-64 raw loader instead of a host shared library.
65    raw_jit: bool,
66
67    // `last` keeps guest flags separate from emulator flags after `--`.
68    #[arg(last = true)]
69    // Pass all values after `--` to the guest process without CLI interpretation.
70    guest_args: Vec<String>,
71}
72
73/*
74
75 --- todo ----
76
77- config flags
78- tracing ?
79- https://docs.rs/clap/latest/clap/
80- mehr dgb/prints für nnil sachen
81*/
82
83fn main() {
84    // `clap` converts command-line text to typed values before runtime setup starts.
85    let cli = Cli::parse();
86
87    println!("\n--------- remu ---------");
88
89    // Create a per-run directory before JIT work so all generated artifacts stay together.
90    let output_dir = match create_output_dir(&cli.input_file) {
91        Ok(path) => path,
92        Err(err) => {
93            eprintln!("error while creating output dir: {err}");
94            return;
95        }
96    };
97
98    // A `match` handles the error here because `main` returns no `Result` to the operating system.
99    let image = match fs::read(&cli.input_file) {
100        Ok(bytes) => bytes,
101        Err(err) => {
102            eprintln!(
103                "error while reading input file {}: {err}",
104                cli.input_file.display()
105            );
106            return;
107        }
108    };
109
110    let program = match ProgramImage::from_elf_bytes(&image) {
111        Ok(program) => program,
112        Err(err) => {
113            eprintln!("error while loading ELF: {err}");
114            return;
115        }
116    };
117
118    println!("\n\n----- ELF loaded -----");
119    println!("Input: {}", cli.input_file.display());
120    println!("Output dir: {}", output_dir.display());
121    println!("Entry point: 0x{:x}", program.entry_pc);
122
123    // `if let` enters this block only when both optional values exist.
124    if let Some(value) = &cli.entry_symbol
125        && let Some(entry_sym) = program.find_symbol(value)
126    {
127        println!("{} symbol gefunden: 0x{:x}", value, entry_sym.value);
128        // todo - start ab diesem entry point
129    }
130
131    // Guest argv[0] uses a guest-visible name instead of exposing the host input path.
132    let mut argv = vec![input_basename(&cli.input_file)];
133    argv.extend(cli.guest_args);
134    // `None` tells the JIT to use per-run files and to skip all disk-cache lookup.
135    let jit_cache_dir = if cli.no_jit_cache {
136        None
137    } else {
138        // Use one project cache so separate run directories can reuse compiled regions.
139        match project_root() {
140            Ok(root) => Some(root.join("jit-tmp/cache")),
141            Err(err) => {
142                eprintln!("error while finding project root: {err}");
143                return;
144            }
145        }
146    };
147    let process = RemuConfig {
148        argv,
149        envp: Vec::new(),
150        sysroot: cli.sysroot,
151        workdir: cli.workdir,
152        jit_opt_level: cli.jit_opt_level,
153        jit_cache_dir,
154        raw_jit: cli.raw_jit,
155    };
156
157    let mut runtime =
158        // The closure gives the runtime a lifter without making the JIT crate depend on RISC-V.
159        match JitRuntime::init_runtime(program, process, output_dir.clone(), |memory, pc| {
160            // Fetch through the MMU so the lifter respects execute permissions and mapping changes.
161            let view = MemoryView { memory };
162
163            // Loaded executables use RV64. XLEN, the integer-register width, is therefore 64 bits.
164            match lift_block(RiscVArch::Rv64, &view, pc) {
165                Some(nnil) => Ok(nnil),
166                None => Err(io::Error::other(format!(
167                    "no executable region contains guest pc 0x{pc:x}"
168                ))
169                .into()),
170            }
171        }) {
172            Ok(runtime) => runtime,
173            Err(err) => {
174                eprintln!("error while creating runtime: {err}");
175                return;
176            }
177        };
178
179    runtime.set_verbose(cli.verbose);
180
181    if cli.verbose {
182        println!("AOT precompile: {}", if cli.no_aot { "off" } else { "on" });
183    }
184
185    if !cli.no_aot {
186        // Best-effort precompilation reduces pauses before lazy JIT fallback begins.
187        println!("\n----- pre-compiling as much as possible now -----");
188        let compiled_regions = match runtime.precompile() {
189            Ok(count) => count,
190            Err(err) => {
191                eprintln!("aot precompile failed: {err}");
192                return;
193            }
194        };
195
196        if cli.verbose {
197            println!("amount of precompiled regions: {}", compiled_regions);
198        }
199    }
200
201    println!("\n----- starting execution + JIT now -----");
202
203    let exit_reason = match if cli.debug {
204        run_debugger(&mut runtime)
205    } else {
206        runtime.run()
207    } {
208        Ok(reason) => reason,
209        Err(err) => {
210            eprintln!("oh oh: {err}");
211            return;
212        }
213    };
214
215    let state = runtime.state();
216
217    println!("Exit reason: {exit_reason:?}");
218    println!("pc = 0x{:x}", state.pc);
219    // The psABI uses x10 for return values and x2 for the stack pointer.
220    println!("a0/x10 = {}", state.regs[10]);
221    println!("sp/x2 = 0x{:x}", state.regs[2]);
222    println!("fault_addr = 0x{:x}", state.fault_addr);
223    println!("fault_access = {:?}", state.fault_access);
224    println!("cached regions = {}", runtime.cache_len());
225
226    match exit_reason {
227        ExitReason::UnhandledHostCall => {
228            println!("UnhandledHostCall");
229        }
230        ExitReason::GuestExit => {
231            println!("GuestExit");
232        }
233        _ => {}
234    }
235}
236
237
238// todo - refactor
239fn run_debugger<L>(runtime: &mut JitRuntime<L>) -> Result<ExitReason, Box<dyn std::error::Error>>
240where
241    L: Fn(&GuestMemory, u64) -> Result<liil::nnil::Nnil, Box<dyn std::error::Error>>,
242{
243    // The Ctrl-C crate runs this closure on a dedicated thread.
244    // An atomic flag requests a pause without borrowing or locking the runtime.
245    let pause = runtime.pause_flag();
246    // Relaxed order is sufficient because this atomic communicates only one independent stop flag.
247    // `?` returns the setup error to the caller instead of continuing without Ctrl-C support.
248    ctrlc::set_handler(move || pause.store(true, Ordering::Relaxed))?;
249
250    // Keep a user checkpoint separate from the runtime's private reset snapshot.
251    let mut checkpoint = None;
252    let mut reason = ExitReason::Paused;
253    print_stop(runtime, reason);
254
255    loop {
256        print!("(remu) ");
257        io::stdout().flush()?;
258        let mut line = String::new();
259
260        // A zero-byte read means end-of-file. This also lets a pipe close the debugger cleanly.
261        if io::stdin().read_line(&mut line)? == 0 {
262            return Ok(reason);
263        }
264
265        let mut words = line.split_whitespace();
266        let Some(command) = words.next() else {
267            continue;
268        };
269
270        match command {
271            "continue" | "c" => {
272                // u64::MAX is an effective unlimited block count without a separate run mode.
273                reason = match runtime.run_for_blocks(u64::MAX) {
274                    Ok(reason) => reason,
275                    Err(err) => {
276                        println!("execution error: {err}");
277                        print_stop(runtime, runtime.state().exit_reason);
278                        continue;
279                    }
280                };
281                print_stop(runtime, reason);
282
283                if reason == ExitReason::GuestExit {
284                    return Ok(reason);
285                }
286            }
287            "step" | "s" => {
288                // Generated code checks debugger controls at each basic-block entry.
289                // A limit of one therefore executes one block before it stops.
290                reason = match runtime.run_for_blocks(1) {
291                    Ok(reason) => reason,
292                    Err(err) => {
293                        println!("execution error: {err}");
294                        print_stop(runtime, runtime.state().exit_reason);
295                        continue;
296                    }
297                };
298                print_stop(runtime, reason);
299            }
300            "break" | "b" => {
301                if let Some(value) = words.next() {
302                    match parse_address(runtime.program(), value) {
303                        Some(address) => match runtime.add_breakpoint(address) {
304                            Ok(()) => println!("breakpoint at 0x{address:x}"),
305                            Err(err) => println!("error: {err}"),
306                        },
307                        None => println!("unknown address or symbol: {value}"),
308                    }
309                } else {
310                    for address in runtime.breakpoints() {
311                        println!("0x{address:x}");
312                    }
313                }
314            }
315            "delete" => match words.next() {
316                Some("all") => runtime.clear_breakpoints(),
317                Some(value) => match parse_address(runtime.program(), value) {
318                    Some(address) => runtime.remove_breakpoint(address),
319                    None => println!("unknown address or symbol: {value}"),
320                },
321                None => println!("usage: delete <address|symbol|all>"),
322            },
323            "regs" => print_registers(runtime.state()),
324            "mem" => {
325                let Some(value) = words.next() else {
326                    println!("usage: mem <address> [length]");
327                    continue;
328                };
329                let Some(address) = parse_address(runtime.program(), value) else {
330                    println!("unknown address or symbol: {value}");
331                    continue;
332                };
333                // Show 64 bytes by default. Limit input to 4096 bytes to bound debugger output.
334                let length = words.next().and_then(parse_number).unwrap_or(64).min(4096) as usize;
335                print_memory(runtime.memory(), address, length);
336            }
337            "maps" => {
338                for (start, end, perms) in runtime.memory().mappings() {
339                    // A letter means that its permission bit is set. A dash means that it is clear.
340                    // Sixteen hexadecimal digits show all 64 address bits and keep columns aligned.
341                    println!(
342                        "0x{start:016x}-0x{end:016x} {}{}{}",
343                        if perms.contains(jit::PagePerms::READ) {
344                            'r'
345                        } else {
346                            '-'
347                        },
348                        if perms.contains(jit::PagePerms::WRITE) {
349                            'w'
350                        } else {
351                            '-'
352                        },
353                        if perms.contains(jit::PagePerms::EXEC) {
354                            'x'
355                        } else {
356                            '-'
357                        },
358                    );
359                }
360            }
361            "where" => print_stop(runtime, reason),
362            "nnil" => match runtime.lift_at(runtime.state().pc) {
363                Ok(nnil) => println!("{nnil:?}"),
364                Err(err) => println!("error: {err}"),
365            },
366            "trace" => {
367                for pc in runtime.trace() {
368                    println!("0x{pc:016x}");
369                }
370            }
371            "snapshot" => {
372                checkpoint = Some(runtime.snapshot()?);
373                println!("snapshot saved");
374            }
375            "restore" => match &checkpoint {
376                Some(snapshot) => {
377                    runtime.restore(snapshot)?;
378                    reason = ExitReason::Paused;
379                    print_stop(runtime, reason);
380                }
381                None => println!("wip! no snapshot saved"),
382            },
383            "reset" => {
384                runtime.reset()?;
385                reason = ExitReason::Paused;
386                print_stop(runtime, reason);
387            }
388            "help" | "h" => println!(
389                "ACHTUNG, NOCH WORK-IN-PROGRES. Commands: continue, step, break <address oder symbol>, delete <address oder symbol oder all>, regs, mem <address> [length], maps, where, nnil, trace, snapshot, restore, reset, quit"
390            ),
391            "quit" | "q" => return Ok(reason),
392            _ => println!("unknown command, use help"),
393        }
394    }
395}
396
397fn parse_number(value: &str) -> Option<u64> {
398    // Prefix 0x selects radix 16. Without this prefix, debugger input is decimal.
399    value
400        .strip_prefix("0x")
401        .and_then(|value| u64::from_str_radix(value, 16).ok())
402        .or_else(|| value.parse().ok())
403}
404
405fn parse_address(program: &ProgramImage, value: &str) -> Option<u64> {
406    // Prefer numeric input so a symbol that looks like a number cannot change command meaning.
407    parse_number(value).or_else(|| program.find_symbol(value).map(|symbol| symbol.value))
408}
409
410fn print_stop<L>(runtime: &JitRuntime<L>, reason: ExitReason)
411where
412    L: Fn(&GuestMemory, u64) -> Result<liil::nnil::Nnil, Box<dyn std::error::Error>>,
413{
414    let state = runtime.state();
415    print!("stopped: {reason:?}, pc=0x{:x}", state.pc);
416    if let Some(symbol) = runtime.program().symbol_at(state.pc) {
417        print!(" <{}+0x{:x}>", symbol.name, state.pc - symbol.value);
418    }
419    println!();
420
421    // Read the first halfword because its low bits select the RISC-V instruction length.
422    if let Some(half) = runtime.memory().fetch_u16(state.pc) {
423        // Instructions of at least 32 bits end in binary 11. This debugger supports the 32-bit form.
424        // Other low-bit values select 16-bit compressed RISC-V (RVC) instructions.
425        // Mask 3 is binary 11, so `half & 3` keeps only these two length bits.
426        if half & 3 == 3 {
427            if let Some(word) = runtime.memory().fetch_u32(state.pc) {
428                // Eight hex digits show all 32 instruction bits, including leading zero bits.
429                println!("instruction: 0x{word:08x}");
430            }
431        } else {
432            // Four hex digits show all 16 compressed-instruction bits.
433            println!("instruction: 0x{half:04x}");
434        }
435    }
436    if state.fault_access != jit::AccessKind::None {
437        println!(
438            "fault: {:?} at 0x{:x}",
439            state.fault_access, state.fault_addr
440        );
441    }
442    if let Some([nr, pc, a0, a1, a2, a3, a4, a5]) = runtime.last_syscall() {
443        // RISC-V Linux puts as many as six syscall arguments in a0 through a5.
444        // Keep the syscall number and PC beside them so a stop report identifies the call site.
445        println!(
446            "last syscall: {nr} at 0x{pc:x} (0x{a0:x}, 0x{a1:x}, 0x{a2:x}, 0x{a3:x}, 0x{a4:x}, 0x{a5:x})"
447        );
448    }
449}
450
451fn print_registers(state: &GuestState) {
452    // The base RISC-V ISA defines 32 integer registers. The psABI gives them these readable names.
453    const NAMES: [&str; 32] = [
454        "zero", "ra", "sp", "gp", "tp", "t0", "t1", "t2", "s0", "s1", "a0", "a1", "a2", "a3", "a4",
455        "a5", "a6", "a7", "s2", "s3", "s4", "s5", "s6", "s7", "s8", "s9", "s10", "s11", "t3", "t4",
456        "t5", "t6",
457    ];
458    // A fixed 16-digit width makes changed bits easy to compare between debugger stops.
459    println!("pc   0x{:016x}", state.pc);
460    for (index, name) in NAMES.iter().enumerate() {
461        println!("x{index:<2} {name:<4} 0x{:016x}", state.regs[index]);
462    }
463    // The runtime stores 32 floating-point registers directly after the 32 integer registers.
464    for index in 0..32 {
465        println!("f{index:<2}      0x{:016x}", state.regs[32 + index]);
466    }
467}
468
469fn print_memory(memory: &GuestMemory, address: u64, length: usize) {
470    // Sixteen bytes per row is a common hex-dump width and keeps addresses easy to scan.
471    for offset in (0..length).step_by(16) {
472        // The last row can contain fewer than 16 bytes.
473        let count = 16.min(length - offset);
474        let Some(bytes) = memory.load_bytes(address + offset as u64, count) else {
475            println!("0x{:016x}: <unreadable>", address + offset as u64);
476            return;
477        };
478        print!("0x{:016x}:", address + offset as u64);
479        for byte in &bytes {
480            // Two hexadecimal digits show all eight bits of one byte, including a leading zero.
481            print!(" {byte:02x}");
482        }
483        println!();
484    }
485}
486
487// hier landen libjit.so und generated.rs
488// todo - generated.rs mergen
489fn create_output_dir(input_file: &Path) -> io::Result<PathBuf> {
490    let root = project_root()?.join("output");
491    fs::create_dir_all(&root)?;
492
493    let file_name = input_basename(input_file);
494    let timestamp = Local::now().format("%Y-%m-%d_%H-%M-%S"); // todo - kann sein dass es mal doppelung gibt!
495
496    // Timestamps separate and order normal runs. Two runs in one second can still reuse a directory.
497    let output_dir = root.join(format!("run-{file_name}-{timestamp}"));
498    fs::create_dir_all(&output_dir)?;
499
500    Ok(output_dir)
501}
502
503fn project_root() -> io::Result<PathBuf> {
504    // Derive the root from the crate location so the current working directory can be a guest workdir.
505    // The first ancestor is `crates/remu`, the second is `crates`, and the third at index 2 is the root.
506    Path::new(env!("CARGO_MANIFEST_DIR"))
507        .ancestors()
508        .nth(2)
509        .map(Path::to_path_buf)
510        .ok_or_else(|| io::Error::other("project root not found!"))
511}
512
513fn input_basename(input_file: &Path) -> String {
514    // Use the complete input only when it has no final file-name component.
515    input_file
516        .file_name()
517        .unwrap_or(input_file.as_os_str())
518        .to_string_lossy()
519        .into_owned()
520}