Hallo again :)

This time we gonna write the assemblers core function. To do so we first need to have a look at the gameboys Op-Codes.

It seems like there are a lot of them but all in all there are just a handful of opcodes with different arguments.

Those arguments can be represented with those types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Flags for conditional jumps and calls.
type Flag =        
    | Always = 0   // Not a real Flag from the flag register but we'll use it to tell that a conditional jump has no condition.
    | z = 1        // Zero flag is set
    | c = 2        // Carry flag is set
    | nz = 3       // Zero flag is not set
    | nc = 4       // Carry flag is not set.


// We have a few 8 bit registeres which are used as arguments:
type Reg8  = A    | B | C | D | E | H | L    
// And also a few 16 bit registers.
type Reg16 = AF   | BC    | DE    | HL    | SP

// Constant Value (16 bit)
type Word = 
    | Calc of Operation list // We will use patch expressions from the linker to represent them

// Constent Value (8 bit)
type Byte = 
    | Calc of Operation list

// Constant Value (8 bit signed)
type SByte = 
    | Calc of Operation list

// a dereference of an address
type Deref =
    | Addr of Word // which is either a constant value
    | Reg of Reg16 // or one of the 16 bit registers. (HL in most times.)

// an argument value can be any of the above
type Val =
    | Deref of Deref
    | N of Byte
    | R8 of Reg8
    | NN of Word
    | R16 of Reg16
    | R of SByte

and as said before we only have a handful of opcodes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
type OpCode =
    | NOP
    | LD of dest:Val * src:Val
    | LDH of dest:Val * src:Val
    | INC of Val
    | DEC of Val
    | RLCA
    | RRCA
    | RLA
    | RRA
    | ADD of dest:Val * src:Val
    | ADC of dest:Val * src:Val
    | SUB of dest:Val * src:Val
    | SBC of dest:Val * src:Val
    | STOP
    | HALT
    | JR of Flag * SByte
    | LDI of dest:Val * src:Val
    | LDD of dest:Val * src:Val
    | DAA
    | CPL
    | SCF
    | CCF
    | AND of Val
    | XOR of Val
    | CP of Val
    | OR of Val
    | RET of Flag
    | RETI
    | EI
    | DI
    | POP of Reg16
    | PUSH of Reg16
    | JP of Flag * Val
    | CALL of Flag * Word
    | RST of byte
    | RLC of Val
    | RRC of Val
    | RL of Val
    | RR of Val
    | SLA of Val
    | SRA of Val
    | SRL of Val
    | SWAP of Val
    | BIT of (byte*Val)
    | RES of (byte*Val)
    | SET of (byte*Val)

the actual assembling function is the part where fsharp can really shine because of the pattern matching. As you may have noticed in the Op-Code Table, in general there a 6 kinds of opcodes:

1) a single byte value from the first opcode table (I call it the simple opcode table) without an additional encoded argument 2) a single byte value from the extended opcode table. Those opcodes are prefixed with 0xCB. 3) an opcode from the simple opcode table with one extra byte for the argument which needs to be patched. 4) an opcode from the simple opcode table with one extra signed byte for the argument which needs to be patched. 5) an opcode from the simple opcode table with one extra signed byte which is a relative address value for the argument which needs to be patched. 6) an opcode from the simple opcode table with two extra bytes (one 16 bit value) for the argument which needs to be patched.

lets start with the simple op codes:

1
2
3
4
5
6
7
8
// assembleOpCode returns a byte[] which represents the opcode and maybe also an patch operation for the linker.
let private assembleOpCode (opCode: OpCode) : Result<byte[] * Patch option, string> = 
   
    // We start with some helper functions to help encoding the opcodes.
    // for case 1: Single byte opcode from the simple opcode table
    let s oc = Ok ([|byte oc|], None) // s = Simple Table
    // for case 2: single byte opcode from the extended opcode table
    let e oc = Ok ([|0xCBuy ;byte oc|], None) // e = Extended Table

Imagine you have something like load the current address to the HL register, then the “Current address” should not be the location of the patched argument but the address of the start of the opcode. so we need to change the calculation from LD HL, CURRENT_ADDR to LD HL, CURRENT_ADDR - 1 since the patch operations are in RPN we just replace every [CurAddr] with [CurAddr; PUSH 1; Sub]. This also needs to be done for relative addresses from symbol names,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
let rec fixRelativeExpr patch exp =
    match exp with
    | [] -> []
    | head::cons ->
        let hRes = patch head
        List.append hRes (fixRelativeExpr patch cons)

let relExpr h = 
    match h with
    | SymAdr a -> [SymAdr a; CurAdr; Push 1; Add; Sub]
    | x -> [x]

let patchCurAdr x = 
    match x with
    | CurAdr -> [CurAdr; Push 1; Sub]
    | x -> [x]

let fix = fixRelativeExpr patchCurAdr

so let’s continue with the actual assembling:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    // for case 3: opcode from the simple table with an byte as argument:
    let sb oc p = // sb = Simple Table, Byte argument 
        let (Byte.Calc c) = p
        let patch = { 
            Offset = 1;    // there is one fixed byte in front of the byte we need to patch
            Size = BYTE;   // the size is byte.
            Expr = (fix c) // we need to fix the offset for the curret address 
        } 
        Ok ([|byte oc; 0uy|], Some patch)

    // case 3 + 4 are close to another but one needs to patch the relative symbol addresses too
    let Sbyte patch oc p = 
        let (SByte.Calc c) = p
        let patch = { Offset = 1; Size = SBYTE; Expr = (patch (fix c)) }
        Ok ([|byte oc; 0uy|], Some patch)

    let ss = Sbyte (fun c -> c)              // ss = Simple Table, signed byte argument
    let rl = Sbyte (fixRelativeExpr relExpr) // rl = relative value

    // for case 6: one byte opcode from the simple table with a word as argument we can use:
    let sw oc p =  // sw = Simple Table, word argument.
        let (Word.Calc c) = p
        let patch = { Offset = 1; Size = WORD; Expr = (fix c) }
        Ok ([|byte oc; 0uy; 0uy|], Some patch)

The actual assembling is then just a giant pattern matching:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
    match opCode with 
    // first of all some simple 
    | NOP                              -> s  0x00    // NOP
    | RLCA                             -> s  0x07    // RLCA
    // ... and so on
    // now some opcodes with a little pattern matching:
    | INC (R8 C)                       -> s  0x0C    // INC C
    | INC (R16 DE)                     -> s  0x13    // INC DE
    | INC (R8 D)                       -> s  0x14    // INC D
    // Sample with arguments:
    | LD ((R16 BC), NN v)              -> sw 0x01 v  // LD BC, nn
    // or the extended opcode table:
    | SET (0uy, R8 B)                  -> e  0xC0    // SET 0, B
    | SET (0uy, R8 C)                  -> e  0xC1    // SET 0, C
    // or relative jumps:
    | JR (Flag.Always, v)              -> rl 0x18 v  // JR N
    | JR (Flag.nz, v)                  -> rl 0x20 v  // JR NZ n
    | JR (Flag.c, v)                   -> rl 0x38 v  // JR C, n

I guess you get the point. The whole source can be found here. Next time we do the rest of the assembler core functions. After that the only thing left to do is to parse the input file and pass the parsed result to the linker. Stay tuned!