github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/builder/objcopy.go (about)

     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  }