/*
 *	Copyright (C) 1991, Commodore Business Machines, Inc.
 *
 *	Given a pointer to an ELF file that has been loaded into
 *	memory, and a pointer to a structure which contains, among
 *	other things, the virtual address to which the start of text
 *	is to be bound, performs relocation for the text and data
 *	segments, and fills in other fields in the passed ELF file
 *	info structure.
 *
 *	The text segment is bound to the specified starting virtual
 *	address, followed immediately by initialized data, and then
 *	uninitialized data.  Certain assumptions are made about the
 *	existance and ordering of the text, data, and bss sections.
 *	There are other limitations, such as only handling the
 *	minimal number of relocation types necessary.
 *
 */

#if (DBUG || TEST)
#  include <stdio.h>
#endif

#if DBUG
#  include <local/dbug.h>
#else
#  define DBUG_ENTER(a1)
#  define DBUG_RETURN(a1) return(a1)
#  define DBUG_VOID_RETURN return
#  define DBUG_EXECUTE(keyword,a1)
#  define DBUG_PRINT(keyword,arglist)
#  define DBUG_PUSH(a1)
#  define DBUG_POP()
#  define DBUG_PROCESS(a1)
#  define DBUG_FILE (stderr)
#  define DBUG_SETJMP setjmp
#  define DBUG_LONGJMP longjmp
#endif

#include <elf.h>
#include <sys/elf_68K.h>

#include <exec/types.h>
#include <inline/exec.h>

#include "bootdata.h"

#if TEST
#  define COMPLAIN(fmt,arg)	fprintf(stderr,fmt,arg)
#  define RELOC(string)		string
#else
#  include <exec/alerts.h>
#  define COMPLAIN(fmt,arg)	Alert(0x52454C41|AT_DeadEnd,0x4)
#  define RELOC(string)		reloc(string)
#endif

#if !TEST
#define reloc(adr) (adr)
#endif

extern int streq (char *s1, char *s2);
extern int streqn (char *s1, char *s2, int nbytes);

struct globals {
    char *elfdata;		/* Pointer to in memory file copy */
    Elf32_Ehdr *Ehdr;		/* Pointer to ELF file header */
    Elf32_Shdr *Shdrs;		/* Pointer to ELF section header table */
    Elf32_Phdr *Phdrs;		/* Pointer to ELF program header table */
    Elf32_Shdr *thdr;		/* Pointer to text section header */
    Elf32_Shdr *dhdr;		/* Pointer to data section header */
    Elf32_Shdr *bhdr;		/* Pointer to bss section header */
    Elf32_Shdr *symhdr;		/* Pointer to symbol table section header */
    Elf32_Addr end;		/* Current end of text+data+bss */
};

struct nrel {
    unsigned long nr_offset;
    unsigned long nr_value;
};

static Elf32_Addr symvaddr (struct globals *gbls, Elf32_Sym *sym,
			    unsigned int symidx)
{
    Elf32_Shdr *rshdr;
    Elf32_Addr vaddr = 0;

    DBUG_ENTER ("symvaddr");
    if (sym -> st_shndx > 0 && sym -> st_shndx < gbls -> Ehdr -> e_shnum) {
	DBUG_PRINT ("symdef", ("defined in section %d", sym -> st_shndx));
	rshdr = gbls -> Shdrs + sym -> st_shndx;
	if (rshdr -> sh_addr == 0) {
	    COMPLAIN ("symbol[%d] is", symidx);
	    COMPLAIN (" in unbound section %d\n", sym -> st_shndx);
	} else {
	    vaddr = rshdr -> sh_addr + sym -> st_value;
	}
    } else if (sym -> st_shndx == SHN_ABS) {
	DBUG_PRINT ("abssym", ("is an absolute symbol"));
	vaddr = sym -> st_value;
    } else {
	COMPLAIN ("symbol[%d]", symidx);
	COMPLAIN (" references section %d\n", sym -> st_shndx);
    }
    DBUG_PRINT ("symval", ("symbol[%d] value is %#lx", symidx, vaddr));
    DBUG_RETURN (vaddr);
}

static int relocsection (struct globals *gbls, Elf32_Shdr *shdr,
			 Elf32_Shdr *relsechdr, Elf32_Shdr *symtabhdr)
{
    char *secdata;
    char *datap;
    Elf32_Sym *syms;
    Elf32_Sym *sym;
    Elf32_Rela *relp;
    Elf32_Addr vaddr;
    unsigned int nrel;
    int reltype;
    unsigned int symidx;

    
    DBUG_ENTER ("relocsection");
    secdata = (char *) (gbls -> elfdata + relsechdr -> sh_offset);
    syms = (Elf32_Sym *) (gbls -> elfdata + symtabhdr -> sh_offset);
    relp = (Elf32_Rela *) (gbls -> elfdata + shdr -> sh_offset);
    nrel = shdr -> sh_size / shdr -> sh_entsize;
    DBUG_PRINT ("nrel", ("section has %u relocation records", nrel));    
    while (nrel-- > 0) {
	reltype = ELF32_R_TYPE (relp -> r_info);
	symidx = ELF32_R_SYM (relp -> r_info);
	sym = syms + symidx;
	datap = secdata + relp -> r_offset;
	vaddr = symvaddr (gbls, sym, symidx);
	switch (reltype) {
	    case R_68K_32:
		*(long *)datap = vaddr + relp -> r_addend;
		DBUG_PRINT ("reloc", ("reloc to %#lx", *(long *)datap));
		break;
	    default:
		COMPLAIN ("unknown relocation type %d\n", reltype);
		break;
	}
	relp++;
    }
    DBUG_RETURN (0);
}

static int allocbss (struct globals *gbls)
{
    Elf32_Sym *syms;
    Elf32_Sym *sym;
    Elf32_Addr vaddr;
    unsigned int nsyms;
    unsigned int symidx;

    DBUG_ENTER ("allocbss");
    nsyms = gbls -> symhdr -> sh_size / gbls -> symhdr -> sh_entsize;
    DBUG_PRINT ("nsyms", ("found %#x symbols", nsyms));
    syms = (Elf32_Sym *) (gbls -> elfdata + gbls -> symhdr -> sh_offset);
    for (symidx = 0; symidx < nsyms; symidx++) {
	sym = syms + symidx;
	if (sym -> st_shndx == SHN_COMMON) {
	    if (gbls -> bhdr == NULL) {
		COMPLAIN ("symbol[%d] in missing bss section\n", symidx);
		break;
	    } else {
		sym -> st_shndx = gbls -> bhdr - gbls -> Shdrs;
		vaddr = gbls -> bhdr -> sh_addr + gbls -> bhdr -> sh_size;
		vaddr += (sym -> st_value - 1);
		vaddr &= ~(sym -> st_value - 1);
		DBUG_PRINT ("bss", ("alloc sym[%d] at bss vaddr %#lx",
				    symidx, vaddr));
		sym -> st_value = vaddr - gbls -> bhdr -> sh_addr;
		gbls -> end = vaddr + sym -> st_size;
		DBUG_PRINT ("end", ("end moved to %#lx", gbls -> end));
		gbls -> bhdr -> sh_size = gbls -> end - gbls -> bhdr -> sh_addr;
	    }
	}
    }
    DBUG_RETURN (0);
}

static int specialsyms (struct globals *gbls)
{
    Elf32_Sym *syms;
    Elf32_Sym *sym;
    Elf32_Addr vaddr;
    unsigned int nsyms;
    unsigned int symidx;
    Elf32_Shdr *strtabhdr;
    char *strtab;
    char *symname;

    DBUG_ENTER ("specialsyms");
    nsyms = gbls -> symhdr -> sh_size / gbls -> symhdr -> sh_entsize;
    DBUG_PRINT ("nsyms", ("found %#x symbols", nsyms));
    syms = (Elf32_Sym *) (gbls -> elfdata + gbls -> symhdr -> sh_offset);
    strtabhdr = gbls -> Shdrs + gbls -> symhdr -> sh_link;
    strtab = gbls -> elfdata + strtabhdr -> sh_offset;
    for (symidx = 0; symidx < nsyms; symidx++) {
	sym = syms + symidx;
	if (sym -> st_name) {
	    symname = strtab + sym -> st_name;
	    DBUG_PRINT ("sym", ("symbol[%d] = '%s'", symidx, symname));
	    if (streq (symname, RELOC ("etext"))) {
#if 0
		sym -> st_value = gbls -> thdr -> sh_addr + gbls -> thdr -> sh_size;
#endif
		sym -> st_shndx = SHN_ABS;
	    } else if (streq (symname, RELOC ("edata"))) {
		sym -> st_value = gbls -> dhdr -> sh_addr + gbls -> dhdr -> sh_size;
		sym -> st_shndx = SHN_ABS;
	    } else if (streq (symname, RELOC ("end"))) {
		sym -> st_value = gbls -> end;
		sym -> st_shndx = SHN_ABS;
	    }
	}
    }
    DBUG_RETURN (0);
}

static int findsections (struct BootData *bip, struct globals *gbls)
{
    unsigned int sectnum;
    Elf32_Shdr *shdr;
    Elf32_Shdr *strtabhdr;
    char *strtab;
    char *sectname;
    
    DBUG_ENTER ("findsections");
    DBUG_PRINT ("sects", ("look at %d sections", gbls -> Ehdr -> e_shnum));
    strtabhdr = gbls -> Shdrs + gbls -> Ehdr -> e_shstrndx;
    strtab = gbls -> elfdata + strtabhdr -> sh_offset;
    for (sectnum = 0; sectnum < gbls -> Ehdr -> e_shnum; sectnum++) {
	shdr = gbls -> Shdrs + sectnum;
	sectname = strtab + shdr -> sh_name;
	DBUG_PRINT ("sect", ("section[%d] '%s'", sectnum, sectname));
	if (shdr -> sh_type == SHT_PROGBITS) {
	    if (shdr -> sh_flags & SHF_EXECINSTR) {
		if (shdr -> sh_size > 0) {
		    gbls -> thdr = shdr;
		}
	    } else if (shdr -> sh_flags & SHF_WRITE) {
		if (shdr -> sh_size > 0) {
		    gbls -> dhdr = shdr;
		}
	    }
	} else if (shdr -> sh_type == SHT_NOBITS) {
	    if (shdr -> sh_flags & SHF_WRITE) {
		if (shdr -> sh_size > 0) {
		    gbls -> bhdr = shdr;
		}
	    }
	} else if (shdr -> sh_type == SHT_SYMTAB) {
	    gbls -> symhdr = shdr;
	}
    }
    DBUG_RETURN (0);
}

static int bindsections (struct BootData *bip, struct globals *gbls)
{
    unsigned int sectnum;
    Elf32_Shdr *shdr;
    Elf32_Shdr *strtabhdr;
    char *strtab;
    char *sectname;
    
    DBUG_ENTER ("bindsections");
    if (gbls -> thdr != NULL) {
	gbls -> thdr -> sh_addr = bip -> bd_entry;
	DBUG_PRINT ("text", ("text @ %#lx", gbls -> thdr -> sh_addr));
	DBUG_PRINT ("text", ("text size %#lx", gbls -> thdr -> sh_size));
	bip -> bd_tvaddr = gbls -> thdr -> sh_addr;
	bip -> bd_toffset = gbls -> thdr -> sh_offset;
	bip -> bd_tsize = gbls -> thdr -> sh_size;
	gbls -> end = gbls -> thdr -> sh_addr + gbls -> thdr -> sh_size;
	DBUG_PRINT ("end", ("end = %#lx", gbls -> end));
	if (gbls -> dhdr != NULL) {
	    gbls -> dhdr -> sh_addr = gbls -> thdr -> sh_addr + gbls -> thdr -> sh_size;
	    DBUG_PRINT ("data", ("data @ %#lx", gbls -> dhdr -> sh_addr));
	    DBUG_PRINT ("data", ("data size %#lx", gbls -> dhdr -> sh_size));
	    bip -> bd_dvaddr = gbls -> dhdr -> sh_addr;
	    bip -> bd_doffset = gbls -> dhdr -> sh_offset;
	    bip -> bd_dsize = gbls -> dhdr -> sh_size;
	    gbls -> end = gbls -> dhdr -> sh_addr + gbls -> dhdr -> sh_size;
	    DBUG_PRINT ("end", ("end = %#lx", gbls -> end));
	    if (gbls -> bhdr != NULL) {
		gbls -> bhdr -> sh_addr = gbls -> dhdr -> sh_addr + gbls -> dhdr -> sh_size;
		gbls -> end = gbls -> bhdr -> sh_addr + gbls -> bhdr -> sh_size;
		DBUG_PRINT ("bss", ("bss @ %#lx", gbls -> bhdr -> sh_addr));
		DBUG_PRINT ("bss", ("bss size %#lx", gbls -> bhdr -> sh_size));
		DBUG_PRINT ("end", ("end = %#lx", gbls -> end));
	    }
	}
    }
    strtabhdr = gbls -> Shdrs + gbls -> Ehdr -> e_shstrndx;
    strtab = gbls -> elfdata + strtabhdr -> sh_offset;
    for (sectnum = 0; sectnum < gbls -> Ehdr -> e_shnum; sectnum++) {
	shdr = gbls -> Shdrs + sectnum;
	sectname = strtab + shdr -> sh_name;
	DBUG_PRINT ("sect", ("section[%d] '%s'", sectnum, sectname));
	if (streq (sectname, RELOC (".uvblock"))) {
	    shdr -> sh_addr = 0x40000000;
	    DBUG_PRINT ("sect", (".uvblock bound to %#lx", shdr -> sh_addr));
	} else if (streq (sectname, RELOC (".kvsysseg"))) {
	    shdr -> sh_addr = 0x40040000;
	    DBUG_PRINT ("sect", (".kvsysseg bound to %#lx", shdr -> sh_addr));
	} else if (streq (sectname, RELOC (".kvsegmap"))) {
	    shdr -> sh_addr = 0x40440000;
	    DBUG_PRINT ("sect", (".kvsegmap bound to %#lx", shdr -> sh_addr));
	} else if (streq (sectname, RELOC (".kvsegu"))) {
	    shdr -> sh_addr = 0x48440000;
	    DBUG_PRINT ("sect", (".kvsegu bound to %#lx", shdr -> sh_addr));
	}
    }
    DBUG_RETURN (0);
}

static int relocsections (struct BootData *bip, struct globals *gbls)
{
    unsigned int sectnum;
    Elf32_Shdr *shdr;
    Elf32_Shdr *symtabhdr;
    Elf32_Shdr *relsechdr;

    DBUG_ENTER ("relocsections");
    for (sectnum = 0; sectnum < gbls -> Ehdr -> e_shnum; sectnum++) {
	shdr = gbls -> Shdrs + sectnum;
	DBUG_PRINT ("sectoff", ("sect @ offset %#lx", shdr -> sh_offset));
	if (shdr -> sh_type == SHT_RELA) {
	    DBUG_PRINT ("rela", ("found relocation section"));
	    symtabhdr = gbls -> Shdrs + shdr -> sh_link;
	    relsechdr = gbls -> Shdrs + shdr -> sh_info;
	    relocsection (gbls, shdr, relsechdr, symtabhdr);
	}	
    }
    DBUG_RETURN (0);
}

static void compactreloc (struct globals *gbls)
{
    Elf32_Shdr *relshdr;
    struct nrel *nrelp;
    unsigned int nrelcount;
    char *textbase;
    unsigned long relvalue;
    unsigned long offset;

    DBUG_ENTER ("compactreloc");
    relshdr = gbls -> Shdrs + 3;
    nrelcount = relshdr -> sh_size / relshdr -> sh_entsize;
    DBUG_PRINT ("nrelcount", ("found %d reloc entries", nrelcount));
    nrelp = (struct nrel *) (gbls -> elfdata + relshdr -> sh_offset);
    textbase = gbls -> elfdata + gbls -> thdr -> sh_offset;
    while (nrelcount-- != 0) {
	relvalue = nrelp -> nr_value;
	offset = nrelp -> nr_offset;
	if (!(offset & 0x80000000)) {
	    relvalue += gbls -> thdr -> sh_addr;
	}
	DBUG_PRINT ("rel", ("text[%x] = %x", offset & 0x7FFFFFFF, relvalue));
	*(long *)(textbase + (offset & 0x7FFFFFFF)) = relvalue;
	nrelp++;
    }
    DBUG_VOID_RETURN;
}

/*
 *	Note that if the ELF file is fully executable (all relocation
 *	already done, the first entry in the program header table is
 *	presumed to be the entry for the combined text and data segment.
 *	This is true with the current development tools and procedures,
 *	but is not required by the ELF format.  Should be fixed.
 */

int rel (char *elfdata, Elf32_Addr vaddr, struct BootData *bip)
{
    struct globals gblvars;
    Elf32_Shdr *shdr;
    int rtnval = 1;

    DBUG_ENTER ("rel");
    DBUG_PRINT ("vaddr", ("text starts at %#lx", vaddr));
    gblvars.Ehdr = (Elf32_Ehdr *) elfdata;
    if (!streqn ((char *) (gblvars.Ehdr -> e_ident), RELOC (ELFMAG), SELFMAG)) {
	DBUG_PRINT ("elf", ("found a non-ELF format file"));
	COMPLAIN ("found a non-ELF format file\n", 0);
	rtnval = 0;	
    } else if (gblvars.Ehdr -> e_type == ET_EXEC) {
	DBUG_PRINT ("elf", ("found an executable, fully linked file"));
	gblvars.Phdrs = (Elf32_Phdr *) (elfdata + gblvars.Ehdr -> e_phoff);
	bip -> bd_entry = gblvars.Ehdr -> e_entry;
	bip -> bd_tvaddr = gblvars.Phdrs -> p_vaddr;
	bip -> bd_toffset = gblvars.Phdrs -> p_offset;
	bip -> bd_tsize = gblvars.Phdrs -> p_filesz;
	bip -> bd_dvaddr = bip -> bd_tvaddr + bip -> bd_tsize;;
	bip -> bd_doffset = bip -> bd_toffset + bip -> bd_tsize;
	bip -> bd_dsize = 0;
    } else if (gblvars.Ehdr -> e_type == ET_LOPROC) {
	DBUG_PRINT ("elf", ("found an compact relocatable ELF file"));
	gblvars.elfdata = elfdata;
	gblvars.Shdrs = (Elf32_Shdr *) (gblvars.elfdata + gblvars.Ehdr -> e_shoff);
	gblvars.thdr = gblvars.Shdrs + 1;
	gblvars.thdr -> sh_addr = vaddr;
	bip -> bd_entry = vaddr;
	bip -> bd_tvaddr = vaddr;
	bip -> bd_toffset = gblvars.thdr -> sh_offset;
	bip -> bd_tsize = gblvars.thdr -> sh_size;
	bip -> bd_dvaddr = bip -> bd_tvaddr + bip -> bd_tsize;
	bip -> bd_doffset = bip -> bd_toffset + bip -> bd_tsize;
	bip -> bd_dsize = 0;
	compactreloc (&gblvars);
    } else if (gblvars.Ehdr -> e_type == ET_REL) {
	DBUG_PRINT ("elf", ("found a relocatable ELF file"));
	bip -> bd_entry = vaddr;
	gblvars.elfdata = elfdata;
	gblvars.Shdrs = (Elf32_Shdr *) (gblvars.elfdata + gblvars.Ehdr -> e_shoff);
	gblvars.thdr = NULL;
	gblvars.dhdr = NULL;
	gblvars.bhdr = NULL;
	gblvars.end = 0;
	findsections (bip, &gblvars);
	bindsections (bip, &gblvars);
	allocbss (&gblvars);
	specialsyms (&gblvars);
	relocsections (bip, &gblvars);
    } else {
	COMPLAIN ("ELF e_file type %d not handled\n", gblvars.Ehdr -> e_type);
	rtnval = 0;
    }
    DBUG_RETURN (rtnval);
}

