Jelajahi Sumber

Merge pull request #37 from clairechingching/master

feat: sbpf-linker
Claire Fan 1 bulan lalu
induk
melakukan
b3eb7ef94b

+ 4 - 2
Cargo.toml

@@ -21,10 +21,11 @@ termcolor = "1.4"
 sbpf-assembler = { workspace = true }
 object = "0.37.3"
 sbpf-disassembler = { workspace = true }
+sbpf-linker = { workspace = true }
 sbpf-common = { workspace = true }
 
 [workspace]
-members = ["crates/assembler", "crates/common", "crates/disassembler"]
+members = ["crates/assembler", "crates/common", "crates/disassembler", "crates/linker"]
 exclude = ["examples"]
 
 [workspace.package]
@@ -38,4 +39,5 @@ anyhow = "1.0.86"
 
 sbpf-assembler = { path = "crates/assembler" }
 sbpf-disassembler = { path = "crates/disassembler" }
-sbpf-common = { path = "crates/common" }
+sbpf-linker = { path = "crates/linker" }
+sbpf-common = { path = "crates/common" }

+ 2 - 0
crates/assembler/Cargo.toml

@@ -14,6 +14,8 @@ num-traits = { workspace = true }
 thiserror = { workspace = true }
 anyhow = { workspace = true }
 object = "0.37.3"
+phf = "0.13.1"
+phf_macros = "0.13.1"
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 wasm-bindgen = { version = "0.2.92", features = ["serde-serialize"] }

+ 53 - 109
crates/assembler/src/instruction.rs

@@ -1,7 +1,9 @@
 use crate::opcode::Opcode;
 use crate::lexer::{Token, ImmediateValue};
-use std::ops::Range;
 use crate::dynsym::RelocationType;
+use crate::syscall::SYSCALLS;
+
+use std::ops::Range;
 
 #[derive(Debug, Clone)]
 pub struct Instruction {
@@ -68,78 +70,55 @@ impl Instruction {
         let span = 0..bytes.len();
 
         let opcode = Opcode::from_u8(bytes[0]).unwrap();
+        let reg = bytes[1];
+        let src = reg >> 4;
+        let dst = reg & 0x0f;
+        let off = i16::from_le_bytes([bytes[2], bytes[3]]);
+        let imm = match opcode {
+            Opcode::Lddw => {
+                let imm_low =   // 
+                    i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
+                let imm_high =  // 
+                    i32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
+                let imm64 = ((imm_high as i64) << 32) | (imm_low as u32 as i64);
+                imm64
+            }
+            _ => i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]) as i64,
+        };
+
 
         match opcode {
             Opcode::Lddw => {
-                // lddw format: [opcode(1)] [dst_reg(1)] [0(1)] [0(1)] [imm32_low(4)] [0(4)] [imm32_high(4)]
-                if bytes.len() < 16 {
-                    return None;
-                }
-            
-                let dst_reg = bytes[1];
-                let imm_low = i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
-                let imm_high = i32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]);
-                let imm64 = ((imm_high as i64) << 32) | (imm_low as u32 as i64);
-            
-                operands.push(Token::Register(dst_reg, 1..2));
-                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm64), 4..12));
+                operands.push(Token::Register(dst, 1..2));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm), 4..12));
             }
         
             Opcode::Call => {
-                // call format: [opcode(1)] [0(1)] [0(1)] [0(1)] [imm32(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let imm32 = i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
-                // HARDCODE call imm 4242 to sol_log_
-                if imm32 == 4242 {
-                    operands.push(Token::Identifier("sol_log_".to_string(), 4..8));
+                if let Some(name) = SYSCALLS.get(&(imm as u32)) {
+                    operands.push(Token::Identifier(name.to_string(), 4..8));
                 } else {
-                    operands.push(Token::ImmediateValue(ImmediateValue::Int(imm32 as i64), 4..8));
+                    operands.push(Token::ImmediateValue(ImmediateValue::Int(imm as i64), 4..8));
                 }
             }
         
             Opcode::Ja => {
-                // ja format: [opcode(1)] [0(1)] [imm16(2)] [0(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let imm16 = i16::from_le_bytes([bytes[2], bytes[3]]);
-                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm16 as i64), 2..4));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(off as i64), 2..4));
             }
         
-            // Jump instructions with immediate values
             Opcode::JeqImm | Opcode::JgtImm | Opcode::JgeImm | Opcode::JltImm | 
             Opcode::JleImm | Opcode::JsetImm | Opcode::JneImm | Opcode::JsgtImm | 
             Opcode::JsgeImm | Opcode::JsltImm | Opcode::JsleImm => {
-                // Format: [opcode(1)] [dst_reg(1)] [0(1)] [0(1)] [imm32(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let dst_reg = bytes[1];
-                let imm32 = i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
-            
-                operands.push(Token::Register(dst_reg, 1..2));
-                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm32 as i64), 4..8));
+                operands.push(Token::Register(dst, 1..2));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm as i64), 4..8));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(off as i64), 2..4));
             }
         
-            // Jump instructions with register operands
             Opcode::JeqReg | Opcode::JgtReg | Opcode::JgeReg | Opcode::JltReg | 
             Opcode::JleReg | Opcode::JsetReg | Opcode::JneReg | Opcode::JsgtReg | 
             Opcode::JsgeReg | Opcode::JsltReg | Opcode::JsleReg => {
-                // Format: [opcode(1)] [dst_reg(1)] [src_reg(1)] [0(1)] [0(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let dst_reg = bytes[1];
-                let src_reg = bytes[2];
-            
-                operands.push(Token::Register(dst_reg, 1..2));
-                operands.push(Token::Register(src_reg, 2..3));
+                operands.push(Token::Register(dst, 1..2));
+                operands.push(Token::Register(src, 1..2));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(off as i64), 2..4));
             }
         
             // Arithmetic instructions with immediate values
@@ -153,16 +132,8 @@ impl Instruction {
             Opcode::Arsh64Imm | Opcode::Hor64Imm | Opcode::Lmul64Imm | Opcode::Uhmul64Imm |
             Opcode::Udiv64Imm | Opcode::Urem64Imm | Opcode::Shmul64Imm | Opcode::Sdiv64Imm |
             Opcode::Srem64Imm => {
-                // Format: [opcode(1)] [dst_reg(1)] [0(1)] [0(1)] [imm32(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let dst_reg = bytes[1];
-                let imm32 = i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
-            
-                operands.push(Token::Register(dst_reg, 1..2));
-                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm32 as i64), 4..8));
+                operands.push(Token::Register(dst, 1..2));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm as i64), 4..8));
             }
         
             // Arithmetic instructions with register operands
@@ -175,59 +146,32 @@ impl Instruction {
             Opcode::Rsh64Reg | Opcode::Mod64Reg | Opcode::Xor64Reg | Opcode::Mov64Reg |
             Opcode::Arsh64Reg | Opcode::Lmul64Reg | Opcode::Uhmul64Reg | Opcode::Udiv64Reg |
             Opcode::Urem64Reg | Opcode::Shmul64Reg | Opcode::Sdiv64Reg | Opcode::Srem64Reg => {
-                // Format: [opcode(1)] [dst_reg(1)] [src_reg(1)] [0(1)] [0(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let dst_reg = bytes[1];
-                let src_reg = bytes[2];
+                operands.push(Token::Register(dst   , 1..2));
+                operands.push(Token::Register(src, 1..2));
+            }
+
+            Opcode::Ldxw | Opcode::Ldxh | Opcode::Ldxb | Opcode::Ldxdw => {
+                operands.push(Token::Register(dst, 1..2));
+                operands.push(Token::Register(src, 1..2));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(off as i64), 2..4));
+            }
             
-                operands.push(Token::Register(dst_reg, 1..2));
-                operands.push(Token::Register(src_reg, 2..3));
+            Opcode::Stw | Opcode::Sth | Opcode::Stb | Opcode::Stdw => {
+                operands.push(Token::Register(dst, 1..2));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(off as i64), 2..4));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm as i64), 4..8));
             }
-        
-            // Load/Store instructions with immediate offset
-            Opcode::Ldxb | Opcode::Ldxh | Opcode::Ldxw | Opcode::Ldxdw |
-            Opcode::Stb | Opcode::Sth | Opcode::Stw | Opcode::Stdw |
+
             Opcode::Stxb | Opcode::Stxh | Opcode::Stxw | Opcode::Stxdw => {
-                // Format: [opcode(1)] [dst_reg(1)] [src_reg(1)] [offset16(2)] [imm32(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let dst_reg = bytes[1];
-                let offset16 = u16::from_le_bytes([bytes[3], bytes[4]]);
-                let imm32 = i32::from_le_bytes([bytes[5], bytes[6], bytes[7], 0]);
-            
-                operands.push(Token::Register(dst_reg, 1..2));
-                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm32 as i64), 5..8));
-                operands.push(Token::ImmediateValue(ImmediateValue::Int(offset16 as i64), 3..5));
+                operands.push(Token::Register(dst, 1..2));
+                operands.push(Token::Register(src, 1..2));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(off as i64), 2..4));
+                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm as i64), 4..8));
             }
-        
+            
             // Unary operations
             Opcode::Neg32 | Opcode::Neg64 | Opcode::Exit => {
-                // Format: [opcode(1)] [dst_reg(1)] [0(1)] [0(1)] [0(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let dst_reg = bytes[1];
-                operands.push(Token::Register(dst_reg, 1..2));
-            }
-        
-            // Endianness operations
-            Opcode::Le | Opcode::Be => {
-                // Format: [opcode(1)] [dst_reg(1)] [0(1)] [0(1)] [imm32(4)]
-                if bytes.len() < 8 {
-                    return None;
-                }
-            
-                let dst_reg = bytes[1];
-                let imm32 = i32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
-            
-                operands.push(Token::Register(dst_reg, 1..2));
-                operands.push(Token::ImmediateValue(ImmediateValue::Int(imm32 as i64), 4..8));
+                operands.push(Token::Register(dst, 1..2));
             }
         
             _ => {

+ 1 - 14
crates/assembler/src/lib.rs

@@ -4,7 +4,6 @@ use anyhow::Result;
 pub mod parser;
 pub mod lexer;
 pub mod opcode;
-pub mod byteparser;
 
 // Error handling and diagnostics
 pub mod macros;
@@ -16,6 +15,7 @@ pub mod ast;
 pub mod astnode;
 pub mod dynsym;
 pub mod instruction;
+pub mod syscall;
 
 // ELF header, program, section
 pub mod header;
@@ -34,7 +34,6 @@ pub use self::{
     parser::parse_tokens,
     program::Program,
     lexer::tokenize,
-    byteparser::parse_bytecode,
 };
 
 pub fn assemble(source: &str) -> Result<Vec<u8>, Vec<CompileError>>{
@@ -55,15 +54,3 @@ pub fn assemble(source: &str) -> Result<Vec<u8>, Vec<CompileError>>{
     Ok(bytecode)
     
 }
-
-pub fn link_program(source: &Vec<u8>) -> Result<Vec<u8>, String> {
-    let parse_result = match parse_bytecode(source) {
-        Ok(program) => program,
-        Err(errors) => {
-            return Err(errors);
-        }
-    };
-    let program = Program::from_parse_result(parse_result);
-    let bytecode = program.emit_bytecode();
-    Ok(bytecode)
-}

+ 35 - 0
crates/assembler/src/syscall.rs

@@ -0,0 +1,35 @@
+use phf::Map;
+use phf_macros::phf_map;
+
+pub static SYSCALLS: Map<u32, &'static str> = phf_map! {
+    0xb6fc1a11u32 => "abort",
+    0x686093bbu32 => "sol_panic_",
+    0x207559bdu32 => "sol_log_",
+    0x5c2a3178u32 => "sol_log_64_",
+    0x52ba5096u32 => "sol_log_compute_units_",
+    0x7ef088cau32 => "sol_log_pubkey",
+    0x9377323cu32 => "sol_create_program_address",
+    0x48504a38u32 => "sol_try_find_program_address",
+    0x11f49d86u32 => "sol_sha256",
+    0xd7793abbu32 => "sol_keccak256",
+    0x17e40350u32 => "sol_secp256k1_recover",
+    0x174c5122u32 => "sol_blake3",
+    0xaa2607cau32 => "sol_curve_validate_point",
+    0xdd1c41a6u32 => "sol_curve_group_op",
+    0xd56b5fe9u32 => "sol_get_clock_sysvar",
+    0x23a29a61u32 => "sol_get_epoch_schedule_sysvar",
+    0x3b97b73cu32 => "sol_get_fees_sysvar",
+    0xbf7188f6u32 => "sol_get_rent_sysvar",
+    0x717cc4a3u32 => "sol_memcpy_",
+    0x434371f8u32 => "sol_memmove_",
+    0x5fdcde31u32 => "sol_memcmp_",
+    0x3770fb22u32 => "sol_memset_",
+    0xa22b9c85u32 => "sol_invoke_signed_c",
+    0xd7449092u32 => "sol_invoke_signed_rust",
+    0x83f00e8fu32 => "sol_alloc_free_",
+    0xa226d3ebu32 => "sol_set_return_data",
+    0x5d2245e4u32 => "sol_get_return_data",
+    0x7317b434u32 => "sol_log_data",
+    0xadb8efc8u32 => "sol_get_processed_sibling_instruction",
+    0x85532d94u32 => "sol_get_stack_height",
+};

+ 2 - 2
crates/common/src/instruction.rs

@@ -1,7 +1,7 @@
-use std::ops::Range;
-
 use crate::opcode::Opcode;
 
+use std::ops::Range;
+
 #[derive(Debug, Clone)]
 pub struct Register {
     pub n: u8,

+ 56 - 0
crates/linker/Cargo.toml

@@ -0,0 +1,56 @@
+[package]
+name = "sbpf-linker"
+edition = "2024"
+version.workspace = true
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "sbpf_linker"
+
+[dependencies]
+sbpf-assembler = { path = "../assembler" }
+clap = { version = "4.5.13", features = ["derive"] }
+object = "0.37.3"
+anyhow = "1.0"
+bpf-linker = "0.9.15"
+
+aya-rustc-llvm-proxy = { version = "0.9.5", optional = true }
+llvm-sys-19 = { package = "llvm-sys", features = ["disable-alltargets-init"], version = "191.0.0", optional = true }
+llvm-sys-20 = { package = "llvm-sys", features = ["disable-alltargets-init"], version = "201.0.1", optional = true }
+llvm-sys-21 = { package = "llvm-sys", features = ["disable-alltargets-init"], version = "211.0.0-rc1", optional = true }
+
+tracing-appender = "0.2"
+tracing-subscriber = { version = "0.3", features = ["env-filter", "registry"] }
+tracing-tree = "0.4"
+
+tracing = "0.1"
+thiserror.workspace = true
+
+[[bin]]
+name = "sbpf-linker"
+
+[features]
+llvm-19 = ["dep:llvm-sys-19"]
+llvm-20 = ["dep:llvm-sys-20"]
+llvm-21 = ["dep:llvm-sys-21"]
+rust-llvm-19 = [
+    "dep:aya-rustc-llvm-proxy",
+    "llvm-19",
+    "llvm-sys-19/no-llvm-linking",
+]
+rust-llvm-20 = [
+    "dep:aya-rustc-llvm-proxy",
+    "llvm-20",
+    "llvm-sys-20/no-llvm-linking",
+]
+rust-llvm-21 = [
+    "dep:aya-rustc-llvm-proxy",
+    "llvm-21",
+    "llvm-sys-21/no-llvm-linking",
+]
+default = [
+    "llvm-21",
+    "rust-llvm-21",
+    "rustc-build-sysroot",
+]
+rustc-build-sysroot = []

+ 222 - 0
crates/linker/src/bin/sbpf-linker.rs

@@ -0,0 +1,222 @@
+use std::{
+    env, fs,
+    ffi::CString,
+    path::PathBuf,
+    str::FromStr,
+};
+
+#[cfg(any(
+    feature = "rust-llvm-19",
+    feature = "rust-llvm-20",
+    feature = "rust-llvm-21"
+))]
+use aya_rustc_llvm_proxy as _;
+use anyhow::Context;
+use thiserror::Error;
+use bpf_linker::{Cpu, Linker, LinkerOptions, OptLevel, OutputType};
+use clap::{
+    error::ErrorKind,
+    Parser,
+};
+use sbpf_linker::link_program;
+
+#[derive(Debug, Error)]
+enum CliError {
+    #[error("optimization level needs to be between 0-3, s or z (instead was `{0}`)")]
+    InvalidOptimization(String),
+//     #[error("unknown emission type: `{0}` - expected one of: `llvm-bc`, `asm`, `llvm-ir`, `obj`")]
+//     InvalidOutputType(String),
+}
+
+#[derive(Copy, Clone, Debug)]
+struct CliOptLevel(OptLevel);
+
+impl FromStr for CliOptLevel {
+    type Err = CliError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(Self(match s {
+            "0" => OptLevel::No,
+            "1" => OptLevel::Less,
+            "2" => OptLevel::Default,
+            "3" => OptLevel::Aggressive,
+            "s" => OptLevel::Size,
+            "z" => OptLevel::SizeMin,
+            _ => return Err(CliError::InvalidOptimization(s.to_string())),
+        }))
+    }
+}
+
+#[derive(Debug, Parser)]
+#[command(version)]
+struct CommandLine {
+    /// Target BPF processor. Can be one of `generic`, `probe`, `v1`, `v2`, `v3`
+    #[clap(long, default_value = "generic")]
+    cpu: Cpu,
+    
+    /// Write output to <output>
+    #[clap(short, long)]
+    output: PathBuf,
+
+    /// Emit BTF information
+    #[clap(long)]
+    btf: bool,
+
+    /// Permit automatic insertion of __bpf_trap calls.
+    /// See: https://github.com/llvm/llvm-project/commit/ab391beb11f733b526b86f9df23734a34657d876
+    #[clap(long)]
+    allow_bpf_trap: bool,
+
+    /// Add a directory to the library search path
+    #[clap(short = 'L', number_of_values = 1)]
+    libs: Vec<PathBuf>,
+
+    /// Optimization level. 0-3, s, or z
+    #[clap(short = 'O', default_value = "2")]
+    optimize: Vec<CliOptLevel>,
+
+    /// Export the symbols specified in the file `path`. The symbols must be separated by new lines
+    #[clap(long, value_name = "path")]
+    export_symbols: Option<PathBuf>,
+
+    /// Try hard to unroll loops. Useful when targeting kernels that don't support loops
+    #[clap(long)]
+    unroll_loops: bool,
+
+    /// Ignore `noinline`/`#[inline(never)]`. Useful when targeting kernels that don't support function calls
+    #[clap(long)]
+    ignore_inline_never: bool,
+
+    /// Dump the final IR module to the given `path` before generating the code
+    #[clap(long, value_name = "path")]
+    dump_module: Option<PathBuf>,
+
+    /// Extra command line arguments to pass to LLVM
+    #[clap(long, value_name = "args", use_value_delimiter = true, action = clap::ArgAction::Append)]
+    llvm_args: Vec<CString>,
+
+    /// Disable passing --bpf-expand-memcpy-in-order to LLVM.
+    #[clap(long)]
+    disable_expand_memcpy_in_order: bool,
+
+    /// Disable exporting memcpy, memmove, memset, memcmp and bcmp. Exporting
+    /// those is commonly needed when LLVM does not manage to expand memory
+    /// intrinsics to a sequence of loads and stores.
+    #[clap(long)]
+    disable_memory_builtins: bool,
+
+    /// Input files. Can be object files or static libraries
+    #[clap(required = true)]
+    inputs: Vec<PathBuf>,
+
+    /// Comma separated list of symbols to export. See also `--export-symbols`
+    #[clap(long, value_name = "symbols", use_value_delimiter = true, action = clap::ArgAction::Append)]
+    export: Vec<String>,
+
+    /// Whether to treat LLVM errors as fatal.
+    #[clap(long, action = clap::ArgAction::Set, default_value_t = true)]
+    fatal_errors: bool,
+
+    // The options below are for wasm-ld compatibility
+    #[clap(long = "debug", hide = true)]
+    _debug: bool,
+}
+
+fn main() -> anyhow::Result<()> {
+    let args = env::args().map(|arg| {
+        if arg == "-flavor" {
+            "--flavor".to_string()
+        } else {
+            arg
+        }
+    });
+
+    let CommandLine {
+        cpu,
+        output,
+        btf,
+        allow_bpf_trap,
+        libs,
+        optimize,
+        export_symbols,
+        unroll_loops,
+        ignore_inline_never,
+        dump_module,
+        llvm_args,
+        disable_expand_memcpy_in_order,
+        disable_memory_builtins,
+        inputs,
+        export,
+        fatal_errors,
+        _debug,
+    } = match Parser::try_parse_from(args) {
+        Ok(command_line) => command_line,
+        Err(err) => match err.kind() {
+            ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => {
+                print!("{err}");
+                return Ok(());
+            }
+            _ => return Err(err.into()),
+        },
+    };
+
+    let export_symbols = export_symbols.map(fs::read_to_string).transpose()?;
+
+    // TODO: the data is owned by this call frame; we could make this zero-alloc.
+    let export_symbols = export_symbols
+        .as_deref()
+        .into_iter()
+        .flat_map(str::lines)
+        .map(str::to_owned)
+        .chain(export)
+        .map(Into::into)
+        .collect();
+    
+    let optimize = match *optimize.as_slice() {
+        [] => unreachable!("emit has a default value"),
+        [.., CliOptLevel(optimize)] => optimize,
+    };
+    
+    let mut linker = Linker::new(LinkerOptions {
+        target: Some("bpf".to_string()),
+        cpu,
+        cpu_features: "".to_string(),
+        inputs,
+        output: output.clone(),
+        output_type: OutputType::Object,
+        libs,
+        optimize,
+        export_symbols,
+        unroll_loops,
+        ignore_inline_never,
+        dump_module,
+        llvm_args: llvm_args.into_iter().map(|cstring| cstring.into_string().unwrap_or_default()).collect(),
+        disable_expand_memcpy_in_order,
+        disable_memory_builtins,
+        btf,
+        allow_bpf_trap,
+    });
+
+    linker.link()?;
+
+    if fatal_errors && linker.has_errors() {
+        return Err(anyhow::anyhow!(
+            "LLVM issued diagnostic with error severity"
+        ));
+    }
+
+    let program = std::fs::read(&output).context("Failed to read bytecode")?;
+    let bytecode = link_program(&program)
+        .map_err(|e| anyhow::anyhow!("Link error: {}", e))?;
+    let src_name = std::path::Path::new(&output)
+        .file_stem()
+        .and_then(|s| s.to_str())
+        .unwrap_or("main");
+    let output_path = std::path::Path::new(&output)
+        .parent()
+        .unwrap_or_else(|| std::path::Path::new("."))
+        .join(format!("{}.so", src_name));
+    std::fs::write(output_path, bytecode)?;
+
+    Ok(())
+}

+ 29 - 23
crates/assembler/src/byteparser.rs → crates/linker/src/byteparser.rs

@@ -1,14 +1,15 @@
-use crate::ast::AST;
-use crate::astnode::{ASTNode, ROData};
-use crate::instruction::Instruction;
-use crate::lexer::{ImmediateValue, Token};
-use crate::opcode::Opcode;
-use crate::parser::ParseResult;
+use sbpf_assembler::ast::AST;
+use sbpf_assembler::astnode::{ASTNode, ROData};
+use sbpf_assembler::instruction::Instruction;
+use sbpf_assembler::lexer::{ImmediateValue, Token};
+use sbpf_assembler::opcode::Opcode;
+use sbpf_assembler::parser::ParseResult;
 
 use object::{File, Object, ObjectSection, ObjectSymbol};
 use object::RelocationTarget::Symbol;
 
 use anyhow::Result;
+use std::collections::HashMap;
 
 pub fn parse_bytecode(bytes: &[u8]) -> Result<ParseResult, String> {
     let mut ast = AST::new();
@@ -17,6 +18,7 @@ pub fn parse_bytecode(bytes: &[u8]) -> Result<ParseResult, String> {
 
     // only handle symbols in the .rodata section for now
     let ro_section = obj.section_by_name(".rodata").unwrap();
+    let mut rodata_table = HashMap::new();
     let mut rodata_offset = 0;
     for symbol in obj.symbols() {
         if symbol.section_index() == Some(ro_section.index()) && symbol.size() > 0 {
@@ -29,11 +31,12 @@ pub fn parse_bytecode(bytes: &[u8]) -> Result<ParseResult, String> {
                 rodata: ROData {
                     name: symbol.name().unwrap().to_string(),
                     args: vec![Token::Directive(String::from("byte"), 0..1) //
-                            , Token::VectorLiteral(bytes, 0..1)],
+                            , Token::VectorLiteral(bytes.clone(), 0..1)],
                     span: 0..1,
                 },
-                offset: symbol.address(),
+                offset: rodata_offset,
             });
+            rodata_table.insert(symbol.address(), symbol.name().unwrap().to_string());
             rodata_offset += symbol.size();
         }
     }
@@ -56,6 +59,7 @@ pub fn parse_bytecode(bytes: &[u8]) -> Result<ParseResult, String> {
                 });
                 offset += node_len;
             }
+            
             // handle relocations
             for rel in section.relocations() {
                 // only handle relocations for symbols in the .rodata section for now
@@ -63,22 +67,24 @@ pub fn parse_bytecode(bytes: &[u8]) -> Result<ParseResult, String> {
                     Symbol(sym) => Some(obj.symbol_by_index(sym).unwrap()), 
                     _ => None
                 };
-                assert!(symbol.unwrap().section_index() == Some(ro_section.index()));
-                // addend is not explicit in the relocation entry, but implicitly encoded
-                // as the immediate value of the instruction
-                let addend = //
-                    match ast.get_instruction_at_offset(rel.0 as u64).unwrap()
-                        .operands.last().unwrap().clone() {
-                            Token::ImmediateValue(ImmediateValue::Int(val), _) => val,
-                            _ => 0 
-                    };
+                
+                if symbol.unwrap().section_index() == Some(ro_section.index()) {
+                    // addend is not explicit in the relocation entry, but implicitly encoded
+                    // as the immediate value of the instruction
+                    let addend = //
+                        match ast.get_instruction_at_offset(rel.0 as u64).unwrap()
+                            .operands.last().unwrap().clone() {
+                                Token::ImmediateValue(ImmediateValue::Int(val), _) => val,
+                                _ => 0 
+                        };
 
-                // Replace the immediate value with the rodata label
-                let ro_label = ast.get_rodata_at_offset(addend as u64).unwrap();
-                let ro_label_name = ro_label.name.clone();
-                let node: &mut Instruction = ast.get_instruction_at_offset(rel.0 as u64).unwrap();
-                let last_idx = node.operands.len() - 1;
-                node.operands[last_idx] = Token::Identifier(ro_label_name, 0..1);
+                    // Replace the immediate value with the rodata label
+                    let ro_label = rodata_table.get(&(addend as u64)).unwrap();
+                    let ro_label_name = ro_label.clone();
+                    let node: &mut Instruction = ast.get_instruction_at_offset(rel.0 as u64).unwrap();
+                    let last_idx = node.operands.len() - 1;
+                    node.operands[last_idx] = Token::Identifier(ro_label_name, 0..1);
+                }
             }
             ast.set_text_size(section.size());
         }

+ 19 - 0
crates/linker/src/lib.rs

@@ -0,0 +1,19 @@
+pub mod byteparser;
+
+use byteparser::parse_bytecode;
+
+use sbpf_assembler::Program;
+
+use anyhow::Result;
+
+pub fn link_program(source: &Vec<u8>) -> Result<Vec<u8>, String> {
+    let parse_result = match parse_bytecode(source) {
+        Ok(program) => program,
+        Err(errors) => {
+            return Err(errors);
+        }
+    };
+    let program = Program::from_parse_result(parse_result);
+    let bytecode = program.emit_bytecode();
+    Ok(bytecode)
+}

+ 1 - 1
src/commands/link.rs

@@ -1,5 +1,5 @@
 use anyhow::{Result, Context};
-use sbpf_assembler::link_program;
+use sbpf_linker::link_program;
 
 pub fn link(source: &str) -> Result<()> {
     let program = std::fs::read(source).context("Failed to read bytecode")?;