use crate::INSN_SIZE;
#[derive(Debug, thiserror::Error)]
pub enum AssembleError {
#[error("empty text")]
Empty,
#[error("unknown mnemonic {0:?}")]
UnknownMnemonic(String),
#[error("malformed operand {0:?} for {1}")]
BadOperand(String, &'static str),
#[error("expected {expected} operands for {mnemonic}, got {got}")]
WrongArity {
mnemonic: String,
expected: usize,
got: usize,
},
#[error("immediate {value:#x} doesn't fit in {bits} bits")]
ImmediateOverflow { value: u64, bits: u8 },
#[error("branch offset {0} doesn't fit in i16")]
OffsetOverflow(i64),
#[error("register {0} out of range 0..=10")]
BadRegister(u32),
#[error("not a known textual form")]
NotRecognised,
}
pub fn assemble_bpf(text: &str) -> Result<Vec<u8>, AssembleError> {
let text = text.trim();
if text.is_empty() {
return Err(AssembleError::Empty);
}
if let Some(rest) = text.strip_prefix("<bpf 0x") {
let hex = rest.strip_suffix('>').ok_or(AssembleError::NotRecognised)?;
let v = u64::from_str_radix(hex, 16).map_err(|_| AssembleError::NotRecognised)?;
return Ok(v.to_le_bytes().to_vec());
}
if let Some(rest) = text.strip_prefix("<lddw-cont 0x") {
let hex = rest.strip_suffix('>').ok_or(AssembleError::NotRecognised)?;
let high = u32::from_str_radix(hex, 16).map_err(|_| AssembleError::NotRecognised)?;
return Ok(encode_slot(0x00, 0, 0, 0, high as i32));
}
if text == "exit" {
return Ok(encode_slot(0x95, 0, 0, 0, 0));
}
let (mnemonic, rest) = match text.find(char::is_whitespace) {
Some(i) => (&text[..i], text[i..].trim()),
None => (text, ""),
};
let operands = split_operands(rest);
match mnemonic {
"lddw" => assemble_lddw(&operands),
"ldxw" | "ldxh" | "ldxb" | "ldxdw" => assemble_ldx(mnemonic, &operands),
"stw" | "sth" | "stb" | "stdw" => assemble_st(mnemonic, &operands),
"stxw" | "stxh" | "stxb" | "stxdw" => assemble_stx(mnemonic, &operands),
"le16" | "le32" | "le64" | "be16" | "be32" | "be64" => assemble_endian(mnemonic, &operands),
"ja" => assemble_ja(&operands),
"call" => assemble_call(&operands, 0),
"call_internal" => assemble_call(&operands, 1),
"call_local" => assemble_call_local(&operands),
"callx" => assemble_callx(&operands),
other => assemble_alu_or_jmp(other, &operands),
}
}
fn encode_slot(opcode: u8, dst: u8, src: u8, offset: i16, imm: i32) -> Vec<u8> {
let mut out = vec![0u8; INSN_SIZE];
out[0] = opcode;
out[1] = (dst & 0x0f) | ((src & 0x0f) << 4);
out[2..4].copy_from_slice(&offset.to_le_bytes());
out[4..8].copy_from_slice(&imm.to_le_bytes());
out
}
fn assemble_lddw(operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
arity(operands, 2, "lddw")?;
let dst = parse_reg(operands[0])?;
let imm64 = parse_uint(operands[1], "lddw")?;
#[allow(clippy::cast_possible_truncation)]
let low = imm64 as u32 as i32;
Ok(encode_slot(0x18, dst, 0, 0, low))
}
const BPF_MODE_MEM: u8 = 0x60;
fn assemble_ldx(mnemonic: &str, operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
arity(operands, 2, mnemonic)?;
let dst = parse_reg(operands[0])?;
let (src, offset) = parse_mem(operands[1])?;
let size_bits = size_letter_to_bits(&mnemonic[3..]);
let opcode = BPF_MODE_MEM | size_bits | 0x01;
Ok(encode_slot(opcode, dst, src, offset, 0))
}
fn assemble_st(mnemonic: &str, operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
arity(operands, 2, mnemonic)?;
let (dst, offset) = parse_mem(operands[0])?;
let imm = parse_int(operands[1], "st")?;
let size_bits = size_letter_to_bits(&mnemonic[2..]);
let opcode = BPF_MODE_MEM | size_bits | 0x02;
Ok(encode_slot(opcode, dst, 0, offset, imm))
}
fn assemble_stx(mnemonic: &str, operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
arity(operands, 2, mnemonic)?;
let (dst, offset) = parse_mem(operands[0])?;
let src = parse_reg(operands[1])?;
let size_bits = size_letter_to_bits(&mnemonic[3..]);
let opcode = BPF_MODE_MEM | size_bits | 0x03;
Ok(encode_slot(opcode, dst, src, offset, 0))
}
fn size_letter_to_bits(suffix: &str) -> u8 {
match suffix {
"h" => 0x08,
"b" => 0x10,
"dw" => 0x18,
_ => 0x00,
}
}
fn assemble_endian(mnemonic: &str, operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
arity(operands, 1, mnemonic)?;
let dst = parse_reg(operands[0])?;
let opcode = match &mnemonic[..2] {
"le" => 0xd4,
"be" => 0xdc,
_ => return Err(AssembleError::UnknownMnemonic(mnemonic.into())),
};
let width: i32 = match &mnemonic[2..] {
"16" => 16,
"32" => 32,
"64" => 64,
_ => return Err(AssembleError::UnknownMnemonic(mnemonic.into())),
};
Ok(encode_slot(opcode, dst, 0, 0, width))
}
fn assemble_ja(operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
arity(operands, 1, "ja")?;
let off = parse_branch_offset(operands[0])?;
Ok(encode_slot(0x05, 0, 0, off, 0))
}
fn assemble_call(operands: &[&str], src: u8) -> Result<Vec<u8>, AssembleError> {
arity(operands, 1, "call")?;
let imm = parse_int_signed(operands[0], "call")?;
Ok(encode_slot(0x85, 0, src, 0, imm))
}
fn assemble_call_local(operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
arity(operands, 1, "call_local")?;
let imm = parse_int_signed(operands[0], "call_local")?;
Ok(encode_slot(0x8d, 0, 0, 0, imm))
}
pub fn assemble_bpf_ifblock_cond(
cond_text: &str,
slot_offset: i16,
) -> Result<Vec<u8>, AssembleError> {
let (lhs, op, rhs) = parse_ifblock_cond(cond_text)?;
let mnemonic = match op {
"!=" => "jeq",
"==" => "jne",
"<=" => "jgt",
"<" => "jge",
">=" => "jlt",
">" => "jle",
_ => return Err(AssembleError::UnknownMnemonic(op.into())),
};
let offset_text = if slot_offset >= 0 {
format!("+0x{slot_offset:x}")
} else {
format!("-0x{:x}", -i32::from(slot_offset))
};
assemble_bpf(&format!("{mnemonic} {lhs}, {rhs}, {offset_text}"))
}
pub fn assemble_bpf_ja(slot_offset: i16) -> Result<Vec<u8>, AssembleError> {
let offset_text = if slot_offset >= 0 {
format!("+0x{slot_offset:x}")
} else {
format!("-0x{:x}", -i32::from(slot_offset))
};
assemble_bpf(&format!("ja {offset_text}"))
}
fn parse_ifblock_cond(cond: &str) -> Result<(&str, &str, &str), AssembleError> {
let cond = cond.trim();
if cond.starts_with('(') {
return Err(AssembleError::NotRecognised);
}
for op in ["!=", "==", "<=", ">="] {
if let Some(at) = find_top_level_op(cond, op) {
let lhs = cond[..at].trim();
let rhs = cond[at + op.len()..].trim();
return Ok((lhs, op, rhs));
}
}
for op in ["<", ">"] {
if let Some(at) = find_top_level_op(cond, op) {
let lhs = cond[..at].trim();
let rhs = cond[at + op.len()..].trim();
return Ok((lhs, op, rhs));
}
}
Err(AssembleError::NotRecognised)
}
fn find_top_level_op(cond: &str, op: &str) -> Option<usize> {
cond.find(op)
}
fn slot_offset_from(target: u64, insn_addr: u64) -> Option<i64> {
let next_slot = insn_addr.wrapping_add(INSN_SIZE as u64);
#[allow(clippy::cast_possible_wrap)]
let delta = (target as i64).wrapping_sub(next_slot as i64);
if delta % (INSN_SIZE as i64) != 0 {
return None;
}
Some(delta / (INSN_SIZE as i64))
}
fn is_symbolic_callee(name: &str) -> bool {
let name = name.trim();
if name.is_empty() {
return false;
}
if name.starts_with("0x") || name.starts_with("0X") {
return false;
}
if name.starts_with("sub_") {
return false;
}
let first = name.as_bytes()[0];
if first.is_ascii_digit() {
return false;
}
if name.contains(',') {
return false;
}
true
}
fn parse_int_signed(text: &str, ctx: &'static str) -> Result<i32, AssembleError> {
let t = text.trim();
if let Some(rest) = t.strip_prefix('-') {
let v = parse_uint(rest, ctx)?;
if v > 0x8000_0000 {
return Err(AssembleError::ImmediateOverflow { value: v, bits: 32 });
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
return Ok(-(v as i64) as i32);
}
parse_int(t, ctx)
}
fn assemble_callx(operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
arity(operands, 1, "callx")?;
let dst = parse_reg(operands[0])?;
Ok(encode_slot(0x8d, dst, 0, 0, 0))
}
fn assemble_alu_or_jmp(mnemonic: &str, operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
let (base, alu64, jmp32) = if let Some(b) = mnemonic.strip_suffix("64") {
(b, true, false)
} else if let Some(b) = mnemonic.strip_suffix("32") {
(b, false, true)
} else {
(mnemonic, false, false)
};
if let Some(op_nibble) = jmp_op_nibble(base) {
return assemble_jmp(op_nibble, jmp32, operands);
}
let op_nibble =
alu_op_nibble(base).ok_or_else(|| AssembleError::UnknownMnemonic(mnemonic.into()))?;
let alu_class: u8 = if alu64 { 0x07 } else { 0x04 };
if op_nibble == 0x8 {
arity(operands, 1, mnemonic)?;
let dst = parse_reg(operands[0])?;
let opcode = (op_nibble << 4) | alu_class;
return Ok(encode_slot(opcode, dst, 0, 0, 0));
}
arity(operands, 2, mnemonic)?;
let dst = parse_reg(operands[0])?;
let (is_reg, src, imm) = parse_alu_rhs(operands[1])?;
let src_bit: u8 = if is_reg { 0x08 } else { 0x00 };
let opcode = (op_nibble << 4) | src_bit | alu_class;
Ok(encode_slot(opcode, dst, src, 0, imm))
}
fn assemble_jmp(op_nibble: u8, is_32: bool, operands: &[&str]) -> Result<Vec<u8>, AssembleError> {
let mnemonic_for_err = "jcc";
if operands.len() != 3 {
return Err(AssembleError::WrongArity {
mnemonic: mnemonic_for_err.into(),
expected: 3,
got: operands.len(),
});
}
let dst = parse_reg(operands[0])?;
let (is_reg, src, imm) = parse_alu_rhs(operands[1])?;
let off = parse_branch_offset(operands[2])?;
let src_bit: u8 = if is_reg { 0x08 } else { 0x00 };
let class: u8 = if is_32 { 0x06 } else { 0x05 };
let opcode = (op_nibble << 4) | src_bit | class;
Ok(encode_slot(opcode, dst, src, off, imm))
}
fn alu_op_nibble(base: &str) -> Option<u8> {
Some(match base {
"add" => 0x0,
"sub" => 0x1,
"mul" => 0x2,
"div" | "udiv" => 0x3,
"or" => 0x4,
"and" => 0x5,
"lsh" => 0x6,
"rsh" => 0x7,
"neg" => 0x8,
"mod" | "urem" => 0x9,
"xor" => 0xa,
"mov" => 0xb,
"arsh" => 0xc,
"sdiv" => 0xe,
"srem" => 0xf,
_ => return None,
})
}
fn jmp_op_nibble(base: &str) -> Option<u8> {
Some(match base {
"jeq" => 0x1,
"jgt" => 0x2,
"jge" => 0x3,
"jset" => 0x4,
"jne" => 0x5,
"jsgt" => 0x6,
"jsge" => 0x7,
"jlt" => 0xa,
"jle" => 0xb,
"jslt" => 0xc,
"jsle" => 0xd,
_ => return None,
})
}
fn split_operands(rest: &str) -> Vec<&str> {
if rest.is_empty() {
return Vec::new();
}
let mut out: Vec<&str> = Vec::new();
let bytes = rest.as_bytes();
let mut depth: i32 = 0;
let mut start = 0usize;
for (i, &b) in bytes.iter().enumerate() {
match b {
b'[' => depth += 1,
b']' => depth -= 1,
b',' if depth == 0 => {
out.push(rest[start..i].trim());
start = i + 1;
}
_ => {}
}
}
out.push(rest[start..].trim());
out
}
fn arity(operands: &[&str], expected: usize, mnemonic: &str) -> Result<(), AssembleError> {
if operands.len() == expected {
Ok(())
} else {
Err(AssembleError::WrongArity {
mnemonic: mnemonic.into(),
expected,
got: operands.len(),
})
}
}
fn parse_reg(text: &str) -> Result<u8, AssembleError> {
let t = text.trim();
let rest = t
.strip_prefix('r')
.ok_or_else(|| AssembleError::BadOperand(t.into(), "register"))?;
let n: u32 = rest
.parse()
.map_err(|_| AssembleError::BadOperand(t.into(), "register number"))?;
if n > 10 {
return Err(AssembleError::BadRegister(n));
}
#[allow(clippy::cast_possible_truncation)]
Ok(n as u8)
}
fn parse_uint(text: &str, ctx: &'static str) -> Result<u64, AssembleError> {
let t = text.trim();
if let Some(hex) = t.strip_prefix("0x") {
return u64::from_str_radix(hex, 16).map_err(|_| AssembleError::BadOperand(t.into(), ctx));
}
t.parse::<u64>()
.map_err(|_| AssembleError::BadOperand(t.into(), ctx))
}
fn parse_int(text: &str, ctx: &'static str) -> Result<i32, AssembleError> {
let v = parse_uint(text, ctx)?;
if v > u64::from(u32::MAX) {
return Err(AssembleError::ImmediateOverflow { value: v, bits: 32 });
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
Ok(v as u32 as i32)
}
fn parse_alu_rhs(text: &str) -> Result<(bool, u8, i32), AssembleError> {
let t = text.trim();
if t.starts_with('r') {
let r = parse_reg(t)?;
return Ok((true, r, 0));
}
let imm = parse_int(t, "alu rhs")?;
Ok((false, 0, imm))
}
fn parse_mem(text: &str) -> Result<(u8, i16), AssembleError> {
let t = text.trim();
let inner = t
.strip_prefix('[')
.and_then(|s| s.strip_suffix(']'))
.ok_or_else(|| AssembleError::BadOperand(t.into(), "memory operand"))?
.trim();
if let Some(idx) = inner.rfind(" + ") {
let reg = parse_reg(inner[..idx].trim())?;
let off = parse_offset_value(&inner[idx + 3..])?;
let off_i16 = i16::try_from(off).map_err(|_| AssembleError::OffsetOverflow(off))?;
return Ok((reg, off_i16));
}
if let Some(idx) = inner.rfind(" - ") {
let reg = parse_reg(inner[..idx].trim())?;
let off = parse_offset_value(&inner[idx + 3..])?;
let neg = -off;
let off_i16 = i16::try_from(neg).map_err(|_| AssembleError::OffsetOverflow(neg))?;
return Ok((reg, off_i16));
}
Ok((parse_reg(inner)?, 0))
}
fn parse_offset_value(text: &str) -> Result<i64, AssembleError> {
let t = text.trim();
if let Some(hex) = t.strip_prefix("0x") {
let v = u64::from_str_radix(hex, 16)
.map_err(|_| AssembleError::BadOperand(t.into(), "offset"))?;
#[allow(clippy::cast_possible_wrap)]
return Ok(v as i64);
}
t.parse::<i64>()
.map_err(|_| AssembleError::BadOperand(t.into(), "offset"))
}
fn parse_branch_offset(text: &str) -> Result<i16, AssembleError> {
let t = text.trim();
let (sign, rest) = if let Some(r) = t.strip_prefix('+') {
(1i64, r)
} else if let Some(r) = t.strip_prefix('-') {
(-1i64, r)
} else {
return Err(AssembleError::BadOperand(t.into(), "branch offset"));
};
let v = if let Some(hex) = rest.strip_prefix("0x") {
u64::from_str_radix(hex, 16)
.map_err(|_| AssembleError::BadOperand(t.into(), "branch offset"))?
} else {
rest.parse::<u64>()
.map_err(|_| AssembleError::BadOperand(t.into(), "branch offset"))?
};
#[allow(clippy::cast_possible_wrap)]
let signed = sign * (v as i64);
i16::try_from(signed).map_err(|_| AssembleError::OffsetOverflow(signed))
}
#[must_use]
pub fn desymbolize_bpf_text(text: &str, insn_addr: u64, opcode_hint: Option<u8>) -> Option<String> {
if let Some(rest) = text.strip_prefix("call_local sub_") {
let target = u64::from_str_radix(rest.trim(), 16).ok()?;
let slots = slot_offset_from(target, insn_addr)?;
return Some(format!("call_local {slots}"));
}
if let Some(rest) = text.strip_prefix("call sub_") {
let target = u64::from_str_radix(rest.trim(), 16).ok()?;
let slots = slot_offset_from(target, insn_addr)?;
let mnemonic = match opcode_hint {
Some(0x8d) => "call_local",
_ => "call_internal",
};
return Some(format!("{mnemonic} {slots}"));
}
if let Some(name) = text.strip_prefix("call ") {
if is_symbolic_callee(name) {
return Some("call_internal -1".to_string());
}
}
if let Some(name) = text.strip_prefix("call_local ") {
if is_symbolic_callee(name) {
return Some("call_local -1".to_string());
}
}
if let Some(label_at) = text.find(", label_").or_else(|| text.find(" label_")) {
let prefix = &text[..label_at];
let suffix_offset = label_at
+ match text.as_bytes().get(label_at) {
Some(b',') => 2, _ => 1, };
let label_name = &text[suffix_offset..];
let hex = label_name.strip_prefix("label_")?;
let target = u64::from_str_radix(hex.trim(), 16).ok()?;
let next_slot = insn_addr.wrapping_add(INSN_SIZE as u64);
#[allow(clippy::cast_possible_wrap)]
let delta = (target as i64).wrapping_sub(next_slot as i64);
if delta % (INSN_SIZE as i64) != 0 {
return None;
}
let slot_offset = delta / (INSN_SIZE as i64);
let offset_text = if slot_offset >= 0 {
format!("+0x{slot_offset:x}")
} else {
format!("-0x{:x}", -slot_offset)
};
let separator = if text.as_bytes().get(label_at) == Some(&b',') {
", "
} else {
" "
};
return Some(format!("{prefix}{separator}{offset_text}"));
}
if let Some(rest) = text.strip_prefix("lddw ") {
if let Some(at) = rest.find(" @0x") {
let head_with_reg = &rest[..at]; let imm_text = &rest[at + 4..]; if let Some(comma) = head_with_reg.find(',') {
let reg = head_with_reg[..comma].trim();
return Some(format!("lddw {reg}, 0x{}", imm_text.trim()));
}
}
}
let mut s = text.to_string();
let mut changed = false;
if s.contains("[local_") {
s = rewrite_stack_slot(&s, "[local_", "[r10 - 0x");
changed = true;
}
if s.contains("[arg_") {
s = rewrite_stack_slot(&s, "[arg_", "[r10 + 0x");
changed = true;
}
if changed {
return Some(s);
}
Some(text.to_string())
}
fn rewrite_stack_slot(text: &str, prefix: &str, replacement: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(at) = rest.find(prefix) {
out.push_str(&rest[..at]);
let after = &rest[at + prefix.len()..];
if let Some(close) = after.find(']') {
let hex = &after[..close];
out.push_str(replacement);
out.push_str(hex);
out.push(']');
rest = &after[close + 1..];
} else {
out.push_str(&rest[at..]);
return out;
}
}
out.push_str(rest);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{decode, format_insn, BpfVariant};
fn roundtrip(bytes: &[u8], variant: BpfVariant) {
let insns = decode(bytes, 0, variant).expect("decode");
let mut cursor = 0usize;
for insn in &insns {
let text = format_insn(insn, variant);
let asm =
assemble_bpf(&text).unwrap_or_else(|e| panic!("assemble failed: {text:?} → {e:?}"));
assert_eq!(
asm.as_slice(),
&bytes[cursor..cursor + INSN_SIZE],
"mismatch on {text:?}: assembled {asm:?}, original {:?}",
&bytes[cursor..cursor + INSN_SIZE]
);
cursor += INSN_SIZE;
}
}
#[test]
fn alu_immediate_and_register() {
roundtrip(
&[
0xb7, 0x01, 0x00, 0x00, 0x2a, 0x00, 0x00, 0x00, 0xbf, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x0f, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb4, 0x03, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ],
BpfVariant::Linux,
);
}
#[test]
fn loads_and_stores_all_widths() {
roundtrip(
&[
0x79, 0xa1, 0xf8, 0xff, 0x00, 0x00, 0x00, 0x00, 0x71, 0x12, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x69, 0x13, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x61, 0x14, 0xfc, 0xff, 0x00, 0x00, 0x00, 0x00, 0x7b, 0x1a, 0xe0, 0xff, 0x00, 0x00, 0x00, 0x00, 0x73, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x62, 0x0a, 0xf0, 0xff, 0x42, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ],
BpfVariant::Linux,
);
}
#[test]
fn branches_and_calls() {
roundtrip(
&[
0x15, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5d, 0x21, 0xfb, 0xff, 0x00, 0x00, 0x00, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x85, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00,
0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ],
BpfVariant::Linux,
);
}
#[test]
fn unrecognised_mnemonic_text_returns_err() {
let err = assemble_bpf("<jcc?> r1, r2, +0x1").unwrap_err();
assert!(matches!(err, AssembleError::UnknownMnemonic(_)));
}
#[test]
fn desymbolise_internal_call_round_trips() {
let text = "call sub_2000";
let desym = desymbolize_bpf_text(text, 0x1000, None).unwrap();
assert_eq!(desym, "call_internal 511"); let bytes = assemble_bpf(&desym).unwrap();
assert_eq!(bytes[0], 0x85); assert_eq!(bytes[1], 0x10); let imm = i32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(imm, 511);
}
#[test]
fn desymbolise_backward_call() {
let text = "call sub_1000";
let desym = desymbolize_bpf_text(text, 0x2000, None).unwrap();
assert_eq!(desym, "call_internal -513");
let bytes = assemble_bpf(&desym).unwrap();
let imm = i32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(imm, -513);
}
#[test]
fn desymbolise_jcc_label_round_trips() {
let text = "jeq r1, 0x0, label_1010";
let desym = desymbolize_bpf_text(text, 0x1000, None).unwrap();
assert_eq!(desym, "jeq r1, 0x0, +0x1");
let bytes = assemble_bpf(&desym).unwrap();
assert_eq!(bytes[0], 0x15); let off = i16::from_le_bytes(bytes[2..4].try_into().unwrap());
assert_eq!(off, 1);
}
#[test]
fn desymbolise_backward_jcc() {
let text = "jgt r2, r3, label_1000";
let desym = desymbolize_bpf_text(text, 0x1020, None).unwrap();
assert_eq!(desym, "jgt r2, r3, -0x5");
let bytes = assemble_bpf(&desym).unwrap();
let off = i16::from_le_bytes(bytes[2..4].try_into().unwrap());
assert_eq!(off, -5);
}
#[test]
fn desymbolise_ja_label() {
let text = "ja label_1008";
let desym = desymbolize_bpf_text(text, 0x1000, None).unwrap();
assert_eq!(desym, "ja +0x0");
let bytes = assemble_bpf(&desym).unwrap();
assert_eq!(bytes[0], 0x05);
let off = i16::from_le_bytes(bytes[2..4].try_into().unwrap());
assert_eq!(off, 0);
}
#[test]
fn desymbolise_non_symbolic_text_passes_through() {
let text = "ldxdw r0, [r5 - 0xff8]";
assert_eq!(desymbolize_bpf_text(text, 0x1000, None).unwrap(), text);
}
#[test]
fn desymbolise_syscall_call_yields_relocation_placeholder() {
let dsym = desymbolize_bpf_text("call sol_log_", 0x1000, None).unwrap();
assert_eq!(dsym, "call_internal -1");
let bytes = assemble_bpf(&dsym).unwrap();
assert_eq!(bytes, vec![0x85, 0x10, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff]);
}
#[test]
fn desymbolise_lddw_with_string_and_addr_annotation() {
let text = r#"lddw r3, "src/extension/mod.rs" @0x52b20"#;
let dsym = desymbolize_bpf_text(text, 0x1000, None).unwrap();
assert_eq!(dsym, "lddw r3, 0x52b20");
let bytes = assemble_bpf(&dsym).unwrap();
assert_eq!(bytes, vec![0x18, 0x03, 0x00, 0x00, 0x20, 0x2b, 0x05, 0x00]);
}
#[test]
fn desymbolise_call_local_explicit_form() {
let dsym = desymbolize_bpf_text("call_local sub_1010", 0x1000, None).unwrap();
assert_eq!(dsym, "call_local 1");
let bytes = assemble_bpf(&dsym).unwrap();
assert_eq!(bytes[0], 0x8d);
let imm = i32::from_le_bytes(bytes[4..8].try_into().unwrap());
assert_eq!(imm, 1);
}
#[test]
fn lddw_pair() {
roundtrip(
&[
0x18, 0x01, 0x00, 0x00, 0xbe, 0xba, 0xfe, 0xca, 0x00, 0x00, 0x00, 0x00, 0xef, 0xbe, 0xad, 0xde, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ],
BpfVariant::Linux,
);
}
#[test]
fn callx_and_exit() {
roundtrip(
&[
0x8d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ],
BpfVariant::Sbfv1,
);
}
#[test]
fn endian_ops() {
roundtrip(
&[
0xd4, 0x01, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xd4, 0x02, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0xd4, 0x03, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0xdc, 0x04, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0xdc, 0x05, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x95, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, ],
BpfVariant::Linux,
);
}
#[test]
fn raw_bpf_form_passes_through() {
let bytes = [0xee, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
let text = format!("<bpf 0x{:016x}>", u64::from_le_bytes(bytes));
let asm = assemble_bpf(&text).unwrap();
assert_eq!(asm.as_slice(), &bytes);
}
#[test]
fn symbolic_text_not_recognised() {
let r = assemble_bpf("call sub_4ab28");
assert!(matches!(r, Err(AssembleError::BadOperand(_, _))));
}
}