- Rust 95.1%
- Nix 4.9%
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
BoSSA-Nova Assembler (basm)
Turns BoSSA-Nova Assembly (.basl) files into machine code.
Usage
$ basm <OUTFILE> <WORD_WIDTH> [-v]
The -v flag activates verbose mode.
It is a positional argument, not a flag in the POSIX sense.
It is optional, both other arguments are mandatory.
Note: The output function is still a work in progress.
Therefore, to view the assembled binary, use the -v argument which will output all code sections and their start addresses.
WORD_WIDTH may be 1, 2, 3 or 4 (the width of the word given in bytes).
Description
Assembles source given in the basm assembly language (basl) to machine code accepted by a single- or multi-byte version of BoSSA-Nova.
Zero AI-generated code, this is all man-made slop.
It is also my first Rust project, so if you have improvement and/or cleanup ideas, feel free to open issues. However, I won't be replacing functionality I wrote myself with crates, as I want the binary to be completely self-contained and descriptive.
For example programs, see src/examples/ in the BoSSA-Nova repository.
Assembly language description
Important: In a basl file, all constants are defined as hexadecimal numbers with the number of digits corresponding to the ISA width.
Hex digits are written in UPPERCASE or lowercase, and are preceeded by a $.
So, a one-byte ISA would define them as $00, $01, ... $FF, whereas a two-byte one would have $0000, $0001, ... $FFFF.
Omitting leading zeroes is not permitted.
An assembly language file is a plaintext file in UTF-8 encoding with the extension .basl.
The assembler takes that language and converts it to a .bin memory image starting at address 0.
Each line in an assembly file can be one of the following:
- Empty line
- Instruction
- Preprocessor directive
- Comment
Empty lines contain only a newline or whitespaces.
Carriage returns (\r) are permitted, so Windows-style CRLF endings are supported.
Instructions are written in the following format:
<MNEMONIC> <SOURCE>, <DESTINATION> [COMMENT]
For a summary of all instructions and their mnemonics, see the BoSSA-Nova repository.
Alternatively, a single memory address can be specified for both source and destination.
This can come in handy, for example with the JMP instruction, which technically does take two arguments, but uses only one of them, so setting them both to the same address is OK.
Another use might be an INC instruction which writes its result to the same memory address:
INC <SOURCE_&_DESTINATION> [COMMENT]
A whitespace is only an ASCII 0x20.
Whitespaces are allowed at the start of the line.
One or more whitespace can be given in places where they are expected as separators.
All whitespaces after the first one are ignored.
Comments at the end of the line are permitted, but not mandatory, and should start as any other comment (see example below).
Source and destination operands are constructed in the following way:
#REF for direct memory addresses, and *REF for addresses where a pointer to the operand is stored.
A REF might be a numerical constant (in hexadecimal) or a label.
A preprocessor directive might be used to define a:
- Starting address for the following section using
.ADDR <$XX>(or, on a 2-byte-word machine,.ADDR <$XXXX>). Only a numerical constant is permitted as the argument. The assembler starts placing assembled code from subsequent lines on addresses XX, XX+1, XX+2 and so on. Remember, each instruction takes 3 machine words.
The assembler will check for overlapping sections, but still, be careful.
Gaps between sections are to be filled with zeroes, however this is not guaranteed and referencing such gaps in code is not permitted. - Sequence of machine words using
.WORDS <XX> [YY] [ZZ] ...(on a 1-byte-word machine; on a 2-byte-word machine, you would have.WORDS XXXX YYYY ...etc.), where XX, YY, ZZ etc. are one or more numerical constants or labels. All hexadecimal numbers / addresses of labels following .WORDS are placed consecutively in memory. Multiple .WORDS directives can be placed in direct succession, the result is the same as writing all words on a single line. - Label to a section of the code/data using
.LABEL <NAME>, where NAME is an unbroken string from the character range[0-9a-zA-Z_](all ASCII letters and digits plus underline), starting however with only the range [a-zA-Z_] (no digit as initial character). The string is case-sensitive.
Labels may be defined for code sections, as well as for sections of words.
Upon assembly, the assembler expands them to the correct memory address.
Labels can be defined anywhere, and can be used before or after the definition.
When using labels, the name MUST NOT be preceeded by a $.
The label is used directly.
Comments are lines which start with ; (without leading whitespaces) and go on until the end of the line.
Multiline comments are not supported.
Examples for a 1-byte-word machine:
; This is a comment. Below is an empty line:
; This is a jump instruction to the code section. It will live at address 0x00:
JMP #CODE
; This sets the starting address of the following section to byte 16 (0x10):
.ADDR $10
; Define a numerical constant, which will live at address 0x10:
.WORDS $B0
; These two constants will live on addresses 0x11 and 0x12, respectively:
.WORDS $88 $41
.LABEL DATASEC
.WORDS $6A $4B $13
; These three words will live after the previous ones. After assembly, all references to DATASEC will be replaced with 0x13, the address of the first word.
.LABEL CODE
INC #$15, #$15 ; Increment the number at address 0x15 (in this case 0x13 on the first run) and write it back to address 0x15.
ADD *$15, #DATASEC ; Add the value from the *address pointed to by the word on address 0x15*, so in this case address 0x14, to the value at constant address DATASEC (0x13) and write it to address DATASEC.
INC #SP, #SP ; Increment stack pointer
CAL #Func, *SP ; Call Func and store the return address on the stack
DEC #SP, #SP ; Decrement stack pointer
.LABEL Infloop
JMP #Infloop ; Infinite loop
; Store the address where CODE starts in memory at assembly time twice
.WORDS CODE CODE
; Stack pointer lives at address 0x80 and points to stack starting at 0xE0.
; Since the stack pointer points to the last non-free space, it is actually initialized with $E0 - 1.
; Note that the order of the .ADDR and .LABEL directives matters! Otherwise the label would point to the last address from the *above* section.
.ADDR $80
.LABEL SP
.WORDS $DF
.ADDR $30
.LABEL Func
DEC #Counter, #Counter
JGZ #Func, #Counter ; Loop back to `Func` while the value at address `Counter` is greater than 0.
JMP *SP ; Jump to the address on the top of the stack (return)
.LABEL Counter
.WORDS $0E
Building
With nix:
$ nix run .#basm
or just:
$ nix run
Without nix:
$ cargo run
Development
Nix devshell:
$ nix develop -i --keep TERM --keep LS_COLORS
TODO
Do we need some kind of "preprocessor constants"? Probably the only constants you would want to define are addresses (done via .LABEL) and numerical constants (which have to be defined as .WORDS somewhere anyway).
Objdump-style disassembler.
basm port in native assembly.