aboutsummaryrefslogtreecommitdiffhomepage
path: root/builder/objcopy.go
blob: cf10547b4d5fc98c6b62fd0caea599b68739ecc9 (plain)
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package builder

import (
	"debug/elf"
	"io"
	"os"
	"sort"

	"github.com/marcinbor85/gohex"
)

// maxPadBytes is the maximum allowed bytes to be padded in a rom extraction
// this value is currently defined by Nintendo Switch Page Alignment (4096 bytes)
const maxPadBytes = 4095

// objcopyError is an error returned by functions that act like objcopy.
type objcopyError struct {
	Op  string
	Err error
}

func (e objcopyError) Error() string {
	if e.Err == nil {
		return e.Op
	}
	return e.Op + ": " + e.Err.Error()
}

type progSlice []*elf.Prog

func (s progSlice) Len() int           { return len(s) }
func (s progSlice) Less(i, j int) bool { return s[i].Paddr < s[j].Paddr }
func (s progSlice) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

// extractROM extracts a firmware image and the first load address from the
// given ELF file. It tries to emulate the behavior of objcopy.
func extractROM(path string) (uint64, []byte, error) {
	f, err := elf.Open(path)
	if err != nil {
		return 0, nil, objcopyError{"failed to open ELF file to extract text segment", err}
	}
	defer f.Close()

	// The GNU objcopy command does the following for firmware extraction (from
	// the man page):
	// > When objcopy generates a raw binary file, it will essentially produce a
	// > memory dump of the contents of the input object file. All symbols and
	// > relocation information will be discarded. The memory dump will start at
	// > the load address of the lowest section copied into the output file.

	// Find the lowest section address.
	startAddr := ^uint64(0)
	for _, section := range f.Sections {
		if section.Type != elf.SHT_PROGBITS || section.Flags&elf.SHF_ALLOC == 0 {
			continue
		}
		if section.Addr < startAddr {
			startAddr = section.Addr
		}
	}

	progs := make(progSlice, 0, 2)
	for _, prog := range f.Progs {
		if prog.Type != elf.PT_LOAD || prog.Filesz == 0 || prog.Off == 0 {
			continue
		}
		progs = append(progs, prog)
	}
	if len(progs) == 0 {
		return 0, nil, objcopyError{"file does not contain ROM segments: " + path, nil}
	}
	sort.Sort(progs)

	var rom []byte
	for _, prog := range progs {
		romEnd := progs[0].Paddr + uint64(len(rom))
		if prog.Paddr > romEnd && prog.Paddr < romEnd+16 {
			// Sometimes, the linker seems to insert a bit of padding between
			// segments. Simply zero-fill these parts.
			rom = append(rom, make([]byte, prog.Paddr-romEnd)...)
		}
		if prog.Paddr != progs[0].Paddr+uint64(len(rom)) {
			diff := prog.Paddr - (progs[0].Paddr + uint64(len(rom)))
			if diff > maxPadBytes {
				return 0, nil, objcopyError{"ROM segments are non-contiguous: " + path, nil}
			}
			// Pad the difference
			rom = append(rom, make([]byte, diff)...)
		}
		data, err := io.ReadAll(prog.Open())
		if err != nil {
			return 0, nil, objcopyError{"failed to extract segment from ELF file: " + path, err}
		}
		rom = append(rom, data...)
	}
	if progs[0].Paddr < startAddr {
		// The lowest memory address is before the first section. This means
		// that there is some extra data loaded at the start of the image that
		// should be discarded.
		// Example: ELF files where .text doesn't start at address 0 because
		// there is a bootloader at the start.
		return startAddr, rom[startAddr-progs[0].Paddr:], nil
	} else {
		return progs[0].Paddr, rom, nil
	}
}

// objcopy converts an ELF file to a different (simpler) output file format:
// .bin or .hex. It extracts only the .text section.
func objcopy(infile, outfile, binaryFormat string) error {
	f, err := os.OpenFile(outfile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
	if err != nil {
		return err
	}
	defer f.Close()

	// Read the .text segment.
	addr, data, err := extractROM(infile)
	if err != nil {
		return err
	}

	// Write to the file, in the correct format.
	switch binaryFormat {
	case "hex":
		// Intel hex file, includes the firmware start address.
		mem := gohex.NewMemory()
		err := mem.AddBinary(uint32(addr), data)
		if err != nil {
			return objcopyError{"failed to create .hex file", err}
		}
		return mem.DumpIntelHex(f, 16)
	case "bin":
		// The start address is not stored in raw firmware files (therefore you
		// should use .hex files in most cases).
		_, err := f.Write(data)
		return err
	default:
		panic("unreachable")
	}
}