objcopy.go raw

   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