From 373c4cdc7ea07ac62f81c853cc6791ae998ccba0 Mon Sep 17 00:00:00 2001 From: Philipp Koppe <145699635+pkoppe@users.noreply.github.com> Date: Wed, 27 May 2026 01:12:16 +0200 Subject: [PATCH 1/8] Added benchmarks, Streamer rework, LLVM object caching --- CMakeLists.txt | 5 + bindings/rust/build.rs | 6 +- bindings/rust/examples/benchmark.rs | 213 ++++++++++ examples/benchmark.cpp | 156 ++++++++ include/nyxstone.h | 42 +- src/ELFStreamerWrapper.cpp | 125 ------ src/ELFStreamerWrapper.h | 67 ---- src/FastStreamer.cpp | 13 + src/FastStreamer.h | 54 +++ src/ObjectWriterWrapper.cpp | 333 ---------------- src/ObjectWriterWrapper.h | 134 ------- src/nyxstone.cpp | 587 ++++++++++++++++++++-------- 12 files changed, 900 insertions(+), 835 deletions(-) create mode 100644 bindings/rust/examples/benchmark.rs create mode 100644 examples/benchmark.cpp delete mode 100644 src/ELFStreamerWrapper.cpp delete mode 100644 src/ELFStreamerWrapper.h create mode 100644 src/FastStreamer.cpp create mode 100644 src/FastStreamer.h delete mode 100644 src/ObjectWriterWrapper.cpp delete mode 100644 src/ObjectWriterWrapper.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 67f672a..8998ee5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,11 @@ if(NYXSTONE_BUILD_EXAMPLES) nyxstone::nyxstone ) + add_executable(benchmark examples/benchmark.cpp) + target_link_libraries(benchmark PRIVATE + nyxstone::nyxstone + ) + include(CTest) add_test(NAME TestExample COMMAND $) add_test(NAME TestCLI COMMAND "${CMAKE_CURRENT_LIST_DIR}/tool/test-cli.sh") diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs index ca0ac6a..e426f33 100644 --- a/bindings/rust/build.rs +++ b/bindings/rust/build.rs @@ -10,8 +10,7 @@ const ENV_FORCE_FFI_LINKING: &str = "NYXSTONE_LINK_FFI"; fn main() { let headers = [ "nyxstone/include/nyxstone.h", - "nyxstone/src/ELFStreamerWrapper.h", - "nyxstone/src/ObjectWriterWrapper.h", + "nyxstone/src/FastStreamer.h", "nyxstone/src/Target/AArch64/MCTargetDesc/AArch64FixupKinds.h", "nyxstone/src/Target/AArch64/MCTargetDesc/AArch64MCExpr.h", "src/nyxstone_ffi.hpp", @@ -19,8 +18,7 @@ fn main() { let sources = [ "nyxstone/src/nyxstone.cpp", - "nyxstone/src/ObjectWriterWrapper.cpp", - "nyxstone/src/ELFStreamerWrapper.cpp", + "nyxstone/src/FastStreamer.cpp", "src/nyxstone_ffi.cpp", ]; diff --git a/bindings/rust/examples/benchmark.rs b/bindings/rust/examples/benchmark.rs new file mode 100644 index 0000000..a7b3483 --- /dev/null +++ b/bindings/rust/examples/benchmark.rs @@ -0,0 +1,213 @@ +//! Cross-language counterpart to `examples/benchmark.cpp`. +//! +//! Same architectures, same instructions, same package sizes (1 and 10), same +//! measurement methodology and output layout — so output from this binary can +//! be compared directly against the C++ benchmark (e.g. `diff` after dropping +//! the `[C++]` / `[Rust]` tag on the first line). +//! +//! Run with: `cargo run --release --example benchmark -- [seconds_per_measurement]` + +extern crate anyhow; +extern crate nyxstone; + +use std::env; +use std::time::{Duration, Instant}; + +use anyhow::Result; +use nyxstone::{Nyxstone, NyxstoneConfig}; + +struct ArchConfig { + name: &'static str, + triple: &'static str, + instructions: &'static [&'static str], +} + +const ARCHES: &[ArchConfig] = &[ + ArchConfig { + name: "x86_64", + triple: "x86_64-linux-gnu", + instructions: &[ + "mov rax, rbx", + "xor rax, rax", + "add rsp, 8", + "sub rsp, 8", + "push rbx", + "pop rbx", + "inc rax", + "dec rax", + "and rax, rbx", + "or rax, rbx", + ], + }, + ArchConfig { + name: "x86_32", + triple: "i686-linux-gnu", + instructions: &[ + "mov eax, ebx", + "xor eax, eax", + "add esp, 8", + "sub esp, 8", + "push ebx", + "pop ebx", + "inc eax", + "dec eax", + "and eax, ebx", + "or eax, ebx", + ], + }, + ArchConfig { + name: "aarch64", + triple: "aarch64-linux-gnueabihf", + instructions: &[ + "mov x0, x1", + "add x0, x0, #1", + "sub x0, x0, #1", + "mov x1, x2", + "add x1, x1, #1", + "sub x1, x1, #1", + "mov x2, x3", + "add sp, sp, #16", + "sub sp, sp, #16", + "ret", + ], + }, + ArchConfig { + name: "armv8m", + triple: "armv8m.main-none-eabi", + instructions: &[ + "mov r0, r1", + "add r0, r0, #1", + "sub r0, r0, #1", + "mov r1, r2", + "add r1, r1, #1", + "sub r1, r1, #1", + "mov r2, r3", + "nop", + "push {r0}", + "pop {r0}", + ], + }, +]; + +const PACKAGE_SIZES: &[usize] = &[1, 10]; + +fn make_package(arch: &ArchConfig, package_size: usize) -> String { + let mut out = String::new(); + for i in 0..package_size { + if i > 0 { + out.push_str("; "); + } + out.push_str(arch.instructions[i % arch.instructions.len()]); + } + out +} + +fn run_for_at_least(target: Duration, mut f: F) -> f64 { + for _ in 0..10 { + f(); + } + + let mut count: usize = 0; + let start = Instant::now(); + loop { + for _ in 0..50 { + f(); + count += 1; + } + let elapsed = start.elapsed(); + if elapsed >= target { + return count as f64 / elapsed.as_secs_f64(); + } + } +} + +fn fmt_rate(value: f64) -> String { + let body = if value >= 1e9 { + format!("{:.2} G", value / 1e9) + } else if value >= 1e6 { + format!("{:.2} M", value / 1e6) + } else if value >= 1e3 { + format!("{:.2} k", value / 1e3) + } else { + format!("{:.2} ", value) + }; + format!("{:>10}", body) +} + +fn run(arch: &ArchConfig, target: Duration) { + println!("\n=== {} ({}) ===", arch.name, arch.triple); + + let nyxstone = match Nyxstone::new(arch.triple, NyxstoneConfig::default()) { + Ok(n) => n, + Err(e) => { + eprintln!(" [skipped] {e}"); + return; + } + }; + + for &package_size in PACKAGE_SIZES { + let assembly = make_package(arch, package_size); + + let bytes = match nyxstone.assemble(&assembly, 0x1000) { + Ok(b) => b, + Err(e) => { + eprintln!(" [skipped {package_size}] {e}"); + continue; + } + }; + + let insn_count = nyxstone + .assemble_to_instructions(&assembly, 0x1000) + .map(|v| v.len()) + .unwrap_or(package_size); + + let asm_ops_per_s = run_for_at_least(target, || { + let _ = nyxstone.assemble(&assembly, 0x1000); + }); + let dis_ops_per_s = run_for_at_least(target, || { + let _ = nyxstone.disassemble(&bytes, 0x1000, 0); + }); + + println!( + " Package {:>2} ({} insns, {} bytes)", + package_size, + insn_count, + bytes.len() + ); + println!( + " assemble :{} ops/s {} insns/s {} bytes/s", + fmt_rate(asm_ops_per_s), + fmt_rate(asm_ops_per_s * insn_count as f64), + fmt_rate(asm_ops_per_s * bytes.len() as f64), + ); + println!( + " disassemble :{} ops/s {} insns/s {} bytes/s", + fmt_rate(dis_ops_per_s), + fmt_rate(dis_ops_per_s * insn_count as f64), + fmt_rate(dis_ops_per_s * bytes.len() as f64), + ); + } +} + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + let mut seconds_per_bench: f64 = 0.5; + if args.len() > 1 { + match args[1].parse::() { + Ok(v) if v > 0.0 => seconds_per_bench = v, + _ => { + eprintln!("Usage: {} [seconds_per_measurement]", args[0]); + std::process::exit(1); + } + } + } + let target = Duration::from_secs_f64(seconds_per_bench); + + print!("Nyxstone benchmark [Rust] (target {seconds_per_bench}s per measurement)"); + + for arch in ARCHES { + run(arch, target); + } + + Ok(()) +} diff --git a/examples/benchmark.cpp b/examples/benchmark.cpp new file mode 100644 index 0000000..b63db3f --- /dev/null +++ b/examples/benchmark.cpp @@ -0,0 +1,156 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "nyxstone.h" + +using nyxstone::Nyxstone; +using nyxstone::NyxstoneBuilder; +using clock_type = std::chrono::steady_clock; + +namespace { + +struct ArchConfig { + const char* name; + const char* triple; + std::vector instructions; +}; + +const std::vector ARCHES { + { "x86_64", "x86_64-linux-gnu", + { "mov rax, rbx", "xor rax, rax", "add rsp, 8", "sub rsp, 8", "push rbx", "pop rbx", "inc rax", "dec rax", + "and rax, rbx", "or rax, rbx" } }, + { "x86_32", "i686-linux-gnu", + { "mov eax, ebx", "xor eax, eax", "add esp, 8", "sub esp, 8", "push ebx", "pop ebx", "inc eax", "dec eax", + "and eax, ebx", "or eax, ebx" } }, + { "aarch64", "aarch64-linux-gnueabihf", + { "mov x0, x1", "add x0, x0, #1", "sub x0, x0, #1", "mov x1, x2", "add x1, x1, #1", "sub x1, x1, #1", + "mov x2, x3", "add sp, sp, #16", "sub sp, sp, #16", "ret" } }, + { "armv8m", "armv8m.main-none-eabi", + { "mov r0, r1", "add r0, r0, #1", "sub r0, r0, #1", "mov r1, r2", "add r1, r1, #1", "sub r1, r1, #1", + "mov r2, r3", "nop", "push {r0}", "pop {r0}" } }, +}; + +constexpr std::array PACKAGE_SIZES { 1, 10 }; + +std::string make_package(const ArchConfig& arch, size_t package_size) +{ + std::string out; + for (size_t i = 0; i < package_size; ++i) { + if (i > 0) { + out += "; "; + } + out += arch.instructions[i % arch.instructions.size()]; + } + return out; +} + +template double run_for_at_least(double target_seconds, Fn&& fn) +{ + for (size_t i = 0; i < 10; ++i) { + fn(); + } + + size_t count = 0; + const auto start = clock_type::now(); + while (true) { + for (size_t i = 0; i < 50; ++i) { + fn(); + ++count; + } + const double elapsed = std::chrono::duration(clock_type::now() - start).count(); + if (elapsed >= target_seconds) { + return static_cast(count) / elapsed; + } + } +} + +std::string fmt_rate(double value) +{ + std::ostringstream stream; + stream << std::fixed << std::setprecision(2); + if (value >= 1e9) { + stream << (value / 1e9) << " G"; + } else if (value >= 1e6) { + stream << (value / 1e6) << " M"; + } else if (value >= 1e3) { + stream << (value / 1e3) << " k"; + } else { + stream << value << " "; + } + std::string out = stream.str(); + while (out.size() < 10) { + out.insert(out.begin(), ' '); + } + return out; +} + +void run(const ArchConfig& arch, double seconds_per_bench) +{ + std::cout << "\n=== " << arch.name << " (" << arch.triple << ") ===\n"; + + auto build_result = NyxstoneBuilder(std::string { arch.triple }).build(); + if (!build_result) { + std::cerr << " [skipped] " << build_result.error() << "\n"; + return; + } + const auto& nyxstone = build_result.value(); + + for (size_t package_size : PACKAGE_SIZES) { + const std::string assembly = make_package(arch, package_size); + + auto bytes_result = nyxstone->assemble(assembly, 0x1000, {}); + if (!bytes_result) { + std::cerr << " [skipped " << package_size << "] " << bytes_result.error() << "\n"; + continue; + } + const std::vector bytes = bytes_result.value(); + + auto instr_result = nyxstone->assemble_to_instructions(assembly, 0x1000, {}); + const size_t insn_count = instr_result ? instr_result.value().size() : package_size; + + const double asm_ops_per_s + = run_for_at_least(seconds_per_bench, [&] { (void)nyxstone->assemble(assembly, 0x1000, {}); }); + const double dis_ops_per_s + = run_for_at_least(seconds_per_bench, [&] { (void)nyxstone->disassemble(bytes, 0x1000, 0); }); + + std::cout << " Package " << std::setw(2) << package_size << " (" << insn_count << " insns, " << bytes.size() + << " bytes)\n" + << " assemble :" << fmt_rate(asm_ops_per_s) << " ops/s " + << fmt_rate(asm_ops_per_s * static_cast(insn_count)) << " insns/s " + << fmt_rate(asm_ops_per_s * static_cast(bytes.size())) << " bytes/s\n" + << " disassemble :" << fmt_rate(dis_ops_per_s) << " ops/s " + << fmt_rate(dis_ops_per_s * static_cast(insn_count)) << " insns/s " + << fmt_rate(dis_ops_per_s * static_cast(bytes.size())) << " bytes/s\n"; + } +} + +} // namespace + +int main(int argc, char** argv) +{ + double seconds_per_bench = 0.5; + if (argc > 1) { + char* end = nullptr; + const double parsed = std::strtod(argv[1], &end); + if (end == argv[1] || parsed <= 0.0) { + std::cerr << "Usage: " << argv[0] << " [seconds_per_measurement]\n"; + return 1; + } + seconds_per_bench = parsed; + } + + std::cout << "Nyxstone benchmark [C++] (target " << seconds_per_bench << "s per measurement)"; + + for (const auto& arch : ARCHES) { + run(arch, seconds_per_bench); + } + + return 0; +} diff --git a/include/nyxstone.h b/include/nyxstone.h index 86b05e5..7bb8f83 100644 --- a/include/nyxstone.h +++ b/include/nyxstone.h @@ -4,9 +4,15 @@ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" +#include +#include #include +#include +#include +#include #include #include +#include #include #include #include @@ -14,6 +20,22 @@ namespace nyxstone { +/// Minimal MCObjectFileInfo replacement: only registers `.text` with the +/// MCContext, skipping the ~40 other section creations performed by +/// `MCObjectFileInfo::initELFMCObjectFileInfo` and the LLVM +/// `createMCObjectFileInfo` factory. Nyxstone restricts its output to +/// `.text`, so the parser never queries other sections in practice. +class TextOnlyObjectFileInfo : public llvm::MCObjectFileInfo { +public: + void initTextOnly(llvm::MCContext& ctx) + { + // MCObjectFileInfo::Ctx is private; the parser never inspects it, + // it only queries section pointers we populate here. + TextSection = ctx.getELFSection( + ".text", llvm::ELF::SHT_PROGBITS, llvm::ELF::SHF_ALLOC | llvm::ELF::SHF_EXECINSTR); + } +}; + using u8 = uint8_t; using u64 = uint64_t; @@ -57,7 +79,9 @@ class Nyxstone { Nyxstone(llvm::Triple&& triple, const llvm::Target& target, llvm::MCTargetOptions&& target_options, std::unique_ptr&& register_info, std::unique_ptr&& assembler_info, std::unique_ptr&& instruction_info, std::unique_ptr&& subtarget_info, - std::unique_ptr&& instruction_printer) noexcept + std::unique_ptr&& instruction_printer, + std::unique_ptr&& asm_backend, std::unique_ptr&& disasm_context, + std::unique_ptr&& disassembler) noexcept : triple(std::move(triple)) , target(target) , target_options(std::move(target_options)) @@ -66,6 +90,9 @@ class Nyxstone { , instruction_info(std::move(instruction_info)) , subtarget_info(std::move(subtarget_info)) , instruction_printer(std::move(instruction_printer)) + , asm_backend(std::move(asm_backend)) + , disasm_context(std::move(disasm_context)) + , disassembler(std::move(disassembler)) { } @@ -139,6 +166,19 @@ class Nyxstone { std::unique_ptr instruction_info; std::unique_ptr subtarget_info; std::unique_ptr instruction_printer; + // MCAsmBackend has no MCContext dependency, so it is cached on the + // Nyxstone instance and reused across `assemble()` calls. + std::unique_ptr asm_backend; + // The disassembler doesn't define symbols/sections, so its MCContext can + // be cached alongside it. Declared after the *_info members because + // MCContext borrows references to them. + std::unique_ptr disasm_context; + std::unique_ptr disassembler; + // The assembler's MCContext is NOT cached: LLVM's parser/backend internals + // call MCContext::reportError with SMLocs tied to the per-call SourceMgr. + // A cached context with no matching SourceMgr would crash inside LLVM's + // SourceMgr::GetMessage on any error path. Constructing fresh per-call + // costs only ~1 us, so we keep the safer per-call MCContext. }; /** diff --git a/src/ELFStreamerWrapper.cpp b/src/ELFStreamerWrapper.cpp deleted file mode 100644 index 64a3b90..0000000 --- a/src/ELFStreamerWrapper.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "ELFStreamerWrapper.h" - -#include "nyxstone.h" - -#include - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#pragma GCC diagnostic pop - -#include -#include - -using namespace llvm; - -namespace nyxstone { -void ELFStreamerWrapper::emitInstruction(const MCInst& Inst, const MCSubtargetInfo& STI) -{ - MCELFStreamer::emitInstruction(Inst, STI); - - // Only record instruction details if requested - if (instructions == nullptr) { - return; - } - - // Get .text section - MCSection* text_section = nullptr; - for (MCSection& section : getAssembler()) { - if (section.getName().str() == ".text") { - text_section = §ion; - break; - } - } - if (text_section == nullptr) { - extended_error += "[emitInstruction] Object has no .text section."; - return; - } - - // Get length of already recorded instructions - const size_t insn_byte_length = std::accumulate(instructions->begin(), instructions->end(), static_cast(0), - [](size_t acc, const Nyxstone::Instruction& insn) { return acc + insn.bytes.size(); }); - - // Iterate fragments - size_t frag_byte_length = 0; - for (MCFragment& fragment : *text_section) { - // Get Content - MutableArrayRef contents; - switch (fragment.getKind()) { - default: - continue; - case MCFragment::FT_Data: { - auto& data_fragment = cast(fragment); - contents = data_fragment.getContents(); - break; - } - case MCFragment::FT_Relaxable: { - auto& relaxable_fragment = cast(fragment); - contents = relaxable_fragment.getContents(); - break; - } - } - frag_byte_length += contents.size(); - - // Check for new instruction bytes - if (frag_byte_length > insn_byte_length) { - auto insn_length = frag_byte_length - insn_byte_length; - - // Pedantic check - if (insn_length > contents.size()) { - std::stringstream error_stream; - error_stream << "Internal error (= insn_length: " << insn_length - << ", fragment size: " << contents.size() << " )"; - extended_error += error_stream.str(); - return; - } - - // Copy bytes to new nyxstone instruction - Nyxstone::Instruction new_insn {}; - auto pos = contents.size() - insn_length; - new_insn.bytes.reserve(insn_length); - std::copy(contents.begin() + pos, contents.end(), std::back_inserter(new_insn.bytes)); - - // Print instruction assembly to nyxstone instruction - raw_string_ostream str_stream(new_insn.assembly); - instruction_printer.printInst(&Inst, /* Address */ 0, /* Annot */ "", STI, str_stream); - - // left trim - new_insn.assembly.erase(0, new_insn.assembly.find_first_not_of(" \t\n\r")); - // convert tabulators to spaces - std::replace(new_insn.assembly.begin(), new_insn.assembly.end(), '\t', ' '); - - instructions->push_back(new_insn); - break; - } - } -} - -std::unique_ptr ELFStreamerWrapper::createELFStreamerWrapper(MCContext& context, - std::unique_ptr&& assembler_backend, std::unique_ptr&& object_writer, - std::unique_ptr&& code_emitter, bool RelaxAll, std::vector* instructions, - std::string& extended_error, MCInstPrinter& instruction_printer) -{ - auto streamer = std::make_unique(context, std::move(assembler_backend), - std::move(object_writer), std::move(code_emitter), instructions, extended_error, instruction_printer); - - if (RelaxAll) { - streamer->getAssembler().setRelaxAll(true); - } - return streamer; -} -} // namespace nyxstone diff --git a/src/ELFStreamerWrapper.h b/src/ELFStreamerWrapper.h deleted file mode 100644 index d3c65cb..0000000 --- a/src/ELFStreamerWrapper.h +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include "nyxstone.h" - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" -#include -#include -#include -#include -#pragma GCC diagnostic pop - -namespace nyxstone { -/// This class derives from LLVM's MCELFStreamer, which enables us to receive -/// information during assembly such as preliminary instruction size and bytes -/// before relaxation and fixups (via method 'emitInstruction()'). -class ELFStreamerWrapper : public llvm::MCELFStreamer { - // Sink to record instruction details - std::vector* instructions; - - // Reference to the nyxstone error string - std::string& extended_error; - - // Instruction printer - llvm::MCInstPrinter& instruction_printer; - -public: - /// @brief ELFStreamerWrapper constructor - /// @param context The MCContext used for reporting errors. - /// @param assembler_backend Backend for the wrapped MCELFStreamer. - /// @param object_writer Writer for the wrapped MCELFStreamer. - /// @param code_emitter Emitter for the wrapped MCELFStreamer. - /// @param RelaxAll Relax all instructions that can be relaxed. - /// @param instructions Vector to store instruction information. - /// @param extended_error Reference to nyxstone error string. - /// @param instruction_printer Instruction printer used to generate the instruction assembly. - ELFStreamerWrapper(llvm::MCContext& context, std::unique_ptr&& assembler_backend, - std::unique_ptr&& object_writer, std::unique_ptr&& code_emitter, - std::vector* instructions, std::string& extended_error, - llvm::MCInstPrinter& instruction_printer) - : llvm::MCELFStreamer(context, std::move(assembler_backend), std::move(object_writer), std::move(code_emitter)) - , instructions(instructions) - , extended_error(extended_error) - , instruction_printer(instruction_printer) - { - } - - /// @brief Creates a UniquePtr holding the ELFStreamerWrapper. - /// @param context The MCContext used for reporting errors. - /// @param assembler_backend Backend for the wrapped MCELFStreamer. - /// @param object_writer Writer for the wrapped MCELFStreamer. - /// @param code_emitter Emitter for the wrapped MCELFStreamer. - /// @param RelaxAll Relax all instructions that can be relaxed. - /// @param instructions Vector to store instruction information. - /// @param extended_error Reference to nyxstone error string. - /// @param instruction_printer Instruction printer used to generate the instruction assembly. - /// @return unique_ptr holding the ELFStreamerWrapper - static std::unique_ptr createELFStreamerWrapper(llvm::MCContext& context, - std::unique_ptr&& assembler_backend, std::unique_ptr&& object_writer, - std::unique_ptr&& code_emitter, bool RelaxAll, - std::vector* instructions, std::string& extended_error, - llvm::MCInstPrinter& instruction_printer); - - /// @brief Calls `MCELFStreamer::emitInstruction` and records instruction details. - void emitInstruction(const llvm::MCInst& Inst, const llvm::MCSubtargetInfo& STI) override; -}; -} // namespace nyxstone diff --git a/src/FastStreamer.cpp b/src/FastStreamer.cpp new file mode 100644 index 0000000..14d879f --- /dev/null +++ b/src/FastStreamer.cpp @@ -0,0 +1,13 @@ +#include "FastStreamer.h" + +namespace nyxstone { +void FastStreamer::emitInstruction(const llvm::MCInst& inst, const llvm::MCSubtargetInfo& sti) +{ + m_events.push_back(Event { EventKind::Instruction, nullptr, inst, &sti }); +} + +void FastStreamer::emitLabel(llvm::MCSymbol* symbol, llvm::SMLoc /*loc*/) +{ + m_events.push_back(Event { EventKind::Label, symbol, llvm::MCInst {}, nullptr }); +} +} // namespace nyxstone diff --git a/src/FastStreamer.h b/src/FastStreamer.h new file mode 100644 index 0000000..68c6763 --- /dev/null +++ b/src/FastStreamer.h @@ -0,0 +1,54 @@ +#pragma once + +#include "nyxstone.h" + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#include +#include +#include +#include +#include +#pragma GCC diagnostic pop + +#include + +namespace nyxstone { +/// Records the ordered sequence of `emitLabel` / `emitInstruction` callbacks +/// the assembly parser issues, deferring all encoding to a single post-parse +/// pass run by `Nyxstone::assemble_impl`. By skipping the full MC object-file +/// emission pipeline, the assembler avoids the per-call ELF section/symbol +/// bookkeeping that dominated the previous path's profile. +class FastStreamer : public llvm::MCStreamer { +public: + enum class EventKind : uint8_t { Label, Instruction }; + + struct Event { + EventKind kind; + llvm::MCSymbol* symbol; // valid when kind == Label + llvm::MCInst inst; // valid when kind == Instruction + const llvm::MCSubtargetInfo* sti; // valid when kind == Instruction + }; + + explicit FastStreamer(llvm::MCContext& ctx) + : llvm::MCStreamer(ctx) + { + } + + const std::vector& events() const { return m_events; } + + void emitInstruction(const llvm::MCInst& inst, const llvm::MCSubtargetInfo& sti) override; + void emitLabel(llvm::MCSymbol* symbol, llvm::SMLoc /*loc*/ = llvm::SMLoc()) override; + + // Pure-virtual stubs — assemble() never produces directives that hit these. + bool emitSymbolAttribute(llvm::MCSymbol* /*sym*/, llvm::MCSymbolAttr /*attr*/) override { return true; } + void emitCommonSymbol(llvm::MCSymbol* /*sym*/, uint64_t /*size*/, llvm::Align /*align*/) override { } + void emitZerofill(llvm::MCSection* /*section*/, llvm::MCSymbol* /*sym*/ = nullptr, uint64_t /*size*/ = 0, + llvm::Align /*align*/ = llvm::Align(1), llvm::SMLoc /*loc*/ = llvm::SMLoc()) override + { + } + +private: + std::vector m_events; +}; +} // namespace nyxstone diff --git a/src/ObjectWriterWrapper.cpp b/src/ObjectWriterWrapper.cpp deleted file mode 100644 index 7b5fb27..0000000 --- a/src/ObjectWriterWrapper.cpp +++ /dev/null @@ -1,333 +0,0 @@ -#include "ObjectWriterWrapper.h" - -#include "nyxstone.h" -#include - -#include "Target/AArch64/MCTargetDesc/AArch64FixupKinds.h" -#include "Target/AArch64/MCTargetDesc/AArch64MCExpr.h" -#include "Target/ARM/MCTargetDesc/ARMFixupKinds.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -using namespace llvm; - -namespace nyxstone { -/// @brief Validates the given arm fixup with our custom validatinon routines. -/// -/// @param fixup The fixup to be validated. -/// @param Layout The ASM Layout after fixups have been applied. -/// @param context The MCContext used to report errors. -void validate_arm_thumb(const MCFixup& fixup, const MCAsmLayout& Layout, MCContext& context) -{ - // For all instructions we are checking here, we need to make sure that the fixup is a SymbolRef. - // If it is not, we do not need to check the instruction. - const bool fixup_is_symbolref = fixup.getValue() != nullptr && fixup.getValue()->getKind() == MCExpr::SymbolRef; - if (!fixup_is_symbolref) { - return; - } - - // Check for missaligned target address in 2-byte `ADR` and `LDR` instructions that only allow multiples of four. - if (fixup.getTargetKind() == ARM::fixup_thumb_adr_pcrel_10 || fixup.getTargetKind() == ARM::fixup_arm_thumb_cp) { - const auto& symbol = cast(fixup.getValue())->getSymbol(); - auto address = Layout.getFragmentOffset(symbol.getFragment()) + symbol.getOffset(); - - // Check that target is 4-byte aligned - if ((address & 3U) != 0) { - context.reportError(fixup.getLoc(), "misaligned label address (reported by nyxstone)"); - } - } - - // Check for out-of-bounds ARM Thumb2 `ADR` instruction - if (fixup.getTargetKind() == ARM::fixup_t2_adr_pcrel_12) { - auto offset = static_cast(cast(fixup.getValue())->getSymbol().getOffset()); - offset -= 4; // Source address is (PC + 4) - - // Check min/max bounds of instruction encoding - // Symmetric bounds as `addw` and `subw` are used internally - if (offset <= -4096 || offset >= 4096) { - context.reportError(fixup.getLoc(), "out of range pc-relative fixup value (reported by Nyxstone)"); - } - } - - // Check for misaligned target address for all ARM Thumb branch instructions - if (fixup.getTargetKind() == ARM::fixup_arm_thumb_br || fixup.getTargetKind() == ARM::fixup_arm_thumb_bl - || fixup.getTargetKind() == ARM::fixup_arm_thumb_bcc || fixup.getTargetKind() == ARM::fixup_t2_uncondbranch - || fixup.getTargetKind() == ARM::fixup_t2_condbranch) { - auto& symbol = cast(fixup.getValue())->getSymbol(); - auto address = Layout.getFragmentOffset(symbol.getFragment()) + symbol.getOffset(); - - // Check that target is 2-byte aligned - if ((address & 1) != 0) { - context.reportError(fixup.getLoc(), "misaligned label address (reported by nyxstone)"); - } - } - - // Check for out-of-bounds and misaligned label for ARM Thumb2 'LDC' instruction - if (fixup.getTargetKind() == ARM::fixup_t2_pcrel_10) { - auto& symbol = cast(fixup.getValue())->getSymbol(); - auto address = Layout.getFragmentOffset(symbol.getFragment()) + symbol.getOffset(); - - auto offset = static_cast(symbol.getOffset()) - 4; // Source address is (PC + 4) - - // Since llvm only wrongly assembles for offsets which differ from the allowed value for delta < 4 - // it is enough to check that the offset is validly aligned to 4. For better error reporting, - // we still check the offsets here. - if (offset < -1020 || offset > 1020) { - context.reportError(fixup.getLoc(), "out of range pc-relative fixup value (reported by Nyxstone)"); - } - - // check that target is 4 byte aligned - if ((address & 3) != 0) { - context.reportError(fixup.getLoc(), "misaligned label address (reported by Nyxstone)"); - } - } -} - -/// @brief Validates the given AARCH64 fixup with our custom validatinon routines. -/// -/// @param fixup The fixup to be validated. -/// @param Layout The ASM Layout after fixups have been applied. -/// @param context The MCContext used to report errors. -void validate_aarch64(const MCFixup& fixup, [[maybe_unused]] const MCAsmLayout& Layout, MCContext& context) -{ - // Check for out-of-bounds AArch64 `ADR` instruction - if (context.getTargetTriple().isAArch64() && fixup.getTargetKind() == AArch64::fixup_aarch64_pcrel_adr_imm21 - && fixup.getValue() != nullptr && fixup.getValue()->getKind() == MCExpr::Target - && cast(fixup.getValue())->getSubExpr() != nullptr - && cast(fixup.getValue())->getSubExpr()->getKind() == MCExpr::SymbolRef) { - const auto* const sub_expr = cast(fixup.getValue())->getSubExpr(); - const auto offset = static_cast(cast(sub_expr)->getSymbol().getOffset()); - - // Check min/max bounds of instruction encoding - // Asymmetric bounds as two's complement is used - if (offset < -0x100000 || offset >= 0x100000) { - context.reportError(fixup.getLoc(), "fixup value out of range (reported by Nyxstone)"); - } - } -} - -void ObjectWriterWrapper::validate_fixups(const MCFragment& fragment, const MCAsmLayout& Layout) -{ - // Get fixups - const SmallVectorImpl* fixups = nullptr; - switch (fragment.getKind()) { - default: - return; - case MCFragment::FT_Data: { - fixups = &cast(fragment).getFixups(); - break; - } - case MCFragment::FT_Relaxable: { - fixups = &cast(fragment).getFixups(); - break; - } - } - - // Iterate fixups - for (const auto& fixup : *fixups) { - // Additional validations for ARM Thumb instructions - if (is_ArmT16_or_ArmT32(context.getTargetTriple())) { - validate_arm_thumb(fixup, Layout, context); - } - - // Additional validations for AArch64 instructions - if (context.getTargetTriple().isAArch64()) { - validate_aarch64(fixup, Layout, context); - } - } -} - -void ObjectWriterWrapper::executePostLayoutBinding(llvm::MCAssembler& Asm, const llvm::MCAsmLayout& Layout) -{ - inner_object_writer->executePostLayoutBinding(Asm, Layout); -} - -bool ObjectWriterWrapper::resolve_relocation(MCAssembler& assembler, const MCAsmLayout& layout, - const MCFragment* fragment, const MCFixup& fixup, MCValue target, uint64_t& fixed_value) -{ - (void)layout; - (void)fragment; - (void)target; - - // LLVM performs relocation for the AArch64 instruction `adrp` during the linking step. - // Therefore, we need to perform the relocation ourselves. - // Semantics: REG := page of PC + page of .LABEL on 4k aligned page - if (context.getTargetTriple().isAArch64()) { - auto const IsPCRel - = assembler.getBackend().getFixupKindInfo(fixup.getKind()).Flags & MCFixupKindInfo::FKF_IsPCRel; - auto const kind = fixup.getTargetKind(); - - if ((IsPCRel != 0U) && kind == AArch64::fixup_aarch64_pcrel_adrp_imm21) { - const auto* const aarch64_expr = cast(fixup.getValue()); - const auto* const symbol_ref = cast(aarch64_expr->getSubExpr()); - MCSymbol const& symbol = symbol_ref->getSymbol(); - - if (!symbol.isDefined()) { - return false; - } - - // Perform the fixup - constexpr u64 PAGE_SIZE { 0x1000 }; - - // We need to compute the absolute address of both this instruction and the target label, - // so that we can compute the offsets of their two pages, since adrp zeros both - // the lower 12 bits of the pc and of the offset... - u64 local_addr = start_address + fixup.getOffset(); - u64 target_addr = start_address + symbol.getOffset(); - - u64 local_page = local_addr & ~(PAGE_SIZE - 1); - u64 target_page = target_addr & ~(PAGE_SIZE - 1); - - fixed_value = target_page - local_page; - - return true; - } - } - - return false; -} - -// cppcheck-suppress unusedFunction -void ObjectWriterWrapper::recordRelocation(MCAssembler& Asm, const MCAsmLayout& Layout, const MCFragment* Fragment, - const MCFixup& Fixup, MCValue Target, uint64_t& FixedValue) -{ - bool labels_defined { true }; - - if (auto const* sym_ref = Target.getSymA(); sym_ref != nullptr) { - labels_defined &= sym_ref->getSymbol().isDefined(); - } - - if (auto const* sym_ref = Target.getSymB(); sym_ref != nullptr) { - labels_defined &= sym_ref->getSymbol().isDefined(); - } - - if (!labels_defined) { - context.reportError(Fixup.getLoc(), "Label undefined (reported by Nyxstone)"); - return; - } - - bool const resolved = resolve_relocation(Asm, Layout, Fragment, Fixup, Target, FixedValue); - - if (!resolved) { - context.reportError(Fixup.getLoc(), "Could not resolve relocation/label (reported by Nyxstone)"); - return; - } -} - -uint64_t ObjectWriterWrapper::writeObject(MCAssembler& Asm, const MCAsmLayout& Layout) -{ - // If any label is undefined, continuing can lead to a segfault in the execution later. - if (!extended_error.empty()) { - return 0; - } - - // Get .text section - const auto& sections = Layout.getSectionOrder(); - const MCSection* const* text_section_it = std::find_if( - std::begin(sections), std::end(sections), [](auto section) { return section->getName().str() == ".text"; }); - - if (text_section_it == sections.end()) { - extended_error += "[writeObject] Object has no .text section."; - return 0; - } - - const MCSection* text_section = *text_section_it; - - // Iterate fragments - size_t curr_insn = 0; - for (const MCFragment& fragment : *text_section) { - // Additional validation of fixups that LLVM is missing - validate_fixups(fragment, Layout); - - // If requested, do post processing of instruction details (that corrects for relocations and applied fixups) - if (instructions == nullptr) { - continue; - } - - // Data fragment may contain multiple instructions that did not change in size - if (fragment.getKind() == MCFragment::FT_Data) { - const auto& data_fragment = cast(fragment); - const ArrayRef contents = data_fragment.getContents(); - - // Update bytes of multiple instructions - size_t frag_pos = 0; - while (true) { - auto& insn_bytes = instructions->at(curr_insn).bytes; - auto insn_len = insn_bytes.size(); - - // Check if fragment contains this instruction - if (frag_pos + insn_len > contents.size()) { - break; - } - - // Update instruction bytes - insn_bytes.clear(); - insn_bytes.reserve(insn_len); - std::copy(contents.begin() + frag_pos, contents.begin() + frag_pos + insn_len, - std::back_inserter(insn_bytes)); - - // Prepare next iteration - frag_pos += insn_len; - curr_insn++; - - // Check if more instructions to be updated exist - if (curr_insn >= instructions->size()) { - break; - } - } - } else if (fragment.getKind() == MCFragment::FT_Relaxable) { - // Relaxable fragment contains exactly one instruction that may have increased in size - const auto& relaxable_fragment = cast(fragment); - const ArrayRef contents = relaxable_fragment.getContents(); - - // Update instruction bytes - auto& insn_bytes = instructions->at(curr_insn).bytes; - insn_bytes.clear(); - insn_bytes.reserve(contents.size()); - std::copy(contents.begin(), contents.end(), std::back_inserter(insn_bytes)); - - // Prepare next iteration - curr_insn++; - } - - // Check if more instructions to be updated exist - if (curr_insn >= instructions->size()) { - break; - } - } - - // Generate output - if (write_text_section_only) { - // Write .text section bytes only - const uint64_t start = stream.tell(); - Asm.writeSectionData(stream, text_section, Layout); - return stream.tell() - start; - } - - // Write complete object file - return inner_object_writer->writeObject(Asm, Layout); -} - -std::unique_ptr ObjectWriterWrapper::createObjectWriterWrapper( - std::unique_ptr&& object_writer, raw_pwrite_stream& stream, MCContext& context, - bool write_text_section_only, u64 start_address, std::string& extended_error, - std::vector* instructions) -{ - return std::make_unique(std::move(object_writer), stream, context, write_text_section_only, - start_address, extended_error, instructions); -} -} // namespace nyxstone diff --git a/src/ObjectWriterWrapper.h b/src/ObjectWriterWrapper.h deleted file mode 100644 index 369b711..0000000 --- a/src/ObjectWriterWrapper.h +++ /dev/null @@ -1,134 +0,0 @@ -#pragma once - -#include "nyxstone.h" - -#include - -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wunused-parameter" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#pragma GCC diagnostic pop - -namespace nyxstone { -/// This class enables us to limit the final output byte stream to the -/// relevant bytes (as opposed to the whole ELF object file) and grab final -/// instruction bytes (after relaxation and fixups) (via 'writeObject()'). -/// -/// This class is also used to insert custom relocations and fixup validations. -/// These are necessary when a relocation is normally performed at link time or when -/// LLVM does not verify a fixup according to the specification, leading to wrong -/// output for specific instruction/label combinations. -class ObjectWriterWrapper : public llvm::MCObjectWriter { - // Wrapped MCObjectWriter, f. i., ELFSingleObjectWriter - std::unique_ptr inner_object_writer; - - /// Output stream - llvm::raw_pwrite_stream& stream; - - /// MCContext - llvm::MCContext& context; - - /// Write section bytes only or complete object file - bool write_text_section_only; - - /// The start address of the assembly - u64 start_address; - - /// Reference to the nyxstone error string - std::string& extended_error; - - /// Sink to record instruction details - std::vector* instructions; - - /// @brief Additional validation checks for fixups. - /// - /// For some fixup kinds LLVM is missing out-of-bounds and alignment checks and - /// produces wrong instruction bytes instead of an error message. - /// - /// @param fragment Fragment holding the fixups. - /// @param Layout The ASM Layout after fixups have been applied. - void validate_fixups(const llvm::MCFragment& fragment, const llvm::MCAsmLayout& Layout); - -public: - /// @brief Creates an ObjectWriterWrapper. - /// - /// The ObjectWriterWrapper is created using the wrapped MCObjectWriter, some additional llvm classes - /// used for querying additional information from LLVM, and a (nullable) pointer to the instruction information. - /// - /// @param object_writer The object writer object to wrap, must implement the function used by this class. - /// @param stream Stream (used for the @p object_writer) to write the .text section to if requested via @p - /// write_text_section_only. - /// @param write_text_section_only If only the .text section should be written to @p stream. - /// @param start_address Absolute start address of the instructions - /// @param extended_error Accumulation for llvm errors, since Exceptions might not be supported by the linked LLVM. - /// @param instructions Instruction information for which the bytes should be corrected. - ObjectWriterWrapper(std::unique_ptr&& object_writer, llvm::raw_pwrite_stream& stream, - llvm::MCContext& context, bool write_text_section_only, u64 start_address, std::string& extended_error, - std::vector* instructions) - : inner_object_writer(std::move(object_writer)) - , stream(stream) - , context(context) - , write_text_section_only(write_text_section_only) - , start_address(start_address) - , extended_error(extended_error) - , instructions(instructions) - { - } - - /// @brief Creates a UniquePtr holding the the ObjectWriterWrapper - /// - /// @param object_writer The object writer object to wrap, must implement the function used by this class. - /// @param stream Stream (used for the @p object_writer) to write the .text section to if requested via @p - /// write_text_section_only. - /// @param write_text_section_only If only the .text section should be written to @p stream. - /// @param start_address Absolute start address of the instructions - /// @param extended_error Accumulation for llvm errors, since Exceptions might not be supported by the linked LLVM. - /// @param instructions Instruction information for which the bytes should be corrected. - static std::unique_ptr createObjectWriterWrapper( - std::unique_ptr&& object_writer, llvm::raw_pwrite_stream& stream, - llvm::MCContext& context, bool write_text_section_only, u64 start_address, std::string& extended_error, - std::vector* instructions); - - /// @brief Simple function wrapper calling the wrapped object wrapper function directly. - void executePostLayoutBinding(llvm::MCAssembler& Asm, const llvm::MCAsmLayout& Layout) override; - - /// @brief Internal resolve for relocations. - /// - /// Implements relocation for: - /// - `adrp` - bool resolve_relocation(llvm::MCAssembler& assembler, const llvm::MCAsmLayout& layout, - const llvm::MCFragment* fragment, const llvm::MCFixup& fixup, llvm::MCValue target, uint64_t& fixed_value); - - /// @brief Tries to resolve relocations (that are normally resolved at link time) instead of recording them - /// - /// This function serves multiple purposes: - /// - Resolve (some) relocations - /// - Ensure relocations which can not be resolved are an error instead of invalid machine bytes. - /// - Ensure that any missing label is correctly reported. - /// - /// Normally, this function records relocations, which are resolved by the linker. Since we do not have a linking - /// step, we must assume that any relocation which would be recorded is not yet correctly assembled. Thus, we try to - /// resolve relocations ourself, although this must be implemented on a relocation basis, and we currently - /// implement: - /// - `adrp` - /// - /// Any missing label in the assembly must be resolved by the linker and leads to this function being called, - /// we use this fact to emit errors for missing labels. - void recordRelocation(llvm::MCAssembler& Asm, const llvm::MCAsmLayout& Layout, const llvm::MCFragment* Fragment, - const llvm::MCFixup& Fixup, llvm::MCValue Target, uint64_t& FixedValue) override; - - /// @brief Write object to the stream and update the bytes of the instruction details. - uint64_t writeObject(llvm::MCAssembler& Asm, const llvm::MCAsmLayout& Layout) override; -}; -} // namespace nyxstone diff --git a/src/nyxstone.cpp b/src/nyxstone.cpp index 733a5d9..1bbe976 100644 --- a/src/nyxstone.cpp +++ b/src/nyxstone.cpp @@ -1,23 +1,38 @@ #include "nyxstone.h" -#include "ELFStreamerWrapper.h" -#include "ObjectWriterWrapper.h" +#include "FastStreamer.h" +#include "Target/AArch64/MCTargetDesc/AArch64FixupKinds.h" +#include "Target/AArch64/MCTargetDesc/AArch64MCExpr.h" +#include "Target/ARM/MCTargetDesc/ARMFixupKinds.h" #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #include +#include +#include +#include +#include #include #include +#include +#include +#include +#include #include +#include +#include #include #include #include #include +#include #include +#include #include #include #pragma GCC diagnostic pop +#include #include #include #include @@ -118,17 +133,35 @@ tl::expected, std::string> NyxstoneBuilder::build() break; } - return std::make_unique(std::move(triple), - // target is a static object, thus it is safe to take its reference here: - *target, std::move(target_options), std::move(register_info), std::move(assembler_info), - std::move(instruction_info), std::move(subtarget_info), std::move(instruction_printer)); + auto asm_backend = std::unique_ptr( + target->createMCAsmBackend(*subtarget_info, *register_info, target_options)); + if (!asm_backend) { + return tl::unexpected("Could not create LLVM object (= MCAsmBackend )"); + } + + auto disasm_context = std::make_unique( + triple, assembler_info.get(), register_info.get(), subtarget_info.get(), nullptr, &target_options); + auto disassembler + = std::unique_ptr(target->createMCDisassembler(*subtarget_info, *disasm_context)); + if (!disassembler) { + return tl::unexpected("Could not create LLVM object (= MCDisassembler )"); + } + + if (!triple.isOSBinFormatELF()) { + std::stringstream error_stream; + error_stream << "ELF does not support target triple '" << triple.getTriple() << "'."; + return tl::unexpected(error_stream.str()); + } + return std::make_unique(std::move(triple), *target, std::move(target_options), std::move(register_info), + std::move(assembler_info), std::move(instruction_info), std::move(subtarget_info), + std::move(instruction_printer), std::move(asm_backend), std::move(disasm_context), std::move(disassembler)); } tl::expected, std::string> Nyxstone::assemble( const std::string& assembly, uint64_t address, const std::vector& labels) const { std::vector bytes; - return assemble_impl(assembly, address, labels, bytes, nullptr).transform([bytes]() { return bytes; }); + return assemble_impl(assembly, address, labels, bytes, nullptr).transform([&bytes]() { return std::move(bytes); }); } tl::expected, std::string> Nyxstone::assemble_to_instructions( @@ -137,10 +170,7 @@ tl::expected, std::string> Nyxstone::assemble std::vector instructions; std::vector output_bytes; return assemble_impl(assembly, address, labels, output_bytes, &instructions) - .and_then([instructions, &output_bytes]() -> tl::expected, std::string> { - // Pedantic: Ensure accumulated instruction byte length matches output byte length - // This also leads to nyxstone not supporting directives which insert data into the assembly, - // since the bytes will not match the assembled instructions. + .and_then([&instructions, &output_bytes]() -> tl::expected, std::string> { const size_t insn_byte_length = std::accumulate(instructions.begin(), instructions.end(), static_cast(0), [](size_t acc, const Instruction& insn) { return acc + insn.bytes.size(); }); if (insn_byte_length != output_bytes.size()) { @@ -149,8 +179,7 @@ tl::expected, std::string> Nyxstone::assemble << output_bytes.size() << ")"; return tl::unexpected(error_stream.str()); } - - return instructions; + return std::move(instructions); }); } @@ -158,8 +187,8 @@ tl::expected Nyxstone::disassemble( const std::vector& bytes, uint64_t address, size_t count) const { std::string disassembly; - return disassemble_impl(bytes, address, count, &disassembly, nullptr).transform([disassembly]() { - return disassembly; + return disassemble_impl(bytes, address, count, &disassembly, nullptr).transform([&disassembly]() { + return std::move(disassembly); }); } @@ -167,51 +196,81 @@ tl::expected, std::string> Nyxstone::disassem const std::vector& bytes, uint64_t address, size_t count) const { std::vector instructions; - return disassemble_impl(bytes, address, count, nullptr, &instructions).transform([instructions]() { - return instructions; + return disassemble_impl(bytes, address, count, nullptr, &instructions).transform([&instructions]() { + return std::move(instructions); }); } namespace { - const char* const PREPENDED_ASSEMBLY { "bkpt #0x42\n" }; - constexpr std::array PREPENDED_BYTES { 0x42, 0xbe }; - - std::string prepend_bkpt(std::string&& assembly, bool needs_prepend) + /// Validates an ARM Thumb fixup: range and alignment checks LLVM is missing + /// for some kinds, producing wrong bytes instead of an error. Alignment + /// and range checks operate on the runtime addresses (`address + + /// target_offset`, `address + fixup_offset + 4`-aligned) so that + /// 2-byte-aligned-but-not-4-byte-aligned start addresses validate + /// correctly. + void validate_arm_thumb_fixup(const llvm::MCFixup& fixup, uint64_t address, uint64_t target_offset, + uint64_t fixup_offset, llvm::MCContext& context) { - if (needs_prepend) { - assembly.insert(0, PREPENDED_ASSEMBLY); + if (fixup.getValue() == nullptr || fixup.getValue()->getKind() != llvm::MCExpr::SymbolRef) { + return; } - return assembly; - } + const auto kind = fixup.getTargetKind(); + const uint64_t target_addr = address + target_offset; + // Thumb PC-relative reference point for ADR/LDR (literal) and similar + // fixups: `Align(instr_addr + 4, 4)`. + const uint64_t pc_aligned = (address + fixup_offset + 4) & ~uint64_t { 3 }; - tl::expected, std::string> remove_bkpt( - llvm::SmallVector bytes, std::vector* instructions, bool has_prepend) - { - // If bkpt got prepended for alignment reasons, remove it from instructions - // and bytes - if (has_prepend) { - if (instructions != nullptr) { - const auto& first_instr_bytes = instructions->front().bytes; - const bool has_prepended_bytes = !instructions->empty() && first_instr_bytes.size() == 2 - && std::equal(begin(first_instr_bytes), end(first_instr_bytes), begin(PREPENDED_BYTES)); - if (!has_prepended_bytes) { - return tl::unexpected("Did not find prepended bkpt at first instruction."); - } - instructions->erase(instructions->begin()); + if (kind == llvm::ARM::fixup_thumb_adr_pcrel_10 || kind == llvm::ARM::fixup_arm_thumb_cp) { + if ((target_addr & 3U) != 0) { + context.reportError(fixup.getLoc(), "misaligned label address (reported by nyxstone)"); } + } - if (bytes.size() < 2 || memcmp(PREPENDED_BYTES.data(), bytes.data(), 2) != 0) { - std::ostringstream error_stream; - error_stream << "Did not find prepended bkpt at first two bytes."; - error_stream << " Found bytes 0x" << std::hex << static_cast(bytes[0]) << " 0x" - << static_cast(bytes[1]); - return tl::unexpected(error_stream.str()); + if (kind == llvm::ARM::fixup_t2_adr_pcrel_12) { + const auto offset = static_cast(target_addr) - static_cast(pc_aligned); + if (offset <= -4096 || offset >= 4096) { + context.reportError(fixup.getLoc(), "out of range pc-relative fixup value (reported by Nyxstone)"); + } + } + + if (kind == llvm::ARM::fixup_arm_thumb_br || kind == llvm::ARM::fixup_arm_thumb_bl + || kind == llvm::ARM::fixup_arm_thumb_bcc || kind == llvm::ARM::fixup_t2_uncondbranch + || kind == llvm::ARM::fixup_t2_condbranch) { + if ((target_addr & 1U) != 0) { + context.reportError(fixup.getLoc(), "misaligned label address (reported by nyxstone)"); + } + } + + if (kind == llvm::ARM::fixup_t2_pcrel_10) { + const auto offset = static_cast(target_addr) - static_cast(pc_aligned); + if (offset < -1020 || offset > 1020) { + context.reportError(fixup.getLoc(), "out of range pc-relative fixup value (reported by Nyxstone)"); } - bytes.erase(bytes.begin(), bytes.begin() + 2); + if ((target_addr & 3U) != 0) { + context.reportError(fixup.getLoc(), "misaligned label address (reported by Nyxstone)"); + } + } + } + + /// Validates an AArch64 ADR fixup: range check LLVM is missing. + void validate_aarch64_fixup(const llvm::MCFixup& fixup, uint64_t target_offset, llvm::MCContext& context) + { + if (fixup.getTargetKind() != llvm::AArch64::fixup_aarch64_pcrel_adr_imm21) { + return; + } + if (fixup.getValue() == nullptr || fixup.getValue()->getKind() != llvm::MCExpr::Target) { + return; + } + const auto* sub_expr = llvm::cast(fixup.getValue())->getSubExpr(); + if (sub_expr == nullptr || sub_expr->getKind() != llvm::MCExpr::SymbolRef) { + return; } - return bytes; + const auto offset = static_cast(target_offset); + if (offset < -0x100000 || offset >= 0x100000) { + context.reportError(fixup.getLoc(), "fixup value out of range (reported by Nyxstone)"); + } } } // namespace @@ -228,93 +287,77 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem return {}; } - // ARM Thumb has mixed 2-byte and 4-byte instructions. The base address used - // for branch/load/store offset calculations is aligned down to the last - // 4-byte boundary `base = Align(PC, 4)`. LLVM always assembles at address - // zero and external label definitions are adjusted accordingly. This - // combination leads to alignment issues resulting in wrong instruction bytes. - // Hence, for ARM Thumb Nyxstone prepends 2 bytes for 2-byte aligned, but not - // 4-byte aligned, start addresses to create the correct alignment behavior. - // In the label offset computation Nyxstone compensates for these extra 2 - // bytes. The breakpoint instruction `bkpt #0x42` was chosen as prepend - // mechanism as it only has a 2-byte encoding on ARMv6/7/8-M and is an unusual - // instruction. The prepended breakpoint instruction gets removed by Nyxstone - // in output `bytes` and `instructions` to have a clean result. - // - // tl;dr - // For Thumb prepend 2 byte in assembly if `(address % 4) == 2` to get correct - // alignment behavior. - const bool needs_prepend { is_ArmT16_or_ArmT32(triple) && address % 4 == 2 }; - const std::string input_assembly { prepend_bkpt(std::string { assembly }, needs_prepend) }; - - // Add input assembly text llvm::SourceMgr source_manager; - source_manager.AddNewSourceBuffer(llvm::MemoryBuffer::getMemBuffer(input_assembly), llvm::SMLoc()); + source_manager.AddNewSourceBuffer(llvm::MemoryBuffer::getMemBuffer(assembly), llvm::SMLoc()); std::string extended_error; - // Equip context with info objects and custom error handling llvm::MCContext context( triple, assembler_info.get(), register_info.get(), subtarget_info.get(), &source_manager, &target_options); - context.setDiagnosticHandler([&extended_error](const llvm::SMDiagnostic& SMD, bool IsInlineAsm, - const llvm::SourceMgr& SrcMgr, std::vector const& LocInfos) { - // Suppress unused parameter warning - (void)IsInlineAsm; - (void)SrcMgr; - (void)LocInfos; - + context.setDiagnosticHandler([&extended_error](const llvm::SMDiagnostic& SMD, bool /*IsInlineAsm*/, + const llvm::SourceMgr& /*SrcMgr*/, + std::vector const& /*LocInfos*/) { llvm::SmallString<128> error_msg; - llvm::raw_svector_ostream error_stream(error_msg); SMD.print(nullptr, error_stream, /* ShowColors */ false); extended_error += error_msg.c_str(); }); - auto object_file_info - = std::unique_ptr(target.createMCObjectFileInfo(context, /*PIC=*/false)); - if (!object_file_info) { - return tl::unexpected("Could not create LLVM object (= MCObjectFileInfo )"); + if (!triple.isOSBinFormatELF()) { + std::stringstream error_stream; + error_stream << "ELF does not support target triple '" << triple.getTriple() << "'."; + return tl::unexpected(error_stream.str()); } - context.setObjectFileInfo(object_file_info.get()); - - // Create code emitter (for llvm 15) - auto code_emitter = std::unique_ptr(target.createMCCodeEmitter(*instruction_info, context)); - if (!code_emitter) { + TextOnlyObjectFileInfo object_file_info; + object_file_info.initTextOnly(context); + context.setObjectFileInfo(&object_file_info); + auto* text_section = object_file_info.getTextSection(); + + auto code_emitter_uptr + = std::unique_ptr(target.createMCCodeEmitter(*instruction_info, context)); + if (!code_emitter_uptr) { return tl::unexpected("Could not create LLVM object (= MCCodeEmitter )"); } - // Create assembler backend - auto assembler_backend = std::unique_ptr( + // MCAsmBackend is cached on Nyxstone (no MCContext dependency). MCAssembler + // ctor requires unique_ptr ownership of *a* backend, so create a throwaway + // per call for that role; our own applyFixup/relax calls use the cached + // backend directly. + auto throwaway_backend = std::unique_ptr( target.createMCAsmBackend(*subtarget_info, *register_info, target_options)); - if (!assembler_backend) { + if (!throwaway_backend) { return tl::unexpected("Could not create LLVM object (= MCAsmBackend )"); } - // Create object writer and object writer wrapper (that handles custom fixup - // and output handling) - llvm::SmallVector output_bytes; - llvm::raw_svector_ostream stream(output_bytes); - auto object_writer = assembler_backend->createObjectWriter(stream); - auto object_writer_wrapper - = ObjectWriterWrapper::createObjectWriterWrapper(std::move(object_writer), stream, context, - /* write_text_section_only */ true, address, extended_error, instructions); - if (!object_writer_wrapper) { - return tl::unexpected("Could not create LLVM object (= ObjectWriterWrapper )"); - } + llvm::SmallVector dummy_writer_buffer; + llvm::raw_svector_ostream dummy_writer_stream(dummy_writer_buffer); + auto object_writer_uptr = throwaway_backend->createObjectWriter(dummy_writer_stream); - // Create object streamer and object streamer wrapper (that records - // instruction details) - if (!triple.isOSBinFormatELF()) { - std::stringstream error_stream; - error_stream << "ELF does not support target triple '" << triple.getTriple() << "'."; - return tl::unexpected(error_stream.str()); - } - auto streamer = ELFStreamerWrapper::createELFStreamerWrapper(context, std::move(assembler_backend), - std::move(object_writer_wrapper), std::move(code_emitter), - /* RelaxAll */ false, instructions, extended_error, *instruction_printer); + auto* code_emitter = code_emitter_uptr.get(); + auto* asm_backend_p = asm_backend.get(); + + // MCAssembler is only used to satisfy the `const MCAssembler&` parameter of + // MCAsmBackend::applyFixup (some backends call Asm.getContext() for error + // reporting). The backend/emitter/writer it owns are not invoked through it. + llvm::MCAssembler assembler(context, std::move(throwaway_backend), std::move(code_emitter_uptr), + std::move(object_writer_uptr)); + + auto streamer = std::make_unique(context); streamer->setUseAssemblerInfoForParsing(true); + streamer->switchSection(text_section); + + // Dummy data fragment to attach symbol definitions to so that MCSymbol::isDefined() + // returns true during parsing — required by the parser but never inspected for content. + llvm::MCDataFragment dummy_fragment_storage; + auto* dummy_fragment = &dummy_fragment_storage; + + // Inject external label definitions before parsing so the parser knows their addresses. + for (const auto& label : labels) { + auto* inj_symbol = context.getOrCreateSymbol(label.name); + inj_symbol->setOffset(label.address - address); + inj_symbol->setFragment(dummy_fragment); + } - // Create assembly parser and target specific assembly parser auto parser = std::unique_ptr(createMCAsmParser(source_manager, context, *streamer, *assembler_info)); if (!parser) { @@ -329,44 +372,253 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem parser->setAssemblerDialect(1); parser->setTargetParser(*target_parser); - // Initialize .text section - streamer->initSections(false, parser->getTargetParser().getSTI()); + const bool error = parser->Run(/* NoInitialTextSection= */ true); + if (error || !extended_error.empty()) { + std::ostringstream error_stream; + error_stream << "Error during assembly"; + if (!extended_error.empty()) { + error_stream << ": " << extended_error; + } + return tl::unexpected(error_stream.str()); + } - // Search first data fragment - auto& fragments = *streamer->getCurrentSectionOnly(); - auto fragment_it = std::find_if(fragments.begin(), fragments.end(), - [](auto& fragment) { return fragment.getKind() == llvm::MCFragment::FT_Data; }); - if (fragment_it == std::end(fragments)) { - return tl::unexpected("Could not find initial data fragment."); + // Encode each instruction once up front. Labels and instructions stay in the + // same ordered vector so subsequent relaxation passes can recompute output + // offsets after re-encoding any instruction that grew. + struct EmittedEvent { + bool is_label; + size_t output_offset; + llvm::MCSymbol* symbol; // label only + llvm::MCInst inst; // instruction only + const llvm::MCSubtargetInfo* sti; // instruction only + llvm::SmallVector bytes; // instruction only — current encoding + llvm::SmallVector fixups; // instruction only + }; + llvm::SmallVector emitted; + + for (const auto& ev : streamer->events()) { + if (ev.kind == FastStreamer::EventKind::Label) { + emitted.push_back(EmittedEvent { /*is_label=*/true, /*output_offset=*/0, ev.symbol, llvm::MCInst {}, + /*sti=*/nullptr, {}, {} }); + continue; + } + EmittedEvent e { /*is_label=*/false, /*output_offset=*/0, /*symbol=*/nullptr, ev.inst, ev.sti, {}, {} }; + code_emitter->encodeInstruction(e.inst, e.bytes, e.fixups, *e.sti); + emitted.push_back(std::move(e)); } - // Inject user-defined labels - const uint64_t compensate_prepended_bkpt = (needs_prepend) ? PREPENDED_BYTES.size() : 0u; - for (const auto& label : labels) { - auto* inj_symbol = context.getOrCreateSymbol(label.name); - inj_symbol->setOffset(label.address - address + compensate_prepended_bkpt); - inj_symbol->setFragment(&*fragment_it); + auto recompute_offsets = [&]() { + size_t offset = 0; + for (auto& e : emitted) { + e.output_offset = offset; + if (e.is_label) { + e.symbol->setOffset(offset); + e.symbol->setFragment(dummy_fragment); + } else { + offset += e.bytes.size(); + } + } + }; + recompute_offsets(); + + auto evaluate_target = [&](const llvm::MCFixup& fixup, int64_t& out_value, llvm::MCValue& out_mc_value) -> bool { + const llvm::MCExpr* expr = fixup.getValue(); + if (expr == nullptr) { + context.reportError(fixup.getLoc(), "Fixup has no value expression (reported by Nyxstone)"); + return false; + } + if (!expr->evaluateAsRelocatable(out_mc_value, nullptr, &fixup)) { + context.reportError(fixup.getLoc(), "Could not evaluate fixup expression (reported by Nyxstone)"); + return false; + } + if (out_mc_value.getSymA() != nullptr && !out_mc_value.getSymA()->getSymbol().isDefined()) { + context.reportError(fixup.getLoc(), "Label undefined (reported by Nyxstone)"); + return false; + } + if (out_mc_value.getSymB() != nullptr && !out_mc_value.getSymB()->getSymbol().isDefined()) { + context.reportError(fixup.getLoc(), "Label undefined (reported by Nyxstone)"); + return false; + } + int64_t v = out_mc_value.getConstant(); + if (out_mc_value.getSymA() != nullptr) { + v += static_cast(out_mc_value.getSymA()->getSymbol().getOffset()); + } + if (out_mc_value.getSymB() != nullptr) { + v -= static_cast(out_mc_value.getSymB()->getSymbol().getOffset()); + } + out_value = v; + return true; + }; + + auto compute_fixup_value + = [&](const llvm::MCFixup& fixup, int64_t target_offset, uint64_t fixup_byte_offset) -> uint64_t { + const auto info = asm_backend_p->getFixupKindInfo(fixup.getKind()); + const bool is_pcrel = (info.Flags & llvm::MCFixupKindInfo::FKF_IsPCRel) != 0; + if (triple.isAArch64() && fixup.getTargetKind() == llvm::AArch64::fixup_aarch64_pcrel_adrp_imm21) { + constexpr uint64_t PAGE_SIZE { 0x1000 }; + const uint64_t local_addr = address + fixup_byte_offset; + const uint64_t target_addr = address + static_cast(target_offset); + return (target_addr & ~(PAGE_SIZE - 1)) - (local_addr & ~(PAGE_SIZE - 1)); + } + if (is_pcrel) { + // Include `address` so that the FKF_IsAlignedDownTo32Bits flag's + // align-down operates on the runtime PC. Without this, Thumb's + // `Align(PC, 4)` PC-relative loads/ADRs at 2-byte-aligned addresses + // produced wrong bytes — the old code papered over this by + // prepending a `bkpt` to force alignment-parity in the output. + uint64_t pc = address + fixup_byte_offset; + if ((info.Flags & llvm::MCFixupKindInfo::FKF_IsAlignedDownTo32Bits) != 0) { + pc &= ~uint64_t { 3 }; + } + const uint64_t target_addr = address + static_cast(target_offset); + return target_addr - pc; + } + return static_cast(target_offset) + address; + }; + + // Relax loop. For each fixup, compute its value and ask the backend whether + // it would need relaxation. If so and the instruction is relaxable, swap + // for the relaxed form and restart. Mirrors MCAssembler::layoutOnce() but + // without fragments. The Layout is required by some backends' relaxation + // predicate but unused by the ones nyxstone supports here. + llvm::MCAsmLayout layout(assembler); + constexpr size_t MAX_RELAX_ITERS { 32 }; + for (size_t iter = 0; iter < MAX_RELAX_ITERS; ++iter) { + recompute_offsets(); + bool relaxed_one = false; + for (auto& e : emitted) { + if (e.is_label) { + continue; + } + for (const auto& fx : e.fixups) { + int64_t target_offset = 0; + llvm::MCValue mc_value; + if (!evaluate_target(fx, target_offset, mc_value)) { + break; + } + const uint64_t fixup_byte_offset = e.output_offset + fx.getOffset(); + const uint64_t value = compute_fixup_value(fx, target_offset, fixup_byte_offset); + + // Only ask the backend whether the fixup needs relaxation when the + // instruction itself supports it; for non-relaxable instructions + // some backends return spurious "needs relaxation" answers from + // fixupNeedsRelaxationAdvanced. + if (asm_backend_p->mayNeedRelaxation(e.inst, *e.sti) + && asm_backend_p->fixupNeedsRelaxationAdvanced( + fx, /*Resolved=*/true, value, nullptr, layout, /*WasForced=*/false)) { + llvm::MCInst relaxed = e.inst; + asm_backend_p->relaxInstruction(relaxed, *e.sti); + e.inst = relaxed; + e.bytes.clear(); + e.fixups.clear(); + code_emitter->encodeInstruction(e.inst, e.bytes, e.fixups, *e.sti); + relaxed_one = true; + break; + } + } + if (relaxed_one || !extended_error.empty()) { + break; + } + } + if (!relaxed_one || !extended_error.empty()) { + break; + } + } + if (!extended_error.empty()) { + std::ostringstream error_stream; + error_stream << "Error during assembly: " << extended_error; + return tl::unexpected(error_stream.str()); } - // Perform assembly process - const bool error = parser->Run(/* NoInitialTextSection= */ true); - if (error || !extended_error.empty()) { + // Build the final output and apply fixups. + llvm::SmallVector output; + for (const auto& e : emitted) { + if (!e.is_label) { + output.append(e.bytes.begin(), e.bytes.end()); + } + } + + for (auto& e : emitted) { + if (e.is_label) { + continue; + } + for (const auto& fx : e.fixups) { + int64_t target_offset = 0; + llvm::MCValue mc_value; + if (!evaluate_target(fx, target_offset, mc_value)) { + break; + } + const uint64_t fixup_byte_offset = e.output_offset + fx.getOffset(); + const uint64_t value = compute_fixup_value(fx, target_offset, fixup_byte_offset); + + llvm::MutableArrayRef data( + reinterpret_cast(output.data()) + e.output_offset, e.bytes.size()); + asm_backend_p->applyFixup(assembler, fx, mc_value, data, value, /*IsResolved=*/true, e.sti); + } + if (!extended_error.empty()) { + break; + } + } + if (!extended_error.empty()) { std::ostringstream error_stream; - error_stream << "Error during assembly"; + error_stream << "Error during assembly: " << extended_error; + return tl::unexpected(error_stream.str()); + } + + // Re-run the nyxstone-specific validators on the final layout. These check + // ranges/alignments LLVM's backend misses for certain ARM/AArch64 fixups. + for (const auto& e : emitted) { + if (e.is_label) { + continue; + } + for (const auto& fx : e.fixups) { + int64_t target_offset = 0; + llvm::MCValue mc_value; + if (!evaluate_target(fx, target_offset, mc_value)) { + break; + } + const uint64_t fixup_byte_offset = e.output_offset + fx.getOffset(); + if (is_ArmT16_or_ArmT32(triple)) { + validate_arm_thumb_fixup( + fx, address, static_cast(target_offset), fixup_byte_offset, context); + } + if (triple.isAArch64()) { + validate_aarch64_fixup(fx, static_cast(target_offset), context); + } + } if (!extended_error.empty()) { - error_stream << ": " << extended_error; + break; } + } + if (!extended_error.empty()) { + std::ostringstream error_stream; + error_stream << "Error during assembly: " << extended_error; return tl::unexpected(error_stream.str()); } - auto res = remove_bkpt(output_bytes, instructions, needs_prepend).transform([&bytes](const auto& output) -> void { - // Copy bytes to output - bytes.clear(); - bytes.reserve(output.size()); - std::copy(output.begin(), output.end(), std::back_inserter(bytes)); - }); + if (instructions != nullptr) { + size_t offset = 0; + for (const auto& e : emitted) { + if (e.is_label) { + continue; + } + const size_t n = e.bytes.size(); + Nyxstone::Instruction insn; + insn.bytes.assign(output.begin() + offset, output.begin() + offset + n); + std::string text; + llvm::raw_string_ostream text_stream(text); + instruction_printer->printInst(&e.inst, /*Address=*/0, /*Annot=*/"", *e.sti, text_stream); + text.erase(0, text.find_first_not_of(" \t\n\r")); + std::replace(text.begin(), text.end(), '\t', ' '); + insn.assembly = std::move(text); + insn.address = offset; + instructions->push_back(std::move(insn)); + offset += n; + } + } + + bytes.assign(output.begin(), output.end()); - // Assign addresses if instruction details requested if (instructions != nullptr) { uint64_t current_address = address; for (Nyxstone::Instruction& insn : *instructions) { @@ -375,7 +627,7 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem } } - return res; + return {}; } tl::expected Nyxstone::disassemble_impl(const std::vector& bytes, uint64_t address, @@ -396,33 +648,21 @@ tl::expected Nyxstone::disassemble_impl(const std::vector error_msg; - llvm::MCContext context( - triple, assembler_info.get(), register_info.get(), subtarget_info.get(), nullptr, &target_options); - context.setDiagnosticHandler([&error_msg](const llvm::SMDiagnostic& SMD, bool IsInlineAsm, - const llvm::SourceMgr& SrcMgr, std::vector const& LocInfos) { - // Suppress unused parameter warning - (void)IsInlineAsm; - (void)SrcMgr; - (void)LocInfos; - + disasm_context->setDiagnosticHandler([&error_msg](const llvm::SMDiagnostic& SMD, bool /*IsInlineAsm*/, + const llvm::SourceMgr& /*SrcMgr*/, + std::vector const& /*LocInfos*/) { llvm::raw_svector_ostream error_stream(error_msg); SMD.print(nullptr, error_stream, /* ShowColors */ false); }); - // Create disassembler - auto disassembler = std::unique_ptr(target.createMCDisassembler(*subtarget_info, context)); - if (!disassembler) { - return tl::unexpected("Invalid architecture / LLVM target triple"); - } - - // Disassemble const llvm::ArrayRef data(bytes.data(), bytes.size()); uint64_t pos = 0; uint64_t insn_count = 0; + // Reused across iterations: avoids a heap allocation per disassembled + // instruction. The printer appends to the string; we clear it each round. + llvm::SmallString<64> insn_buf; while (true) { - // Decompose one instruction llvm::MCInst insn; uint64_t insn_size = 0; auto res = disassembler->getInstruction(insn, insn_size, data.slice(pos), address + pos, llvm::nulls()); @@ -435,38 +675,43 @@ tl::expected Nyxstone::disassemble_impl(const std::vectorprintInst(&insn, /* Address */ address + pos, /* Annot */ "", *subtarget_info, str_stream); - // left trim - insn_str.erase(0, insn_str.find_first_not_of(" \t\n\r")); - // convert tabulators to spaces - std::replace(insn_str.begin(), insn_str.end(), '\t', ' '); + // Trim leading whitespace and replace tabs with spaces in-place. + size_t start = 0; + while (start < insn_buf.size() + && (insn_buf[start] == ' ' || insn_buf[start] == '\t' || insn_buf[start] == '\n' + || insn_buf[start] == '\r')) { + ++start; + } + for (size_t i = start; i < insn_buf.size(); ++i) { + if (insn_buf[i] == '\t') { + insn_buf[i] = ' '; + } + } + const llvm::StringRef insn_view(insn_buf.data() + start, insn_buf.size() - start); - // Add instruction to results if (disassembly != nullptr) { - *disassembly += insn_str + "\n"; + disassembly->append(insn_view.data(), insn_view.size()); + disassembly->push_back('\n'); } if (instructions != nullptr) { Nyxstone::Instruction new_insn; new_insn.address = address + pos; - new_insn.assembly = insn_str; - new_insn.bytes.reserve(insn_size); - std::copy(data.begin() + pos, data.begin() + pos + insn_size, std::back_inserter(new_insn.bytes)); - instructions->push_back(new_insn); + new_insn.assembly.assign(insn_view.data(), insn_view.size()); + new_insn.bytes.assign(data.begin() + pos, data.begin() + pos + insn_size); + instructions->push_back(std::move(new_insn)); } - // Abort after n instructions if requested insn_count += 1; if (count != 0 && insn_count >= count) { break; } - // Prepare next iteration pos += insn_size; if (pos >= data.size()) { break; From c21c9eef7b463827fc4df491fae558877e7b072b Mon Sep 17 00:00:00 2001 From: Philipp Koppe <145699635+pkoppe@users.noreply.github.com> Date: Wed, 27 May 2026 01:23:48 +0200 Subject: [PATCH 2/8] clang-format fixes --- bindings/rust/src/nyxstone_ffi.cpp | 10 +++---- include/nyxstone.h | 10 +++---- src/nyxstone.cpp | 44 ++++++++++++++---------------- 3 files changed, 30 insertions(+), 34 deletions(-) diff --git a/bindings/rust/src/nyxstone_ffi.cpp b/bindings/rust/src/nyxstone_ffi.cpp index c65676f..fc044dd 100644 --- a/bindings/rust/src/nyxstone_ffi.cpp +++ b/bindings/rust/src/nyxstone_ffi.cpp @@ -40,9 +40,8 @@ ByteResult NyxstoneFFI::assemble( { std::vector cpp_labels {}; cpp_labels.reserve(labels.size()); - std::transform(std::begin(labels), std::end(labels), std::back_inserter(cpp_labels), [](const auto& label) { - return Nyxstone::LabelDefinition { std::string(label.name), label.address }; - }); + std::transform(std::begin(labels), std::end(labels), std::back_inserter(cpp_labels), + [](const auto& label) { return Nyxstone::LabelDefinition { std::string(label.name), label.address }; }); auto result = nyxstone->assemble(std::string { assembly }, address, cpp_labels).map([](const auto& cpp_bytes) { rust::Vec bytes {}; @@ -59,9 +58,8 @@ InstructionResult NyxstoneFFI::assemble_to_instructions( { std::vector cpp_labels; cpp_labels.reserve(labels.size()); - std::transform(std::begin(labels), std::end(labels), std::back_inserter(cpp_labels), [](const auto& label) { - return Nyxstone::LabelDefinition { std::string(label.name), label.address }; - }); + std::transform(std::begin(labels), std::end(labels), std::back_inserter(cpp_labels), + [](const auto& label) { return Nyxstone::LabelDefinition { std::string(label.name), label.address }; }); std::vector cpp_instructions {}; auto result = nyxstone->assemble_to_instructions(std::string { assembly }, address, cpp_labels) diff --git a/include/nyxstone.h b/include/nyxstone.h index 7bb8f83..74ee0c4 100644 --- a/include/nyxstone.h +++ b/include/nyxstone.h @@ -8,12 +8,12 @@ #include #include #include -#include #include #include #include #include #include +#include #include #include #pragma GCC diagnostic pop @@ -31,8 +31,8 @@ class TextOnlyObjectFileInfo : public llvm::MCObjectFileInfo { { // MCObjectFileInfo::Ctx is private; the parser never inspects it, // it only queries section pointers we populate here. - TextSection = ctx.getELFSection( - ".text", llvm::ELF::SHT_PROGBITS, llvm::ELF::SHF_ALLOC | llvm::ELF::SHF_EXECINSTR); + TextSection + = ctx.getELFSection(".text", llvm::ELF::SHT_PROGBITS, llvm::ELF::SHF_ALLOC | llvm::ELF::SHF_EXECINSTR); } }; @@ -79,8 +79,8 @@ class Nyxstone { Nyxstone(llvm::Triple&& triple, const llvm::Target& target, llvm::MCTargetOptions&& target_options, std::unique_ptr&& register_info, std::unique_ptr&& assembler_info, std::unique_ptr&& instruction_info, std::unique_ptr&& subtarget_info, - std::unique_ptr&& instruction_printer, - std::unique_ptr&& asm_backend, std::unique_ptr&& disasm_context, + std::unique_ptr&& instruction_printer, std::unique_ptr&& asm_backend, + std::unique_ptr&& disasm_context, std::unique_ptr&& disassembler) noexcept : triple(std::move(triple)) , target(target) diff --git a/src/nyxstone.cpp b/src/nyxstone.cpp index 1bbe976..174e212 100644 --- a/src/nyxstone.cpp +++ b/src/nyxstone.cpp @@ -7,23 +7,23 @@ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" -#include #include +#include #include #include -#include #include #include #include #include #include -#include #include #include #include #include #include +#include #include +#include #include #include #include @@ -294,14 +294,14 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem llvm::MCContext context( triple, assembler_info.get(), register_info.get(), subtarget_info.get(), &source_manager, &target_options); - context.setDiagnosticHandler([&extended_error](const llvm::SMDiagnostic& SMD, bool /*IsInlineAsm*/, - const llvm::SourceMgr& /*SrcMgr*/, - std::vector const& /*LocInfos*/) { - llvm::SmallString<128> error_msg; - llvm::raw_svector_ostream error_stream(error_msg); - SMD.print(nullptr, error_stream, /* ShowColors */ false); - extended_error += error_msg.c_str(); - }); + context.setDiagnosticHandler( + [&extended_error](const llvm::SMDiagnostic& SMD, bool /*IsInlineAsm*/, const llvm::SourceMgr& /*SrcMgr*/, + std::vector const& /*LocInfos*/) { + llvm::SmallString<128> error_msg; + llvm::raw_svector_ostream error_stream(error_msg); + SMD.print(nullptr, error_stream, /* ShowColors */ false); + extended_error += error_msg.c_str(); + }); if (!triple.isOSBinFormatELF()) { std::stringstream error_stream; @@ -339,8 +339,8 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem // MCAssembler is only used to satisfy the `const MCAssembler&` parameter of // MCAsmBackend::applyFixup (some backends call Asm.getContext() for error // reporting). The backend/emitter/writer it owns are not invoked through it. - llvm::MCAssembler assembler(context, std::move(throwaway_backend), std::move(code_emitter_uptr), - std::move(object_writer_uptr)); + llvm::MCAssembler assembler( + context, std::move(throwaway_backend), std::move(code_emitter_uptr), std::move(object_writer_uptr)); auto streamer = std::make_unique(context); streamer->setUseAssemblerInfoForParsing(true); @@ -551,8 +551,7 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem const uint64_t fixup_byte_offset = e.output_offset + fx.getOffset(); const uint64_t value = compute_fixup_value(fx, target_offset, fixup_byte_offset); - llvm::MutableArrayRef data( - reinterpret_cast(output.data()) + e.output_offset, e.bytes.size()); + llvm::MutableArrayRef data(reinterpret_cast(output.data()) + e.output_offset, e.bytes.size()); asm_backend_p->applyFixup(assembler, fx, mc_value, data, value, /*IsResolved=*/true, e.sti); } if (!extended_error.empty()) { @@ -579,8 +578,7 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem } const uint64_t fixup_byte_offset = e.output_offset + fx.getOffset(); if (is_ArmT16_or_ArmT32(triple)) { - validate_arm_thumb_fixup( - fx, address, static_cast(target_offset), fixup_byte_offset, context); + validate_arm_thumb_fixup(fx, address, static_cast(target_offset), fixup_byte_offset, context); } if (triple.isAArch64()) { validate_aarch64_fixup(fx, static_cast(target_offset), context); @@ -649,12 +647,12 @@ tl::expected Nyxstone::disassemble_impl(const std::vector error_msg; - disasm_context->setDiagnosticHandler([&error_msg](const llvm::SMDiagnostic& SMD, bool /*IsInlineAsm*/, - const llvm::SourceMgr& /*SrcMgr*/, - std::vector const& /*LocInfos*/) { - llvm::raw_svector_ostream error_stream(error_msg); - SMD.print(nullptr, error_stream, /* ShowColors */ false); - }); + disasm_context->setDiagnosticHandler( + [&error_msg](const llvm::SMDiagnostic& SMD, bool /*IsInlineAsm*/, const llvm::SourceMgr& /*SrcMgr*/, + std::vector const& /*LocInfos*/) { + llvm::raw_svector_ostream error_stream(error_msg); + SMD.print(nullptr, error_stream, /* ShowColors */ false); + }); const llvm::ArrayRef data(bytes.data(), bytes.size()); uint64_t pos = 0; From 0512cdd808111d9f939e7c8503cc87896f30b5c0 Mon Sep 17 00:00:00 2001 From: Philipp Koppe <145699635+pkoppe@users.noreply.github.com> Date: Wed, 27 May 2026 01:33:55 +0200 Subject: [PATCH 3/8] cppcheck fix --- src/FastStreamer.cpp | 2 ++ src/FastStreamer.h | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/FastStreamer.cpp b/src/FastStreamer.cpp index 14d879f..0e59616 100644 --- a/src/FastStreamer.cpp +++ b/src/FastStreamer.cpp @@ -1,11 +1,13 @@ #include "FastStreamer.h" namespace nyxstone { +// cppcheck-suppress unusedFunction // virtual override called via MCStreamer void FastStreamer::emitInstruction(const llvm::MCInst& inst, const llvm::MCSubtargetInfo& sti) { m_events.push_back(Event { EventKind::Instruction, nullptr, inst, &sti }); } +// cppcheck-suppress unusedFunction // virtual override called via MCStreamer void FastStreamer::emitLabel(llvm::MCSymbol* symbol, llvm::SMLoc /*loc*/) { m_events.push_back(Event { EventKind::Label, symbol, llvm::MCInst {}, nullptr }); diff --git a/src/FastStreamer.h b/src/FastStreamer.h index 68c6763..94686f0 100644 --- a/src/FastStreamer.h +++ b/src/FastStreamer.h @@ -41,8 +41,11 @@ class FastStreamer : public llvm::MCStreamer { void emitLabel(llvm::MCSymbol* symbol, llvm::SMLoc /*loc*/ = llvm::SMLoc()) override; // Pure-virtual stubs — assemble() never produces directives that hit these. + // cppcheck-suppress unusedFunction // virtual override called via MCStreamer bool emitSymbolAttribute(llvm::MCSymbol* /*sym*/, llvm::MCSymbolAttr /*attr*/) override { return true; } + // cppcheck-suppress unusedFunction // virtual override called via MCStreamer void emitCommonSymbol(llvm::MCSymbol* /*sym*/, uint64_t /*size*/, llvm::Align /*align*/) override { } + // cppcheck-suppress unusedFunction // virtual override called via MCStreamer void emitZerofill(llvm::MCSection* /*section*/, llvm::MCSymbol* /*sym*/ = nullptr, uint64_t /*size*/ = 0, llvm::Align /*align*/ = llvm::Align(1), llvm::SMLoc /*loc*/ = llvm::SMLoc()) override { From 101079e24ea2dd6297dc561b425d72612a861406 Mon Sep 17 00:00:00 2001 From: Philipp Koppe <145699635+pkoppe@users.noreply.github.com> Date: Wed, 27 May 2026 03:52:58 +0200 Subject: [PATCH 4/8] Added LLVM15-20 compat --- CMakeLists.txt | 8 ++- bindings/rust/build.rs | 9 ++- cmake/FindLLVM-Wrapper.cmake | 50 +++++++++++++--- src/FastStreamer.h | 12 ++++ .../AArch64/MCTargetDesc/AArch64MCExpr.h | 5 ++ src/nyxstone.cpp | 59 +++++++++++++++---- 6 files changed, 117 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8998ee5..b72d79e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,8 +20,14 @@ option(NYXSTONE_SANITIZERS "Enable sanitizers" OFF) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") if(DEFINED ENV{NYXSTONE_LLVM_PREFIX}) - # Ignore LLVM_ROOT variables + # Ignore LLVM_ROOT and system search paths so that a user-supplied prefix + # is the only place find_package(LLVM ...) looks. Without these, + # find_package would walk system paths and could pick up a different + # LLVM version (e.g., a newer one matching the iterated version) instead + # of the one the user pinned. set(CMAKE_FIND_USE_PACKAGE_ROOT_PATH OFF) + set(CMAKE_FIND_USE_CMAKE_SYSTEM_PATH OFF) + set(CMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH OFF) set(CMAKE_PREFIX_PATH $ENV{NYXSTONE_LLVM_PREFIX}) endif() diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs index e426f33..20c2660 100644 --- a/bindings/rust/build.rs +++ b/bindings/rust/build.rs @@ -132,8 +132,8 @@ fn search_llvm_config() -> Result { let version = version.parse::().context("Parsing LLVM version")?; ensure!( - (15..=18).contains(&version), - "LLVM major version is {}, must be 15-18.", + (15..=20).contains(&version), + "LLVM major version is {}, must be 15-20.", version ); @@ -196,7 +196,10 @@ fn target_os_is(name: &str) -> bool { /// Return an iterator over possible names for the llvm-config binary. fn llvm_config_binary_names() -> impl Iterator { - let base_names = (15..=18) + // Newest-first so $PATH can carry multiple llvm-config-N binaries without + // the older one masking the newest the user actually has installed. + let base_names = (15..=20) + .rev() .flat_map(|version| { [ format!("llvm-config-{}", version), diff --git a/cmake/FindLLVM-Wrapper.cmake b/cmake/FindLLVM-Wrapper.cmake index 0fed369..1895848 100644 --- a/cmake/FindLLVM-Wrapper.cmake +++ b/cmake/FindLLVM-Wrapper.cmake @@ -23,19 +23,51 @@ if(LLVM-Wrapper_FIND_REQUIRED) list(APPEND FIND_ARGS "REQUIRED") endif() -set(ALLOWED_LLVM_VERSIONS 15 16 17 18.0 18.1) +# Whitelist of supported major versions, ordered newest-first. Any minor/patch +# within these majors is accepted (LLVMConfigVersion.cmake requires exact +# major.minor matching, but we resolve the install before invoking +# find_package, so a new 20.2 or 19.3 works without a repo change). +# +# LLVM 21 and 22 are not yet supported: their reworked +# applyFixup/fixupNeedsRelaxationAdvanced APIs are MCFragment-centric and +# need deeper integration than our detached dummy fragment provides (ARM +# Thumb fixups crash inside libLLVM). +set(ALLOWED_LLVM_MAJORS 20 19 18 17 16 15) + +# When NYXSTONE_LLVM_PREFIX is set in CMakeLists.txt it pins CMAKE_PREFIX_PATH +# and disables system search paths, so find_package will resolve to the +# user-pinned install. Otherwise probe known per-major install layouts +# newest-first so that, with multiple LLVM versions present on the system, we +# pick the newest supported one rather than whatever happens to be first in +# CMake's default search order. +if(NOT DEFINED LLVM_DIR AND NOT DEFINED ENV{NYXSTONE_LLVM_PREFIX}) + foreach(MAJOR ${ALLOWED_LLVM_MAJORS}) + foreach(CANDIDATE + "/usr/lib/llvm-${MAJOR}/lib/cmake/llvm" + "/opt/homebrew/opt/llvm@${MAJOR}/lib/cmake/llvm" + "/usr/local/opt/llvm@${MAJOR}/lib/cmake/llvm") + if(EXISTS "${CANDIDATE}/LLVMConfig.cmake") + set(LLVM_DIR "${CANDIDATE}" CACHE PATH "LLVMConfig.cmake directory") + break() + endif() + endforeach() + if(DEFINED LLVM_DIR) + break() + endif() + endforeach() +endif() -# Find LLVM -foreach(VERSION ${ALLOWED_LLVM_VERSIONS}) - find_package(LLVM ${VERSION} QUIET ${FIND_ARGS}) - if(${LLVM_FOUND}) - break() - endif() -endforeach() +find_package(LLVM QUIET ${FIND_ARGS} CONFIG) unset(FIND_ARGS) if(NOT ${LLVM_FOUND}) - message(FATAL_ERROR "Did not find LLVM that has a compatible version. Allowed versions are 15-18.") + message(FATAL_ERROR "Did not find LLVM. Allowed major versions are ${ALLOWED_LLVM_MAJORS}.") +endif() + +if(NOT LLVM_VERSION_MAJOR IN_LIST ALLOWED_LLVM_MAJORS) + message(FATAL_ERROR + "Found LLVM ${LLVM_PACKAGE_VERSION}, but major version ${LLVM_VERSION_MAJOR} is not supported. " + "Supported major versions: ${ALLOWED_LLVM_MAJORS}.") endif() if(NOT LLVM-Wrapper_FIND_QUIETLY) diff --git a/src/FastStreamer.h b/src/FastStreamer.h index 94686f0..984db95 100644 --- a/src/FastStreamer.h +++ b/src/FastStreamer.h @@ -4,6 +4,7 @@ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" +#include #include #include #include @@ -41,8 +42,18 @@ class FastStreamer : public llvm::MCStreamer { void emitLabel(llvm::MCSymbol* symbol, llvm::SMLoc /*loc*/ = llvm::SMLoc()) override; // Pure-virtual stubs — assemble() never produces directives that hit these. + // The alignment parameter changed type in LLVM 16 (unsigned -> Align). // cppcheck-suppress unusedFunction // virtual override called via MCStreamer bool emitSymbolAttribute(llvm::MCSymbol* /*sym*/, llvm::MCSymbolAttr /*attr*/) override { return true; } +#if LLVM_VERSION_MAJOR < 16 + // cppcheck-suppress unusedFunction // virtual override called via MCStreamer + void emitCommonSymbol(llvm::MCSymbol* /*sym*/, uint64_t /*size*/, unsigned /*align*/) override { } + // cppcheck-suppress unusedFunction // virtual override called via MCStreamer + void emitZerofill(llvm::MCSection* /*section*/, llvm::MCSymbol* /*sym*/ = nullptr, uint64_t /*size*/ = 0, + unsigned /*align*/ = 0, llvm::SMLoc /*loc*/ = llvm::SMLoc()) override + { + } +#else // cppcheck-suppress unusedFunction // virtual override called via MCStreamer void emitCommonSymbol(llvm::MCSymbol* /*sym*/, uint64_t /*size*/, llvm::Align /*align*/) override { } // cppcheck-suppress unusedFunction // virtual override called via MCStreamer @@ -50,6 +61,7 @@ class FastStreamer : public llvm::MCStreamer { llvm::Align /*align*/ = llvm::Align(1), llvm::SMLoc /*loc*/ = llvm::SMLoc()) override { } +#endif private: std::vector m_events; diff --git a/src/Target/AArch64/MCTargetDesc/AArch64MCExpr.h b/src/Target/AArch64/MCTargetDesc/AArch64MCExpr.h index a6b6fe0..c508fd1 100644 --- a/src/Target/AArch64/MCTargetDesc/AArch64MCExpr.h +++ b/src/Target/AArch64/MCTargetDesc/AArch64MCExpr.h @@ -14,6 +14,7 @@ #ifndef LLVM_LIB_TARGET_AARCH64_MCTARGETDESC_AARCH64MCEXPR_H #define LLVM_LIB_TARGET_AARCH64_MCTARGETDESC_AARCH64MCEXPR_H +#include #include #include @@ -160,7 +161,11 @@ class AArch64MCExpr : public MCTargetExpr { MCFragment* findAssociatedFragment() const override; +#if LLVM_VERSION_MAJOR < 19 bool evaluateAsRelocatableImpl(MCValue& Res, const MCAsmLayout* Layout, const MCFixup* Fixup) const override; +#else + bool evaluateAsRelocatableImpl(MCValue& Res, const MCAssembler* Asm, const MCFixup* Fixup) const override; +#endif void fixELFSymbolsInTLSFixups(MCAssembler& Asm) const override; diff --git a/src/nyxstone.cpp b/src/nyxstone.cpp index 174e212..1886549 100644 --- a/src/nyxstone.cpp +++ b/src/nyxstone.cpp @@ -8,8 +8,13 @@ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-parameter" #include +#include #include +#if LLVM_VERSION_MAJOR < 19 +// MCAsmLayout was removed in LLVM 19; in 19+ the relaxation API takes +// MCAssembler directly. #include +#endif #include #include #include @@ -202,6 +207,23 @@ tl::expected, std::string> Nyxstone::disassem } namespace { + /// Cross-version `MCCodeEmitter::encodeInstruction` wrapper. LLVM 15-16 + /// expose only the `raw_ostream&` overload; LLVM 17+ adds the public + /// `SmallVectorImpl&` overload (and makes the stream variant + /// `protected`). We always carry our own SmallVector, so wrap it in + /// `raw_svector_ostream` for the older versions. + inline void encode_instruction(llvm::MCCodeEmitter& emitter, const llvm::MCInst& inst, + llvm::SmallVectorImpl& bytes, llvm::SmallVectorImpl& fixups, + const llvm::MCSubtargetInfo& sti) + { +#if LLVM_VERSION_MAJOR < 17 + llvm::raw_svector_ostream os(bytes); + emitter.encodeInstruction(inst, os, fixups, sti); +#else + emitter.encodeInstruction(inst, bytes, fixups, sti); +#endif + } + /// Validates an ARM Thumb fixup: range and alignment checks LLVM is missing /// for some kinds, producing wrong bytes instead of an error. Alignment /// and range checks operate on the runtime addresses (`address + @@ -403,7 +425,7 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem continue; } EmittedEvent e { /*is_label=*/false, /*output_offset=*/0, /*symbol=*/nullptr, ev.inst, ev.sti, {}, {} }; - code_emitter->encodeInstruction(e.inst, e.bytes, e.fixups, *e.sti); + encode_instruction(*code_emitter, e.inst, e.bytes, e.fixups, *e.sti); emitted.push_back(std::move(e)); } @@ -452,22 +474,24 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem auto compute_fixup_value = [&](const llvm::MCFixup& fixup, int64_t target_offset, uint64_t fixup_byte_offset) -> uint64_t { + const auto kind = fixup.getTargetKind(); const auto info = asm_backend_p->getFixupKindInfo(fixup.getKind()); const bool is_pcrel = (info.Flags & llvm::MCFixupKindInfo::FKF_IsPCRel) != 0; - if (triple.isAArch64() && fixup.getTargetKind() == llvm::AArch64::fixup_aarch64_pcrel_adrp_imm21) { + const bool is_aligned_down_32 = (info.Flags & llvm::MCFixupKindInfo::FKF_IsAlignedDownTo32Bits) != 0; + if (triple.isAArch64() && kind == llvm::AArch64::fixup_aarch64_pcrel_adrp_imm21) { constexpr uint64_t PAGE_SIZE { 0x1000 }; const uint64_t local_addr = address + fixup_byte_offset; const uint64_t target_addr = address + static_cast(target_offset); return (target_addr & ~(PAGE_SIZE - 1)) - (local_addr & ~(PAGE_SIZE - 1)); } if (is_pcrel) { - // Include `address` so that the FKF_IsAlignedDownTo32Bits flag's - // align-down operates on the runtime PC. Without this, Thumb's - // `Align(PC, 4)` PC-relative loads/ADRs at 2-byte-aligned addresses - // produced wrong bytes — the old code papered over this by - // prepending a `bkpt` to force alignment-parity in the output. + // Include `address` so that the align-down-to-4 operation works on + // the runtime PC. Without this, Thumb's `Align(PC, 4)` PC-relative + // loads/ADRs at 2-byte-aligned addresses produced wrong bytes — + // the old code papered over this by prepending a `bkpt` to force + // alignment-parity in the output. uint64_t pc = address + fixup_byte_offset; - if ((info.Flags & llvm::MCFixupKindInfo::FKF_IsAlignedDownTo32Bits) != 0) { + if (is_aligned_down_32) { pc &= ~uint64_t { 3 }; } const uint64_t target_addr = address + static_cast(target_offset); @@ -479,9 +503,11 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem // Relax loop. For each fixup, compute its value and ask the backend whether // it would need relaxation. If so and the instruction is relaxable, swap // for the relaxed form and restart. Mirrors MCAssembler::layoutOnce() but - // without fragments. The Layout is required by some backends' relaxation - // predicate but unused by the ones nyxstone supports here. + // without fragments. The Layout/Assembler ref is required by some backends' + // relaxation predicate but unused by the ones nyxstone supports here. +#if LLVM_VERSION_MAJOR < 19 llvm::MCAsmLayout layout(assembler); +#endif constexpr size_t MAX_RELAX_ITERS { 32 }; for (size_t iter = 0; iter < MAX_RELAX_ITERS; ++iter) { recompute_offsets(); @@ -503,15 +529,22 @@ tl::expected Nyxstone::assemble_impl(const std::string& assem // instruction itself supports it; for non-relaxable instructions // some backends return spurious "needs relaxation" answers from // fixupNeedsRelaxationAdvanced. - if (asm_backend_p->mayNeedRelaxation(e.inst, *e.sti) +#if LLVM_VERSION_MAJOR < 19 + const bool needs_relax = asm_backend_p->mayNeedRelaxation(e.inst, *e.sti) + && asm_backend_p->fixupNeedsRelaxationAdvanced( + fx, /*Resolved=*/true, value, nullptr, layout, /*WasForced=*/false); +#else + const bool needs_relax = asm_backend_p->mayNeedRelaxation(e.inst, *e.sti) && asm_backend_p->fixupNeedsRelaxationAdvanced( - fx, /*Resolved=*/true, value, nullptr, layout, /*WasForced=*/false)) { + assembler, fx, /*Resolved=*/true, value, nullptr, /*WasForced=*/false); +#endif + if (needs_relax) { llvm::MCInst relaxed = e.inst; asm_backend_p->relaxInstruction(relaxed, *e.sti); e.inst = relaxed; e.bytes.clear(); e.fixups.clear(); - code_emitter->encodeInstruction(e.inst, e.bytes, e.fixups, *e.sti); + encode_instruction(*code_emitter, e.inst, e.bytes, e.fixups, *e.sti); relaxed_one = true; break; } From c1542e6146b6f0889132d643a1d063ec2e11c96e Mon Sep 17 00:00:00 2001 From: Philipp Koppe <145699635+pkoppe@users.noreply.github.com> Date: Wed, 27 May 2026 04:03:08 +0200 Subject: [PATCH 5/8] CI fixes --- .github/workflows/cpp.yml | 2 +- bindings/rust/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index 811c5fd..f83207f 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -19,7 +19,7 @@ jobs: - name: Packages run: | sudo apt-get update - sudo apt-get install zlib1g-dev libzstd-dev cppcheck clang-tidy clang-format llvm-15 llvm-15-dev + sudo apt-get install zlib1g-dev libzstd-dev cppcheck clang-tidy clang-format libc++-dev llvm-15 llvm-15-dev - name: Code quality - fmt run: ./tool/format.sh check - name: Code quality - cppcheck diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs index bcd0796..d709e01 100644 --- a/bindings/rust/src/lib.rs +++ b/bindings/rust/src/lib.rs @@ -139,7 +139,7 @@ impl Nyxstone { /// - `assembly`: The instructions to assemble. /// - `address`: The start location of the instructions. /// - `labels`: Additional label definitions by absolute address, expects a reference to some `Map, u64>` - /// which can be iterated over. + /// which can be iterated over. /// /// # Returns: /// Ok() and bytecode on success, Err() otherwise. From b1ece4a46f4c1e7f7d88dc235d8ec6ebb07edf99 Mon Sep 17 00:00:00 2001 From: Philipp Koppe <145699635+pkoppe@users.noreply.github.com> Date: Wed, 27 May 2026 04:12:13 +0200 Subject: [PATCH 6/8] CI fix --- bindings/rust/build.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs index 20c2660..05c2046 100644 --- a/bindings/rust/build.rs +++ b/bindings/rust/build.rs @@ -476,6 +476,12 @@ fn get_link_libraries(llvm_config_path: &Path, preferences: &LinkingPreferences) fn extract_library(s: &str, kind: LibraryKind) -> Vec { s.split(&[' ', '\n'] as &[char]) .filter(|s| !s.is_empty()) + // Polly is an LLVM polyhedral optimizer that nyxstone does not use, + // but `llvm-config --libnames` lists it. Ubuntu's `llvm-N-dev` apt + // package ships the dynamic Polly libs but not the static archives, + // so a default `cargo build` (which prefers static linking) fails + // with `could not find native static library Polly`. Skip them. + .filter(|s| !s.starts_with("libPolly") && !s.starts_with("Polly")) .map(|name| { // --libnames gives library filenames. Extract only the name that // we need to pass to the linker. From 7d69138d654abab65029bfb58b72e3ab49658a94 Mon Sep 17 00:00:00 2001 From: Philipp Koppe <145699635+pkoppe@users.noreply.github.com> Date: Wed, 27 May 2026 16:59:29 +0200 Subject: [PATCH 7/8] Updated READMEs --- README.md | 208 +++++++++++++++++------------------ bindings/python/README.md | 57 +++++----- bindings/rust/README.md | 43 ++++++-- cmake/FindLLVM-Wrapper.cmake | 3 +- 4 files changed, 163 insertions(+), 148 deletions(-) diff --git a/README.md b/README.md index 6237f4f..14f4417 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PyPI](https://img.shields.io/pypi/v/nyxstone.svg)](https://pypi.org/project/nyxstone) [![cpp-docs](https://github.com/emproof-com/nyxstone/actions/workflows/doxygen.yml/badge.svg)](https://emproof-com.github.io/nyxstone/) -Nyxstone is a powerful assembly and disassembly library based on LLVM. It doesn’t require patches to the LLVM source tree and links against standard LLVM libraries available in most Linux distributions. Implemented as a C++ library, Nyxstone also offers Rust and Python bindings. It supports all official LLVM architectures and allows to configure architecture-specific target settings. +Nyxstone is a fast assembly and disassembly library built on top of LLVM. It does not require patches to the LLVM source tree and links against the standard LLVM libraries shipped by most Linux distributions, Homebrew, and `apt.llvm.org`. The core is a C++ library with Rust and Python bindings. Nyxstone supports every architecture the linked LLVM ships with and lets you configure architecture-specific CPU and feature settings. ![Nyxstone Python binding demo](/images/demo.svg) @@ -22,28 +22,28 @@ Nyxstone is a powerful assembly and disassembly library based on LLVM. It doesn 4. [Roadmap](#roadmap) 5. [License](#license) 6. [Contributing](#contributing) -7. [Contributors](#contributors) +7. [Maintainers](#maintainers) ## Core Features -* Assembles and disassembles code for all architectures supported by the linked LLVM, including x86, ARM, MIPS, RISC-V and others. +* Assembles and disassembles code for every architecture supported by the linked LLVM, including x86, ARM, AArch64, MIPS, RISC-V, and others. -* C++ library based on LLVM with Rust and Python bindings. +* C++ library built on LLVM, with Rust and Python bindings. * Native platform support for Linux and macOS. -* Supports labels in the assembler, including the specification of label-to-address mappings +* Supports labels in the assembler, including user-provided label-to-address mappings. -* Assembles and disassembles to raw bytes and text, but also provides detailed instruction objects containing the address, raw bytes, and the assembly representation. +* Produces raw bytes, text disassembly, or detailed instruction objects that carry the address, raw bytes, and assembly text together. -* Disassembly can be limited to a user-specified number of instructions from byte sequences. +* Disassembly can be limited to a user-specified number of instructions. -* Allows to configure architecture-specific target features, such as ISA extensions and hardware features. +* Configurable per-architecture target settings (CPU, ISA extensions, hardware features). -For a comprehensive list of supported architectures, you can use `clang -print-targets`. For a comprehensive list of features for each architecture, refer to `llc -march=ARCH -mattr=help`. +For the list of supported architectures, run `clang -print-targets`. For per-architecture features, run `llc -march=ARCH -mattr=help`. > [!NOTE] -> Disclaimer: Nyxstone has been primarily developed and tested for x86_64, AArch64, and ARM32 architectures. We have a high degree of confidence in its ability to accurately generate assembly and identify errors for these platforms. For other architectures, Nyxstone's effectiveness depends on the reliability and performance of their respective LLVM backends. +> Nyxstone has been primarily developed and tested on x86_64, AArch64, and ARM32. We have high confidence in its correctness and error reporting for those targets. For other architectures, the result depends on the underlying LLVM backend. ## Using Nyxstone @@ -51,65 +51,62 @@ This section provides instructions on how to get started with Nyxstone, covering ### Prerequisites -Before building Nyxstone, ensure clang and LLVM are present on your system. Nyxstone supports LLVM major versions 15-18. -When building it looks for `llvm-config` in your system's `$PATH` or the specified environment variable `$NYXSTONE_LLVM_PREFIX/bin`. +Before building Nyxstone, ensure clang and LLVM are present on your system. **Nyxstone supports LLVM major versions 15-20.** Any minor/patch within those majors works; the build picks the newest LLVM it can find unless you pin one. -Installation Options for LLVM versions 15-18: +The build resolves LLVM in this order: -* Ubuntu -```bash -sudo apt install llvm-${version} llvm-${version}-dev -``` +1. `$NYXSTONE_LLVM_PREFIX`, if set. The build searches that prefix exclusively (system paths are ignored), so this is the way to pin a specific version when multiple are installed. +2. Known per-major install layouts probed newest-first: `/usr/lib/llvm-` (Debian/Ubuntu), `/opt/homebrew/opt/llvm@` (Homebrew on Apple Silicon), `/usr/local/opt/llvm@` (Homebrew on x86 macOS), `/opt/brew/opt/llvm@` (custom-prefix Homebrew on Linux). +3. CMake's default `find_package(LLVM)` search. -* Debian -LLVM version 15 and 16 are available through debian repositories. Installation is the same as for Ubuntu. -For versions 17 or 18 refer to [https://apt.llvm.org/](https://apt.llvm.org/) for installation instructions. +If the resolved version is outside 15-20 the configure step fails with a clear error. -* Arch -```bash -sudo pacman -S llvm llvm-libs -``` +#### Installation -* Homebrew (macOS, Linux and others): -```bash -brew install llvm@18 -export NYXSTONE_LLVM_PREFIX=/opt/homebrew/opt/llvm@18 -``` +* **Debian / Ubuntu** + ```bash + sudo apt install llvm-${version} llvm-${version}-dev + ``` + Debian trixie ships 17-19, Ubuntu ships 15-17 in the default repos. For versions not in your distro's repos, follow the instructions at [apt.llvm.org](https://apt.llvm.org/). The script `apt.llvm.org/llvm.sh ` is the easiest path. -* From LLVM Source: +* **Arch** + ```bash + sudo pacman -S llvm llvm-libs + ``` -_Note_: On Windows you need to run these commands from a Visual Studio 2022 x64 command prompt. Additionally replace `~lib/my-llvm-18` with a different path. +* **Homebrew (macOS / Linux)** + ```bash + brew install llvm@20 + export NYXSTONE_LLVM_PREFIX="$(brew --prefix llvm@20)" + ``` -```bash -# checkout llvm -git clone -b release/18.x --single-branch https://github.com/llvm/llvm-project.git -cd llvm-project +* **From source** -# build LLVM with custom installation directory -cmake -S llvm -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_PARALLEL_LINK_JOBS=1 -cmake --build build -cmake --install build --prefix ~/lib/my-llvm-18 + On Windows, run these from a Visual Studio 2022 x64 command prompt and replace `~/lib/my-llvm-20` with a path of your choice. -# export path -export NYXSTONE_LLVM_PREFIX=~/lib/my-llvm-18 -``` + ```bash + git clone -b release/20.x --single-branch https://github.com/llvm/llvm-project.git + cd llvm-project + cmake -S llvm -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DLLVM_PARALLEL_LINK_JOBS=1 + cmake --build build + cmake --install build --prefix ~/lib/my-llvm-20 + export NYXSTONE_LLVM_PREFIX=~/lib/my-llvm-20 + ``` -Also make sure to install any system dependent libraries needed by your LLVM version for static linking. They can be viewed with the command `llvm-config --system-libs`; the list can be empty. On Ubuntu/Debian, you will need the packages `zlib1g-dev` and `zlibstd-dev`. +You may also need any system libraries LLVM was built against. Check with `llvm-config --system-libs`; on Debian/Ubuntu this is typically `zlib1g-dev` and `libzstd-dev`. ### CLI Tool -Nyxstone comes with a handy [CLI tool](examples/nyxstone-cli.cpp) for quick assembly and disassembly tasks. Checkout the Nyxstone repository, and build the tool with CMake: +Nyxstone ships a [CLI tool](examples/nyxstone-cli.cpp) for one-off assembly and disassembly. Clone the repository and build it with CMake: ```bash -# clone directory git clone https://github.com/emproof-com/nyxstone cd nyxstone - -# build nyxstone -mkdir build && cd build && cmake .. && make +cmake -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j ``` -Then, `nyxstone` can be used from the command line. Here's an output of its help menu: +The resulting `nyxstone` binary lands in `build/`. Its help menu: ``` $ ./nyxstone -h @@ -145,14 +142,14 @@ Notes: The features for a target can be found with 'llc -mtriple= -mattr=help'. ``` -Now, we can assemble an instruction for the x86_64 architecture: +Assemble a single x86_64 instruction: ``` $ ./nyxstone -t x86_64 "mov rax, rbx" 0x00000000: mov rax, rbx ; 48 89 d8 ``` -We can also assemble a sequence of instructions. In the following, we make use of label-based addressing and assume the first instruction is mapped to address `0xdeadbeef`: +Assemble a sequence with internal labels, anchored at `0xdeadbeef`: ``` $ ./nyxstone -t x86_64 -p 0xdeadbeef "cmp rax, rbx; jz .exit; inc rax; .exit: ret" @@ -162,14 +159,14 @@ $ ./nyxstone -t x86_64 -p 0xdeadbeef "cmp rax, rbx; jz .exit; inc rax; .exit: re 0xdeadbef7: ret ; c3 ``` -Furthermore, we can disassemble instructions for different instruction sets, here the ARM32 thumb instruction set: +Disassemble for a different ISA, here ARM Thumb: ``` $ ./nyxstone -t thumbv8 -d "13 37" 0x00000000: adds r7, #19 ; 13 37 ``` -Using the support for user-defined labels, we can assemble this snippet which does not contain the label `.label` by specifying its memory location ourself. +Pin an external label to a specific address with `--labels`: ``` $ ./nyxstone -p "0x1000" -l ".label=0x1238" "jmp .label" @@ -178,51 +175,46 @@ $ ./nyxstone -p "0x1000" -l ".label=0x1238" "jmp .label" ### C++ Library -To use Nyxstone as a C++ library, your C++ code has to be linked against Nyxstone and LLVM. +Link your code against the `nyxstone::nyxstone` CMake target. Nyxstone propagates its LLVM dependency transitively, so consumers do not need to call `find_package(LLVM)` themselves. -The following cmake example assumes Nyxstone in a subdirectory `nyxstone` in your project: +Assuming Nyxstone lives in a `nyxstone/` subdirectory: ```cmake add_subdirectory(nyxstone) add_executable(my_executable main.cpp) -target_link_libraries(my_executable nyxstone::nyxstone) +target_link_libraries(my_executable PRIVATE nyxstone::nyxstone) ``` -The corresponding C++ usage example: - +A minimal C++ usage example: ```c++ #include -#include +#include #include "nyxstone.h" -int main(int, char**) { - // Create the nyxstone instance: - auto nyxstone { - NyxstoneBuilder("x86_64") - .build() - .value() - }; +using namespace nyxstone; - // Assemble to bytes - std::vector bytes = - nyxstone->assemble(/*assembly=*/"mov rax, rbx", /*address=*/0x1000, /* labels= */ {}).value(); +int main() { + auto nyxstone = NyxstoneBuilder("x86_64").build().value(); - std::vector expected {0x48, 0x89, 0xd8}; - assert(bytes == expected); + // Assemble to bytes. + auto bytes = nyxstone->assemble(/*assembly=*/"mov rax, rbx", + /*address=*/0x1000, + /*labels=*/{}).value(); - return 0; + const std::vector expected{0x48, 0x89, 0xd8}; + assert(bytes == expected); } ``` -For a comprehensive C++ example, take a look at [example.cpp](examples/example.cpp). +For a fuller walkthrough, including `assemble_to_instructions`, label definitions, and disassembly, see [examples/example.cpp](examples/example.cpp). ### Rust Bindings -To use Nyxstone as a Rust library, add it to your `Cargo.toml`and use it as shown in the following example: +Add `nyxstone` to your `Cargo.toml` and use it like so: ```rust use anyhow::Result; @@ -245,17 +237,17 @@ fn main() -> Result<()> { } ``` -For more instructions regarding the Rust binding, take a look at the corresponding [README](bindings/rust/README.md). +See the [Rust binding README](bindings/rust/README.md) for build options (static vs. dynamic LLVM linking, FFI quirks). ### Python Bindings -To use Nyxstone from Python, install it using pip: +Install via pip: ```bash pip install nyxstone ``` -Then, you can use it from Python: +Then from Python: ``` $ python -q @@ -264,41 +256,47 @@ $ python -q >>> nyxstone.assemble("jne .loop", 0x1100, {".loop": 0x1000}) ``` -Detailed instructions are available in the corresponding [README](bindings/python/README.md). +See the [Python binding README](bindings/python/README.md) for build-from-source instructions. ## How it works -Nyxstone leverages public C++ API functions from LLVM such as `Target::createMCAsmParser` and `Target::createMCDisassembler` to perform assembly and disassembly tasks. Nyxstone also extends two LLVM classes, `MCELFStreamer` and `MCObjectWriter`, to inject custom logic and extract additional information. Specifically, Nyxstone augments the assembly process with the following steps: +Nyxstone drives the LLVM MC layer directly through its public C++ API (`Target::createMCAsmParser`, `Target::createMCDisassembler`, `MCAsmBackend`, `MCCodeEmitter`, …) instead of going through the object-file emission pipeline. This avoids both the cost of producing a complete ELF and the need to extend `MCELFStreamer` / `MCObjectWriter` as earlier versions did. -* `ELFStreamerWrapper::emitInstruction`: Captures assembly representation and initial raw bytes of instructions if detailed instruction objects are requested by the user. +The assembly path is structured as follows: -* `ObjectWriterWrapper::writeObject`: Writes the final raw bytes of instructions (with relocation adjustments) to detailed instruction objects. Furthermore, it switches raw bytes output from complete ELF file to just the .text section. +* **Parse into events.** A minimal `MCStreamer` subclass, [`FastStreamer`](src/FastStreamer.h), captures the parser's `emitLabel` / `emitInstruction` calls into a flat event list, deferring all encoding. The full ELF object-file machinery is bypassed. -* `ObjectWriterWrapper::validate_fixups`: Conducts extra checks, such as verifying the range and alignment of relocations. +* **Skip the rest of the MC pipeline.** Nyxstone installs a minimal `TextOnlyObjectFileInfo` that registers only `.text` with the `MCContext`, skipping the ~40 other section creations `MCObjectFileInfo::initMCObjectFileInfo` performs by default. -* `ObjectWriterWrapper::recordRelocation`: Applies additional relocations. `MCObjectWriter` skips some relocations that are only applied during linking. Right now, this is only relevant for the `fixup_aarch64_pcrel_adrp_imm21` relocation of the Aarch64 instruction `adrp`. +* **Encode and lay out.** Each captured `MCInst` is encoded once with `MCCodeEmitter::encodeInstruction`. Nyxstone then runs its own iterative relax loop, mirroring `MCAssembler::layoutOnce` without fragments, and computes per-fixup PC-relative values directly from the user-supplied base address. -While extending LLVM classes introduces some drawbacks, like a strong dependency on a specific LLVM version, we believe this approach is still preferable over alternatives that require hard to maintain patches in the LLVM source tree. +* **Apply fixups.** After layout converges, fixups are evaluated and `MCAsmBackend::applyFixup` patches the final bytes. -We are committed to further reduce the project's complexity and open to suggestions for improvement. Looking ahead, we may eliminate the need to extend LLVM classes by leveraging the existing LLVM infrastructure in a smarter way or incorporating additional logic in a post-processing step. +* **Validate.** Nyxstone runs additional range and alignment checks for ARM Thumb (`adr`, `ldr` literal, `b/bl/bcc`, …) and AArch64 (`adr`) fixup kinds that LLVM's backend silently mis-encodes when out of range. +The disassembly path is much simpler: an `MCDisassembler` and its `MCContext` are constructed once on each `Nyxstone` instance and reused across calls, since disassembly never mutates the context. +* **Version coupling.** Nyxstone uses MC headers that LLVM does not promise stable across major versions. Each new major release tends to require a small number of version-conditional shims; the current range (15-20) is covered by `#if LLVM_VERSION_MAJOR` guards in [src/nyxstone.cpp](src/nyxstone.cpp) and [src/FastStreamer.h](src/FastStreamer.h). The vendored LLVM-internal headers under [src/Target/](src/Target/) (`AArch64FixupKinds.h`, `AArch64MCExpr.h`, `ARMFixupKinds.h`) are tracked similarly, because LLVM does not install them. -## Roadmap -Below are some ideas and improvements we believe would significantly advance Nyxstone. The items are not listed in any particular order: -* [ ] Check thread safety +## Roadmap -* [ ] Add support for more LLVM versions (auto select depending on found LLVM library version) +Recent work: -* [ ] Explore option to make LLVM apply all relocations (including `adrp`) by configuring `MCObjectWriter` differently or using a different writer +* [x] Replace the `MCELFStreamer` / `MCObjectWriter` extensions with the lighter `FastStreamer` + own layout/fixup loop. +* [x] Handle relocations LLVM's writer skipped (e.g. AArch64 `adrp`) directly in our fixup pass. +* [x] Run range/alignment validators as a post-processing step on the laid-out output instead of inside a custom writer. +* [x] Drop the ARM Thumb `bkpt`-prepend workaround by making the PC computation address-aware. +* [x] Cache `MCAsmBackend`, `MCContext`, and `MCDisassembler` across calls for substantially faster assembly and disassembly. +* [x] Support LLVM 15-20 with auto-selection of the newest installed version. -* [ ] Explore option to generate detailed instructions objects by disassembling the raw bytes output of the assembly process instead of relying on the extension of LLVM classes +Still open: -* [ ] Explore option to implement extra range and alignment of relocations in a post-processing step instead of relying on the extension of LLVM classes +* [ ] Verify and document thread safety beyond `NyxstoneBuilder::build()` (LLVM init is mutex-guarded; subsequent `assemble`/`disassemble` are not formally verified concurrent-safe on a single instance). +* [ ] Extend support to LLVM 21+ (the reworked fragment-centric `applyFixup` / `fixupNeedsRelaxationAdvanced` API needs deeper integration than the current detached dummy fragment provides). ## License @@ -307,28 +305,24 @@ Nyxstone is available under the [MIT license](LICENSE). ## Contributing -We welcome contributions from the community! If you encounter any issues with Nyxstone, please feel free to open a GitHub issue. +Contributions are welcome. If you hit a bug, please open a GitHub issue with a reproducer (target triple, input, expected vs. actual output). -If you are interested in contributing directly to the project, you can for example: +If you'd like to send code, useful starting points are: -* Address an existing issue -* Develop new features -* Improve documentation +* Picking up an open roadmap item or an existing issue. +* Adding tests, particularly for architectures other than x86/AArch64/ARM, where coverage is thinnest. +* Improving the documentation. -Once you're ready, submit a pull request with your changes. We are looking forward to your contribution! +PRs welcome, please run the existing C++ and Rust tests, plus `tool/format.sh check` and `tool/static-analysis-{cppcheck,tidy}.sh`, before opening one. -## Contributors +## Maintainers -The current contributors are: +Nyxstone is maintained by [emproof](https://emproof.com). For questions or anything that needs a real human, ping: -**Core**: -* Philipp Koppe (emproof) -* Rachid Mzannar (emproof) -* Darius Hartlief (emproof) +* [Philipp Koppe](https://github.com/pkoppe) +* [Darius Hartlief](https://github.com/stuxnot) -**Minor**: -* Marc Fyrbiak (emproof) -* Tim Blazytko (emproof) +For the full list of everyone who has contributed code, see [the contributors graph](https://github.com/emproof-com/nyxstone/graphs/contributors). ## Acknowledgements diff --git a/bindings/python/README.md b/bindings/python/README.md index 92e3fb4..bd02ee5 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -5,82 +5,83 @@ ## Installation -You need to have LLVM (with major version in the range 15-18) installed to build the nyxstone bindings. The `setup.py` searches for LLVM in the `PATH` or in the directory set in the environment variable `NYXSTONE_LLVM_PREFIX`. Specifically, it searches for the binary `$NYXSTONE_LLVM_PREFIX/bin/llvm-config` and uses it to set the required libraries and cpp flags. +Building the bindings requires LLVM with a major version in **15-20** installed on your system. `setup.py` resolves LLVM by running `$NYXSTONE_LLVM_PREFIX/bin/llvm-config` if `NYXSTONE_LLVM_PREFIX` is set, otherwise `llvm-config` on `$PATH`. -Running +Install the published wheel from PyPI: ```sh pip install nyxstone ``` -will download nyxstone from PyPI. Alternatively, you can install nyxstone directly from source by running the following command in the python bindings subdirectory. +Or build and install from source (run from the `bindings/python` directory): -``` +```sh pip install . ``` -If you are using Arch Linux, you will need to use a virtual environment to install nyxstone: +On Arch Linux (and other distros that mark the system Python as externally managed), do this inside a virtualenv: -``` -mkdir env -python -mvenv env/ -source env/bin/activate[.fish|.zsh] -pip install [.|nyxstone] +```sh +python -m venv env +source env/bin/activate # or activate.fish / activate.zsh +pip install . # or `pip install nyxstone` ``` ## Example -After you have installed nyxstone, import the `Nyxstone` and `Instruction` classes from nyxstone. +Import the `Nyxstone` and `Instruction` classes: ```python from nyxstone import Nyxstone, Instruction ``` -Now you can create the `Nyxstone` object. +Create a `Nyxstone` instance for the target architecture: ```python nyxstone = Nyxstone("x86_64") ``` -The nyxstone object can be used to assemble and disassemble for the architecture it was initialized for. +Assemble and disassemble raw bytes / text: ```python -assert(nyxstone.assemble("mov rax, rbx") == [0x48, 0x89, 0xd8]) -assert(nyxstone.disassemble([0x48, 0x89, 0xd8]) == "mov rax, rbx\n") +assert nyxstone.assemble("mov rax, rbx") == [0x48, 0x89, 0xd8] +assert nyxstone.disassemble([0x48, 0x89, 0xd8]) == "mov rax, rbx\n" ``` -Nyxstone can also assemble and disassemble to instruction information holding the address, bytes, and assembly of the assembled or disassembled instructions. +Or work with `Instruction` objects that bundle address, bytes, and assembly text: ```python instructions = [Instruction(0x0, "mov rax, rbx", [0x48, 0x89, 0xd8])] -assert(nyxstone.assemble_to_instructions("mov rax, rbx") == instructions) -assert(nyxstone.disassemble_to_instructions([0x48, 0x89, 0xd8]) == instructions) +assert nyxstone.assemble_to_instructions("mov rax, rbx") == instructions +assert nyxstone.disassemble_to_instructions([0x48, 0x89, 0xd8]) == instructions ``` -When assembling, you can also specify the address of the instructions, as well as external labels. If you need to assemble inline labels, Nyxstone also got you covered. +When assembling, you can supply a base address and a label-to-address map for external labels; inline labels are resolved automatically: ```python -assert(nyxstone.assemble("jmp .label", address = 0x1000, labels = {".label": 0x1200}) == [0xe9, 0xfb, 0x01, 0x00, 0x00]) -assert(nyxstone.assemble("jmp .label; nop; .label:", address = 0x1000) == [0xeb, 0x01, 0x90]) +assert nyxstone.assemble("jmp .label", address=0x1000, labels={".label": 0x1200}) == [0xe9, 0xfb, 0x01, 0x00, 0x00] +assert nyxstone.assemble("jmp .label; nop; .label:", address=0x1000) == [0xeb, 0x01, 0x90] ``` -When disassembling, you can also specify the address, as well as the number of instructions to disassemble. Here, `0` means all instructions. +When disassembling, you can specify the base address and a maximum instruction count (`0` means "until the bytes are exhausted"): ```python -assert(nyxstone.disassemble([0x48, 0x31, 0xc0, 0x48, 0x01, 0xd8], 0x1000, 0) == "xor rax, rax\nadd rax, rbx\n") -assert(nyxstone.disassemble([0x48, 0x31, 0xc0, 0x48, 0x01, 0xd8], 0x1000, 1) == "xor rax, rax\n") +assert nyxstone.disassemble([0x48, 0x31, 0xc0, 0x48, 0x01, 0xd8], 0x1000, 0) == "xor rax, rax\nadd rax, rbx\n" +assert nyxstone.disassemble([0x48, 0x31, 0xc0, 0x48, 0x01, 0xd8], 0x1000, 1) == "xor rax, rax\n" ``` ## Building -If you just want to build the python bindings, run: -``` +To build without installing: + +```sh python setup.py build ``` ## Packaging -To package the python bindings, use -``` +To produce a sdist / wheel: + +```sh python -m build ``` diff --git a/bindings/rust/README.md b/bindings/rust/README.md index 5d12963..9c2b422 100644 --- a/bindings/rust/README.md +++ b/bindings/rust/README.md @@ -3,30 +3,49 @@ [![crates.io](https://img.shields.io/crates/v/nyxstone.svg)](https://crates.io/crates/nyxstone) [![Github Rust CI Badge](https://github.com/emproof-com/nyxstone/actions/workflows/rust.yml/badge.svg)](https://github.com/emproof-com/nyxstone/actions/workflows/rust.yml) -Official bindings for the Nyxstone assembler/disassembler engine. +Official Rust bindings for the [Nyxstone](https://github.com/emproof-com/nyxstone) assembler/disassembler engine. ## Building -The project can be build via `cargo build`, as long as LLVM with a major version in the range 15-18 is installed in the `$PATH` or the environment variable `$NYXSTONE_LLVM_PREFIX` points to the installation location of a LLVM library. +`cargo build` works as long as a supported LLVM is reachable. The build script discovers LLVM in this order: -LLVM might be linked against FFI, but not correctly report this fact via `llvm-config`. If your LLVM does link FFI and -Nyxstone fails to run, set the `NYXSTONE_LINK_FFI` environment variable to `1`, which will ensure that Nyxstone -links against `libffi`. +1. `$NYXSTONE_LLVM_PREFIX`, if set searched exclusively. +2. `llvm-config-N`, `llvm-configN`, or `llvmN-config` on `$PATH`, probed newest-first for `N` in 15-20. +3. Plain `llvm-config` on `$PATH` as a final fallback. + +**Supported LLVM major versions: 15-20.** Any minor/patch within those majors works. + +### LLVM linking + +By default the build links LLVM statically. The following Cargo features change that: + +| Feature | Effect | +|------------------|-------------------------------------------------------| +| `prefer-static` | Prefer static, fall back to dynamic. | +| `prefer-dynamic` | Prefer dynamic, fall back to static. | +| `force-static` | Static only; fail if static archives are missing. | +| `force-dynamic` | Dynamic only; fail if shared libraries are missing. | + +On Linux, `cargo build` defaults to `prefer-static`. On macOS it defaults to `prefer-dynamic` (Homebrew's LLVM ships shared libraries first). + +### FFI workaround + +LLVM may be linked against `libffi` without `llvm-config` reporting it. If you get unresolved-symbol errors involving FFI, set `NYXSTONE_LINK_FFI=1` so Nyxstone links `libffi` explicitly. ## Installation -Add nyxstone as a dependency in your `Cargo.toml`: -``` +Add Nyxstone as a dependency: + +```toml [dependencies] nyxstone = "0.1" ``` -Building nyxstone requires a C/C++ compiler to be installed on your system. Additionally, Nyxstone requires LLVM with -a major version in the range 15-18. +You will need a C/C++ compiler available, plus an LLVM in the supported range. ## Sample -In the following is a short sample of what using Nyxstone can look like: +A short tour of the API: ```rust extern crate anyhow; @@ -86,8 +105,8 @@ fn main() -> Result<()> { ## Technical overview -The nyxstone-rs bindings are generated via the `cxx` crate. Since nyxstone is specifically a c++ library, we currently do not plan to support C bindings via bindgen. +The Rust bindings are generated with the [`cxx`](https://cxx.rs/) crate. Because the underlying engine is a C++ library, we don't plan to ship `bindgen`-based C bindings. ## Acknowledgements -The build script of the rust bindings borrow heavily from the [llvm-sys](https://gitlab.com/taricorp/llvm-sys.rs) build script. +The build script borrows heavily from [llvm-sys](https://gitlab.com/taricorp/llvm-sys.rs). diff --git a/cmake/FindLLVM-Wrapper.cmake b/cmake/FindLLVM-Wrapper.cmake index 1895848..b38b8a7 100644 --- a/cmake/FindLLVM-Wrapper.cmake +++ b/cmake/FindLLVM-Wrapper.cmake @@ -45,7 +45,8 @@ if(NOT DEFINED LLVM_DIR AND NOT DEFINED ENV{NYXSTONE_LLVM_PREFIX}) foreach(CANDIDATE "/usr/lib/llvm-${MAJOR}/lib/cmake/llvm" "/opt/homebrew/opt/llvm@${MAJOR}/lib/cmake/llvm" - "/usr/local/opt/llvm@${MAJOR}/lib/cmake/llvm") + "/usr/local/opt/llvm@${MAJOR}/lib/cmake/llvm" + "/opt/brew/opt/llvm@${MAJOR}/lib/cmake/llvm") if(EXISTS "${CANDIDATE}/LLVMConfig.cmake") set(LLVM_DIR "${CANDIDATE}" CACHE PATH "LLVMConfig.cmake directory") break() From ee6fd3c3549e4fd5f5bea547dd11bc0a4f1d12b5 Mon Sep 17 00:00:00 2001 From: Philipp Koppe <145699635+pkoppe@users.noreply.github.com> Date: Wed, 27 May 2026 17:18:24 +0200 Subject: [PATCH 8/8] Added benchmark section --- README.md | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 14f4417..c374300 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,11 @@ Nyxstone is a fast assembly and disassembly library built on top of LLVM. It doe 4. [Rust Bindings](#rust-bindings) 5. [Python Bindings](#python-bindings) 3. [How it works](#how-it-works) -4. [Roadmap](#roadmap) -5. [License](#license) -6. [Contributing](#contributing) -7. [Maintainers](#maintainers) +4. [Benchmarks](#benchmarks) +5. [Roadmap](#roadmap) +6. [License](#license) +7. [Contributing](#contributing) +8. [Maintainers](#maintainers) ## Core Features @@ -280,7 +281,35 @@ The disassembly path is much simpler: an `MCDisassembler` and its `MCContext` ar * **Version coupling.** Nyxstone uses MC headers that LLVM does not promise stable across major versions. Each new major release tends to require a small number of version-conditional shims; the current range (15-20) is covered by `#if LLVM_VERSION_MAJOR` guards in [src/nyxstone.cpp](src/nyxstone.cpp) and [src/FastStreamer.h](src/FastStreamer.h). The vendored LLVM-internal headers under [src/Target/](src/Target/) (`AArch64FixupKinds.h`, `AArch64MCExpr.h`, `ARMFixupKinds.h`) are tracked similarly, because LLVM does not install them. +## Benchmarks +Numbers below were collected with the bundled benchmark binaries on a 13th Gen Intel Core i7-1370P (Linux, LLVM 18, release build, 2 s per measurement). Reproduce with: + +```bash +# C++ +cmake --build build --target benchmark +./build/benchmark 2 + +# Rust +cargo run --release --example benchmark -- 2 +``` + +Each call assembles/disassembles a package of 1 or 10 instructions for the named architecture. The 10-instruction package amortizes fixed per-call overhead, which is why `ops/s` drops but `insns/s` (parenthesized) rises. + +| Architecture | Package | C++ assemble (ops/s) | C++ disassemble (ops/s) | +| ------------ | ------------ | -------------------- | ----------------------- | +| x86_64 | 1 instr | 79 k | 6.36 M | +| x86_64 | 10 instr | 47 k (470 k insns/s) | 675 k (6.75 M insns/s) | +| x86_32 | 1 instr | 80 k | 6.42 M | +| x86_32 | 10 instr | 48 k (484 k insns/s) | 685 k (6.85 M insns/s) | +| aarch64 | 1 instr | 26 k | 4.49 M | +| aarch64 | 10 instr | 7.0 k (70 k insns/s) | 373 k (3.73 M insns/s) | +| armv8m | 1 instr | 79 k | 5.68 M | +| armv8m | 10 instr | 30 k (300 k insns/s) | 388 k (3.88 M insns/s) | + +The Rust binding adds the cxx-bridge call overhead. For assembly this is negligible (within ~3 % of the C++ numbers); for the much faster disassembly path it costs roughly 30 % on single-instruction calls (e.g. x86_64 disassemble: 6.36 M ops/s in C++ vs 4.31 M ops/s in Rust) and shrinks to near-zero as the call does more work. + +AArch64 assembly is markedly slower per instruction than the other targets because that backend exercises the iterative relaxation loop more heavily. ## Roadmap