rsasm 0.1.0

A multi-syntax, multi-architecture assembler
Documentation
  • Coverage
  • 37.02%
    241 out of 651 items documented1 out of 283 items with examples
  • Size
  • Source code size: 364.39 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 5.6 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 5s Average build duration of successful builds.
  • all releases: 5s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • KarpelesLab/rsasm
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • MagicalTux

rsasm

An assembler written in Rust, aiming at three things at once: accept the asm text people actually have, target many CPUs, and let one source file emit code for more than one of them.

$ cat hello.s
        .section .rodata
msg:    .ascii  "Hello from rsasm!\n"
msglen = . - msg

        .text
        .globl  _start
_start:
        movq    $1, %rax                # write
        movq    $1, %rdi                # stdout
        leaq    msg(%rip), %rsi
        movq    $msglen, %rdx
        syscall
        movq    $60, %rax               # exit
        xorq    %rdi, %rdi
        syscall

$ rsasm -o hello.o hello.s && ld -o hello hello.o && ./hello
Hello from rsasm!

Install

$ cargo install rsasm

Prebuilt binaries for Linux, macOS and Windows are attached to each release.

Status

Early. The pipeline is complete end to end — lex, parse, encode, lay out, relax, relocate, write — and there is one real architecture behind it.

Working

  • x86-64, in both AT&T and Intel syntax, switchable mid-file
  • .code16 / .code32 / .code64
  • ELF64 relocatable objects and flat binaries
  • branch relaxation, alignment, .org, symbol arithmetic, conditionals
  • diagnostics with source snippets, and assembly that continues past the first error

Not yet

  • macros (.macro / .rept / .irp)
  • other architectures — the seam they plug into is described below
  • the NASM dialect (its lexing rules are in place; its directives are not)
  • ELF32, Mach-O, PE/COFF
  • DWARF line tables (.loc and .cfi_* parse and are ignored)

Usage

rsasm [options] <input.s>...

  -o <file>          write output to <file> (default: a.out)
  -a, --arch <name>  target architecture (default: the host, if supported)
  -f, --format <fmt> output format: elf (default) or bin
  -s, --syntax <s>   initial operand syntax: att (default) or intel
  -d, --dialect <d>  source dialect: gas (default) or nasm
  -I <dir>           add <dir> to the .include search path
  -D <sym>[=<val>]   define <sym> before assembling
      --base <addr>  base address for `bin` output
      --hex          print the output as hex instead of writing a file
      --list-arch    list the architectures this build supports

Architectures are cargo features, all on by default:

$ cargo build --no-default-features --features x86

Compatibility with GNU as

Where rsasm and GNU as both accept a program, they are meant to produce the same bytes. tools/gas-diff/run.sh assembles a corpus with both and compares .text; it currently reports 118 of 118 matching, and the expected encodings in tests/x86_encoding.rs came from those runs.

One deliberate difference: alignment padding in executable sections uses a different mix of multi-byte no-ops. GNU as picks its sequence by -mtune, so the bytes differ between GNU as versions too; only the total length is fixed.

Design

The interesting problems in an assembler are mostly about when things are known, and the design is shaped around that.

Lexing is pull-based, and its configuration is mutable. ; separates statements in GAS and starts a comment in NASM. 1b is a reference to a local label in GAS and the binary constant 2 in NASM. A directive halfway down the file can change which of those is true, so the lexer asks its configuration again for every token rather than tokenizing the file up front.

The parser stops at the statement level. It finds labels, directives and mnemonics; it does not look inside operands. disp(base,index,scale) and [base + index*scale + disp] are x86's problem, and register lists are ARM's, so the architecture backend gets the raw token tail and parses it itself.

Expressions evaluate to relocatable values, not numbers: plus - minus + addend. That is what lets end - start be a constant while end alone is not, and it is the same representation whether the answer comes out as bytes or as a relocation.

Positional references are bound where they are written. . and 1f mean different things depending on where in the file they appear, which is information that no longer exists by the time an expression is evaluated. Both are rewritten into ordinary symbol references as soon as the statement containing them is parsed.

A fragment that might change size carries every candidate. An instruction whose branch could be short or long is encoded both ways at parse time; the layout pass picks an index into that list. Since the index only ever increases, the loop terminates. This costs a little memory and buys a relaxation pass with no re-entry into the parser.

Adding an architecture

Implement arch::Architecture and register it in arch::lookup. The trait is the whole seam:

fn assemble(&self, cx: &mut AsmCtx<'_>, insn: &InsnRequest<'_>) -> Option<Vec<Variant>>;

The backend receives a mnemonic and the tokens after it, and returns the candidate encodings with their fixups. It never touches sections, symbols or addresses — those belong to the core, which is why two backends can write into the same file. .arch switches between them at any point.

Building

$ cargo build --release
$ cargo test
$ tools/gas-diff/run.sh     # needs binutils

License

MIT — see LICENSE.