December 26th, 2024

A Simple ELF

The article examines the complexities of creating a simple Linux program, contrasting a standard C version with one using direct system calls, emphasizing that simplicity reduces complexity, not necessarily ease.

Read original articleLink Icon
CuriosityAppreciationConfusion
A Simple ELF

The article discusses the complexities involved in creating a simple program in Linux, specifically focusing on the ELF (Executable and Linkable Format) structure. It begins with a seemingly straightforward C program that prints "Hello Simplicity!" but reveals the underlying complexity when examining the compiled output. The author highlights the numerous symbols and sections generated by the compiler, emphasizing that even simple tasks involve significant overhead due to standard libraries and initialization routines. To illustrate the concept of simplicity versus complexity, the author proposes a version of the program that eliminates reliance on the standard library by using system calls directly. This approach involves writing assembly code to handle output and process termination, thus providing a custom entry point. The article concludes that while the new program may not be easier to understand, it is indeed simpler in terms of dependencies and structure, aligning with the notion that simplicity is not synonymous with ease but rather the absence of unnecessary complexity.

- The article explores the complexities of creating a simple program in Linux.

- It contrasts a standard C program with a version that uses system calls directly.

- The author emphasizes that simplicity is about reducing complexity, not necessarily making things easier.

- The discussion includes an analysis of the ELF structure and the overhead introduced by standard libraries.

- The final program demonstrates a custom entry point and direct system calls, showcasing a simpler approach.

AI: What people are saying
The comments reflect a deep interest in low-level programming and system calls in Linux, with several users sharing their own experiences and projects. Common themes include:
  • Discussion of minimal ELF file creation techniques and tools, with users sharing links to their projects.
  • References to alternative libraries and methods for writing C programs without standard libraries, emphasizing the simplicity of direct system calls.
  • Questions about the complexity and purpose of the article, with some commenters expressing confusion over the need for such low-level programming.
  • Insights into the Linux system call interface being more accessible compared to other operating systems.
  • Comments on the historical context of programming in assembly and the evolution of programming practices.
Link Icon 18 comments
By @Retr0id - 22 days
I 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/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/

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)

By @boricj - 22 days
The 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/...

[2] https://github.com/boricj/ghidra-delinker-extension/tree/mas...

By @jart - 23 days
I 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
By @matheusmoreira - 23 days
I 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

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.

By @jcalvinowens - 22 days
If you think this sort of thing is fun, you'll enjoy this: 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.

By @nils-m-holm - 22 days
My 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
By @sylware - 22 days
The 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.

By @akdas - 22 days
A while ago, I created an interactive explanation of the different parts of a minimal ELF file: 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.

By @ptspts - 22 days
For 32-bit x86 (i386 and i686), I've written a libc and a toolchain to.automate this: 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.

By @ryukoposting - 22 days
I keep a little book of "cursed things you can do with C." I'll definitely be adding "emojis in linker scripts." Good read.
By @compiler-guy - 22 days
If 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.
By @josephcsible - 22 days
The 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.
By @CaesarA - 22 days
I still don't understand how people were able to write software in the days when assembly was the only option for speedy execution.
By @ericyd - 22 days
I 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?
By @einpoklum - 22 days
1. 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?

By @EGreg - 23 days
An ELF, and almost in time for Christmas!
By @moonlion_eth - 22 days
Rich Hickey mentioned
By @quotemstr - 22 days
Christ, why couldn't PE have won?