Welcome back to my small series about creating a gameboy linker and assembler. Now that we have all data assigned to their final location and also patched all operations we just need to create the output files, which are the actual rom and a symbol file to help with debugging.

The symbol file is rather simple, it just a line containing the bank number, the address and the name of the symbol. I will drop the whole source here and add some comments:

 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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
let private linkerResFromSections header (sections: Section list) =
    // a writer takes a stream and writes the actual data to that stream.
    let symbolWriter : Writer =
        // recursively write each section
        let rec writeSec (sections:Section list) (s:Stream) =
            match sections with
            | head:: rest ->
                let rb = getBankNo head  // get the bank number of the current section. For example 1 form RAM bank 1 and so on.
                let symName (s:Symbol) = // symName returns the name of the symbol with the following pattern: GLOBAL[.LOCAL]
                    match s.Name.Local with
                    | None -> s.Name.Global
                    | Some l -> sprintf "%s.%s" s.Name.Global l

                // symLine returns the entry for the symbol in the symbol file
                let symLine (s:Symbol) = sprintf "%03X:%04X %s\n" rb (head.Origin.Value + s.Offset) (symName s)
            
                do head.Symbols                                            // for each symbol
                |> List.sortBy (fun sym -> sym.Offset)                     // sorted by the offset 
                |> List.map symLine                                        // map the symbol to the a line of text
                |> List.map System.Text.Encoding.UTF8.GetBytes             // convert it to bytes with the UTF8 encoder
                |> List.iter (fun buf -> s.Write(buf, 0, buf.Length))      // and write them to the stream.

                writeSec rest s  // continue with the other sections
            | [] -> () // No more sections, so we are done.

        
        sections                            // for each section
        |> List.sortBy (fun s -> s.Origin)  // sorted by the origin of the section
        |> writeSec                         // write each section.

    // now we create the writer for the rom file
    let romWriter = 
        // getSectionAddr returns the file-offset for a rom bank + section offset.
        let getSectionAddr (bankNo:BankNo) (offset: uint16): int =
            let offset = int offset
            match bankNo with
            | 0 -> offset
            | x -> (0x4000 * (x-1)) + offset

        // fill writes "0" to the stream until the stream is at the required position.
        let rec fill toPos pos (s:Stream) =
            let buffSize = min (toPos - pos) 512
            match buffSize with
            | 0 -> ()
            | bs -> 
                let buf = Array.zeroCreate bs
                s.Write(buf, 0, buffSize)
                fill toPos (pos + buffSize) s

        // writePackets writes a list of packets in the form of (startOffset:int * Data:byte[]) to the stream
        let rec writePackets romSize pos (s:Stream) packets =
            match packets with
            | head :: cons -> 
                let (idx, data) = head
                let seek = idx - pos
                let buf = Array.zeroCreate(seek)
                s.Write(buf, 0, buf.Length)
                s.Write(data, 0, data.Length)
                writePackets romSize (idx+data.Length) s cons
            | [] -> fill romSize pos s

        // sorts the input and calls writePackets
        let createWriter romBankCount (input: (int*byte[])list) (w: Stream) = 
            do input 
            |> List.sortBy (fun (i, _) -> i)
            |> writePackets romBankCount 0 w
        
        // calculate the global checksum from all other rom data for the cartridge header and convert it to a "packet"
        let addGlobalChecksum items =
            let cs = 
                items
                |> List.map (fun (_, x) -> Array.fold (fun cur n -> cur + (uint16 n)) 0us x)
                |> List.fold (fun cur n -> cur + (uint16 n)) 0us
            (GLOBALCHECK, [| (byte (cs >>> 8)); byte cs |])::items

        
        sections                                                             // for each section
        |> List.choose (fun s ->                                             // take all "ROM" section
            match s.Rom with
            | Some rom -> 
                match s.Origin with
                | Some origin -> 
                    let bn = getBankNo s
                    Some ((getSectionAddr bn origin), rom.Data)              // and map it to file-offset + byte[] (called "packet" in the comments before)
                | None -> None
            | None -> None
            )
        |> List.append (List.singleton (HEADERSTART, headerToBytes header))  // append the bytes for the cartridge header
        |> addGlobalChecksum                                                 // calculate and append the "global checksum"
        |> createWriter (getRomSize header.CartridgeType)                    // create a writer rome file.

    // now we can just create those writers.
    { 
        Rom = romWriter
        Symbols = symbolWriter
    }

So we are finally done with the linker, so next time we can start with the assembler :)