
ELF files on Linux - ingve
https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/
======
haberman
I wrote Bloaty
([https://github.com/google/bloaty](https://github.com/google/bloaty)) which
involved writing a totally custom ELF file parser. Here are some epiphanies I
had about ELF while writing it.

ELF (and Mach-O, PE, etc) are designed to optimize the creation of a process
image. The runtime loader mainly just has to mmap() a bunch of file ranges
into memory with various permissions. This is quite different than loading a
.jar file, .pyc, etc. which involve building a runtime heap and loading
objects into that heap.

ELF has two file-level tables: sections and segments (the latter are also
called program headers). Things clicked for me when I realized: sections are
for the linker and segments are for the runtime loader. Sections are the
atomic unit of data that linkers operate on: the linker will never rearrange
data within a section (it may concatenate several input sections into a single
output section though). The loader doesn't even look at the section table
AFAIK, everything needed to load the binary is put into segments / program
headers.

Only some parts of the binary are actually read/loaded when the binary is
executed. Debugging info may bloat the binary but it doesn't cost any RAM at
runtime because it's never loaded unless you run a debugger. Bloaty makes this
clear by showing both VM size and file size:
[https://github.com/google/bloaty#running-
bloaty](https://github.com/google/bloaty#running-bloaty)

~~~
AceJohnny2
I've been wondering about qualitative differences between Mach-O and ELF,
after hearing a Mach developer trash the ELF format, but I don't know enough
about either to comment. Do you have any insight?

~~~
haberman
That is a deep and interesting question. I'm not sure I can give a great
answer, but here are a few thoughts.

If we look at the file format itself (separate from the features/semantics of
the linker and loader), I think ELF is simpler and more orthogonal. You can
iterate over the section/segment tables of an ELF file without knowing
anything about what each section/segment means. ELF nicely decouples the high-
level "container" aspect of the file format from the lower-level semantics of
how you interpret each section/segment in the linker and loader.

Mach on the other hand couples these two concepts together. The top-level
table is an array of "load commands", each with its own type, but you can't
even parse a load command until you know what type it is. Unlike ELF, the
entries of this table do not have a generic format or even a consistent size.
If you haven't written code to specifically recognize a given command type,
all you can do as fallback behavior is skip it. To me ELF feels like a
refactoring of Mach to make it a little more general and layered.

If we consider the actual semantics and features of the file formats, there
are pros and cons to both. Mach-O has built-in support for fat (multi-
architecture) binaries, which is kind of nifty, though I've never actually
used it myself. Mach-O distinguishes between "dylib" and "bundle" for shared
libraries -- for the life of me I can never remember the difference between
these two -- whereas ELF just has one type of shared library.
([https://docstore.mik.ua/orelly/unix3/mac/ch05_03.htm](https://docstore.mik.ua/orelly/unix3/mac/ch05_03.htm)).
The distinction seems to add complexity and I'm not sure I understand the
benefit. Mach-O has two-level namespaces (dynamic symbols are resolved by both
name _and_ the library they come from) -- colliding symbols aren't generally a
problem I've seen with ELF, but maybe it's useful in some cases. ELF makes
symbol interpositioning easy with LD_PRELOAD, though Mach-O seems to have its
own version of this that I've never tried:
[https://stackoverflow.com/questions/12609728/changing-
functi...](https://stackoverflow.com/questions/12609728/changing-function-
reference-in-mach-o-binary). Overall I prefer ELF.

~~~
AceJohnny2
Thanks!

Regarding multi-arch support, Ryan C. Gordon (Linux game porter
extraordinaire, icculus.org) had proposed FatELF [1] back in 2009 (LWN
coverage [2]). It seemed simple enough to implement, but never really picked
up steam (IMHO for reasons that speak of the culture of the Linux ecosystem).

[1] [http://icculus.org/fatelf/](http://icculus.org/fatelf/)

[2] [https://lwn.net/Articles/359070/](https://lwn.net/Articles/359070/)

~~~
yjftsjthsd-h
> reasons that speak of the culture of the Linux ecosystem

"Everyone ships source; just recompile"? It would be convenient, but with
source and a compiler you can hit everything anyways.

~~~
AceJohnny2
Yep, that's indeed my perspective, and I think that mindset dismisses the
effort required to deliver closed-source binaries with long-term support.

------
zimbatm
If you ever need to tweak or inspect an existing binary,
[https://github.com/NixOS/patchelf](https://github.com/NixOS/patchelf) is
great.

~~~
royragsdale
lief - Library to Instrument Executable Formats
[https://lief.quarkslab.com/](https://lief.quarkslab.com/)

is another great programmatic option

------
kccqzy
One thing that's on my mind but haven't been able to spend time investigating
is the fact that on my machine (Ubuntu 19.04), almost all distribution-
installed executables are not ELF executables per se, but ELF shared objects.
Running `file /bin/ls` shows that it's an ELF 64-bit LSB shared object.
Running `readelf -h /bin/ls` also says that the type is DYN. Is the executable
type basically deprecated now?

~~~
jzwinck
Position Independant Executable (PIE) files are detected as shared libraries
because they use the same old identifier as position independent shared
libraries. The ELF folks could have added a new type but did not, leading to
some confusion like this: [https://bugs.launchpad.net/ubuntu/+source/shared-
mime-info/+...](https://bugs.launchpad.net/ubuntu/+source/shared-mime-
info/+bug/1639531)

PIE is a security feature, which is why it has proliferated on newer systems.
See
[https://access.redhat.com/blogs/766093/posts/1975793](https://access.redhat.com/blogs/766093/posts/1975793)

~~~
usr1106
Here is an example to try and see yourself:

    
    
      $ cat show-addr.c
      #include <stdio.h>
      
      int main(int argc, char **argv)
      {
        printf("main() is at %p\n", main);
      }
      $ gcc -o show-addr show-addr.c
      $ file show-addr
      show-addr: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=60c8b61a7040adccc90934bc79e24342eecae15a, not stripped
      $ ./show-addr
      main() is at 0x400526
      $ ./show-addr
      main() is at 0x400526
      $ gcc -pie -fPIC -o show-addr.pie show-addr.c
      $ file show-addr.pie 
      show-addr.pie: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=73ae8065a65aad4b829567b7ce3464fb5d1e3fc3, not stripped
      $ ./show-addr.pie 
      main() is at 0x55c722ae1750
      $ ./show-addr.pie 
      main() is at 0x5649bf97b750
      $
    

(edit: incomplete copy-paste fixed)

------
vectorEQ
nice article but wish people would elaborate more on relocations instead of
always skipping that. it's a very important part of understanding how ELF
works when it's executed.

~~~
jcranmer
Relocations boil down to this:

A relocation entry contains a location of the patch, the symbol to use in
relocation, an optional addend, and a type. The type tells you how to compute
the relocation and is completely defined by the processor-specific ABI.

The simple relocations boil down to "add the addend to the address of the
symbol, subtract the address of the relocation, and store it as signed N-bit
number". There are more complex relocations that involve things such as symbol
sizes, TLS relocations, or the GOT and the PLT.

------
saagarjha
> For those who love to read actual source code, have a look at a documented
> ELF structure header file from Apple.

Surely the Linux kernel would be a better source?

