1 package builder
2 3 import (
4 "debug/elf"
5 "io"
6 "os"
7 "sort"
8 9 "github.com/marcinbor85/gohex"
10 )
11 12 // maxPadBytes is the maximum allowed bytes to be padded in a rom extraction
13 // this value is currently defined by Nintendo Switch Page Alignment (4096 bytes)
14 const maxPadBytes = 4095
15 16 // objcopyError is an error returned by functions that act like objcopy.
17 type objcopyError struct {
18 Op string
19 Err error
20 }
21 22 func (e objcopyError) Error() string {
23 if e.Err == nil {
24 return e.Op
25 }
26 return e.Op + ": " + e.Err.Error()
27 }
28 29 type progSlice []*elf.Prog
30 31 func (s progSlice) Len() int { return len(s) }
32 func (s progSlice) Less(i, j int) bool { return s[i].Paddr < s[j].Paddr }
33 func (s progSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
34 35 // extractROM extracts a firmware image and the first load address from the
36 // given ELF file. It tries to emulate the behavior of objcopy.
37 func extractROM(path string) (uint64, []byte, error) {
38 f, err := elf.Open(path)
39 if err != nil {
40 return 0, nil, objcopyError{"failed to open ELF file to extract text segment", err}
41 }
42 defer f.Close()
43 44 // The GNU objcopy command does the following for firmware extraction (from
45 // the man page):
46 // > When objcopy generates a raw binary file, it will essentially produce a
47 // > memory dump of the contents of the input object file. All symbols and
48 // > relocation information will be discarded. The memory dump will start at
49 // > the load address of the lowest section copied into the output file.
50 51 // Find the lowest section address.
52 startAddr := ^uint64(0)
53 for _, section := range f.Sections {
54 if section.Type != elf.SHT_PROGBITS || section.Flags&elf.SHF_ALLOC == 0 {
55 continue
56 }
57 if section.Addr < startAddr {
58 startAddr = section.Addr
59 }
60 }
61 62 progs := make(progSlice, 0, 2)
63 for _, prog := range f.Progs {
64 if prog.Type != elf.PT_LOAD || prog.Filesz == 0 || prog.Off == 0 {
65 continue
66 }
67 progs = append(progs, prog)
68 }
69 if len(progs) == 0 {
70 return 0, nil, objcopyError{"file does not contain ROM segments: " + path, nil}
71 }
72 sort.Sort(progs)
73 74 var rom []byte
75 for _, prog := range progs {
76 romEnd := progs[0].Paddr + uint64(len(rom))
77 if prog.Paddr > romEnd && prog.Paddr < romEnd+16 {
78 // Sometimes, the linker seems to insert a bit of padding between
79 // segments. Simply zero-fill these parts.
80 rom = append(rom, make([]byte, prog.Paddr-romEnd)...)
81 }
82 if prog.Paddr != progs[0].Paddr+uint64(len(rom)) {
83 diff := prog.Paddr - (progs[0].Paddr + uint64(len(rom)))
84 if diff > maxPadBytes {
85 return 0, nil, objcopyError{"ROM segments are non-contiguous: " + path, nil}
86 }
87 // Pad the difference
88 rom = append(rom, make([]byte, diff)...)
89 }
90 data, err := io.ReadAll(prog.Open())
91 if err != nil {
92 return 0, nil, objcopyError{"failed to extract segment from ELF file: " + path, err}
93 }
94 rom = append(rom, data...)
95 }
96 if progs[0].Paddr < startAddr {
97 // The lowest memory address is before the first section. This means
98 // that there is some extra data loaded at the start of the image that
99 // should be discarded.
100 // Example: ELF files where .text doesn't start at address 0 because
101 // there is a bootloader at the start.
102 return startAddr, rom[startAddr-progs[0].Paddr:], nil
103 } else {
104 return progs[0].Paddr, rom, nil
105 }
106 }
107 108 // objcopy converts an ELF file to a different (simpler) output file format:
109 // .bin or .hex. It extracts only the .text section.
110 func objcopy(infile, outfile, binaryFormat string) error {
111 f, err := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
112 if err != nil {
113 return err
114 }
115 defer f.Close()
116 117 // Read the .text segment.
118 addr, data, err := extractROM(infile)
119 if err != nil {
120 return err
121 }
122 123 // Write to the file, in the correct format.
124 switch binaryFormat {
125 case "hex":
126 // Intel hex file, includes the firmware start address.
127 mem := gohex.NewMemory()
128 err := mem.AddBinary(uint32(addr), data)
129 if err != nil {
130 return objcopyError{"failed to create .hex file", err}
131 }
132 return mem.DumpIntelHex(f, 16)
133 case "bin":
134 // The start address is not stored in raw firmware files (therefore you
135 // should use .hex files in most cases).
136 _, err := f.Write(data)
137 return err
138 default:
139 panic("unreachable")
140 }
141 }
142