Assembler


Introduction

In addition to standard file format modifications, it may be necessary to patch code with custom assembly. This functionality is available through the function:
import lief

elf = lief.ELF.parse("/bin/hello")

syscall_addresses = [
  inst.address for inst in elf.disassemble(0x400090) if inst.is_syscall
]

for syscall_addr in syscall_addresses:
    elf.assemble(syscall_addr, """
    mov x1, x0;
    str x1, [x2, #8];
    """)

Warning

The assembler works well for AArch64/ARM64E and x86/x86-64, but support for other architectures is currently limited.

Technical Details

Similar to the disassembler, this assembler is based on the LLVM MC layer.

The assembly text is consumed by the llvm::MCAsmParser object, and we intercept the raw generated assembly bytes from the llvm::MCObjectWriter.

We also resolve llvm::MCFixup for a vast majority of the generated fixups. An important feature introduced in LIEF 0.17.0 is support for resolving symbols or labels on the fly.

Contextual Assembly Patching

Given assembly code and a target address, we might want to use a context to resolve symbols referenced in the assembly listing.

For example, consider the following patch:

import lief

elf = lief.ELF.parse("/bin/ssh")

elf.assemble(elf.entrypoint, """
  mov rdi, rax;
  call a_custom_function
""")

In this example, a_custom_function is undefined, so the assembler engine cannot resolve it and raises the following error:

warning: Fixup not resolved:
    call a_custom_function
LIEF exposes a interface that can be used to configure the engine and to dynamically resolve symbols used in the assembly listing:
import lief

class MyConfig(lief.assembly.AssemblerConfig):
    def __init__(self):
        super().__init__() # Important!

    @override
    def resolve_symbol(self, name: str) -> int | None:
        if name == "a_custom_function":
            return 0x1000
        return None

elf = lief.ELF.parse("/bin/ssh")

elf.assemble(elf.entrypoint, """
  mov rdi, rax;
  call a_custom_function
""", MyConfig())
This interface can be used to wrap a context, such as a generic :
import lief

class MyConfig(lief.assembly.AssemblerConfig):
    def __init__(self, target: lief.Binary):
        super().__init__() # Important!

        self._target = target

    @override
    def resolve_symbol(self, name: str) -> int | None:
        addr = self._target.get_function_address(name)
        if isinstance(addr, lief.lief_errors):
            return None
        return addr

elf = lief.ELF.parse("/bin/ssh")

config = MyConfig(elf)

elf.assemble(elf.entrypoint, """
  mov rdi, rax;
  call a_custom_function
""", config)
The Rust bindings do not offer the same flexibility to capture the . Nevertheless, the closure associated with the lief::assembly::AssemblerConfig::symbol_resolver can capture most of its context:
let mut elf = lief::elf::Binary::parse("/bin/ssh");

let mut config = lief::assembly::AssemblerConfig::default();

let mut sym_map = HashMap::new();

for sym in ls.exported_symbols() {
    sym_map.insert(sym.name(), sym.value());
}

let resolver = Arc::new(move |symbol: &str| {
    sym_map.get(symbol).copied()
});

config.symbol_resolver = Some(resolver);

elf.assemble(addr, r#"
  mov rdi, rax;
  call a_custom_function;
"#, &config);

Python API

C++ API

Rust API