107

After finding out that several common commands (such as read) are actually Bash builtins (and when running them at the prompt I'm actually running a two-line shell script which just forwards to the builtin), I was looking to see if the same is true for true and false.

Well, they are definitely binaries.

sh-4.2$ which true
/usr/bin/true
sh-4.2$ which false
/usr/bin/false
sh-4.2$ file /usr/bin/true
/usr/bin/true: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=2697339d3c19235
06e10af65aa3120b12295277e, stripped
sh-4.2$ file /usr/bin/false
/usr/bin/false: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=b160fa513fcc13
537d7293f05e40444fe5843640, stripped
sh-4.2$

However, what I found most surprising was their size. I expected them to be only a few bytes each, as true is basically just exit 0 and false is exit 1.

sh-4.2$ true
sh-4.2$ echo $?
0
sh-4.2$ false
sh-4.2$ echo $?
1
sh-4.2$

However I found to my surprise that both files are over 28KB in size.

sh-4.2$ stat /usr/bin/true
  File: '/usr/bin/true'
  Size: 28920           Blocks: 64         IO Block: 4096   regular file
Device: fd2ch/64812d    Inode: 530320      Links: 1                     
Access: (0755/-rwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2018-01-25 19:46:32.703463708 +0000
Modify: 2016-06-30 09:44:27.000000000 +0100
Change: 2017-12-22 09:43:17.447563336 +0000
 Birth: -
sh-4.2$ stat /usr/bin/false
  File: '/usr/bin/false'
  Size: 28920           Blocks: 64         IO Block: 4096   regular file
Device: fd2ch/64812d    Inode: 530697      Links: 1                     
Access: (0755/-rwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2018-01-25 20:06:27.210764704 +0000
Modify: 2016-06-30 09:44:27.000000000 +0100
Change: 2017-12-22 09:43:18.148561245 +0000
 Birth: -
sh-4.2$

So my question is: Why are they so big? What's in the executable other than the return code?

PS: I am using RHEL 7.4

17
  • 18
    You should use command -V true not which. It will output: true is a shell builtin for bash.
    – meuh
    Jan 25, 2018 at 20:53
  • 45
    true and false are builtins in every modern shell, but the systems also includes external program versions of them because it's part of the standard system so that programs invoking commands directly (bypassing the shell) can use them. which ignores builtins, and looks up external commands only, which is why it only showed you the external ones. Try type -a true and type -a false instead.
    – mtraceur
    Jan 25, 2018 at 22:15
  • 87
    It's ironic that you write such a long question to say "Why are true and false 29kb each? What's in the executable other than the return code?" Jan 25, 2018 at 23:51
  • 11
    Some early versions of unix just had an empty file for true since that was a valid sh program that would return exit code 0. I really wish I could find an article I read years ago about the history of the true utility from an empty file to the monstrosity it is today, but all I could find is this: trillian.mit.edu/~jc/humor/ATT_Copyright_true.html
    – Philip
    Jan 26, 2018 at 4:16
  • 17
    Obligatory - the smallest implementation of false: muppetlabs.com/~breadbox/software/tiny/teensy.html
    – d33tah
    Jan 26, 2018 at 14:36

5 Answers 5

156

In the past, /bin/true and /bin/false in the shell were actually scripts.

For instance, in a PDP/11 Unix System 7:

$ ls -la /bin/true /bin/false
-rwxr-xr-x 1 bin         7 Jun  8  1979 /bin/false
-rwxr-xr-x 1 bin         0 Jun  8  1979 /bin/true
$
$ cat /bin/false
exit 1
$
$ cat /bin/true
$  

Nowadays, at least in bash, the trueand false commands are implemented as shell built-in commands. Thus no executable binary files are invoked by default, both when using the false and true directives in the bash command line and inside shell scripts.

From the bashsource, builtins/mkbuiltins.c:

char *posix_builtins[] =
    {
      "alias", "bg", "cd", "command", "false", "fc", "fg", "getopts", "jobs",
      "kill", "newgrp", "pwd", "read", "true", "umask", "unalias", "wait",
      (char *)NULL
    };

Also per @meuh comments:

$ command -V true false
true is a shell builtin
false is a shell builtin

So it can be said with a high degree of certainty the trueand false executable files exist mainly for being called from other programs.

From now on, the answer will focus on the /bin/true binary from the coreutilspackage in Debian 9 / 64 bits. (/usr/bin/true running RedHat. RedHat and Debian use both the coreutils package, analysed the compiled version of the latter having it more at hand).

As it can be seen in the source file false.c, /bin/false is compiled with (almost) the same source code as /bin/true, just returning EXIT_FAILURE (1) instead, so this answer can be applied for both binaries.

#define EXIT_STATUS EXIT_FAILURE
#include "true.c"

As it also can be confirmed by both executables having the same size:

$ ls -l /bin/true /bin/false
-rwxr-xr-x 1 root root 31464 Feb 22  2017 /bin/false
-rwxr-xr-x 1 root root 31464 Feb 22  2017 /bin/true

Alas, the direct answer to the question "why are true and false so large?" could be, because there are not anymore so pressing reasons to care about their top performance. They are not essential to bash performance, not being used anymore by bash (scripting).

Similar comments apply to their size, 26KB for the kind of hardware we have nowadays is insignificant. Space is not at premium for the typical server/desktop anymore, and they do not even bother anymore to use the same binary for false and true, as it is just deployed twice in distributions using coreutils.

Focusing, however, in the real spirit of the question, why something that should be so simple and small, gets so large?

The real distribution of the sections of /bin/true is as these charts shows; the main code+data amounts to roughly 3KB out of a 26KB binary, which amounts to 12% of the size of /bin/true.

The true utility got indeed more cruft code over the years, most notably the standard support for --version and --help.

However, that it is not the (only) main justification for it being so big, but rather, while being dynamically linked (using shared libs), also having part of a generic library commonly used by coreutils binaries linked as a static library. The metada for building an elf executable file also amounts for a significant part of the binary, being it a relatively small file by today´s standards.

The rest of the answer is for explaining how we got to build the following charts detailing the composition of the /bin/true executable binary file and how we arrived to that conclusion.

bintrue bintrue2

As @Maks says, the binary was compiled from C; as per my comment also, it is also confirmed it is from coreutils. We are pointing directly to the author(s) git https://github.com/wertarbyte/coreutils/blob/master/src/true.c, instead of the gnu git as @Maks (same sources, different repositories - this repository was selected as it has the full source of the coreutils libraries)

We can see the various building blocks of the /bin/truebinary here (Debian 9 - 64 bits from coreutils):

$ file /bin/true
/bin/true: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=9ae82394864538fa7b23b7f87b259ea2a20889c4, stripped

$ size /bin/true
    text       data     bss     dec     hex filename
   24583       1160     416   26159    662f true

Of those:

  • text (usually code) is around 24KB
  • data (initialised variables, mostly strings) are around 1KB
  • bss (uninitialized data) 0.5KB

Of the 24KB, around 1KB is for fixing up the 58 external functions.

That still leaves around roughly 23KB for rest of the code. We will show down bellow that the actual main file - main()+usage() code is around 1KB compiled, and explain what the other 22KB are used for.

Drilling further down the binary with readelf -S true, we can see that while the binary is 26159 bytes, the actual compiled code is 13017 bytes, and the rest is assorted data/initialisation code.

However, true.c is not the whole story and 13KB seems pretty much excessive if it were only that file; we can see functions called in main() that are not listed in the external functions seen in the elf with objdump -T true ; functions that are present at:

Those extra functions not linked externally in main() are:

  • set_program_name()
  • close_stdout()
  • version_etc()

So my first suspicion was partly correct, whilst the library is using dynamic libraries, the /bin/true binary is big because it has some static libraries included with it (but that is not the only cause).

Compiling C code is not usually that inefficient for having such space unaccounted for, hence my initial suspicion something was amiss.

The extra space, almost 90% of the size of the binary, is indeed extra libraries/elf metadata.

While using Hopper for disassembling/decompiling the binary to understand where functions are, it can be seen the compiled binary code of true.c/usage() function is actually 833 bytes, and of the true.c/main() function is 225 bytes, which is roughly slightly less than 1KB. The logic for version functions, which is buried in the static libraries, is around 1KB.

The actual compiled main()+usage()+version()+strings+vars are only using up around 3KB to 3.5KB.

It is indeed ironic, such small and humble utilities have became bigger in size for the reasons explained above.

related question: Understanding what a Linux binary is doing

true.c main() with the offending function calls:

int
main (int argc, char **argv)
{
  /* Recognize --help or --version only if it's the only command-line
     argument.  */
  if (argc == 2)
    {
      initialize_main (&argc, &argv);
      set_program_name (argv[0]);           <-----------
      setlocale (LC_ALL, "");
      bindtextdomain (PACKAGE, LOCALEDIR);
      textdomain (PACKAGE);

      atexit (close_stdout);             <-----

      if (STREQ (argv[1], "--help"))
        usage (EXIT_STATUS);

      if (STREQ (argv[1], "--version"))
        version_etc (stdout, PROGRAM_NAME, PACKAGE_NAME, Version,  AUTHORS,  <------
                     (char *) NULL);
    }

  exit (EXIT_STATUS);
}

The decimal size of the various sections of the binary:

$ size -A -t true 
true  :
section               size      addr
.interp                 28       568
.note.ABI-tag           32       596
.note.gnu.build-id      36       628
.gnu.hash               60       664
.dynsym               1416       728
.dynstr                676      2144
.gnu.version           118      2820
.gnu.version_r          96      2944
.rela.dyn              624      3040
.rela.plt             1104      3664
.init                   23      4768
.plt                   752      4800
.plt.got                 8      5552
.text                13017      5568
.fini                    9     18588
.rodata               3104     18624
.eh_frame_hdr          572     21728
.eh_frame             2908     22304
.init_array              8   2125160
.fini_array              8   2125168
.jcr                     8   2125176
.data.rel.ro            88   2125184
.dynamic               480   2125272
.got                    48   2125752
.got.plt               392   2125824
.data                  128   2126240
.bss                   416   2126368
.gnu_debuglink          52         0
Total                26211

Output of readelf -S true

$ readelf -S true
There are 30 section headers, starting at offset 0x7368:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000000238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000000254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000000274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000000298  00000298
       000000000000003c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000000002d8  000002d8
       0000000000000588  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000000860  00000860
       00000000000002a4  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000000b04  00000b04
       0000000000000076  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000000b80  00000b80
       0000000000000060  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000000be0  00000be0
       0000000000000270  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000000e50  00000e50
       0000000000000450  0000000000000018  AI       5    25     8
  [11] .init             PROGBITS         00000000000012a0  000012a0
       0000000000000017  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000000012c0  000012c0
       00000000000002f0  0000000000000010  AX       0     0     16
  [13] .plt.got          PROGBITS         00000000000015b0  000015b0
       0000000000000008  0000000000000000  AX       0     0     8
  [14] .text             PROGBITS         00000000000015c0  000015c0
       00000000000032d9  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         000000000000489c  0000489c
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         00000000000048c0  000048c0
       0000000000000c20  0000000000000000   A       0     0     32
  [17] .eh_frame_hdr     PROGBITS         00000000000054e0  000054e0
       000000000000023c  0000000000000000   A       0     0     4
  [18] .eh_frame         PROGBITS         0000000000005720  00005720
       0000000000000b5c  0000000000000000   A       0     0     8
  [19] .init_array       INIT_ARRAY       0000000000206d68  00006d68
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .fini_array       FINI_ARRAY       0000000000206d70  00006d70
       0000000000000008  0000000000000008  WA       0     0     8
  [21] .jcr              PROGBITS         0000000000206d78  00006d78
       0000000000000008  0000000000000000  WA       0     0     8
  [22] .data.rel.ro      PROGBITS         0000000000206d80  00006d80
       0000000000000058  0000000000000000  WA       0     0     32
  [23] .dynamic          DYNAMIC          0000000000206dd8  00006dd8
       00000000000001e0  0000000000000010  WA       6     0     8
  [24] .got              PROGBITS         0000000000206fb8  00006fb8
       0000000000000030  0000000000000008  WA       0     0     8
  [25] .got.plt          PROGBITS         0000000000207000  00007000
       0000000000000188  0000000000000008  WA       0     0     8
  [26] .data             PROGBITS         00000000002071a0  000071a0
       0000000000000080  0000000000000000  WA       0     0     32
  [27] .bss              NOBITS           0000000000207220  00007220
       00000000000001a0  0000000000000000  WA       0     0     32
  [28] .gnu_debuglink    PROGBITS         0000000000000000  00007220
       0000000000000034  0000000000000000           0     0     1
  [29] .shstrtab         STRTAB           0000000000000000  00007254
       000000000000010f  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

Output of objdump -T true (external functions dynamically linked on run-time)

$ objdump -T true

true:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 __uflow
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 getenv
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 free
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 abort
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 __errno_location
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 strncmp
0000000000000000  w   D  *UND*  0000000000000000              _ITM_deregisterTMCloneTable
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 _exit
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 __fpending
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 textdomain
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 fclose
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 bindtextdomain
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 dcgettext
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 __ctype_get_mb_cur_max
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 strlen
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.4   __stack_chk_fail
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 mbrtowc
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 strrchr
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 lseek
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 memset
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 fscanf
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 close
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 __libc_start_main
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 memcmp
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 fputs_unlocked
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 calloc
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 strcmp
0000000000000000  w   D  *UND*  0000000000000000              __gmon_start__
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.14  memcpy
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 fileno
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 malloc
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 fflush
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 nl_langinfo
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 ungetc
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 __freading
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 realloc
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 fdopen
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 setlocale
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.3.4 __printf_chk
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 error
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 open
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 fseeko
0000000000000000  w   D  *UND*  0000000000000000              _Jv_RegisterClasses
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 __cxa_atexit
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 exit
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 fwrite
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.3.4 __fprintf_chk
0000000000000000  w   D  *UND*  0000000000000000              _ITM_registerTMCloneTable
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 mbsinit
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.2.5 iswprint
0000000000000000  w   DF *UND*  0000000000000000  GLIBC_2.2.5 __cxa_finalize
0000000000000000      DF *UND*  0000000000000000  GLIBC_2.3   __ctype_b_loc
0000000000207228 g    DO .bss   0000000000000008  GLIBC_2.2.5 stdout
0000000000207220 g    DO .bss   0000000000000008  GLIBC_2.2.5 __progname
0000000000207230  w   DO .bss   0000000000000008  GLIBC_2.2.5 program_invocation_name
0000000000207230 g    DO .bss   0000000000000008  GLIBC_2.2.5 __progname_full
0000000000207220  w   DO .bss   0000000000000008  GLIBC_2.2.5 program_invocation_short_name
0000000000207240 g    DO .bss   0000000000000008  GLIBC_2.2.5 stderr
10
  • 8
    Having done some programming recently with a 64kB+2kB microcontroller, 28kB doesn't seem all that small..
    – Barleyman
    Jan 26, 2018 at 16:49
  • 2
    @Barleyman you have OpenWRT, yocto, uClinux, uclib, busybox, microcoreutils, and other solutions for that kind of environments. Edited the post with your concern. Jan 27, 2018 at 9:09
  • 8
    @Barleyman: If you were optimizing for binary executable size, you can implement true or false with a 45-byte x86 ELF executable, packing the executable code (4 x86 instructions) inside the ELF program header (without support for any command-line options!). A Whirlwind Tutorial on Creating Really Teensy ELF Executables for Linux. (Or slightly larger if you want to avoid depending on Linux ELF loader implementation details :P) Jan 28, 2018 at 23:39
  • 5
    Not really, no. Yocto for example can be crammed into less than a megabyte which is heaps and bounds above 64kB.. In this kind of device you may use RTOS of some kind with rudimentary process / memory management but even those can easily become too heavy. I wrote a simple cooperative multithreading system and used the built in memory protection to protect code from being overwritten. All told the firmware consumes some 55kB right now so not too much room there for additional overhead. Those ginormous 2kB look up tables..
    – Barleyman
    Jan 29, 2018 at 0:09
  • 2
    @PeterCordes for sure but you need couple of magnitudes of more resources before Linux becomes viable. For what it's worth, C++ doesn't really work in that environment either. Well, not the standard libraries anyways. Iostream is right out at around 200kB etc.
    – Barleyman
    Jan 29, 2018 at 0:19
37

The implementation probably comes from GNU coreutils. These binaries are compiled from C; no particular effort has been made to make them smaller than they are by default.

You could try to compile the trivial implementation of true yourself, and you'll notice it's already few KB in size. For example, on my system:

$ echo 'int main() { return 0; }' | gcc -xc - -o true
$ wc -c true
8136 true

Of course, your binaries are even bigger. That's because they also support command line arguments. Try running /usr/bin/true --help or /usr/bin/true --version.

In addition to the string data, the binary includes logic to parse command line flags, etc. That adds up to about 20 KB of code, apparently.

For reference, you can find the source code here: http://git.savannah.gnu.org/cgit/coreutils.git/tree/src/true.c

8
  • 2
    FYI I was complaining about these coreutils implementations on their bug tracker, but no chance to get it fixed lists.gnu.org/archive/html/bug-coreutils/2016-03/msg00040.html
    – rudimeier
    Jan 25, 2018 at 21:43
  • 7
    It is not the logic for arguments, C is not that inefficient...is inline libraries/house keeping tasks. Have a look at my answer for the gory details. Jan 25, 2018 at 21:59
  • 8
    This is misleading because it suggests that compiled machine code (from C or otherwise) is what takes the huge amount of space - the actual size overhead has more to do with massive amounts of standard C library/runtime boilerplate that gets inlined by the compiler in order to interoperate with the C library (glibc, unless you've heard that your system uses something else, probably), and, to a lesser extent, ELF headers/metadata (a lot of which are not strictly necessary, but deemed worthwhile enough to include in default builds).
    – mtraceur
    Jan 25, 2018 at 22:10
  • 2
    The actual main()+usage()+strings on both functions are around 2KB, not 20KB. Jan 25, 2018 at 23:11
  • 2
    @JdeBP logic for --version/version funtions 1KB, --usage/--help 833 bytes, main() 225 bytes and the whole static data of the binary is 1KB Jan 26, 2018 at 9:04
27

Stripping them down to core functionality and writing in assembler yields far smaller binaries.

Original true/false binaries are written in C, which by its nature pulls in various library + symbol references. If you run readelf -a /bin/true this is quite noticeable.

352 bytes for a stripped ELF static executable (with room to save a couple bytes by optimizing the asm for code-size).

$ more true.asm false.asm
::::::::::::::
true.asm
::::::::::::::
global _start
_start:
 mov ebx,0
 mov eax,1     ; SYS_exit from asm/unistd_32.h
 int 0x80      ; The 32-bit ABI is supported in 64-bit code, in kernels compiled with IA-32 emulation
::::::::::::::
false.asm
::::::::::::::
global _start
_start:
 mov ebx,1
 mov eax,1
 int 0x80
$ nasm -f elf64 true.asm && ld -s -o true true.o     # -s means strip
$ nasm -f elf64 false.asm && ld -s -o false false.o
$ ll true false
-rwxrwxr-x. 1 steve steve 352 Jan 25 16:03 false
-rwxrwxr-x. 1 steve steve 352 Jan 25 16:03 true
$ ./true ; echo $?
0
$ ./false ; echo $?
1
$

Or, with a bit of a nasty/ingenious approach (kudos to stalkr), create your own ELF headers, getting it down to 132 127 bytes. We're entering Code Golf territory here.

$ cat true2.asm
BITS 64
  org 0x400000   ; _start is at 0x400080 as usual, but the ELF headers come first

ehdr:           ; Elf64_Ehdr
  db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident
  times 8 db 0
  dw  2         ; e_type
  dw  0x3e      ; e_machine
  dd  1         ; e_version
  dq  _start    ; e_entry
  dq  phdr - $$ ; e_phoff
  dq  0         ; e_shoff
  dd  0         ; e_flags
  dw  ehdrsize  ; e_ehsize
  dw  phdrsize  ; e_phentsize
  dw  1         ; e_phnum
  dw  0         ; e_shentsize
  dw  0         ; e_shnum
  dw  0         ; e_shstrndx
  ehdrsize  equ  $ - ehdr

phdr:           ; Elf64_Phdr
  dd  1         ; p_type
  dd  5         ; p_flags
  dq  0         ; p_offset
  dq  $$        ; p_vaddr
  dq  $$        ; p_paddr
  dq  filesize  ; p_filesz
  dq  filesize  ; p_memsz
  dq  0x1000    ; p_align
  phdrsize  equ  $ - phdr

_start:
  xor  edi,edi         ; int status = 0
      ; or  mov dil,1  for false: high bytes are ignored.
  lea  eax, [rdi+60]   ; rax = 60 = SYS_exit, using a 3-byte instruction: base+disp8 addressing mode
  syscall              ; native 64-bit system call, works without CONFIG_IA32_EMULATION

; less-golfed version:
;      mov  edi, 1    ; for false
;      mov  eax,252   ; SYS_exit_group from asm/unistd_64.h
;      syscall

filesize  equ  $ - $$      ; used earlier in some ELF header fields

$ nasm -f bin -o true2 true2.asm
$ ll true2
-rw-r--r-- 1 peter peter 127 Jan 28 20:08 true2
$ chmod +x true2 ; ./true2 ; echo $?
0
$
4
  • 2
    Comments are not for extended discussion; this conversation has been moved to chat.
    – terdon
    Jan 28, 2018 at 16:27
  • 4
    Also see this excellent write-up: muppetlabs.com/~breadbox/software/tiny/teensy.html
    – mic_e
    Jan 28, 2018 at 21:38
  • 3
    You're using the int 0x80 32-bit ABI in a 64-bit executable, which is unusual but supported. Using syscall wouldn't save you anything. The high bytes of ebx are ignored, so you could use 2-byte mov bl,1. Or of course xor ebx,ebx for zero. Linux inits integer registers to zero, so you could just inc eax to get 1 = __NR_exit (i386 ABI). Jan 28, 2018 at 23:48
  • 1
    I updated the code on your golfed example to use the 64-bit ABI, and golf it down to 127 bytes for true. (I don't see an easy way to manage less than 128 bytes for false, though, other than using the 32-bit ABI or taking advantage of the fact that Linux zeros registers on process startup, so mov al,252 (2 bytes) works. push imm8 / pop rdi would also work instead of lea for setting edi=1, but we still can't beat the 32-bit ABI where we could mov bl,1 without a REX prefix. Jan 29, 2018 at 0:17
4

The accepted answer by Rui F Ribeiro gives a lot of good information, but it's missing some and I feel like it leaves the reader with a misleading impression that the code size is small relative to "ELF overhead" and similar.

So my first suspicion was partly correct, whilst the library is using dynamic libraries, the /bin/true binary is big *because it has some static libraries included with it* (but that is not the only cause).

Static linking is at object file granularity (or even finer with --gc-sections), so it doesn't make sense to talk about a static library being "linked in"; the only part linked in is what's used, and the code size here is code that's actually (gratuitously) used by the coreutils version of true. It does not make sense to count it separately from what appears in true.c.

The extra space, almost 90% of the size of the binary, is indeed extra libraries/elf metadata.

The ELF metadata is pretty much entirely tables necessary for dynamic linking, and is nowhere near 90% of the size. These are the lines from size -A output relevant to it:

section               size      addr
.interp                 28       568
.gnu.hash               60       664
.dynsym               1416       728
.dynstr                676      2144
.gnu.version           118      2820
.gnu.version_r          96      2944
.rela.dyn              624      3040
.rela.plt             1104      3664
.plt                   752      4800
.plt.got                 8      5552
.dynamic               480   2125272
.got                    48   2125752
.got.plt               392   2125824

for a total of around 5.5k, or an average of about 100 bytes per dynamic symbol (not quite fair because some aren't on a per-symbol basis, but it's a somewhat meaningful figure).

One large contributor to size that Rui's answer didn't cover is:

.eh_frame_hdr          572     21728
.eh_frame             2908     22304

This 3.5k is DWARF unwind tables for C++ exception handling, introspective backtrace, etc. They're completely useless for true, but included by GCC policy in all output unless you explicitly omit them with a very verbosely-named option -fno-asynchronous-unwind-tables. The motivations behind this are explained in an answer by me on Stack Overflow.

So I would describe the final breakdown as:

  • 16k of program code and read-only (shareable) data
  • 5.5k of dynamic linking glue
  • 3.5k of unwind tables
  • <1k misc

In particular, it's notable that if the amount of code needed from dynamic-linked libraries were sufficiently small, static linking could be smaller than dynamic linking.

2
l $(which true false)
-rwxr-xr-x 1 root root 27280 Mär  2  2017 /bin/false
-rwxr-xr-x 1 root root 27280 Mär  2  2017 /bin/true

Pretty big on my Ubuntu 16.04 too. exactly the same size? What makes them so big?

strings $(which true)

(excerpt:)

Usage: %s [ignored command line arguments]
  or:  %s OPTION
Exit with a status code indicating success.
      --help     display this help and exit
      --version  output version information and exit
NOTE: your shell may have its own version of %s, which usually supersedes
the version described here.  Please refer to your shell's documentation
for details about the options it supports.
http://www.gnu.org/software/coreutils/
Report %s translation bugs to <http://translationproject.org/team/>
Full documentation at: <%s%s>
or available locally via: info '(coreutils) %s%s'

Ah, there is help for true and false, so let's try it:

true --help 
true --version
#

Nothing. Ah, there was this other line:

NOTE: your shell may have its own version of %s, which usually supersedes
    the version described here.

So on my system, it's /bin/true, not /usr/bin/true

/bin/true --version
true (GNU coreutils) 8.25
Copyright © 2016 Free Software Foundation, Inc.
Lizenz GPLv3+: GNU GPL Version 3 oder höher <http://gnu.org/licenses/gpl.html>
Dies ist freie Software: Sie können sie ändern und weitergeben.
Es gibt keinerlei Garantien, soweit wie es das Gesetz erlaubt.

Geschrieben von Jim Meyering.

LANG=C /bin/true --version
true (GNU coreutils) 8.25
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Jim Meyering.

So there is help, there is version information, binding to a library for internationalization. This explains much of the size, and the shell uses its optimized command anyway and most of the time.

1
  • Including static libraries, and half of the size of binary for elf metada. See my answer. Feb 18, 2018 at 19:13

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .