asm.md 12 KB

Assembler for B32P

The basic way to write code for the B32P is by using the B32P assembly language. The assembler compiles the assembly code to 32 bit machine instructions. The input file is currently hardcoded to code.asm, and the output is printed to stdout. The assembler is written in Python3. A special version written in BCC can be run from the FPGC itself.

!!! info

Since the addition of the C compiler BCC, the focus of the assembler now lies more on assembling BCC's output instead. Still, the bootloaders are written in assembly and some BCC libraries, like the graphics library, also make extensive use of assembly code.

Command line arguments

There are three types of programs the assembler can create, indicated by command line arguments:

  • BDOS OS [arg os]. Special mode for assembling the BDOS operating system. This adds a jumpt to the syscall function on address 4, and adds the length of the binary to address 2.
  • BDOS User Program [arg bdos {offset}]. Special mode for assembling BDOS user programs. No program length is included and the assembler will offset labels to make the code executeable from the given offset.
  • Bare metal program [no args]. Basic mode for assembling bare metal programs without OS. Adds length of the binary to address 2.

Finally, by adding the -O argument at the end will cause the assembler to remove unreached code, which is quite useful for compiled C code because of the lack of dynamic linking. This will save quite some space as C libraries are getting more functions over time.

Output

The assembler does not directly create a binary. Instead, it outputs the assembled code as a text file containing binary strings of 32 ones and zeros, follwed by a space and comments starting with //. This is very useful to see what each instruction is supposed to do, and makes it easy to verify this with the ISA.

Example output:

00010011000000000000110011011101 //Compute r13 + 12 and write result to r13
00011100001001110100010000100010 //Set r2 to 10052

The output can be converted into a binary using other tools. For example: perl -ne 'print pack("B32", $_)' < filename_containing_assembler_output > executable.bin

Assembly process

The assembler performs the following steps in order

  1. Read input file into list of lines while removing all comments
  2. Move all .data, .rdata and .bss sections down so the .code section becomes one part at the top (only relevant for C compiled code)
  3. Insert libraries (only relevant for non-C compiled code)
  4. Remove unreachable code if requested
  5. Process the define statements
  6. Do pass 1: Compiles lines that can be compiled directly (so without labels) and create new lines for instructions that become multiple lines
  7. Add header code
  8. Process labels
  9. Do pass 2: Compile instructions with labels
  10. Check for remaining labels (there should be none left)
  11. Add length of program if requested
  12. Print result to stdout

Header

The assembler creates the first few lines of the program, depending on the program type (header code). Because of a bug in B32P, in some cases the CPU will start at address 3 instead of address 0 after the bootloader. Until this is fixed, the 4th instruction also is a jump to Main. For a -os program, this looks as follows:

Jump Main
Jump Int
[Length of program]
Jump Main
Jump Syscall

Line types

Each line is parsed on its own. There are six types of lines:

  • Assembler directives
  • Includes
  • Comments
  • Defines
  • Labels
  • Instructions

Assembler directives

Because the BCC compiler is a modified MIPS compiler, its output contains directives indicating the purpose of the code below. These directives include:

  • .code, normal code
  • .data and .rdata, static data
  • .bss, object data

The assembler will move each non-code section down so it does not interfere with the code.

Includes

By adding an `include namehere.asm statement, it is possible to add code from other files, like libraries. The way this works in the assembler is by just adding all lines of that file to the code, while recursively importing includes from other files. The assembler makes sure that the same file is never included more than one time. The path to the file is relative to the assembler. This is only relevant for non-C code, as the C compiler should not produce assembly includes.

Comments

Comments can be added by using the ';' character. For each line, only the part until the first ';' occurrence will be used by the assembler. This means that anything can be written after the ';'. This all does not hold for .ds lines. They must not have any comments. This way it is not needed to use escape characters in the strings.

Defines

Defines are the first type of lines that are processed by the assembler. A define line should have the following structure:

define TEXTTOREPLACE = textToReplaceWith

It is not necessary for 'define' to be in lower caps and 'TEXTTOREPLACE' to be in all caps. However, it is recommended to do so as a coding style. Also, it is recommended to place all define statements at the top of the file before the first instruction or label. The define statement is used as a textual replacement. This means that no values or anything will be processed or converted during the processing of the define statements. This also means that is not smart to replace text that are used in instructions or labels. Furthermore, 'TEXTTOREPLACE' should be unique for each define statement. If not, the assembler will complain.

Labels

Labels can be used to get the address in the assembled code of the instruction below the label. To define a label, one should use the following syntax:

LabelName:

LabelName can consist of any character, including numbers and special characters, as long as it is just one word (so no spaces, newlines etc.) The label must end with a ':' character. On the same line after this ':' character, no other text is allowed, except comments.

A label can be referenced by using the LabelName in certain instructions at specific places (see section Instructions). The assembler will eventually compute the address of the label and replace all LabelName occurrences with this address. One can make forward and backward references, which means that one does not have to define a label earlier in the code before referencing, as long as the label is defined somewhere in the code.

Each program should at least contain the following labels (if not, the assembler will complain):

Main: ; this is where the CPU will initially jump to
Int: ; interrupt handler

It is recommended to start each label with a capital letter, however this is not mandatory. You can use two labels directly after each other without any instruction in between, but you should not use a label at the end of the file without anything under it. The assembler will complain if this happens. When two identical labels are defined, the assembler will complain. It does not matter if a label is never referenced. However, it does matter when a reference is made to a label that is not defined. In that case the assembler will complain. Each of the interrupt handler label sections should finish with a reti instruction, otherwise the CPU will not return from the interrupt and will probably lock up. However, this is not checked by the assembler.

Instructions

The instructions are the lines that will be assembled into machine code. Each instruction has its own format with the following description:

Instr   | Arg1  | Arg2 | Arg3   || Description
================================||=====================================================================
HALT    |       |       |       || Halts CPU by jumping to the current address
READ    | C16   | R     | R     || Read from addr in Arg2 with 16 bit offset from Arg1. Write to Arg3
WRITE   | C16   | R     | R     || Write to addr in Arg2 with 16 bit offset from Arg1. Data to write is in Arg3
READINTID| R    |       |       || Stores the interrupt ID from the CPU to Arg1
PUSH    | R     |       |       || Push Arg1 to stack
POP     | R     |       |       || Pop from stack to Arg1
JUMP    | L/C27 |       |       || Jump to Label or 27 bit constant in Arg1
JUMPO   | C27   |       |       || Jump to (unsigned) 27 bit constant offset in Arg1
JUMPR   | C16   | R     |       || Jump to Arg2 with 16 bit offset in Arg1
JUMPRO  | C16   | R     |       || Jump to offset in Arg2 with 16 bit offset in Arg1
BEQ     | R     | R     | C16   || If Arg1 == Arg2, jump to 16 bit offset in Arg3
BGT     | R     | R     | C16   || If Arg1 >  Arg2, jump to 16 bit offset in Arg3
BGTS    | R     | R     | C16   || (signed) If Arg1 >  Arg2, jump to 16 bit offset in Arg3
BGE     | R     | R     | C16   || If Arg1 >= Arg2, jump to 16 bit offset in Arg3
BGES    | R     | R     | C16   || (signed) If Arg1 >= Arg2, jump to 16 bit offset in Arg3
BNE     | R     | R     | C16   || If Arg1 != Arg2, jump to 16 bit offset in Arg3
BLT     | R     | R     | C16   || If Arg1 <  Arg2, jump to 16 bit offset in Arg3
BLTS    | R     | R     | C16   || (signed) If Arg1 <  Arg2, jump to 16 bit offset in Arg3
BLE     | R     | R     | C16   || If Arg1 <= Arg2, jump to 16 bit offset in Arg3
BLES    | R     | R     | C16   || (signed) If Arg1 <= Arg2, jump to 16 bit offset in Arg3
SAVPC   | R     |       |       || Save program counter to Arg1
RETI    |       |       |       || Return from interrupt
OR      | R     | C16/R | R     || Compute Arg1 OR  Arg2, write result to Arg3
AND     | R     | C16/R | R     || Compute Arg1 AND Arg2, write result to Arg3
XOR     | R     | C16/R | R     || Compute Arg1 XOR Arg2, write result to Arg3
ADD     | R     | C16/R | R     || Compute Arg1 +   Arg2, write result to Arg3
SUB     | R     | C16/R | R     || Compute Arg1 -   Arg2, write result to Arg3
SHIFTL  | R     | C16/R | R     || Compute Arg1 <<  Arg2, write result to Arg3
SHIFTR  | R     | C16/R | R     || Compute Arg1 >>  Arg2, write result to Arg3
SHIFTRS | R     | C16/R | R     || Compute Arg1 (signed)>> Arg2, write result to Arg3
NOT     | R     | R     |       || Compute NOT  (~) Arg1, write result to Arg2
MULTS   | R     | C16/R | R     || (signed) Compute Arg1 * Arg2, write result to Arg3
MULTU   | R     | C16/R | R     || (unsigned) Compute Arg1 * Arg2, write result to Arg3
MULTFP  | R     | C16/R | R     || (signed, FixedPoint16.16) Compute Arg1 * Arg2, write result to Arg3
SLT     | R     | C16/R | R     || (signed) If Arg1 < Arg2, write 1 to Arg3, else write 0 to Arg3
SLTU    | R     | C16/R | R     || (unsigned) If Arg1 < Arg2, write 1 to Arg3, else write 0 to Arg3
LOAD    | C16   | R     |       || Load (unsigned) 16 bit constant from Arg1 into Arg2
LOADHI  | C16   | R     |       || Load (unsigned) 16 bit constant from Arg1 into highest 16 bits of Arg2
LOAD32  | C32   | R     |       || Load signed 32 bit constant from Arg1 into Arg2. Is converted into LOAD and LOADHI
NOP     |       |       |       || Does nothing, is converted to the instruction OR r0 r0 r0
ADDR2REG| L     | R     |       || Loads address from Arg1 to Arg2. Is converted into LOAD and LOADHI
.DW     | N32   | *     | *     || Data: Each argument is converted to 32bit binary
.DD     | N16   | *     | *     || Data: Each argument is converted to 16bit binary **
.DB     | N8    | *     | *     || Data: Each argument is converted to 8bit binary **
.DS     | N8    | S     |       || Data: Each character of the string is converted to 8bit ASCII **

/   = Or
R   = Register
Cx  = Constant that fits within x bits
L   = Label
S   = String

Note: All constants are signed except stated otherwise
*  Optional argument with same type as Arg1. Has no limit on number of arguments
** Data is placed after each other to make blocks of 32 bits. If a block cannot be made, it will be padded by zeros

Each Cx type argument (constant) can be written in decimal, binary (with 0b prefix) or hex (with 0x prefix).

BCC version

To allow for assembling code directly from the FPGC/BDOS itself, a special version of the assembler is written in BCC. This version lacks several features like includes, defines and line number errors, since its goal is to only assemble the output of BCC (specifically the BCC version running on the FPGC/BDOS as well). It is also more optimized for speed (the compiler, not the code). It also only assembles for BDOS User Programs, so if you want to assemble manually written assembly, you will need to add the userBDOS assembly wrappers yourself.