How Linux executes binaries: ELF and dynamic linking explained
Posted by Solid-Film-818@reddit | programming | View on Reddit | 58 comments
After 25 years working with Linux internals I wrote this article. It's a deep dive into how Linux executes binaries, focusing on ELF internals and dynamic linking. Covers GOT/PLT, relocations, and what actually happens at runtime (memory mappings, syscalls, dynamic loader).
Happy to discuss or clarify any part.
m-hilgendorf@reddit
One nit: the kernel doesn't load the loader/interpreter/dynamic linker, it just mmap's it. The loader loads itself. There's a tricky bit of code to do this, where the loader has to do its own relocations and initialization before it can do things like "write a global variable" and "call a function." You have to write that code carefully to avoid segfaults during startup (eg: you can't call a function from initializing the loader that hasn't been relocated yet). The loader is also the thing that provides implementations to
<dlfcn.h>, which is why you can'tdlopenfrom a statically linked executable - you don't have a loader.The other thing about the loader is that it's also libc. Most languages don't ship their own loader and rely on the platform's libc (basically musl or glibc), because if you want ffi with C libraries you also need to play nice with their loader.
Another interesting thing about ELF that it's "calling convention" (square quotes because idr if that's what it's called in the spec, but it's how a kernel "calls" start) is two registers, the stack and frame pointer. The stack pointer is obvious, top of the stack is argc followed by null terminated argv, followed by null terminated envp, followed by null terminated auxv. The frame pointer is almost always NULL because no one uses this, but technically, it's supposed to be a callback to some code that runs after exit. So if your program logically is the stuff between
main()is called andreturns, the loader is supposed to fill in the blanks about what happens before main (global ctors run, global state initialized, relocations handled, etc), and what happens after. On linux at least, I don't believe this is supported or even used in practice. But it's interesting to know about.simon_o@reddit
Does anyone have an idea how well setting
INTERPto a different interpreter that, hypothetically, works with libraries created from non-C language that contain concepts not present in "C".sofiles?m-hilgendorf@reddit
The kernel kinda does what it says on the box. It reads the program headers and mmaps the segments like you tell it to, if there's a PT_INTERP program header it looks there then mmap's that binary, sets up the executable stack and aux vector, then jumps to the interpreter's entrypoint. After that it's up to the interpreter to do whatever it needs to do.
But that said rather than extending ELF to fit your evil purposes, there's binfmt_misc and shebangs.
simon_o@reddit
Thanks!
Solid-Film-818@reddit (OP)
Thanks for your constructive feedback, I really appreciate it. Good catch, “loads’is definitely an oversimplification on my side. The kernel maps the interpreter and jumps to it and from there the loader has to bootstrap itself before rellocations are fully in place. That early init phase is pretty fascinating (and easy to get wrong).
m-hilgendorf@reddit
This is a cool paper to read/reference: https://grugq.github.io/docs/ul_exec.txt
Solid-Film-818@reddit (OP)
Directly to bookmarks 🙂
TankorSmash@reddit
Was this written using LLMs?
Solid-Film-818@reddit (OP)
I did use an LLM to fix the grammar in English and the storey telling, summarizing other articles and notes too. Enlgish isn’t my native language (I’m a Spanish speaker).
That said, I have been working with these topics and writing about them for more than 10 years. I’ve also written several related articles in the past:
- https://codigounix.blogspot.com/2012/10/linux-x86-adjacent-memory-overflows.html
- https://codigounix.blogspot.com/2012/05/posix-system-v-executable-and-linkeable.html
- https://codigounix.blogspot.com/2012/05/posix-intmax-and-intmax.html
- https://codigounix.blogspot.com/2012/04/performance-analysis-study-case.html
simon_o@reddit
Just skip that step in the future maybe ... making people question whether the whole thing is made-up slop is devaluing the work you put into it.
Tornado547@reddit
im guessing theres something in the loader that prevents an unpriviliged user from LD_PRELOADing a setuid binary?
sacheie@reddit
Here is an old classic article on ELF that you might find interesting.
Solid-Film-818@reddit (OP)
Thanks you!!!
Dwedit@reddit
On Windows, all the system DLLs get their own predefined base address so the system DLLs don't overlap with each other. If there's no need for relocation of symbols, you can skip all the steps, and just have a simple memory-mapped file for the DLLs (except for the writable sections).
Despite having a predefined base address, they still have all the relocation information necessary to load at a different address.
Madsy9@reddit
Not only that, all the major system DLLs are always mapped, even if you don't link against them. You can get their base addresses via the PIB/TIB structures. No LoadLibrary or GetProcAddress required! It's possible to create Windows applications with no visible imports this way
Dwedit@reddit
I think it's only Kernel32 and its dependencies (KernelBase, NTDLL) that are preloaded that way. User32 and GDI32 etc don't get preloaded for programs that don't import them.
And yes, I have done the thing where you get the address of Kernel32.dll by using the TIB before, then walk down the import table to find the symbols. Here is the code. That's part of a code injection thing to make another process load a DLL file.
Then I saw another injector program take a completely different approach. It just simply assumed that the address of LoadLibraryA/W in the current process would also be correct in the other process. Just call CreateRemoteThread and use the address of LoadLibraryA/W. And that worked! So much for address-space-layout-randomization...
Madsy9@reddit
Sweet! Here's my version from back in the day: https://pimpmycode.blogspot.com/2015/01/win32-hacks-loading-api-functions-from.html?m=1
Solid-Film-818@reddit (OP)
Thanks! Great contribution!!
smarzzz@reddit
Amazing article, on of the best reads of 2026 so far
Solid-Film-818@reddit (OP)
Wow! Thanks so much!
Original_Bend@reddit
Excellent!
Solid-Film-818@reddit (OP)
Thanks!
emazv72@reddit
It reminds me of the good old days playing with the INT 21 calls and messing around with the good old Mark Zbikowski executable containers.
Solid-Film-818@reddit (OP)
Thanks!
nivaOne@reddit
Great article
Solid-Film-818@reddit (OP)
Thanks 🙂
Soggy-Holiday-7400@reddit
the GOT/PLT section is what finally made it click for me.knew about dynamic linking forever but never actually understood what was going on the runtime. bookmarking
Solid-Film-818@reddit (OP)
Wow! Glad to read this!!
Artistic-Big-9472@reddit
especially liked how you connected ELF internals with actual runtime behavior. The GOT/PLT explanation was clear and practical. Definitely one of the more insightful breakdowns on this topic.
Solid-Film-818@reddit (OP)
Thanks!! Happy to read!
Bl4ckb100d@reddit
Saving this to read later, along with your other articles, really glad to be reading such interesting topics from a fellow Argentine :)
Solid-Film-818@reddit (OP)
Another coronation of glory 🙌🇦🇷
gordonmessmer@reddit
I'm short on time today, so I've only glanced over this, but I see you've mentioned auditing the GOT and PLT!
I actually wrote a "got-audit" command using the GEF extension to GDB, after the xz-utils attack. The documentation is here: https://github.com/hugsy/gef-extras/blob/main/docs/commands/got-audit.md
It offers some checks to alarm on symbols that probably resolve into libraries they should not, and Fedora uses it in CI tests for a number of packages.
It needs more work, and it needs to be added as a standard test in order to be more effective at protecting the distribution. I'd love to hear your thoughts!
aes110@reddit
FYI, the first image in the markdown shows as not available from Imgur
Solid-Film-818@reddit (OP)
Fixed! Thanks! 🙌
Solid-Film-818@reddit (OP)
Wow! That’s incredible! I’m teaching at a hacking academy—could I explore your tool and evaluate using it in one of my classes?
gordonmessmer@reddit
Yeah, of course. Let me know if you or your student have feedback or questions.
Solid-Film-818@reddit (OP)
Off course! Thank you!!
AiexReddit@reddit
Thank you for this, super interesting topic and covers tons of stuff I didn't know!
Gentle feedback that I was kind of turned off by the second paragraph, particularly the comment that "nobody bothers" while I am actively making an effort to learn more about a topic I know is important, I'm simply one person buried (as we all are) in an endless backlog of important topics across endless domains, all of which I've love to understand better.
I don't disagree with the fundamental problem, it just rubbed me the wrong way making it sound like a "kids these days" attitude where devs are at fault for not trying hard enough. Many of us are genuinely interested and making an effort, but the ocean is vast and there's only so much time in a day.
Solid-Film-818@reddit (OP)
Well I am 40 years old! 🤣
probability_of_meme@reddit
nice
Solid-Film-818@reddit (OP)
I love Vim
Heittovaihtotiedosto@reddit
Your Hello world! example has a bug :)
Solid-Film-818@reddit (OP)
Wow thanks! Where?
TankorSmash@reddit
The double escaped newline
Solid-Film-818@reddit (OP)
Thanks bro!
RandNho@reddit
https://fasterthanli.me/series/making-our-own-executable-packer is also fun series about same topic.
Solid-Film-818@reddit (OP)
Wow, it's really good!
unique_ptr@reddit
Getting a big fat 404 :(
Solid-Film-818@reddit (OP)
Fixed!
RustOnTheEdge@reddit
Very nice! Quick question, I didn’t understand the fork imagery. It goes Parent -> fork()-> (parent PID=x returns child PID, child PID=0 returns 0)
Does fork output two processes? And why is the child process PID 0, aren’t PIDs unique across processes? Sorry for the maybe dumb question, I understood the text just fine but the image threw me off
narnach@reddit
Fork creates an extra process, the child. So the line of code that calls fork() will return twice:
RustOnTheEdge@reddit
Yeah thanks I hadn’t realized that the child would start from inside a fork() call and would return in both processes, but that makes sense now, thanks a lot!
SirDale@reddit
The child can call getpid() if it wants to know its ownpid.
OffbeatDrizzle@reddit
my pid went out for milk when I was a child and never came back
HyperWinX@reddit
No. Parent calls fork() and the execution continues like normal. Fork() creates a new process and exits, returning child PID to the parent. So from parent's POV its just a regular function call.
Child process begins its execution somewhere in fork() call, because process gets cloned. So child is just a parent's copy, that sees fork() as a regular function call that returns zero.
RustOnTheEdge@reddit
Ahhh of course, I hadn’t realized that the clone would include the execution of fork() itself upto the clone. That makes sense now, thanks!
Pale_Hovercraft333@reddit
take a look at the man page too. man 2 fork