Saturday, March 8, 2014

Static linking with gcc and g++

In this tutorial, we will explain what the static linking is, how this affect the size of final binary, and why statically linking with g++ sometimes is pain. By definition, a statically compiled binary is a group of programmer ‘s routines, external functions, and variables which are packed into the final binary executable. The compiler or the linker produces the final object and embeds all the functions and variables and the linking phase. 



There are two reasons of using dynamic linking and shared libraries: 1) Avoid creating a huge binary, if all the programs use a standard set of libraries why not having the operating system providing to them 2) Compatibility on operating system and machine dependant characteristics: sometimes the libraries must be implemented based on the architecture or the operating system and using dynamic linking is an easy way to avoid this catch. On the other hand, static linking is the ideal way of distributing one software product, paying of course the cost of larger file size compared with the dynamic linking. A programmer can create a binary that has no dependencies in different library editions. New linker options for g++ 4.5 make this information applicable also in g++.

We will use three other different tools except gcc/g++: the nm that list symbols from object files, the ldd that print shared library dependencies and the size that list section sizes and total size.

For this tutorial we will start by using a simple c test file:

:~/toys/c-tests$ cat test3.c 
#include <stdio.h>
#include <stdlib.h>

#define MAX  43
long long unsigned int array[MAX];

int main(int argc, char **argv){
   int i;
   for (i=0;i<MAX;i++)
     array[i]=(rand()%128)*5;

   for (i=0;i<MAX;i++)
     printf(" %4llu",array[i]);

   return 0;
}

The file includes one call to rand and one call to printf function. Let's compile the file without any flag:

:~/toys/c-tests$ gcc test3.c 

Now using the nm we can find the undefined functions.

:~/toys/c-tests$ nm -g a.out 
00000000004006e8 R _IO_stdin_used
                 w _Jv_RegisterClasses
0000000000600e30 D __DTOR_END__
0000000000601028 A __bss_start
0000000000601018 D __data_start
0000000000601020 D __dso_handle
                 w __gmon_start__
0000000000400600 T __libc_csu_fini
0000000000400610 T __libc_csu_init
                 U [email protected]@GLIBC_2.2.5
0000000000601028 A _edata
00000000006011b8 A _end
00000000004006d8 T _fini
0000000000400428 T _init
0000000000400480 T _start
0000000000601060 B array
0000000000601018 W data_start
0000000000400564 T main
                 U [email protected]@GLIBC_2.2.5
                 U [email protected]@GLIBC_2.2.5
:~/toys/c-tests$

We can see that the functions __libc_start_main (entry point of the program), printf, and rand are undefined. Next we use the ldd to see the shared files dependencies:

:~/toys/c-tests$ ldd a.out 
linux-vdso.so.1 =>  (0x00007fffec5ff000)
libc.so.6 => /lib/libc.so.6 (0x00007f528d2ec000)
/lib64/ld-linux-x86-64.so.2 (0x00007f528d65c000)

Finally using the size tool we see that the size of the executable is actually something more that 2KBytes.

:~/toys/c-tests$ size a.out 
   text    data     bss     dec     hex filename
   1363     528     376    2267     8db a.out
:~/toys/c-tests$ 


Next step is to create a static linking binary. We use information from the GCC linker manual.

:~/toys/c-tests$ gcc -static test3.c 
:~/toys/c-tests$ ldd a.out 
not a dynamic executable

The printout of the nm is much bigger because of libc liking. Unfortunately no the binary is much bigger around 624KBytes:

:~/toys/c-tests$ size a.out 
   text    data     bss     dec     hex filename
 608112    3648   12824  624584   987c8 a.out

To decrease the file size of final binary you can use some linking flags such as -Wl,-dead_strip,-gc-sections  after using -ffunction-sections -fdata-sections compiler flags. However, gc-sections is gcc and architecture depentant so use it carefully. Sometimes it  does not even run! For example:

:~/toys/c-tests$ gcc -static  -ffunction-sections -fdata-sections -Wl,-dead_strip,-gc-sections   test3.c 
/usr/bin/ld: warning: cannot find entry symbol ad_strip; defaulting to 00000000004001d0

A warning... who cares?

:~/toys/c-tests$ size a.out 
   text    data     bss     dec     hex filename
 577481    3392   11016  591889   90811 a.out

Good! We decrease the file size to 577 KBytes!

:~/toys/c-tests$ ./a.out 
Segmentation fault
:~/toys/c-tests$ 

Oups, it seems that we must be more careful with the warnings! Tip: remove the '-dead_strip' flag!

The story of g++ is a bit different. g++ uses also the libstdc++ except of libc, so a simple -static-libgcc  or -static is not enough. Fortunately a new option introduced in gcc-4.5, the -static-libstdc++ that solves this problem. You can find more information about this issue in this blog, dated from 2005. Let's make the same experiment with the g++ 4.6.
:~/toys/c-tests$ cat test4.cpp 
#include <iostream>
#include <vector>
#include <cstdio>

using namespace std;

int main(){
  vector<int> coll;    // vector container for int

  // append elements 

  for (int i=1; i<=6; ++i) {
    coll.push_back(i);
  }

  // print all elements 

  for (int i=0; i<coll.size(); ++i) {
    cout << coll[i] << ' ';
  }
  cout << endl;  
  printf("Test!\n");
}

Let's  try again to see if we are missing any symbol:

:~/toys/c-tests$ g++-4.6 test4.cpp 
:~/toys/c-tests$ nm a.out | grep " U "
                 U _Unwind_Resume
                 U _ZNSolsEPFRSoS_E
                 U _ZNSolsEi
                 U _ZNSt8ios_base4InitC1Ev
                 U _ZNSt8ios_base4InitD1Ev
                 U _ZSt17__throw_bad_allocv
                 U _ZSt20__throw_length_errorPKc
                 U _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
                 U _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_c
                 U _ZdlPv
                 U _Znwm
                 U __cxa_atexit
                 U __cxa_begin_catch
                 U __cxa_end_catch
                 U __cxa_rethrow
                 U __gxx_personality_v0
                 U __libc_start_main
                 U memmove
                 U puts

Many, let's size the size:

:~/toys/c-tests$ size a.out 
   text   data    bss    dec    hex filename
   8711    712    304   9727   25ff a.out

8,7 KB is a nice number, but what happens if we statically link the binary? In latest versions of g++ usually only "--static" flag is necessary:

:~/toys/c-tests$ g++-4.6 test4.cpp --static
:~/toys/c-tests$ size   a.out 
   text   data    bss    dec    hex filename
1313651  10672  94441 1418764 15a60c a.out

Huge! Lets see if we decrease a bit the size. We are going to use the '-gc-sections' of the linker. 

:~/toys/c-tests$ g++-4.6 test4.cpp --static  -Wl,-gc-sections,--print-gc-sections    -ffunction-sections -fdata-sections 2>&1 | grep removing | wc -l

2585

We removed 2585 symbols not bad. Lets calculate the size of the binary:

:~/toys/c-tests$ size a.out 
   text   data    bss    dec    hex filename

1094767  10392  85673 1190832 122bb0 a.out

We saved 219 KB. It is not munch, but it is the best that we can do.