diff options
author | Ayke van Laethem <[email protected]> | 2019-11-11 15:26:15 +0100 |
---|---|---|
committer | Ron Evans <[email protected]> | 2019-11-11 20:53:50 +0100 |
commit | 8e6cb89ceb47ec031863b8edb0cc4002215acc43 (patch) | |
tree | d61699293f9f19e9e8d02b42f65b2ab631e9f768 /builder/objcopy.go | |
parent | 946e2dd405b66eecd51a9d9f3f82555c7e13a7d7 (diff) | |
download | tinygo-8e6cb89ceb47ec031863b8edb0cc4002215acc43.tar.gz tinygo-8e6cb89ceb47ec031863b8edb0cc4002215acc43.zip |
main: refactor compile/link part to a builder package
This is a large commit that moves all code directly related to
compiling/linking into a new builder package. This has a number of
advantages:
* It cleanly separates the API between the command line and the full
compilation (with a very small API surface).
* When the compiler finally compiles one package at a time (instead of
everything at once as it does now), something will have to invoke it
once per package. This builder package will be the natural place to
do that, and also be the place where the whole process can be
parallelized.
* It allows the TinyGo compiler to be used as a package. A client can
simply import the builder package and compile code using it.
As part of this refactor, the following additional things changed:
* Exported symbols have been made unexported when they weren't needed.
* The compilation target has been moved into the compileopts.Options
struct. This is done because the target really is just another
compiler option, and the API is simplified by moving it in there.
* The moveFile function has been duplicated. It does not really belong
in the builder API but is used both by the builder and the command
line. Moving it into a separate package didn't seem useful either
for what is essentially an utility function.
* Some doc strings have been improved.
Some future changes/refactors I'd like to make after this commit:
* Clean up the API between the builder and the compiler package.
* Perhaps move the test files (in testdata/) into the builder package.
* Perhaps move the loader package into the builder package.
Diffstat (limited to 'builder/objcopy.go')
-rw-r--r-- | builder/objcopy.go | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/builder/objcopy.go b/builder/objcopy.go new file mode 100644 index 000000000..3ab0eaeff --- /dev/null +++ b/builder/objcopy.go @@ -0,0 +1,131 @@ +package builder + +import ( + "debug/elf" + "io/ioutil" + "os" + "path/filepath" + "sort" + + "github.com/marcinbor85/gohex" +) + +// 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 { + 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 { + if prog.Paddr != progs[0].Paddr+uint64(len(rom)) { + return 0, nil, objcopyError{"ROM segments are non-contiguous: " + path, nil} + } + data, err := ioutil.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 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 filepath.Ext(outfile) { + case ".gba": + // The address is not stored in a .gba file. + _, err := f.Write(data) + return err + case ".bin": + // The address is not stored in a .bin file (therefore you + // should use .hex files in most cases). + _, err := f.Write(data) + return err + case ".hex": + mem := gohex.NewMemory() + err := mem.AddBinary(uint32(addr), data) + if err != nil { + return objcopyError{"failed to create .hex file", err} + } + mem.DumpIntelHex(f, 16) // TODO: handle error + return nil + default: + panic("unreachable") + } +} |