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

1

17

Consider this AMD64 assembly program:

.globl _start
_start:
    xorl %edi, %edi
    movl $60, %eax
    syscall

If I compile that with gcc -nostdlib and run ldd a.out, I get this:

        statically linked

If I instead compile that with gcc -static -nostdlib and run ldd a.out, I get this:

        not a dynamic executable

What's the difference between statically linked and not a dynamic executable? And if my binary was already statically linked, why does adding -static affect anything?

Longley answered 2/5, 2020 at 2:27 Comment(2)
Good question; PIE vs. non-PIE executables introduce multiple ways to have a static exectuable, and there's lots of non-obvious stuff to point out. Seemed like a good place to write a canonical answer about these distinctions and GCC / ld behaviour.Squeal
I've opened a discussion about this question on Meta.Longley
S
15

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
Squeal answered 2/5, 2020 at 5:37 Comment(10)
When I do gcc -nostdlib -static-pie exit.s -o exit-static-pie, file exit-static-pie tells me that it's dynamically linked. Any idea why I'm not getting the same result as you in that case? I tried with GCC 9.3.0-10ubuntu2 from apt on Ubuntu 20.04, as well as GCC 9.3.0 in the gcc:9 Docker container.Longley
It's definitely distro-related. After seeing your latest edit, I tried it in the archlinux Docker container instead, and I got the same result that you did there.Longley
@JosephSible-ReinstateMonica: I'm running Arch GNU/Linux with gcc 9.3.0, binutils 2.34, file-5.38. Arch doesn't do much tweaking to upstream source packages, IDK if Ubuntu did anything weird. Does readelf -a show it has an Interp header?Squeal
@JosephSible-ReinstateMonica: -static-pie is very rarely used; perhaps file misidentified it until recently, or Ubuntu's gcc or ld version doesn't support it properly? gcc invokes ld -static -pie so an ld that doesn't know how to make a static-pie might just have -pie override -static and make a regular PIE. or not, just say your update re: file.Squeal
Yep, that's exactly it! Ubuntu's file is misidentifying it. In the container, Arch's file called it statically linked, but when I copied the binary out, Ubuntu's file called it dynamically linked.Longley
I tried building file from the latest source on Ubuntu to make it work, but it didn't work there either. After some investigation, it turns out that it works in the latest release of file, but a commit to git master since then broke it again (because apparently it was messing something else up 1 2), and Ubuntu cherry-picked that patch, but Arch didn't.Longley
@JosephSible-ReinstateMonica: Amusing, and nice detective work :P. A PIE executable is an ELF shared object; it's basically a shared library with an entry point. So it makes sense that an actual shared library with no further dependencies on other shared libraries (and no ELF interpreter) could get detected as "statically linked" by code that was trying to detect static-pie as statically linked. The difference is that an non-executable shared library won't have an entry point.Squeal
(Or will it: if it includes a self-test function as a _start, it can be runnable while still also being a normal shared library that other programs can link to. I remember reading something before PIE was a real thing about the possibility of having an ELF entry point in a shared library. This previously unofficial functionality is what PIE was built on top of when the benefit of ASLR was realized.)Squeal
And yeah github.com/file/file/commit/FILE5_37-59-g24c9c086 fixes that by moving the linking_style = "dynamically"; to the case PT_INTERP: where it belongs.Squeal
It's a bit misleading to say that commit "fixes" it, since it's from July, and the commit that broke it was from last week and explicitly reverts the one you linked.Longley

© 2022 - 2024 — McMap. All rights reserved.