What's the Difference Between "Statically Linked" and "Not a Dynamic Executable" from Linux Ldd

What's the difference between statically linked and not a dynamic executable from Linux ldd?

There are two separate things here:

  • Requesting an ELF interpreter (ld.so) or not.

    Like #!/bin/sh but for binaries, runs before your _start.

    This is the difference between a static vs. dynamic executable.
  • The list of dynamically linked libraries for ld.so to load happens to be empty.

    This is apparently what ldd calls "statically linked", i.e. that any libraries you might have linked at build time were static libraries.

Other tools like file and readelf give more information and use terminology that matches what you'd expect.


Your GCC is configured so -pie is the default, and gcc doesn't make a static-pie for the special case of no dynamic libraries.

  • gcc -nostdlib just makes a PIE that happens not to link to any libraries but is otherwise identical to a normal PIE, specifying an ELF interpreter.

    ldd confusingly calls this "statically linked".

    file : ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2 ...
  • gcc -nostdlib -static overrides the -pie default and makes a true static executable.

    file : ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked ...
  • gcc -nostdlib -no-pie also chooses to make a static executable as an optimization for the case where there are no dynamic libraries at all. Since a non-PIE executable couldn't have been ASLRed anyway, this makes sense. Byte-for-byte identical to the -static case.
  • gcc -nostdlib -static-pie makes an ASLRable executable that doesn't need an ELF interpreter. GCC doesn't do this by default for gcc -pie -nostdlib, unlike the no-pie case where it chooses to sidestep ld.so when no dynamically-linked libraries are involved.

    file : ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), statically linked ...

    -static-pie is obscure, rarely used, and older file doesn't identify it as statically linked.

-nostdlib doesn't imply -no-pie or -static, and -static-pie has to be explicitly specified to get that.

gcc -static-pie invokes ld -static -pie, so ld has to know what that means. Unlike with the non-PIE case where you don't have to ask for a dynamic executable explicitly, you just get one if you pass ld any .so libraries. I think that's why you happen to get a static executable from gcc -nostdlib -no-pie - GCC doesn't have to do anything special, it's just ld doing that optimization.

But ld doesn't enable -static implicitly when -pie is specified, even when there are no shared libraries to link.


Details

Examples generated with gcc --version gcc (Arch Linux 9.3.0-1) 9.3.0

ld --version GNU ld (GNU Binutils) 2.34 (also readelf is binutils)

ldd --version ldd (GNU libc) 2.31

file --version file-5.38 - note that static-pie detection has changed in recent patches, with Ubuntu cherry-picking an unreleased patch. (Thanks @Joseph for the detective work) - this in 2019 detected dynamic = having a PT_INTERP to handle static-pie, but it was reverted to detect based on PT_DYNAMIC so shared libraries count as dynamic. debian bug #948269. static-pie is an obscure rarely-used feature.

GCC ends up running ld -pie exit.o with a dynamic linker path specified, and no libraries. (And a boatload of other options to support possible LTO link-time optimization, but the keys here are -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie. collect2 is just a wrapper around ld.)

$ gcc -nostdlib exit.s -v      # output manually line wrapped with \ for readability
...
COLLECT_GCC_OPTIONS='-nostdlib' '-v' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/collect2 \
-plugin /usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/liblto_plugin.so \
-plugin-opt=/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/lto-wrapper \
-plugin-opt=-fresolution=/tmp/ccoNx1IR.res \
--build-id --eh-frame-hdr --hash-style=gnu \
-m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie \
-L/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0 \
-L/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/../../../../lib -L/lib/../lib \
-L/usr/lib/../lib \
-L/usr/lib/gcc/x86_64-pc-linux-gnu/9.3.0/../../.. \
/tmp/cctm2fSS.o

You get a dynamic PIE with no dependencies on other libraries. Running it still invokes the "ELF interpreter" /lib64/ld-linux-x86-64.so.2 on it which runs before jumping to your _start. (Although the kernel has already mapped the executable's ELF segments to ASLRed virtual addresses, along with ld.so's text / data / bss).

file and readelf are more descriptive.

PIE non-static executable from gcc -nostdlib

$ gcc -nostdlib exit.s -o exit-default
$ ls -l exit-default
-rwxr-xr-x 1 peter peter 13536 May 2 02:15 exit-default
$ ldd exit-default
statically linked
$ file exit-default
exit-default: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=05a4d1bdbc94d6f91cca1c9c26314e1aa227a3a5, not stripped

$ readelf -a exit-default
...
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1000
...
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000001f8 0x00000000000001f8 R 0x8
INTERP 0x0000000000000238 0x0000000000000238 0x0000000000000238
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000000002b1 0x00000000000002b1 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000009 0x0000000000000009 R E 0x1000
... (the Read+Exec segment to be mapped at virt addr 0x1000 is where your text section was linked.)

If you strace it you can also see the differences:

$ gcc -nostdlib exit.s -o exit-default
$ strace ./exit-default
execve("./exit-default", ["./exit-default"], 0x7ffe1f526040 /* 51 vars */) = 0
brk(NULL) = 0x5617eb1e4000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffcea703380) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9ff5b3e000
arch_prctl(ARCH_SET_FS, 0x7f9ff5b3ea80) = 0
mprotect(0x5617eabac000, 4096, PROT_READ) = 0
exit(0) = ?
+++ exited with 0 +++

vs. -static and -static-pie the first instruction executed in user-space is your _start (which you can also check with GDB using starti).

$ strace ./exit-static-pie 
execve("./exit-static-pie", ["./exit-static-pie"], 0x7ffcdac96dd0 /* 51 vars */) = 0
exit(0) = ?
+++ exited with 0 +++

gcc -nostdlib -static-pie

$ gcc -nostdlib -static-pie exit.s -o exit-static-pie
$ ls -l exit-static-pie
-rwxr-xr-x 1 peter peter 13440 May 2 02:18 exit-static-pie
peter@volta:/tmp$ ldd exit-static-pie
statically linked
peter@volta:/tmp$ file exit-static-pie
exit-static-pie: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=daeb4a8f11bec1bb1aaa13cd48d24b5795af638e, not stripped

$ readelf -a exit-static-pie
...
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1000
...

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000229 0x0000000000000229 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000009 0x0000000000000009 R E 0x1000
... (no Interp header, but still a read+exec text segment)

Notice that the addresses are still relative to the image base, leaving ASLR up to the kernel.

Surprisingly, ldd doesn't say that it's not a dynamic executable. That might be a bug, or a side effect of some implementation detail.


gcc -nostdlib -static traditional non-PIE old-school static executable

$ gcc -nostdlib -static exit.s -o exit-static
$ ls -l exit-static
-rwxr-xr-x 1 peter peter 4744 May 2 02:26 exit-static
peter@volta:/tmp$ ldd exit-static
not a dynamic executable
peter@volta:/tmp$ file exit-static
exit-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, BuildID[sha1]=1b03e3d05709b7288fe3006b4696fd0c11fb1cb2, not stripped
peter@volta:/tmp$ readelf -a exit-static
ELF Header:
...
Type: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x401000
... (Note the absolute entry-point address nailed down at link time)
(And that the ELF type is EXEC, not DYN)

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000010c 0x000000000000010c R 0x1000
LOAD 0x0000000000001000 0x0000000000401000 0x0000000000401000
0x0000000000000009 0x0000000000000009 R E 0x1000
NOTE 0x00000000000000e8 0x00000000004000e8 0x00000000004000e8
0x0000000000000024 0x0000000000000024 R 0x4

Section to Segment mapping:
Segment Sections...
00 .note.gnu.build-id
01 .text
02 .note.gnu.build-id
...

Those are all the program headers; unlike pie / static-pie I'm not leaving any out, just other whole parts of the readelf -a output.

Also note the absolute virtual addresses in the program headers that don't give the kernel a choice where in virtual address space to map the file. This is the difference between EXEC and DYN types of ELF objects. PIE executables are shared objects with an entry point, allowing us to get ASLR for the main executable. Actual EXEC executables have a link-time-chosen memory layout.


ldd apparently only reports "not a dynamic executable" when both:

  • no ELF interpreter (dynamic linker) path
  • ELF type = EXEC

What do 'statically linked' and 'dynamically linked' mean?

There are (in most cases, discounting interpreted code) two stages in getting from source code (what you write) to executable code (what you run).

The first is compilation which turns source code into object modules.

The second, linking, is what combines object modules together to form an executable.

The distinction is made for, among other things, allowing third party libraries to be included in your executable without you seeing their source code (such as libraries for database access, network communications and graphical user interfaces), or for compiling code in different languages (C and assembly code for example) and then linking them all together.

When you statically link a file into an executable, the contents of that file are included at link time. In other words, the contents of the file are physically inserted into the executable that you will run.

When you link dynamically, a pointer to the file being linked in (the file name of the file, for example) is included in the executable and the contents of said file are not included at link time. It's only when you later run the executable that these dynamically linked files are bought in and they're only bought into the in-memory copy of the executable, not the one on disk.

It's basically a method of deferred linking. There's an even more deferred method (called late binding on some systems) that won't bring in the dynamically linked file until you actually try to call a function within it.

Statically-linked files are 'locked' to the executable at link time so they never change. A dynamically linked file referenced by an executable can change just by replacing the file on the disk.

This allows updates to functionality without having to re-link the code; the loader re-links every time you run it.

This is both good and bad - on one hand, it allows easier updates and bug fixes, on the other it can lead to programs ceasing to work if the updates are incompatible - this is sometimes responsible for the dreaded "DLL hell" that some people mention in that applications can be broken if you replace a dynamically linked library with one that's not compatible (developers who do this should expect to be hunted down and punished severely, by the way).


As an example, let's look at the case of a user compiling their main.c file for static and dynamic linking.

Phase     Static                    Dynamic
-------- ---------------------- ------------------------
+---------+ +---------+
| main.c | | main.c |
+---------+ +---------+
Compile........|.........................|...................
+---------+ +---------+ +---------+ +--------+
| main.o | | crtlib | | main.o | | crtimp |
+---------+ +---------+ +---------+ +--------+
Link...........|..........|..............|...........|.......
| | +-----------+
| | |
+---------+ | +---------+ +--------+
| main |-----+ | main | | crtdll |
+---------+ +---------+ +--------+
Load/Run.......|.........................|..........|........
+---------+ +---------+ |
| main in | | main in |-----+
| memory | | memory |
+---------+ +---------+

You can see in the static case that the main program and C runtime library are linked together at link time (by the developers). Since the user typically cannot re-link the executable, they're stuck with the behaviour of the library.

In the dynamic case, the main program is linked with the C runtime import library (something which declares what's in the dynamic library but doesn't actually define it). This allows the linker to link even though the actual code is missing.

Then, at runtime, the operating system loader does a late linking of the main program with the C runtime DLL (dynamic link library or shared library or other nomenclature).

The owner of the C runtime can drop in a new DLL at any time to provide updates or bug fixes. As stated earlier, this has both advantages and disadvantages.

how is ld-linux.so* itself linked and loaded?

  1. ld-linux.so* doesn't depends any other libraries. It is runable by itself when loaded to memory.

  2. ldd is a script, it loads the object file via the loader, and the loader checks whether the object is dynamically or statically linked, try this:

LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2


  1. file reads the magic number or elf header to figure whether the object is dynamically or statically linked, it may output different value from ldd

IMO, ld-linux.so is static linked, because it doesn't have an .interp section which all dynamically linked object must have.

Static Vs Dynamic libraries

Unfortunately, the words "static" and "dynamic" are way too overused, especially in C and C++. So, I prefer the following terminology:

  • Link-time linking, a.k.a "static linking": All symbols are resolved at link time from static libraries. The result is a monolithic, statically linked executable with no load-time dependencies.

  • Load-time linking: This is the standard practice on modern platforms, unresolved symbols are looked up in shared libraries (Unix) or the unfortunately named dynamic link libraries (DLLS) on Windows and only references are recorded at link time, the actual resolution of the symbols and code loading happens at load time.

    This results in a "dynamically linked" executable which must be loaded with a loader (e.g. ld.so on Linux). Loading is part of the OS and usually transparent to the user, though it's open to inspection (e.g. with ldd on Linux). All shared libraries must be available at load time, or the program will not launch.

  • Run-time linking, a.k.a. "dynamic linking": There are no unresolved symbols; rather, the runtime dynamically decides to look up symbols in a shared/dynamic library using dlopen() or LoadLibrary(). Failure to find symbols is a handlable runtime condition that is not an error. This technique is used commonly for plug-in architecture, and on Windows for code injection.

Note however that there is a fundamental technical difference between Linux's shared objects and Windows's DLLs, they're not just the same thing with a different name. Both can however be used both for load-time and run-time linking.

Linux ELF files: Which byte will differ for static and dynamic ELF programs?

How about using ldd.c from μClibc? It should be fairly easy to strip out any unwanted dependencies / checks if you want. I think this is a smarter approach than trying to figure out all the corner cases from reading man 5 elf, though FWIW it looks to be just checking for a PT_INTERP program header as you suspect in the comments.

Update: There's a few more checks. I've tried to extract the relevant parts, but I can't be sure if I've missed anything so check for yourself. The code checks 32-bit and 64-bit x86 ELF files. It assumes a little-endian architecture.

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>
#include <ctype.h>
#include <inttypes.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>

#include <elf.h>

int main(int argc, char* argv[])
{
const char* fname = argv[0];
if (argc >= 2) fname = argv[1];

int fd;
struct stat st;
void *mapping;

if ((fd = open(fname, O_RDONLY)) == -1) {
perror(fname);
return 1;
}

if (fstat(fd, &st)) {
perror("fstat");
close(fd);
return 1;
}

if ((mapping = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
const Elf32_Ehdr* eh = mapping;

if (st.st_size < (off_t)sizeof(Elf32_Ehdr) ||
eh->e_ident[EI_MAG0] != ELFMAG0 ||
eh->e_ident[EI_MAG1] != ELFMAG1 ||
eh->e_ident[EI_MAG2] != ELFMAG2 ||
eh->e_ident[EI_MAG3] != ELFMAG3 ||
eh->e_ident[EI_VERSION] != EV_CURRENT) {
printf("Not a valid ELF file\n");
return 0;
}

if (eh->e_type != ET_EXEC && eh->e_type != ET_DYN) {
printf("Not executable or shared object\n");
return 0;
}

int is_dynamic = 0;

// change as appropriate, but remember that byteswapping might be needed in some cases
if (eh->e_ident[EI_CLASS] == ELFCLASS32 && eh->e_ident[EI_DATA] == ELFDATA2LSB && eh->e_machine == EM_386) {
uint16_t ph_cnt;
for (ph_cnt = 0; ph_cnt < eh->e_phnum; ph_cnt++) {
const Elf32_Phdr* ph = (const Elf32_Phdr*)((const uint8_t*)mapping + eh->e_phoff + ph_cnt * eh->e_phentsize);
if (ph->p_type == PT_DYNAMIC || ph->p_type == PT_INTERP) {
is_dynamic = 1;
}
}
} else if (eh->e_ident[EI_CLASS] == ELFCLASS64 && eh->e_ident[EI_DATA] == ELFDATA2LSB && eh->e_machine == EM_X86_64) {
const Elf64_Ehdr* eh = mapping;
uint16_t ph_cnt;
for (ph_cnt = 0; ph_cnt < eh->e_phnum; ph_cnt++) {
const Elf64_Phdr* ph = (const Elf64_Phdr*)((const uint8_t*)mapping + eh->e_phoff + ph_cnt * eh->e_phentsize);
if (ph->p_type == PT_DYNAMIC || ph->p_type == PT_INTERP) {
is_dynamic = 1;
}
}
} else {
printf("Unsupported architecture\n");
return 0;
}

munmap(mapping, st.st_size);
close(fd);
printf("%s: %sdynamic\n", fname, is_dynamic?"":"not ");
return 0;
}


Related Topics



Leave a reply



Submit