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 }