import argparse
import logging
import os
import subprocess
import tempfile
from abc import ABC, abstractmethod
from pathlib import Path
import lief
from elftools.elf.elffile import ELFFile
nuttxroot = Path(__file__).parent.parent
def run_command(cmd: list[str], stdout=None):
try:
cmd_str = [str(c) for c in cmd]
logging.debug(f"Running command: {' '.join(cmd_str)}")
if stdout:
subprocess.run(
cmd, stdout=stdout, stderr=subprocess.PIPE, text=True, check=True
)
else:
subprocess.run(cmd, text=True, check=True)
except subprocess.CalledProcessError as e:
logging.error(f"Command failed with error: {e.stderr}")
raise RuntimeError(f"Command failed: {' '.join(cmd)}") from e
def elf_parse(elf):
config = lief.ELF.ParserConfig()
config.parse_notes = False
config.parse_relocations = False
config.parse_symtab_symbols = True
return lief.ELF.parse(str(elf), config)
class Toolchain(ABC):
@abstractmethod
def run_cpp(
self,
args,
flash_start: int,
ram_start: int,
heap_size: int,
extern_symbols,
out_ld: str,
flags: list[str] = [],
):
pass
@abstractmethod
def run_cc(self, args, in_src: str, out_obj: str):
pass
@abstractmethod
def run_ld(self, args, in_elf, in_ld, out_elf, gc_sections=True):
pass
@abstractmethod
def run_hex(self, args, in_elf, out_hex, extern):
pass
def get_phdr(self, args, elf):
phdr = " .phdr =\n"
phdr += " {\n"
for ph in elf.segments:
phdr += (
" {\n"
f" .p_type=0x{ph.type.value:x},\n"
f" .p_offset=0x{ph.file_offset:x},\n"
f" .p_vaddr=0x{ph.virtual_address:x},\n"
f" .p_paddr=0x{ph.physical_address:x},\n"
f" .p_filesz=0x{ph.physical_size:x},\n"
f" .p_memsz=0x{ph.virtual_size:x},\n"
f" .p_flags={ph.flags.value},\n"
f" .p_align=0x{ph.alignment:x},\n"
" },\n"
)
phdr += " },\n"
return phdr
class GnuToolchain(Toolchain):
def run_cpp(
self,
args,
flash_start: int,
ram_start: int,
heap_size: int,
extern_symbols,
out_ld: str,
flags: list[str] = [],
):
extern_symbols_str = ""
for name, addr in extern_symbols.items():
extern_symbols_str += f"{name} = 0x{addr:x};"
cmd = (
[
args.cc,
"-E",
"-P",
"-x",
"c",
str(nuttxroot / "libs" / "libc" / "elf" / "gnu-elf.ld.in"),
f"-DTEXT={hex(flash_start)}",
f"-DDATA={hex(ram_start)}",
f"-DEXTERN_SYMBOLS={extern_symbols_str}",
]
+ args.cflags
+ flags
)
if heap_size > 0:
cmd.append(f"-DHEAPSIZE={hex(heap_size)}")
with open(out_ld, "w") as f:
run_command(cmd, stdout=f)
def run_cc(self, args, in_src: str, out_obj: str):
cmd = [
args.cc,
"-c",
in_src,
"-o",
out_obj,
] + args.cflags
run_command(cmd)
def run_ld(self, args, in_elf, in_ld, out_elf, gc_sections=True):
nostart = []
if args.ld.endswith("cc"):
nostart = ["-nostartfiles", "-nostdlib"]
if gc_sections:
nostart.extend(["-Wl,--gc-sections"])
nostart.extend(args.cflags)
else:
nostart = ["--nostdlib"]
cmd = [
args.ld,
"-e",
"__start",
"-T",
str(in_ld),
str(in_elf),
"-o",
out_elf,
]
cmd.extend(nostart)
run_command(cmd)
def run_hex(self, args, in_elf, out_hex, extern=[]):
cmd = [args.objcopy, "-O", "ihex", in_elf, out_hex] + extern
run_command(cmd)
class TaskingToolchain(Toolchain):
def run_cpp(
self,
args,
flash_start: int,
ram_start: int,
heap_size: int,
extern_symbols: str,
out_ld: str,
flags: list[str] = [],
):
tmp_lsl = tempfile.NamedTemporaryFile(suffix=".cpp", mode="w")
tmp_out = tempfile.NamedTemporaryFile(suffix=".lsl", mode="w+")
fd = open(str(nuttxroot / "libs" / "libc" / "elf" / "tasking-elf.lsl"), "r")
tmp_lsl.write(fd.read())
tmp_lsl.flush()
fd.close()
extern_symbols_str = ""
for name, addr in extern_symbols.items():
extern_symbols_str += f'"{name}"=0x{addr:x};'
toolchain_lsl = args.cc.replace("bin/cctc", "include.lsl")
cmd = (
[
args.cc,
"-E",
"--preprocess=+noline",
tmp_lsl.name,
f"-DTEXT={hex(flash_start)}",
f"-DDATA={hex(ram_start)}",
f"-DEXTERN_SYMBOLS={extern_symbols_str}",
f"-I{toolchain_lsl}",
f"-o{tmp_out.name}",
]
+ args.cflags
+ flags
)
if heap_size > 0:
cmd.append(f"-DHEAPSIZE={heap_size}")
run_command(cmd)
tmp_lsl.close()
tmp_out.flush()
tmp_out.seek(0)
with open(out_ld, "w") as f:
for line in tmp_out:
if line.strip() == "" or "__builtin" in line:
continue
f.write(line)
def run_cc(self, args, in_src: str, out_obj: str):
cmd = [
args.cc,
"--create",
in_src,
"-o",
out_obj,
] + args.cflags
run_command(cmd)
def run_ld(self, args, in_elf, in_ld, out_elf, gc_sections=True):
tmp = tempfile.NamedTemporaryFile(suffix=".o", mode="wb")
with open(in_elf, "rb") as f:
tmp.write(f.read())
tmp.flush()
cmd = [
args.ld,
"--user-provided-initialization-code",
f"--lsl-file={in_ld}",
tmp.name,
"-o",
out_elf,
]
run_command(cmd)
tmp.close()
Path(str(out_elf).split(".")[0] + ".mdf").unlink(missing_ok=True)
def run_hex(self, args, in_elf, out_hex, extern=[]):
cmd = [args.objcopy, "-O", "ihex", in_elf, out_hex] + extern
run_command(cmd)
def get_phdr(self, args, elf):
sections = {}
entries = []
bracket_groups = {}
for section in elf.sections:
name = section.name
sections[name] = section
if not name or not name.startswith("["):
continue
inner = name.strip("[]")
if inner.startswith("."):
parts = inner.split(".")
base = "." + parts[1] if len(parts) > 1 else inner
else:
parts = inner.split(".")
base = parts[0]
bracket_groups.setdefault(base, []).append(section)
for section in elf.sections:
name = section.name
if not name:
continue
if not (section.flags & int(lief.ELF.Section.FLAGS.ALLOC)):
continue
if name in bracket_groups:
continue
if name.startswith("["):
continue
p_vaddr = int(section.virtual_address)
p_paddr = int(section.virtual_address)
p_offset = (
int(section.file_offset)
if section.type != lief.ELF.Section.TYPE.NOBITS
else 0
)
p_filesz = (
int(section.size) if section.type != lief.ELF.Section.TYPE.NOBITS else 0
)
p_memsz = int(section.size)
p_flags = 0
p_flags |= 4
if section.flags & int(lief.ELF.Section.FLAGS.WRITE):
p_flags |= 2
if section.flags & int(lief.ELF.Section.FLAGS.EXECINSTR):
p_flags |= 1
p_alignment = int(section.alignment) or 1
entries.append(
{
"p_vaddr": p_vaddr,
"p_paddr": p_paddr,
"p_offset": p_offset,
"p_filesz": p_filesz,
"p_memsz": p_memsz,
"p_flags": p_flags,
"p_alignment": p_alignment,
}
)
for base, src_sections in bracket_groups.items():
target = sections.get(base)
min_src_vaddr = min(int(s.virtual_address) for s in src_sections)
base_vaddr = (
int(target.virtual_address) if target is not None else min_src_vaddr
)
for s in sorted(src_sections, key=lambda x: int(x.virtual_address)):
offset_within_src = int(s.virtual_address) - min_src_vaddr
p_vaddr = base_vaddr + offset_within_src
p_paddr = int(s.virtual_address)
p_offset = (
int(s.file_offset)
if s.file_offset is not None
and s.type != lief.ELF.Section.TYPE.NOBITS
else 0
)
p_filesz = int(s.size) if s.type != lief.ELF.Section.TYPE.NOBITS else 0
p_memsz = int(s.size)
p_flags = 4 | 2
p_alignment = int(s.alignment) or 1
entries.append(
{
"p_vaddr": p_vaddr,
"p_paddr": p_paddr,
"p_offset": p_offset,
"p_filesz": p_filesz,
"p_memsz": p_memsz,
"p_flags": p_flags,
"p_alignment": p_alignment,
}
)
entries.sort(key=lambda e: (e["p_vaddr"], e["p_paddr"]))
merged = []
for e in entries:
if not merged:
merged.append(e.copy())
continue
last = merged[-1]
last_v_end = last["p_vaddr"] + last["p_memsz"]
last_p_end = last["p_paddr"] + last["p_filesz"]
if (
e["p_flags"] == last["p_flags"]
and e["p_vaddr"] == last_v_end
and e["p_paddr"] == last_p_end
):
last["p_filesz"] += e["p_filesz"]
last["p_memsz"] += e["p_memsz"]
last["p_alignment"] = max(last["p_alignment"], e["p_alignment"])
else:
merged.append(e.copy())
phdr = " .phdr =\n"
phdr += " {\n"
for m in merged:
phdr += (
" {\n"
f" .p_type=0x1,\n"
f" .p_offset=0x{m['p_offset']:x},\n"
f" .p_vaddr=0x{m['p_vaddr']:x},\n"
f" .p_paddr=0x{m['p_paddr']:x},\n"
f" .p_filesz=0x{m['p_filesz']:x},\n"
f" .p_memsz=0x{m['p_memsz']:x},\n"
f" .p_flags={m['p_flags']},\n"
f" .p_align=0x{m['p_alignment']:x},\n"
" },\n"
)
phdr += " },\n"
return phdr
def get_elf_fixup_s_size(elf_path: Path) -> int:
elf = ELFFile(open(elf_path, "rb"))
debug_info = elf.get_dwarf_info()
for cu in debug_info.iter_CUs():
for die in cu.iter_DIEs():
if die.tag == "DW_TAG_structure_type":
name = die.attributes.get("DW_AT_name")
if name and name.value.decode("utf-8") == "elf_fixup_s":
return die.attributes["DW_AT_byte_size"].value
return 0
def setup_logging(verbose: bool):
logging.basicConfig(
level=logging.DEBUG if verbose else logging.ERROR,
format="[%(relativeCreated)6dms] - %(levelname)s - %(lineno)d %(message)s",
)
def align_up(value: int, alignment: int) -> int:
if alignment == 0:
return value
return (value + alignment - 1) // alignment * alignment
def align_down(value: int, alignment: int) -> int:
if alignment == 0:
return value
return value // alignment * alignment
def calculate_elf_size(
args,
in_elf: Path,
):
flash_used = 0
flash_aligned = 0
ram_used = 0
ram_aligned = 0
elf = elf_parse(str(in_elf))
for section in elf.sections:
logging.debug(
f"Section: {section.name}, Size: {section.size}, Type: {section.type}, Flags: {section.flags}"
)
if section.flags & int(lief.ELF.Section.FLAGS.ALLOC):
if section.flags & int(lief.ELF.Section.FLAGS.WRITE):
if section.type != lief.ELF.Section.TYPE.NOBITS:
flash_used += align_up(section.size, section.alignment)
ram_used += align_up(section.size, section.alignment)
if ram_aligned < section.alignment:
ram_aligned = section.alignment
else:
flash_used += align_up(section.size, section.alignment)
if flash_aligned < section.alignment:
flash_aligned = section.alignment
return flash_used, flash_aligned, ram_used, ram_aligned
def generate_link_script(
args,
in_elf: Path,
nuttx_symbols,
flash_start: int,
ram_start: int,
out_ld: str,
extern_symbols=None,
heap_size=0,
):
elf = elf_parse(str(in_elf))
if not extern_symbols and nuttx_symbols:
extern_symbols = {}
for symbol in elf.symbols:
if symbol.name == "nx_heapsize":
heap_size = symbol.value
if symbol.shndx == 0 and symbol.name != "":
nuttx_symbol = next(
(s for s in nuttx_symbols if s.name == symbol.name), None
)
if not nuttx_symbol:
continue
extern_symbols[nuttx_symbol.name] = nuttx_symbol.value
args.toolchain.run_cpp(
args,
flash_start,
ram_start,
heap_size,
extern_symbols,
out_ld,
)
return extern_symbols, heap_size
def generate_elf_hex(args):
nuttx_symbols = elf_parse(args.elf).symbols
flash_base, flash_remaining = args.flash
ram_base, ram_remaining = args.ram
for elf_file in sorted(args.indir.rglob("*")):
if not elf_file.is_file():
continue
logging.debug(f"Processing ELF file: {elf_file}")
ld_file = args.outdir / "ld" / f"{elf_file.stem}.ld"
extern_symbols, heap_size = generate_link_script(
args,
elf_file,
nuttx_symbols,
0,
0x80000000,
ld_file,
)
elf_out = str(args.outdir / "elf" / os.path.basename(elf_file))
args.toolchain.run_ld(args, elf_file, ld_file, elf_out)
flash_used, flash_aligned, ram_used, ram_aligned = calculate_elf_size(
args, elf_out
)
flash_remaining -= flash_used
ram_remaining -= ram_used
real_flash_start = align_down(flash_base + flash_remaining, flash_aligned)
real_ram_start = align_down(ram_base + ram_remaining, ram_aligned)
generate_link_script(
args,
elf_file,
nuttx_symbols,
real_flash_start,
real_ram_start,
ld_file,
extern_symbols,
heap_size,
)
logging.info(f"{elf_file.name}: Linker script written to {ld_file}")
args.toolchain.run_ld(args, elf_file, ld_file, elf_out)
hex_out = str(args.outdir / "hex" / f"{elf_file.stem}.hex")
args.toolchain.run_hex(args, elf_out, hex_out)
logging.debug(
f"Remaining: FLASH=0x{flash_remaining:x}, RAM=0x{ram_remaining:x}"
)
print(
f"{elf_file.name} Current Flash used: 0x{flash_used:x}, RAM used: 0x{ram_used:x}"
)
args.flash = (flash_base, flash_remaining)
args.ram = (ram_base, ram_remaining)
def generate_fixup_src(args, in_dir, out_src: str):
elf_fixup = "#include <nuttx/binfmt/elf_fixup.h>\n"
elf_fixup += 'const struct elf_fixup_s g_elf_fixup[] locate_data(".rodata") = \n{\n'
elf_fixup += " {{0}}" + ",\n"
for elf_file in sorted(in_dir.rglob("*")):
elf = elf_parse(str(elf_file))
if elf is None:
logging.error(f"Failed to parse ELF file: {elf_file}")
continue
stacksize = ""
priority = ""
uid = ""
gid = ""
mode = ""
heap_size = ""
heap_start = ""
for symbol in elf.symbols:
if symbol.name == "nx_stacksize":
stacksize = f" .stacksize=0x{symbol.value:x},\n"
elif symbol.name == "nx_priority":
priority = f" .priority=0x{symbol.value:x},\n"
elif symbol.name == "nx_uid":
uid = f" .uid=0x{symbol.value:x},\n"
elif symbol.name == "nx_gid":
gid = f" .gid=0x{symbol.value:x},\n"
elif symbol.name == "nx_mode":
mode = f" .mode=0x{symbol.value:x},\n"
elif symbol.name == "nx_heapsize":
heap_size = f" .heapsize=0x{symbol.value:x},\n"
elif symbol.name == "_sheap":
heap_start = f" .heapstart=0x{symbol.value:x},\n"
if heap_size == "":
heap_start = ""
entry = elf.entrypoint if elf.entrypoint else 0
elf_fixup += (
"\n {\n"
f' .name="{elf_file.stem}",\n'
f" .entry={entry},\n"
f"{stacksize}"
f"{priority}"
f"{uid}"
f"{gid}"
f"{mode}"
f"{heap_start}"
f"{heap_size}"
f"{args.toolchain.get_phdr(args, elf)}"
" },\n"
)
elf_fixup += "};\n"
with open(out_src, "w") as f:
f.write(elf_fixup)
def generate_fixup_hex(args):
generate_fixup_src(args, args.outdir / "elf", args.outdir / "fixup" / "elf_fixup.c")
args.toolchain.run_cc(
args,
str(args.outdir / "fixup" / "elf_fixup.c"),
str(args.outdir / "fixup" / "elf_fixup.o"),
)
rodata_cmd = ["--change-section-address", f".rodata={args.fixup_addr}"]
args.toolchain.run_hex(
args,
str(args.outdir / "fixup" / "elf_fixup.o"),
str(args.outdir / "hex" / "elf_fixup.hex"),
extern=rodata_cmd,
)
def generate_merge_hex(args):
hex_cmd = ["srec_cat", "-o", args.output, "-Intel"]
args.toolchain.run_hex(args, args.elf, str(args.outdir / "hex" / f"{args.elf}.hex"))
for hex_file in sorted((args.outdir / "hex").rglob("*")):
hex_cmd.extend([str(hex_file), "-Intel"])
run_command(hex_cmd)
def parse_args():
parser = argparse.ArgumentParser(
description="Link the relocated ELF file into an executable file using the given memory region."
)
parser.add_argument(
"--flash_start",
type=str,
required=True,
help="Origin address of flash memory",
)
parser.add_argument(
"--flash_size",
type=str,
required=True,
help="Length of flash memory",
)
parser.add_argument(
"--ram_start", type=str, required=True, help="Origin address of RAM"
)
parser.add_argument("--ram_size", type=str, required=True, help="Length of RAM")
parser.add_argument(
"--elf", type=str, required=True, help="Export symbol ELF (nuttx ELF)"
)
parser.add_argument("--indir", type=str, required=True, help="ELF file directory")
parser.add_argument(
"--outdir", type=str, required=True, help="Output directory for generated files"
)
parser.add_argument(
"--output",
type=str,
required=True,
help="Final output hex file path, will merge all hex files",
)
parser.add_argument("--verbose", action="store_true", help="Enable verbose logging")
args = parser.parse_args()
setup_logging(args.verbose)
logging.debug(f"Arguments: {args}")
if not Path(args.indir).exists():
raise FileNotFoundError(f"Path does not exist: {args.indir}")
flash_end = int(args.flash_start, 0) + int(args.flash_size, 0)
count = 1
for _ in Path(args.indir).rglob("*"):
count = count + 1
args.fixup_size = count * get_elf_fixup_s_size(args.elf)
args.fixup_addr = flash_end - args.fixup_size
args.flash = int(args.flash_start, 0), int(args.flash_size, 0) - args.fixup_size
args.ram = int(args.ram_start, 0), int(args.ram_size, 0)
args.outdir = Path(args.outdir)
if not args.outdir.exists():
args.outdir.mkdir(parents=True, exist_ok=True)
(args.outdir / "fixup").mkdir(parents=True, exist_ok=True)
(args.outdir / "elf").mkdir(parents=True, exist_ok=True)
(args.outdir / "ld").mkdir(parents=True, exist_ok=True)
(args.outdir / "hex").mkdir(parents=True, exist_ok=True)
args.indir = Path(args.indir)
args.cc = os.environ.get("CC", "gcc")
args.objcopy = os.environ.get("OBJCOPY", "objcopy")
args.cflags = list(dict.fromkeys(os.environ.get("CFLAGS", "").split()))
args.ld = os.environ.get("LD", "ld")
elf = elf_parse(str(args.elf))
args.is_64bit = elf.header.identity_class == lief.ELF.Header.CLASS.ELF64
args.toolchain = GnuToolchain()
if "cctc" in args.cc:
args.toolchain = TaskingToolchain()
else:
args.toolchain = GnuToolchain()
return args
def run():
args = parse_args()
generate_elf_hex(args)
generate_fixup_hex(args)
generate_merge_hex(args)
print("Fixed ELF generation completed successfully.")
for elf_file in sorted((args.outdir / "elf").rglob("*")):
print(f"Generated ELF file: {elf_file.name}")
for elf_file in sorted((args.outdir / "hex").rglob("*")):
print(f"Generated Hex file: {elf_file.name}")
for elf_file in sorted((args.outdir / "ld").rglob("*")):
print(f"Generated ld script: {elf_file.name}")
if __name__ == "__main__":
run()