5 ms·
A Simple ELF
- jart 2y agoI love articles like this. If you want to see a tutorial on how you can take this a step further, by creating a tiny ELF file that runs on Linux, FreeBSD, NetBSD, and OpenBSD 7.3 then check out https://justine.lol/sizetricks/#elf https://justine.lol/sizetricks/#elf
- matheusmoreira 2y agoI would also recommend the legendary Teensy Files: https://www.muppetlabs.com/~breadbox/software/tiny/ https://www.muppetlabs.com/~breadbox/software/tiny/ They sparked my interest in ELF and freestanding programs.
- cylinder714 2y agoAnd Chris Wellons' "A Magnetized Needle and a Steady Hand," detailing how to build an ELF implementation of 'true' using nothing more than 'echo' or 'printf': https://nullprogram.com/blog/2016/11/17/ https://nullprogram.com/blog/2016/11/17/
- matheusmoreira 2y agoHuge fan of that blog and its author!
- LegionMammal978 2y agoIf anyone's interested, last year I replicated this exercise for an x86-64 Linux executable [0], and also golfed a Hello World as small as I could. I ended up using a little-known pattern (an ET_DYN executable with no interpreter, normally only used for the ld.so binary) to shave off more bytes than anyone else who had tried it, to the best of my knowledge. [0] https://tmpout.sh/3/22.html https://tmpout.sh/3/22.html
- matheusmoreira 2y agoI would like to note that Linux is the only kernel which will allow you to do this! The Linux system call interface is stable and defined at the instruction set level. Linking against some system library is absolutely required on every other system. I've written an article about this idea: https://www.matheusmoreira.com/articles/linux-system-calls https://www.matheusmoreira.com/articles/linux-system-calls You can get incredibly far with just this. I wrote a freestanding lisp interpreter with nothing but Linux system calls. It turned into a little framework for freestanding Linux programs. It's been incredibly fun. Freestanding C is a much better language. A lot of legacy nonsense is in the standard library. The Linux system call interface is really nice to work with. Calling write is not that hard. It's the printf style string building and formatting that I sometimes miss.
- LegionMammal978 2y ago"Absolutely required" is some strong language. It's perfectly possible to, e.g., perform direct syscalls on Windows, and you'll occasionally see malware that does it to avoid certain forms of detection. You just have to switch on the OS version, and update your binary if you want it to be compatible with a newer version.
- matheusmoreira 2y agoI agree that it was too strong a claim. It's not supported by the developers and if you bypass their system libraries your program will break when they change things up. Linux kernel is known to be able to run binaries compiled in the 90s. Breaking user space makes Linus yell at people until the breakage gets reverted. A platform that stable is worth building on top of. Updating executables is a lot of work, sometimes it's straight up impossible.
- oguz-ismail 2y ago> Linking against some system library is absolutely required on every other system. Not on FreeBSD, NetBSD, OpenBSD or Solaris. The article you linked says this but it's not true: > Sometimes it's not even possible to use system calls at all. OpenBSD has implemented system call origin verification, a security mechanism that only allows system calls originating from the system's libc. So not only is the kernel ABI unstable, normal programs are not even allowed to interface with the kernel at all. You can still make system calls from normal programs, you just need to list the addresses of system call instructions in an ELF section named openbsd.syscalls.
- EGreg 2y agoAn ELF, and almost in time for Christmas!
- compiler-guy 2y agoIf one properly specifies the input, output, and clobber constraints to the asm statement, there is no need for the volatile keyword in any of this.
- jcalvinowens 2y agoI don't think that's correct for the sys_exit() call with no outputs: the compiler doesn't know the syscall instruction has side effects, I think it would be within its rights to omit that asm statement without volatile. Adding an output and code to consume the result seems like a waste of space in .text, it doesn't return. It reminds me of a funny little bug in ARM Linux, fixed by adding volatile to an asm statement: https://lore.kernel.org/lkml/92a00580828a1bdf96e7e36545f6d229809af04f.1722154575.git.calvin@wbinvd.org/ https://lore.kernel.org/lkml/92a00580828a1bdf96e7e36545f6d22...
- jcalvinowens 2y agoWell, neither have outputs, doh, so they both need volatile don't they? Adding an output for the %rax result would prevent the call from being omitted without volatile (assuming it is actually consumed by something), but it could still be reordered, right? I suppose with general syscalls that might be okay, but certainly not with sys_exit(). They also need memory clobbers, but I don't think memory clobbers would necessarily prevent reordering? In the case of the ARM bug though, it did: https://lore.kernel.org/lkml/Zqa4SAyPKPuaXdgg@mozart.vkv.me/ https://lore.kernel.org/lkml/Zqa4SAyPKPuaXdgg@mozart.vkv.me/
- boricj 2y agoThe Linux kernel source tree has nolibc [1], a header-only C standard library implementation that is about as barebones and paper-thin as it gets and is the next step up from a pure freestanding environment as shown in this article. I've used it to create a tiny but working program that prints out the ASCII table [2] as part of my Ghidra extension test suite. [1] https://github.com/torvalds/linux/tree/master/tools/include/nolibc https://github.com/torvalds/linux/tree/master/tools/include/... [2] https://github.com/boricj/ghidra-delinker-extension/tree/master/src/test/resources/ascii-table https://github.com/boricj/ghidra-delinker-extension/tree/mas...
- perching_aix 2y ago> from a pure freestanding environment as shown in this article Isn't a freestanding enviroment one without an OS? The author in the article explicitly codes against Linux syscalls and is creating an ELF file (so a hosted executable).
- saurik 2y agoI think of "freestanding" as being related to the "-ffreestanding" flag of modern compilers, which merely means something similar to "don't assume that functions have their usual C standard definitions, as I don't have a normal libc".
- perching_aix 2y agoI looked up the GCC docs, and it says both that and what I said. Bit confusing, but makes sense in hindsight.
- Retr0id 2y agoI haven't done a proper write-up yet but this is my current technique for emitting minimal ELF files written in freestanding C: 1. hand-written minimal ELF headers, with enough asm to do `_exit(main(argc, argv))`: https://github.com/DavidBuchanan314/kurl/blob/main/golfed/elf_entry.s https://github.com/DavidBuchanan314/kurl/blob/main/golfed/el... (currently only implemented for aarch64) 2. "Linux Syscall Support" library for conveniently making raw syscalls from C: https://chromium.googlesource.com/linux-syscall-support/ https://chromium.googlesource.com/linux-syscall-support/ 3. To avoid custom linker scripts (which I hate with a passion), I embed my hand-crafted ELF within a regular ELF, and slice it out at the end (using a python script). The "container" ELF is a regular full-fat ELF, potentially including working debug symbols, but the inner ELF has none of the cruft. Using this technique, I wrote a barely-functional TLS1.3 client that fits in ~3.5KB (see the rest of repo from the first link)
- almostgotcaught 2y ago> To avoid custom linker scripts (which I hate with a passion) lol why? i mean the syntax sucks but this seems like howling into the wind...
- Retr0id 2y agoFirstly, yes, the syntax sucks. But most of all it's a a moving target. Every so often the compiler will decide to emit some fancy new segment or other metadata, and your linker script won't know what to do with it, and you have to re-learn linker script syntax to fix it.
- almostgotcaught 2y ago-Wl,--orphan-handling=error but hey whatever floats your boat
- Retr0id 2y agoThat doesn't solve the problem, though. orphan-handling lets you choose between loud breakage and quiet breakage. I want no breakage!
- einpoklum 2y ago1. X86_64 assumed... 2. Why is it that exiting at the end of main() requires a system call? Wouldn't a `ret` instruction go "back" to somplace where the OS itself will do cleanup work?
- compiler-guy 2y agoNot without libc doing the glue work. A return instruction from main hands things back to libc which does some cleanup and then makes this same syscall.
- boricj 2y ago> Why is it that exiting at the end of main() requires a system call? Wouldn't a `ret` instruction go "back" to somplace where the OS itself will do cleanup work? Usually that's done by the C runtime library, but there isn't one there since this is a freestanding environment. Had the program not exited through a syscall (or entered an infinite loop), it would most likely crash after veering off the main() function.
- cesarb 2y ago> Why is it that exiting at the end of main() requires a system call? Wouldn't a `ret` instruction go "back" to somplace where the OS itself will do cleanup work? The only way for execution to cross the barrier between "user space" and "kernel space" is through a system call or an interrupt (we won't speak of call gates). Even if the OS had put an address on the stack, so that the "ret" would go there after returning from main(), the code there would still need to do a system call to go back to the OS. While nowadays Linux has a shared page of code mapped on every process (the vDSO), that wasn't the case in the past; all code on the "user space" side had to come from either the executable itself, or a library it loaded. Given that, it's natural that it was left to the executable to call the "exit" system call at the end.
- CaesarA 2y agoI still don't understand how people were able to write software in the days when assembly was the only option for speedy execution.
- 6SixTy 2y agoKeeping things pretty simple in project scope and hardware helps quite a lot
- throw-qqqqq 2y agoYou can define macros over the assembly to gain a high level language sort of similar to an untyped dialect of C. For me it would be sort of like writing programs in C versus higher level languages: much more tedious, will take longer and require better planning/upfront design, but doable. With practice you learn some tricks that can seem clever to anyone not writing a lot of asm. It’s “just” a very low level language IMO.
- akdas 2y agoA while ago, I created an interactive explanation of the different parts of a minimal ELF file: https://scratchpad.avikdas.com/elf-explanation/elf-explanation.html https://scratchpad.avikdas.com/elf-explanation/elf-explanati... I wrote this page for my own compiler that I'm working on, but I think it would be a good complement to this article. Note that the page is not that great on mobile, the extra real estate on desktop really helps.
- josephcsible 2y agoThe custom entry points look wrong to me. Aren't they breaking the rules over stack alignment when calling functions? Specifically, that rip is supposed to be congruent to 8 mod 16 at the beginning of a function, and supposed to be divisible by 16 right before a call instruction. The problem is that when code execution starts at the entry point, rip is divisible by 16, but by writing it as a C function, the compiler will assume it's off by 8 from what it actually is.
- oguz-ismail 2y agoDoes it matter unless you're reading a float from varargs? What else can it break?
- josephcsible 2y agoI don't know exactly what, but I know there is more than just that, because calling printf breaks with a misaligned stack even when you're not passing it any floating-point arguments. And even if it doesn't break anything for you today, you're basically committing UB by violating the compiler's assumptions.
- ptspts 2y agoAren't there GCC command-line flags to specify alignment assumptions?
- josephcsible 2y agoYes (see https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/x86-Options.html https://gcc.gnu.org/onlinedocs/gcc-14.2.0/gcc/x86-Options.ht...), but this article doesn't use them.
- saagarjha 2y agoCode that uses vector registers
- fsmv 2y ago
- quotemstr 2y agoChrist, why couldn't PE have won?
- boricj 2y agoAs in the Portable Executable file format? There are no tricks used in this article that rely on the specifics of ELF, unlike some more extreme examples [1] that abuse every trick in the book to shave off more bytes from executables. If anything, PE piggybacks on top of COFF which is a complete mess of a file format. I'm currently writing a standalone library for reading and writing toolchain file formats [2] (to replace some messy bespoke code in my Ghidra extension) and this under-specified, fragmented into multiple dialects, weirdly contorted relic is a pain to deal with. COFF was a stepping stone from a.out to ELF that should've lasted only a couple of years on Unix systems and somehow it managed to metastasize at a crucial point in time inside multiple software ecosystems, most notably Windows and indirectly .NET and UEFI through PE. Frankly, I'd ask instead why couldn't PE and COFF have lost. [1] https://nathanotterness.com/2021/10/tiny_elf_modernized.html https://nathanotterness.com/2021/10/tiny_elf_modernized.html [2] https://github.com/boricj/binary-file-toolkit https://github.com/boricj/binary-file-toolkit
- ptspts 2y agoFor 32-bit x86 (i386 and i686), I've written a libc and a toolchain to.automate this: https://github.com/pts/minilibc686 https://github.com/pts/minilibc686 . It can use mainstream free C compilers (GCC, Clang, OpenWatcom cc386, TinyCC and PCC) and assemblers (GNU as and NASM) out of the box. A printf-hello-world is about 1 KiB. A write-hello-world (syscalls only) is less than 200 bytes. Assembly programming skills not needed to use it.
- jcalvinowens 2y agoIf you think this sort of thing is fun, you'll enjoy this: https://github.com/jcalvinowens/asmhttpd/blob/master/asmhttpd.asm https://github.com/jcalvinowens/asmhttpd/blob/master/asmhttp... It's a webserver written in x86 assembler, which makes raw syscalls. It has no functions, and unmaps the stack so it uses only one 4KB page of memory at runtime.
- moonlion_eth 2y agoRich Hickey mentioned
- nils-m-holm 2y agoMy T3X/9 compiler generates ELF with no sections at all, there is just a code and data segment. A later version even gets rid of the data segment, but that is not ready for publication. http://t3x.org/t3x/index.html#t3x9 http://t3x.org/t3x/index.html#t3x9
- sylware 2y agoThe point: ELF is the issue. I did design my own runtime binary executable/dynamic library format which I do embed in an ELF capsule to be loaded by legacy systems. The thing I need to port though is the core user level drivers:vulkan/drm & alsa-lib. The main issue would be the alsa-lib since some part of its API still "requires" a C runtime (you have to call free() on some returned data). The issue with this "format": it is so much simple, I wonder if it would not be better if each software "dynamic library/user level system interface" should design its own minimal and giga simple "dynamic library" format, taylored for its semantics. Dunno yet. On modern hardware architecture, you load position independent memory segment (code and data). You should need its alignment requirement and you are good to go. Basically, a magic with the alignment, then a table of offsets or re-entrant code (possible on modern hardware architecture which supports try-lock hardware semantics) right after the "header". I chose to use the re-entrant code guarded with an hardware try-lock mechanism, because it is more generic and will be cleaner on the long run than a table of offsets. Bending the product of code generators (assemblers) into some runtime format was a good idea until most hardware architectures support a hardware try-lock mechanism, then it became really nasty legacy.
- ericyd 2y agoI must not be the target audience for this. What exactly is the purpose of this article? How to rewrite a simple C program in a complex combination of assembly and syscalls?
- oguz-ismail 2y ago> rewrite a simple C program in a complex combination of assembly and syscalls That'd be a good introduction to assembly for someone who already knows C well.
- ryukoposting 2y agoI keep a little book of "cursed things you can do with C." I'll definitely be adding "emojis in linker scripts." Good read.