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/6] 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/6] 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/6] 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/6] 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/6] 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/6] 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.