diff --git a/Makefile b/Makefile index 34bd67d..309c7b0 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,10 @@ OBJDIR := obj BINDIR := bin SRCDIR := rt -PYDIR := src -LDDIR := ld TESTDIR:= test +NASM ?= nasm + BITS ?= $(shell getconf LONG_BIT) # -mpreferred-stack-boundary=3 messes up the stack and kills SSE! @@ -23,58 +23,38 @@ CXXOPTFLAGS=$(COPTFLAGS) -fno-exceptions \ CFLAGS=-Wall -Wextra -Wpedantic -std=gnu11 -nostartfiles -fno-PIC $(COPTFLAGS) #-DUSE_DL_FINI CXXFLAGS=-Wall -Wextra -Wpedantic -std=c++11 $(CXXOPTFLAGS) -nostartfiles -fno-PIC -ASFLAGS=-I $(SRCDIR)/ -LDFLAGS_ := -ifeq ($(BITS),32) -# I think prescott is basically nocona but 32-bit only, althought I'm not sure -# if this one is optimal -CFLAGS += -m32 -march=prescott -LDFLAGS += -m32 -ASFLAGS += -f elf32 -LDFLAGS_ := -m32 -else -# I've heard nocona gets slightly smaller binaries than core2 -CFLAGS += -m64 -march=nocona -LDFLAGS += -m64 -ASFLAGS += -f elf64 -LDFLAGS_ := -m64 -endif -LDFLAGS += -nostartfiles -nostdlib -LDFLAGS_ := $(LDFLAGS_) -T $(LDDIR)/link.ld -Wl,--oformat=binary $(LDFLAGS) - CFLAGS += -m$(BITS) $(shell pkg-config --cflags sdl2) CXXFLAGS += -m$(BITS) $(shell pkg-config --cflags sdl2) -LIBS=-lc +ifeq ($(BITS),32) +# I think prescott is basically nocona but 32-bit only, althought I'm not sure +# if this one is optimal +CFLAGS += -march=prescott +else +# I've heard nocona gets slightly smaller binaries than core2 +CFLAGS += -march=nocona +endif -SMOLFLAGS += -ASFLAGS += -DALIGN_STACK -DUSE_INTERP #-DUSE_DNLOAD_LOADER -#-DUSE_DNLOAD_LOADER #-DUSE_DT_DEBUG #-DUSE_DL_FINI #-DNO_START_ARG #-DUNSAFE_DYNAMIC +LIBS = $(filter-out -pthread,$(shell pkg-config --libs sdl2)) -lX11 -lc #-lGL + +PWD ?= . + +SMOLFLAGS = --smolrt "$(PWD)/rt" --smolld "$(PWD)/ld" \ + -falign-stack -fuse-interp \ + --verbose #--keeptmp +# -fuse-dnload-loader -fskip-zero-value -fuse-nx -fskip-entries -fuse-dt-debug +# -fuse-dl-fini -fno-start-arg -funsafe-dynamic -NASM ?= nasm PYTHON3 ?= python3 all: $(BINDIR)/hello-crt $(BINDIR)/sdl-crt $(BINDIR)/flag $(BINDIR)/hello-_start -LIBS += $(filter-out -pthread,$(shell pkg-config --libs sdl2)) -lX11 #-lGL - clean: @$(RM) -vrf $(OBJDIR) $(BINDIR) %/: @mkdir -vp "$@" -# TODO: handle this in a more graceful and future-proof way! -ifneq ($(findstring (GCC) 9,$(shell $(CC) --version)),) -INCLINKOPT := -flinker-output=nolto-rel -else -ifneq ($(findstring (GCC) 10,$(shell $(CC) --version)),) -INCLINKOPT := -flinker-output=nolto-rel -else -INCLINKOPT := -endif -endif - .SECONDARY: $(OBJDIR)/%.lto.o: $(SRCDIR)/%.c $(OBJDIR)/ @@ -87,26 +67,13 @@ $(OBJDIR)/%.o: $(SRCDIR)/%.c $(OBJDIR)/ $(OBJDIR)/%.o: $(TESTDIR)/%.c $(OBJDIR)/ $(CC) $(CFLAGS) -c "$<" -o "$@" -$(OBJDIR)/%.start.o: $(OBJDIR)/%.lto.o $(OBJDIR)/crt1.lto.o - $(CC) $(LDFLAGS) -r $(INCLINKOPT) -o "$@" $^ +$(BINDIR)/%: $(OBJDIR)/%.o $(BINDIR)/ + $(PYTHON3) ./smold.py $(SMOLFLAGS) $(LIBS) "$<" "$@" + $(PYTHON3) ./smoltrunc.py "$@" "$(OBJDIR)/$(notdir $@)" && mv "$(OBJDIR)/$(notdir $@)" "$@" && chmod +x "$@" -$(OBJDIR)/symbols.%.asm: $(OBJDIR)/%.o - $(PYTHON3) $(PYDIR)/smol.py $(SMOLFLAGS) $(LIBS) "$<" "$@" - -$(OBJDIR)/stub.%.o: $(OBJDIR)/symbols.%.asm $(SRCDIR)/header32.asm \ - $(SRCDIR)/loader32.asm - $(NASM) $(ASFLAGS) $< -o $@ - -$(OBJDIR)/stub.%.start.o: $(OBJDIR)/symbols.%.start.asm $(SRCDIR)/header32.asm \ - $(SRCDIR)/loader32.asm - $(NASM) $(ASFLAGS) $< -o $@ - -$(BINDIR)/%: $(OBJDIR)/%.o $(OBJDIR)/stub.%.o $(BINDIR)/ - $(CC) -Wl,-Map=$(BINDIR)/$*.map $(LDFLAGS_) $(OBJDIR)/$*.o $(OBJDIR)/stub.$*.o -o "$@" - ./rmtrailzero.py "$@" "$(OBJDIR)/$(notdir $@)" && mv "$(OBJDIR)/$(notdir $@)" "$@" && chmod +x "$@" - -$(BINDIR)/%-crt: $(OBJDIR)/%.start.o $(OBJDIR)/stub.%.start.o $(BINDIR)/ - $(CC) -Wl,-Map=$(BINDIR)/$*-crt.map $(LDFLAGS_) $(OBJDIR)/$*.start.o $(OBJDIR)/stub.$*.start.o -o "$@" +$(BINDIR)/%-crt: $(OBJDIR)/%.lto.o $(OBJDIR)/crt1.lto.o $(BINDIR)/ + $(PYTHON3) ./smold.py $(SMOLFLAGS) --ldflags=-Wl,-Map=$(BINDIR)/$*-crt.map $(LIBS) "$<" $(OBJDIR)/crt1.lto.o "$@" + $(PYTHON3) ./smoltrunc.py "$@" "$(OBJDIR)/$(notdir $@)" && mv "$(OBJDIR)/$(notdir $@)" "$@" && chmod +x "$@" .PHONY: all clean diff --git a/README.md b/README.md index 17ecbb0..5998956 100644 --- a/README.md +++ b/README.md @@ -6,56 +6,33 @@ PoC by Shiz, bugfixing and 64-bit version by PoroCYon. ## Dependencies -* A C compiler (preferably GCC), GNU ld, binutils, GNU make, ... +* GCC (not clang, as the latter doesn't support `nolto-rel` output), GNU ld, + binutils, GNU make, ... * nasm 2.13 or newer -* scanelf from pax-utils +* `scanelf` from `pax-utils` * Python 3 ## Usage +***NOTE***: Your entrypoint (`_start`) ***must*** be in a section called +`.text.startup._start`! Otherwise, the linker script will fail silently, and +the smol startup/symbol resolving code will jump to an undefined location. + ```sh -./smol.py -lfoo -lbar input.o... smol-output.asm -nasm -I src/ [-Doption ...] -o nasm-output.o smol-output.asm -ld -T ld/link.ld --oformat=binary -o output.elf nasm-output.o input.o... -# or cc -T ld/link.ld -Wl,--oformat=binary -o output.elf nasm-output.o input.o... +./smold.py --use_interp --align_stack [--opts...] -lfoo -lbar input.o... output.elf ``` -* `USE_INTERP`: Include an interp segment in the output ELF file. If not, the - dynamic linker **must** be invoked *explicitely*! (You probably want to - enable this.) Costs the size of a phdr plus the size of the interp string. -* `ALIGN_STACK`: *64-bit only*: realign the stack so that SSE instructions - won't segfault. Costs 1 byte. -* `USE_NX`: Don't use `RWE` segments at all. Not very well tested. Costs the - size of 1 phdr, plus some extra stuff on `i386`. Don't forget to pass `-n` - to `smol.py` as well. -* `USE_DL_FINI`: keep track of the `_dl_fini` function and pass it to your - `_start`. Costs 2 bytes, plus maybe a few more depending on how it's passed - to `__libc_start_main`. -* `USE_DT_DEBUG`: retrieve the `struct link_map` from the `r_debug` linker - data (which is placed at `DT_DEBUG` at startup) instead of exploiting data - leakage from `_dt_start_user`. Might be more compatible and compressable, but - strictly worse size-wise by 10 (i386) or 3 (x86_64) bytes. -* `SKIP_ENTRIES`: skip the first two entries of the `struct link_map`, which - represent the main binary and the vDSO. Costs around 5 bytes. -* `USE_DNLOAD_LOADER`: use the symbol loading mechanism as used in dnload (i.e. - traverse the symtab of the imported libraries). Slightly larger, but probably - better compressable and more compatible with other libcs and future versions - of glibc. -* `NO_START_ARG`: *don't* pass the stack pointer to `_start` as the first arg. - Will make it unable to read argc/argv/environ, but gives you 3 bytes. -* `SKIP_ZERO_VALUE`: skip a `Sym` with a `st_value` field of `0`. If this isn't - enabled, weak symbols etc. might be imported instead of the real ones, - causing breakage. Many libraries don't have weak symbols at all, though. - Costs 4 (i386) or 5 (x86_64) bytes. - ``` -usage: smol.py [-h] [-m TARGET] [-l LIB] [-L DIR] [--nasm NASM] [--cc CC] - [--scanelf SCANELF] [--readelf READELF] - input [input ...] output +usage: smold.py [-h] [-m TARGET] [-l LIB] [-L DIR] [-s] [-n] [-d] [-fuse-interp] [-falign-stack] [-fuse-nx] + [-fuse-dnload-loader] [-fskip-zero-value] [-fuse-dt-debug] [-fuse-dl-fini] [-fskip-entries] + [-fno-start-arg] [-funsafe-dynamic] [--nasm NASM] [--cc CC] [--scanelf SCANELF] [--readelf READELF] + [--cflags CFLAGS] [--asflags ASFLAGS] [--ldflags LDFLAGS] [--smolrt SMOLRT] [--smolld SMOLLD] + [--verbose] [--keeptmp] + input [input ...] output positional arguments: input input object file - output output nasm file + output output binary optional arguments: -h, --help show this help message and exit @@ -64,14 +41,45 @@ optional arguments: -l LIB, --library LIB libraries to link against -L DIR, --libdir DIR directories to search libraries in + -s, --hash16 Use 16-bit (BSD) hashes instead of 32-bit djb2 hashes. Implies -fuse-dnload-loader + -n, --nx Use NX (i.e. don't use RWE pages). Costs the size of one phdr, plus some extra bytes on + i386. + -d, --det Make the order of imports deterministic (default: just use whatever binutils throws at us) + -fuse-interp Include a program interpreter header (PT_INTERP). If not enabled, ld.so has to be invoked + manually by the end user. + -falign-stack Align the stack before running user code (_start). If not enabled, this has to be done + manually. Costs 1 byte. + -fuse-nx Don't use one big RWE segment, but use separate RW and RE ones. Use this to keep strict + kernels (PaX/grsec) happy. Costs at least the size of one program header entry. + -fuse-dnload-loader Use a dnload-style loader for resolving symbols, which doesn't depend on + nonstandard/undocumented ELF and ld.so features, but is slightly larger. If not enabled, a + smaller custom loader is used which assumes glibc. + -fskip-zero-value Skip an ELF symbol with a zero address (a weak symbol) when parsing libraries at runtime. + Try enabling this if you're experiencing sudden breakage. However, many libraries don't use + weak symbols, so this doesn't often pose a problem. Costs ~5 bytes. + -fuse-dt-debug Use the DT_DEBUG Dyn header to access the link_map, which doesn't depend on + nonstandard/undocumented ELF and ld.so features. If not enabled, the link_map is accessed + using data leaked to the entrypoint by ld.so, which assumes glibc. Costs ~10 bytes. + -fuse-dl-fini Pass _dl_fini to the user entrypoint, which should be done to properly comply with all + standards, but is very often not needed at all. Costs 2 bytes. + -fskip-entries Skip the first two entries in the link map (resp. ld.so and the vDSO). Speeds up symbol + resolving, but costs ~5 bytes. + -fno-start-arg Don't pass a pointer to argc/argv/envp to the entrypoint using the standard calling + convention. This means you need to read these yourself in assembly if you want to use them! + (envp is a preprequisite for X11, because it needs $DISPLAY.) Frees 3 bytes. + -funsafe-dynamic Don't end the ELF Dyn table with a DT_NULL entry. This might cause ld.so to interpret the + entire binary as the Dyn table, so only enable this if you're sure this won't break things! --nasm NASM which nasm binary to use - --cc CC which cc binary to use + --cc CC which cc binary to use (MUST BE GCC!) --scanelf SCANELF which scanelf binary to use --readelf READELF which readelf binary to use - -n, --nx Use NX (i.e. don't use RWE pages). Costs the size of - one phdr, plus some extra bytes on i386. Don't forget - to pass -DUSE_NX to the assembly loader as well! - + --cflags CFLAGS Flags to pass to the C compiler for the relinking step + --asflags ASFLAGS Flags to pass to the assembler when creating the ELF header and runtime startup code + --ldflags LDFLAGS Flags to pass to the linker for the final linking step + --smolrt SMOLRT Directory containing the smol runtime sources + --smolld SMOLLD Directory containing the smol linker scripts + --verbose Be verbose about what happens and which subcommands are invoked + --keeptmp Keep temp files (only useful for debugging) ``` A minimal crt (and `_start` funcion) are provided in case you want to use `main`. @@ -83,9 +91,6 @@ imported by a `smol`-ified binary. This can thus be used to detect user mistakes during dynamic linking. (Think of it as an equivalent of `ldd`, except that it also checks whether the imported functions are present as well.) -***NOTE***: `smoldd.py` currently doesn't support 64-bit binaries anymore, as -there's currently no (good) way of retrieving the symbol hash table anymore. - ## Internal workings `smol.py` inspects the input object files for needed library files and symbols. @@ -99,7 +104,7 @@ works for glibc): on both i386 and x86_64, the linker startup code (`_dl_start_user`) leaks the global `struct link_map` to the user code: on i386, a pointer to it is passed directly through `eax`: -```s +```asm # (eax, edx, ecx, esi) = (_dl_loaded, argc, argv, envp) movl _rtld_local@GOTOFF(%ebx), %eax ## [ boring stuff... ] diff --git a/smol/__init__.py b/smol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/smol/cnl.py b/smol/cnl.py new file mode 100644 index 0000000..9d962c8 --- /dev/null +++ b/smol/cnl.py @@ -0,0 +1,40 @@ + +import os.path +import subprocess +import sys + +from .parse import * +from .shared import eprintf + +def cc_relink_objs(verbose, cc_bin, arch, inputs, output, cflags): + archflag = '-m64' if arch == "x86_64" else '-m32' + + cctyp, ccver = get_cc_version(cc_bin) + assert cctyp == "gcc", "A GCC compiler is needed for relinking objects!" + relink_arg = "-flinker-output=rel" if ccver < (9,0) else "-flinker-output=nolto-rel" + + args = [cc_bin, archflag, '-nostartfiles', '-nostdlib', \ + '-r', relink_arg, '-o', output] + cflags + inputs + + if verbose: eprintf("cc: %s" % repr(args)) + subprocess.check_call(args, stdout=subprocess.DEVNULL) + +def nasm_assemble_elfhdr(verbose, nasm_bin, arch, rtdir, intbl, output, asflags): + if rtdir[-1] != '/': rtdir = rtdir + '/' + archflag = 'elf64' if arch == "x86_64" else 'elf32' + + args = [nasm_bin, '-I', rtdir, '-f', archflag] + asflags + [intbl, '-o', output] + + if verbose: eprintf("nasm: %s" % repr(args)) + subprocess.check_call(args, stdout=subprocess.DEVNULL) + +def ld_link_final(verbose, cc_bin, arch, lddir, inobjs, output, ldflags): + archflag = '-m64' if arch == "x86_64" else '-m32' + + args = [cc_bin, archflag, '-T', lddir+'/link.ld', \ + '-Wl,--oformat=binary', '-nostartfiles', '-nostdlib', \ + '-o', output] + inobjs + ldflags + + if verbose: eprintf("ld: %s" % repr(args)) + subprocess.check_call(args, stdout=subprocess.DEVNULL) + diff --git a/src/smolemit.py b/smol/emit.py similarity index 93% rename from src/smolemit.py rename to smol/emit.py index 9117253..55e7e40 100644 --- a/src/smolemit.py +++ b/smol/emit.py @@ -2,7 +2,7 @@ import sys from collections import OrderedDict -from smolshared import * +from .shared import * def sort_imports(libraries, hashfn): #eprintf("in: " + str(libraries)) @@ -34,9 +34,8 @@ def output_x86(libraries, nx, h16, outf, det): for sym, reloc in symrels: usedrelocs.add(reloc) if not(nx) and 'R_386_PC32' in usedrelocs and 'R_386_GOT32X' in usedrelocs: - eprintf("Using a mix of R_386_PC32 and R_386_GOT32X relocations! "+\ + error("Using a mix of R_386_PC32 and R_386_GOT32X relocations! "+\ "Please change a few C compiler flags and recompile your code.") - exit(1) use_jmp_bytes = not nx and 'R_386_PC32' in usedrelocs @@ -111,8 +110,7 @@ global {name} def output_amd64(libraries, nx, h16, outf, det): if h16: - eprintf("--hash16 not supported yet for x86_64 outputs.") - exit(1) + error("--hash16 not supported yet for x86_64 outputs.") if nx: outf.write('%define USE_NX 1\n') # if h16: outf.write('%define USE_HASH16 1\n') @@ -155,8 +153,7 @@ dynamic.end: for sym, reloc in symrels: if reloc not in ['R_X86_64_PLT32', 'R_X86_64_GOTPCRELX', \ 'R_X86_64_REX_GOTPCRELX', 'R_X86_64_GOTPCREL']: - eprintf('Relocation type ' + reloc + ' of symbol ' + sym + ' unsupported!') - sys.exit(1) + error('Relocation type ' + reloc + ' of symbol ' + sym + ' unsupported!') if reloc in ['R_X86_64_GOTPCRELX', 'R_X86_64_REX_GOTPCRELX', \ 'R_X86_64_GOTPCREL']: @@ -192,6 +189,5 @@ def output(arch, libraries, nx, h16, outf, det): if arch == 'i386': output_x86(libraries, nx, h16, outf, det) elif arch == 'x86_64': output_amd64(libraries, nx, h16, outf, det) else: - eprintf("E: cannot emit for arch '" + str(arch) + "'") - sys.exit(1) + error("E: cannot emit for arch '" + str(arch) + "'") diff --git a/src/hackyelf.py b/smol/hackyelf.py similarity index 100% rename from src/hackyelf.py rename to smol/hackyelf.py diff --git a/src/linkmap.py b/smol/linkmap.py similarity index 100% rename from src/linkmap.py rename to smol/linkmap.py diff --git a/src/smolparse.py b/smol/parse.py similarity index 68% rename from src/smolparse.py rename to smol/parse.py index bf58299..09ba4fd 100644 --- a/src/smolparse.py +++ b/smol/parse.py @@ -5,7 +5,7 @@ import subprocess import struct import sys -from smolshared import * +from .shared import * def decide_arch(inpfiles): archs=set({}) @@ -20,8 +20,7 @@ def decide_arch(inpfiles): archs.add(machnum) if len(archs) != 1: - eprintf("Input files have multiple architectures, can't link this...") - sys.exit(1) + error("Input files have multiple architectures, can't link this...") archn = list(archs)[0] @@ -48,24 +47,41 @@ def build_reloc_typ_table(reo): return relocs -def get_needed_syms(readelf_bin, inpfiles): - output = subprocess.check_output([readelf_bin, '-s', '-W']+inpfiles, +def has_lto_object(readelf_bin, files): + for x in files: + with open(x,'rb') as f: + if f.read(2) == b'BC': # LLVM bitcode! --> clang -flto + return True + + output = subprocess.check_output([readelf_bin, '-s', '-W'] + files, stderr=subprocess.DEVNULL) - outrel = subprocess.check_output([readelf_bin, '-r', '-W']+inpfiles, + + curfile = files[0] + for entry in output.decode('utf-8').splitlines(): + stuff = entry.split() + if len(stuff)<2: continue + if stuff[0] == "File:": curfile = stuff[1] + if "__gnu_lto_" in entry or ".gnu.lto" in entry: # assuming nobody uses a symbol called "__gnu_lto_" ... + return True + return False + +def get_needed_syms(readelf_bin, inpfile): + output = subprocess.check_output([readelf_bin, '-s', '-W',inpfile], + stderr=subprocess.DEVNULL) + outrel = subprocess.check_output([readelf_bin, '-r', '-W',inpfile], stderr=subprocess.DEVNULL) relocs = build_reloc_typ_table(outrel) - curfile = inpfiles[0] + curfile = inpfile syms=set({}) for entry in output.decode('utf-8').splitlines(): stuff = entry.split() if len(stuff)<2: continue if stuff[0] == "File:": curfile = stuff[1] if len(stuff)<8: continue - if stuff[7].startswith("__gnu_lto_"): # yikes, an LTO object - eprintf("{} is an LTO object file, can't use this!".format(curfile)) - exit(1) + #if stuff[7].startswith("__gnu_lto_"): # yikes, an LTO object + # error("{} is an LTO object file, can't use this!".format(curfile)) if stuff[4] == "GLOBAL" and stuff[6] == "UND" and len(stuff[7])>0 \ and stuff[7] in relocs: syms.add((stuff[7], relocs[stuff[7]])) @@ -105,6 +121,21 @@ def get_cc_paths(cc_bin): return paths +def get_cc_version(cc_bin): + bak = os.environ.copy() + os.environ['LANG'] = "C" # DON'T output localized search dirs! + output = subprocess.check_output([cc_bin, '--version'], + stderr=subprocess.DEVNULL) + os.environ = bak + + lines = output.decode('utf-8').splitlines() + if "Free Software Foundation" in lines[1]: # GCC + verstr = lines[0].split()[-1] + return ("gcc", tuple(map(int, verstr.split('.')))) + else: # assume clang + verstr = lines[0].split()[-1] + return ("clang", tuple(map(int, verstr.split('.')))) + def is_valid_elf(f): # Good Enough(tm) with open(f, 'rb') as ff: return ff.read(4) == b'\x7FELF' @@ -117,8 +148,7 @@ def find_lib(spaths, wanted): #for f in glob.glob(glob.escape(p) + '/lib' + wanted + '.a' ): return f #for f in glob.glob(glob.escape(p) + '/' + wanted + '.a' ): return f - eprintf("E: couldn't find library '" + wanted + "'.") - sys.exit(1) + error("E: couldn't find library '" + wanted + "'.") def find_libs(spaths, wanted): return map(lambda l: find_lib(spaths, l), wanted) diff --git a/src/smolshared.py b/smol/shared.py similarity index 84% rename from src/smolshared.py rename to smol/shared.py index 6882da1..90fefcb 100644 --- a/src/smolshared.py +++ b/smol/shared.py @@ -21,3 +21,7 @@ def hash_djb2(s): def eprintf(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) +def error(*args, **kwargs): + eprintf(*args, **kwargs) + sys.exit(1) + diff --git a/smold.py b/smold.py new file mode 100755 index 0000000..adb706f --- /dev/null +++ b/smold.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 + +import argparse +import glob +import itertools +import os, os.path +import shutil +import subprocess +import sys +import tempfile + +from smol.shared import * +from smol.parse import * +from smol.emit import * +from smol.cnl import * + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('-m', '--target', default='', \ + help='architecture to generate asm code for (default: auto)') + parser.add_argument('-l', '--library', default=[], metavar='LIB', action='append', \ + help='libraries to link against') + parser.add_argument('-L', '--libdir', default=[], metavar='DIR', action='append', \ + help="directories to search libraries in") + + parser.add_argument('-s', '--hash16', default=False, action='store_true', \ + help="Use 16-bit (BSD) hashes instead of 32-bit djb2 hashes. "+\ + "Implies -fuse-dnload-loader") + parser.add_argument('-n', '--nx', default=False, action='store_true', \ + help="Use NX (i.e. don't use RWE pages). Costs the size of one phdr, "+\ + "plus some extra bytes on i386.") + parser.add_argument('-d', '--det', default=False, action='store_true', \ + help="Make the order of imports deterministic (default: just use " + \ + "whatever binutils throws at us)") + + parser.add_argument('-fuse-interp', default=False, action='store_true', \ + help="Include a program interpreter header (PT_INTERP). If not " +\ + "enabled, ld.so has to be invoked manually by the end user.") + parser.add_argument('-falign-stack', default=False, action='store_true', \ + help="Align the stack before running user code (_start). If not " + \ + "enabled, this has to be done manually. Costs 1 byte.") + parser.add_argument('-fuse-nx', default=False, action='store_true', \ + help="Don't use one big RWE segment, but use separate RW and RE ones."+\ + " Use this to keep strict kernels (PaX/grsec) happy. Costs at "+\ + "least the size of one program header entry.") + parser.add_argument('-fuse-dnload-loader', default=False, action='store_true', \ + help="Use a dnload-style loader for resolving symbols, which doesn't "+\ + "depend on nonstandard/undocumented ELF and ld.so features, but "+\ + "is slightly larger. If not enabled, a smaller custom loader is "+\ + "used which assumes glibc.") + parser.add_argument('-fskip-zero-value', default=False, action='store_true', \ + help="Skip an ELF symbol with a zero address (a weak symbol) when "+\ + "parsing libraries at runtime. Try enabling this if you're "+\ + "experiencing sudden breakage. However, many libraries don't use "+\ + "weak symbols, so this doesn't often pose a problem. Costs ~5 bytes.") + parser.add_argument('-fuse-dt-debug', default=False, action='store_true', \ + help="Use the DT_DEBUG Dyn header to access the link_map, which doesn't"+\ + " depend on nonstandard/undocumented ELF and ld.so features. If "+\ + "not enabled, the link_map is accessed using data leaked to the "+\ + "entrypoint by ld.so, which assumes glibc. Costs ~10 bytes.") + parser.add_argument('-fuse-dl-fini', default=False, action='store_true', \ + help="Pass _dl_fini to the user entrypoint, which should be done to "+\ + "properly comply with all standards, but is very often not "+\ + "needed at all. Costs 2 bytes.") + parser.add_argument('-fskip-entries', default=False, action='store_true', \ + help="Skip the first two entries in the link map (resp. ld.so and "+\ + "the vDSO). Speeds up symbol resolving, but costs ~5 bytes.") + parser.add_argument('-fno-start-arg', default=False, action='store_true', \ + help="Don't pass a pointer to argc/argv/envp to the entrypoint using "+\ + "the standard calling convention. This means you need to read "+\ + "these yourself in assembly if you want to use them! (envp is "+\ + "a preprequisite for X11, because it needs $DISPLAY.) Frees 3 bytes.") + parser.add_argument('-funsafe-dynamic', default=False, action='store_true', \ + help="Don't end the ELF Dyn table with a DT_NULL entry. This might "+\ + "cause ld.so to interpret the entire binary as the Dyn table, "+\ + "so only enable this if you're sure this won't break things!") + + parser.add_argument('--nasm', default=os.getenv('NASM') or shutil.which('nasm'), \ + help="which nasm binary to use") + parser.add_argument('--cc', default=os.getenv('CC') or shutil.which('cc'), \ + help="which cc binary to use (MUST BE GCC!)") + parser.add_argument('--scanelf', default=os.getenv('SCANELF') or shutil.which('scanelf'), \ + help="which scanelf binary to use") + parser.add_argument('--readelf', default=os.getenv('READELF') or shutil.which('readelf'), \ + help="which readelf binary to use") + + parser.add_argument('--cflags', default=[], metavar='CFLAGS', action='append', + help="Flags to pass to the C compiler for the relinking step") + parser.add_argument('--asflags', default=[], metavar='ASFLAGS', action='append', + help="Flags to pass to the assembler when creating the ELF header and runtime startup code") + parser.add_argument('--ldflags', default=[], metavar='LDFLAGS', action='append', + help="Flags to pass to the linker for the final linking step") + parser.add_argument('--smolrt', default=os.getcwd()+"/rt", + help="Directory containing the smol runtime sources") + parser.add_argument('--smolld', default=os.getcwd()+"/ld", + help="Directory containing the smol linker scripts") + + parser.add_argument('--verbose', default=False, action='store_true', \ + help="Be verbose about what happens and which subcommands are invoked") + parser.add_argument('--keeptmp', default=False, action='store_true', \ + help="Keep temp files (only useful for debugging)") + + parser.add_argument('input', nargs='+', help="input object file") + parser.add_argument('output', type=str, help="output binary") + + args = parser.parse_args() + + if args.hash16: + args.fuse_dnload_loader = True + + if args.fskip_zero_value: args.asflags.insert(0, "-DSKIP_ZERO_VALUE") + if args.fuse_nx: args.asflags.insert(0, "-DUSE_NX") + if args.fskip_entries: args.asflags.insert(0, "-DSKIP_ENTRIES") + if args.funsafe_dynamic: args.asflags.insert(0, "-DUNSAFE_DYNAMIC") + if args.fno_start_arg: args.asflags.insert(0, "-DNO_START_ARG") + if args.fuse_dl_fini: args.asflags.insert(0, "-DUSE_DL_FINI") + if args.fuse_dt_debug: args.asflags.insert(0, "-DUSE_DT_DEBUG") + if args.fuse_dnload_loader: args.asflags.insert(0, "-DUSE_DNLOAD_LOADER") + if args.fuse_interp: args.asflags.insert(0, "-DUSE_INTERP") + if args.falign_stack: args.asflags.insert(0, "-DALIGN_STACK") + + for x in ['nasm','cc','scanelf','readelf']: + val = args.__dict__[x] + if val is None or not os.path.isfile(val): + error("'" + x + "' binary" + (" " if val is None + else " ('" + val + "')") + " not found") + + arch = args.target.tolower() if len(args.target) != 0 else decide_arch(args.input) + if arch not in archmagic: + error("Unknown/unsupported architecture '" + str(arch) + "'") + if args.verbose: eprintf("arch: %s" % arch) + + objinput = None + objinputistemp = False + tmp_asm_file = tempfile.mkstemp(prefix='smoltab',suffix='.asm',text=True) + tmp_asm_fd = tmp_asm_file[0] + tmp_asm_file = tmp_asm_file[1] + tmp_elf_file = tempfile.mkstemp(prefix='smolout',suffix='.o') + os.close(tmp_elf_file[0]) + tmp_elf_file = tmp_elf_file[1] + try: + # if >1 input OR input is LTO object: + if len(args.input) > 1 or has_lto_object(args.readelf, args.input): + fd, objinput = tempfile.mkstemp(prefix='smolin',suffix='.o') + os.close(fd) + cc_relink_objs(args.verbose, args.cc, arch, args.input, objinput, args.cflags) + else: objinput = args.input[0] + + # generate smol hashtab + cc_paths = get_cc_paths(args.cc) + syms = get_needed_syms(args.readelf, objinput) + spaths = args.libdir + cc_paths['libraries'] + libraries = cc_paths['libraries'] + libs = list(find_libs(spaths, args.library)) + if args.verbose: eprintf("libs = " + str(libs)) + symbols = {} + for symbol, reloc in syms: + library = find_symbol(args.scanelf, libs, args.library, symbol) + if not library: + error("could not find symbol: {}".format(symbol)) + symbols.setdefault(library, []) + symbols[library].append((symbol, reloc)) + + with os.fdopen(tmp_asm_fd, mode='w') as taf: + output(arch, symbols, args.nx, args.hash16, taf, args.det) + if args.verbose: + eprintf("wrote symtab to %s" % tmp_asm_file) + + # assemble hash table/ELF header + nasm_assemble_elfhdr(args.verbose, args.nasm, arch, args.smolrt, + tmp_asm_file, tmp_elf_file, args.asflags) + + # link with LD into the final executable, w/ special linker script + ld_link_final(args.verbose, args.cc, arch, args.smolld, [objinput, tmp_elf_file], + args.output, args.ldflags) + finally: + if not args.keeptmp: + if objinputistemp: os.remove(objinput) + os.remove(tmp_asm_file) + os.remove(tmp_elf_file) + +if __name__ == '__main__': + rv = main() + if rv is None: pass + else: + try: sys.exit(int(rv)) + except: sys.exit(1) + diff --git a/src/smoldd.py b/smoldd.py similarity index 95% rename from src/smoldd.py rename to smoldd.py index 03746e4..c68a72c 100755 --- a/src/smoldd.py +++ b/smoldd.py @@ -3,9 +3,10 @@ import os.path, struct, sys import argparse, glob, shutil, subprocess -import hackyelf, linkmap -from smolshared import * -from smolparse import * +import smol.hackyelf as hackyelf +import smol.linkmap as linkmap +from smol.shared import * +from smol.parse import * def readbyte(blob, off): return struct.unpack(' in-place! parser.add_argument('input', type=argparse.FileType('rb'), \ help="input file to truncate") parser.add_argument('output', type=argparse.FileType('wb'), \ diff --git a/src/smol.py b/src/smol.py deleted file mode 100755 index efb9648..0000000 --- a/src/smol.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import glob -import itertools -import os.path -import shutil -import subprocess -import sys - -from smolshared import * -from smolparse import * -from smolemit import * - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument('-m', '--target', default='', \ - help='architecture to generate asm code for (default: auto)') - parser.add_argument('-l', '--library', metavar='LIB', action='append', \ - help='libraries to link against') - parser.add_argument('-L', '--libdir', metavar='DIR', action='append', \ - help="directories to search libraries in") - - parser.add_argument('--nasm', default=shutil.which('nasm'), \ - help="which nasm binary to use") - parser.add_argument('--cc', default=shutil.which('cc'), \ - help="which cc binary to use") - parser.add_argument('--scanelf', default=shutil.which('scanelf'), \ - help="which scanelf binary to use") - parser.add_argument('--readelf', default=shutil.which('readelf'), \ - help="which readelf binary to use") - - parser.add_argument('-s', '--hash16', default=False, action='store_true', \ - help="Use 16-bit (BSD) hashes instead of 32-bit djb2 hashes. "\ - +"Must be used with -DUSE_DNLOAD_LOADER") - parser.add_argument('-n', '--nx', default=False, action='store_true', \ - help="Use NX (i.e. don't use RWE pages). Costs the size of one phdr, "\ - +"plus some extra bytes on i386.") - parser.add_argument('-d', '--det', default=False, action='store_true', \ - help="Make the order of imports deterministic (default: just use on "+\ - "whatever binutils throws at us)") - - parser.add_argument('input', nargs='+', help="input object file") - parser.add_argument('output', type=argparse.FileType('w'), \ - help="output nasm file", default=sys.stdout) - - args = parser.parse_args() - - for x in ['nasm','cc','scanelf','readelf']: - val = args.__dict__[x] - if val is None or not os.path.isfile(val): - eprintf("'" + x + "' binary" + (" " if val is None - else " ('" + val + "')") + " not found") - sys.exit(1) - - if args.libdir is None: args.libdir = [] - arch = args.target.tolower() if len(args.target)!=0 \ - else decide_arch(args.input) - if arch not in archmagic: - eprintf("Unknown architecture '" + str(arch) + "'") - sys.exit(1) - - syms = get_needed_syms(args.readelf, args.input) - - paths = get_cc_paths(args.cc) - - spaths = args.libdir + paths['libraries'] - libraries=paths['libraries'] - libnames = args.library - libs = list(find_libs(spaths, libnames)) - symbols = {} - #print("libs = " + str(libs)) - - for symbol, reloc in syms: - library = find_symbol(args.scanelf, libs, libnames, symbol) - if not library: - eprintf("could not find symbol: {}".format(symbol)) - sys.exit(1) - symbols.setdefault(library, []) - symbols[library].append((symbol, reloc)) - - output(arch, symbols, args.nx, args.hash16, args.output, args.det) - -if __name__ == '__main__': - main() -