Files
awgh 4e22595eaa Add relocation CRUD and dyld bind kinds
add remove APIs for ELF/Mach-O/PE relocations and relocation sections
allow ELF alloc relocation sections to grow via new PT_LOAD; stabilize section write order
add Mach-O scattered relocs plus weak/lazy bind stream support
add PE COFF relocation helpers (symbol lookup)
update README and tests
2026-01-28 15:00:15 -08:00

654 lines
16 KiB
Go

package elf
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"strings"
)
// AddRelocation appends a single relocation entry to the target section.
func (f *File) AddRelocation(target *Section, rel interface{}) error {
return f.AddRelocations(target, rel)
}
// AddRelocations appends relocation entries to the target section.
func (f *File) AddRelocations(target *Section, rels interface{}) error {
return f.addRelocations(target, rels, false, nil)
}
// ReplaceRelocations replaces relocation entries for the target section.
func (f *File) ReplaceRelocations(target *Section, rels interface{}) error {
return f.addRelocations(target, rels, true, nil)
}
// RemoveRelocations clears relocation entries for the target section.
func (f *File) RemoveRelocations(target *Section) error {
if target == nil {
return errors.New("target section is nil")
}
targetIndex, ok := f.sectionIndex(target)
if !ok {
return errors.New("target section not found in file")
}
relocSecRel, _ := f.relocationSection(targetIndex, SHT_REL)
relocSecRela, _ := f.relocationSection(targetIndex, SHT_RELA)
if relocSecRel == nil && relocSecRela == nil {
return nil
}
if relocSecRel != nil {
relocSecRel.Replace(bytes.NewReader(nil), 0)
}
if relocSecRela != nil {
relocSecRela.Replace(bytes.NewReader(nil), 0)
}
f.updateDynamicRelocTags()
return nil
}
// AddRelocationsToRelocSection appends relocation entries to a specific relocation section
// (e.g. ".rela.dyn" or ".rel.plt").
func (f *File) AddRelocationsToRelocSection(sectionName string, rels interface{}) error {
relocSec := f.Section(sectionName)
if relocSec == nil {
return fmt.Errorf("relocation section %q not found", sectionName)
}
if relocSec.Type != SHT_REL && relocSec.Type != SHT_RELA {
return fmt.Errorf("section %q is not a relocation section", sectionName)
}
data, relType, entSize, err := encodeRelocations(f.ByteOrder, rels)
if err != nil {
return err
}
if relType != relocSec.Type {
return fmt.Errorf("relocation type mismatch for %q", sectionName)
}
var existing []byte
if relocSec.sr != nil {
existing, err = relocSec.Data()
if err != nil {
return err
}
}
relocSec.Addralign = relocationAlign(f.Class)
relocSec.Entsize = entSize
relocSec.Replace(bytes.NewReader(append(existing, data...)), int64(len(existing)+len(data)))
if modified, err := f.ensureSectionName(relocSec); err != nil {
return err
} else {
if err := f.relayoutRelocationSections(modified); err != nil {
return err
}
}
if err := f.relayoutAllocRelocationSection(relocSec); err != nil {
return err
}
f.updateDynamicRelocTags()
return nil
}
// RemoveRelocationsFromRelocSection clears relocation entries for the named relocation section.
func (f *File) RemoveRelocationsFromRelocSection(sectionName string) error {
relocSec := f.Section(sectionName)
if relocSec == nil {
return fmt.Errorf("relocation section %q not found", sectionName)
}
if relocSec.Type != SHT_REL && relocSec.Type != SHT_RELA {
return fmt.Errorf("section %q is not a relocation section", sectionName)
}
relocSec.Replace(bytes.NewReader(nil), 0)
f.updateDynamicRelocTags()
return nil
}
// AddRelocationForSymbol builds and adds a relocation for the named symbol.
// If addend is nil, a REL entry is created. Otherwise a RELA entry is used.
func (f *File) AddRelocationForSymbol(sectionName, symbolName string, offset uint64, rType uint32, addend *int64) error {
target := f.Section(sectionName)
if target == nil {
return fmt.Errorf("section %q not found", sectionName)
}
symIndex, symtabIndex, err := f.symbolIndexByName(symbolName)
if err != nil {
return err
}
return f.addRelocationEntry(target, symIndex, symtabIndex, offset, rType, addend)
}
// AddRelocationForAddr builds and adds a relocation that references no symbol.
// If addend is nil, a REL entry is created. Otherwise a RELA entry is used.
func (f *File) AddRelocationForAddr(sectionName string, offset uint64, rType uint32, addend *int64) error {
target := f.Section(sectionName)
if target == nil {
return fmt.Errorf("section %q not found", sectionName)
}
return f.addRelocationEntry(target, 0, -1, offset, rType, addend)
}
func (f *File) addRelocationEntry(target *Section, symIndex uint32, symtabIndex int, offset uint64, rType uint32, addend *int64) error {
switch f.Class {
case ELFCLASS32:
if offset > uint64(^uint32(0)) {
return fmt.Errorf("relocation offset %#x overflows 32-bit", offset)
}
if addend == nil {
rel := Rel32{Off: uint32(offset), Info: R_INFO32(symIndex, rType)}
return f.addRelocations(target, []Rel32{rel}, false, pickLink(symtabIndex))
}
if *addend > maxInt32 || *addend < minInt32 {
return fmt.Errorf("addend %d overflows 32-bit", *addend)
}
rel := Rela32{Off: uint32(offset), Info: R_INFO32(symIndex, rType), Addend: int32(*addend)}
return f.addRelocations(target, []Rela32{rel}, false, pickLink(symtabIndex))
case ELFCLASS64:
if addend == nil {
rel := Rel64{Off: offset, Info: R_INFO(symIndex, rType)}
return f.addRelocations(target, []Rel64{rel}, false, pickLink(symtabIndex))
}
rel := Rela64{Off: offset, Info: R_INFO(symIndex, rType), Addend: *addend}
return f.addRelocations(target, []Rela64{rel}, false, pickLink(symtabIndex))
default:
return errors.New("unsupported ELF class")
}
}
const (
minInt32 = -1 << 31
maxInt32 = 1<<31 - 1
)
func pickLink(index int) *int {
if index < 0 {
return nil
}
return &index
}
func (f *File) addRelocations(target *Section, rels interface{}, replace bool, linkOverride *int) error {
if target == nil {
return errors.New("target section is nil")
}
targetIndex, ok := f.sectionIndex(target)
if !ok {
return errors.New("target section not found in file")
}
data, relType, entSize, err := encodeRelocations(f.ByteOrder, rels)
if err != nil {
return err
}
relocSec, _ := f.relocationSection(targetIndex, relType)
newSection := false
if relocSec == nil {
relocSec = &Section{
SectionHeader: SectionHeader{
Name: relocationSectionName(target.Name, relType),
Type: relType,
Flags: 0,
Addr: 0,
Offset: 0,
Size: 0,
Link: 0,
Info: uint32(targetIndex),
Addralign: relocationAlign(f.Class),
Entsize: entSize,
},
}
newSection = true
}
linkIndex := relocSec.Link
if linkOverride != nil {
linkIndex = uint32(*linkOverride)
}
if linkIndex == 0 {
defaultLink, err := f.defaultSymtabIndex()
if err != nil {
return err
}
linkIndex = uint32(defaultLink)
}
if relocSec.Link != 0 && relocSec.Link != linkIndex {
return fmt.Errorf("relocation section links to symtab %d, want %d", relocSec.Link, linkIndex)
}
relocSec.Link = linkIndex
relocSec.Info = uint32(targetIndex)
relocSec.Addralign = relocationAlign(f.Class)
relocSec.Entsize = entSize
oldFileSize := relocSec.FileSize
if !replace {
var existing []byte
if relocSec.sr != nil {
var err error
existing, err = relocSec.Data()
if err != nil {
return err
}
}
data = append(existing, data...)
}
relocSec.Replace(bytes.NewReader(data), int64(len(data)))
shstrModified := false
if modified, err := f.ensureSectionName(relocSec); err != nil {
return err
} else if modified {
shstrModified = true
}
if newSection {
f.Sections = append(f.Sections, relocSec)
shstrModified = true
}
if err := f.relayoutRelocationSections(shstrModified); err != nil {
return err
}
if relocSec.Flags&SHF_ALLOC != 0 && relocSec.FileSize != oldFileSize {
if err := f.relayoutAllocRelocationSection(relocSec); err != nil {
return err
}
}
f.updateDynamicRelocTags()
return nil
}
func encodeRelocations(order binary.ByteOrder, rels interface{}) ([]byte, SectionType, uint64, error) {
buf := bytes.NewBuffer(nil)
switch v := rels.(type) {
case Rel32:
if err := binary.Write(buf, order, v); err != nil {
return nil, 0, 0, err
}
return buf.Bytes(), SHT_REL, uint64(binary.Size(Rel32{})), nil
case []Rel32:
for _, rel := range v {
if err := binary.Write(buf, order, rel); err != nil {
return nil, 0, 0, err
}
}
return buf.Bytes(), SHT_REL, uint64(binary.Size(Rel32{})), nil
case Rela32:
if err := binary.Write(buf, order, v); err != nil {
return nil, 0, 0, err
}
return buf.Bytes(), SHT_RELA, uint64(binary.Size(Rela32{})), nil
case []Rela32:
for _, rel := range v {
if err := binary.Write(buf, order, rel); err != nil {
return nil, 0, 0, err
}
}
return buf.Bytes(), SHT_RELA, uint64(binary.Size(Rela32{})), nil
case Rel64:
if err := binary.Write(buf, order, v); err != nil {
return nil, 0, 0, err
}
return buf.Bytes(), SHT_REL, uint64(binary.Size(Rel64{})), nil
case []Rel64:
for _, rel := range v {
if err := binary.Write(buf, order, rel); err != nil {
return nil, 0, 0, err
}
}
return buf.Bytes(), SHT_REL, uint64(binary.Size(Rel64{})), nil
case Rela64:
if err := binary.Write(buf, order, v); err != nil {
return nil, 0, 0, err
}
return buf.Bytes(), SHT_RELA, uint64(binary.Size(Rela64{})), nil
case []Rela64:
for _, rel := range v {
if err := binary.Write(buf, order, rel); err != nil {
return nil, 0, 0, err
}
}
return buf.Bytes(), SHT_RELA, uint64(binary.Size(Rela64{})), nil
default:
return nil, 0, 0, fmt.Errorf("unsupported relocation type %T", rels)
}
}
func relocationSectionName(targetName string, relType SectionType) string {
if relType == SHT_RELA {
return ".rela" + targetName
}
return ".rel" + targetName
}
func relocationAlign(class Class) uint64 {
if class == ELFCLASS64 {
return 8
}
return 4
}
func (f *File) relocationSection(targetIndex int, relType SectionType) (*Section, int) {
for i, s := range f.Sections {
if s.Type != relType {
continue
}
if int(s.Info) == targetIndex {
return s, i
}
}
return nil, -1
}
func (f *File) sectionIndex(target *Section) (int, bool) {
for i, s := range f.Sections {
if s == target {
return i, true
}
}
return -1, false
}
func (f *File) defaultSymtabIndex() (int, error) {
if idx, ok := f.sectionIndexByName(".symtab"); ok {
return idx, nil
}
if idx, ok := f.sectionIndexByName(".dynsym"); ok {
return idx, nil
}
return -1, errors.New("no symbol table section found")
}
func (f *File) sectionIndexByName(name string) (int, bool) {
for i, s := range f.Sections {
if s.Name == name {
return i, true
}
}
return -1, false
}
func (f *File) symbolIndexByName(name string) (uint32, int, error) {
if symtabIndex, ok := f.sectionIndexByName(".symtab"); ok {
syms, err := f.Symbols()
if err == nil {
for i, sym := range syms {
if sym.Name == name {
return uint32(i + 1), symtabIndex, nil
}
}
} else if !errors.Is(err, ErrNoSymbols) {
return 0, -1, err
}
}
if dynsymIndex, ok := f.sectionIndexByName(".dynsym"); ok {
syms, err := f.DynamicSymbols()
if err == nil {
for i, sym := range syms {
if sym.Name == name {
return uint32(i + 1), dynsymIndex, nil
}
}
} else if !errors.Is(err, ErrNoSymbols) {
return 0, -1, err
}
}
return 0, -1, fmt.Errorf("symbol %q not found", name)
}
func (f *File) ensureSectionName(section *Section) (bool, error) {
if section.Name == "" {
return false, nil
}
if f.ShStrIndex < 0 || f.ShStrIndex >= len(f.Sections) {
return false, errors.New("invalid shstrtab index")
}
shstr := f.Sections[f.ShStrIndex]
data, err := shstr.Data()
if err != nil && shstr.sr != nil {
return false, err
}
nameBytes := append([]byte(section.Name), 0)
if idx := bytes.Index(data, nameBytes); idx >= 0 {
section.Shname = uint32(idx)
return false, nil
}
section.Shname = uint32(len(data))
newData := append(data, nameBytes...)
shstr.Replace(bytes.NewReader(newData), int64(len(newData)))
return true, nil
}
func (f *File) relayoutRelocationSections(shstrModified bool) error {
var moved []*Section
for _, s := range f.Sections {
if s.Type == SHT_REL || s.Type == SHT_RELA {
moved = append(moved, s)
}
}
if shstrModified && f.ShStrIndex >= 0 && f.ShStrIndex < len(f.Sections) {
moved = append(moved, f.Sections[f.ShStrIndex])
}
if len(moved) == 0 {
return nil
}
moveSet := map[*Section]struct{}{}
for _, s := range moved {
if s.Flags&SHF_ALLOC != 0 {
continue
}
moveSet[s] = struct{}{}
}
var maxEnd uint64
for _, s := range f.Sections {
if _, ok := moveSet[s]; ok {
continue
}
if s.Type == SHT_NOBITS || s.FileSize == 0 {
continue
}
end := s.Offset + s.FileSize
if end > maxEnd {
maxEnd = end
}
}
offset := maxEnd
for _, s := range moved {
if s.Flags&SHF_ALLOC != 0 {
continue
}
align := s.Addralign
if align == 0 {
align = 1
}
offset = alignUp(offset, align)
s.Offset = offset
offset += s.FileSize
}
shtAlign := uint64(4)
if f.Class == ELFCLASS64 {
shtAlign = 8
}
f.SHTOffset = int64(alignUp(offset, shtAlign))
return nil
}
func (f *File) relayoutAllocRelocationSection(section *Section) error {
if section.Flags&SHF_ALLOC == 0 {
return nil
}
var prog *Prog
var progIndex int
for i, p := range f.Progs {
if p.Type != PT_LOAD {
continue
}
if section.Addr >= p.Vaddr && section.Addr < p.Vaddr+p.Memsz {
prog = p
progIndex = i
break
}
}
if prog == nil {
return fmt.Errorf("no PT_LOAD segment contains %q", section.Name)
}
var nextLoadOff uint64
for i, p := range f.Progs {
if p.Type != PT_LOAD || i == progIndex {
continue
}
if p.Off > prog.Off && (nextLoadOff == 0 || p.Off < nextLoadOff) {
nextLoadOff = p.Off
}
}
align := section.Addralign
if align == 0 {
align = relocationAlign(f.Class)
}
newOff := alignUp(prog.Off+prog.Filesz, align)
newEnd := newOff + section.FileSize
if nextLoadOff != 0 && newEnd > nextLoadOff {
return f.relayoutAllocRelocationSectionNewLoad(section)
}
newAddr := prog.Vaddr + (newOff - prog.Off)
section.Offset = newOff
section.Addr = newAddr
if newEnd > prog.Off+prog.Filesz {
delta := newEnd - (prog.Off + prog.Filesz)
prog.Filesz += delta
prog.Memsz += delta
}
if end := section.Offset + section.FileSize; int64(end) > f.SHTOffset {
shtAlign := uint64(4)
if f.Class == ELFCLASS64 {
shtAlign = 8
}
f.SHTOffset = int64(alignUp(end, shtAlign))
}
return nil
}
func (f *File) relayoutAllocRelocationSectionNewLoad(section *Section) error {
align := section.Addralign
if align == 0 {
align = relocationAlign(f.Class)
}
var lastLoad *Prog
for _, p := range f.Progs {
if p.Type != PT_LOAD {
continue
}
if lastLoad == nil || p.Off > lastLoad.Off {
lastLoad = p
}
}
if lastLoad == nil {
return fmt.Errorf("no PT_LOAD segments available")
}
baseOff := lastLoad.Off + lastLoad.Filesz
fileEnd := f.maxFileEnd()
if fileEnd > baseOff {
baseOff = fileEnd
}
newOff := alignUp(baseOff, align)
newAddr := lastLoad.Vaddr + (newOff - lastLoad.Off)
section.Offset = newOff
section.Addr = newAddr
end := newOff + section.FileSize
if end > lastLoad.Off+lastLoad.Filesz {
delta := end - (lastLoad.Off + lastLoad.Filesz)
lastLoad.Filesz += delta
lastLoad.Memsz += delta
}
if end := section.Offset + section.FileSize; int64(end) > f.SHTOffset {
shtAlign := uint64(4)
if f.Class == ELFCLASS64 {
shtAlign = 8
}
f.SHTOffset = int64(alignUp(end, shtAlign))
}
return nil
}
func (f *File) maxFileEnd() uint64 {
var maxEnd uint64
for _, s := range f.Sections {
if s.Type == SHT_NOBITS || s.FileSize == 0 {
continue
}
end := s.Offset + s.FileSize
if end > maxEnd {
maxEnd = end
}
}
if f.SHTOffset > int64(maxEnd) {
maxEnd = uint64(f.SHTOffset)
}
return maxEnd
}
func (f *File) updateDynamicRelocTags() {
if len(f.DynTags) == 0 {
return
}
for _, s := range f.Sections {
if s.Flags&SHF_ALLOC == 0 || (s.Type != SHT_REL && s.Type != SHT_RELA) {
continue
}
if s.Addr == 0 {
continue
}
isPlt := strings.Contains(s.Name, ".plt")
if s.Type == SHT_RELA {
if isPlt {
f.setDynTag(DT_JMPREL, s.Addr)
f.setDynTag(DT_PLTRELSZ, s.Size)
f.setDynTag(DT_PLTREL, uint64(DT_RELA))
} else {
f.setDynTag(DT_RELA, s.Addr)
f.setDynTag(DT_RELASZ, s.Size)
f.setDynTag(DT_RELAENT, s.Entsize)
}
} else {
if isPlt {
f.setDynTag(DT_JMPREL, s.Addr)
f.setDynTag(DT_PLTRELSZ, s.Size)
f.setDynTag(DT_PLTREL, uint64(DT_REL))
} else {
f.setDynTag(DT_REL, s.Addr)
f.setDynTag(DT_RELSZ, s.Size)
f.setDynTag(DT_RELENT, s.Entsize)
}
}
}
}
func (f *File) setDynTag(tag DynTag, value uint64) {
for i, entry := range f.DynTags {
if entry.Tag == tag {
f.DynTags[i].Value = value
return
}
}
f.DynTags = append(f.DynTags, DynTagValue{Tag: tag, Value: value})
}
func alignUp(value, align uint64) uint64 {
if align == 0 {
return value
}
rem := value % align
if rem == 0 {
return value
}
return value + (align - rem)
}