linker

An example of Linux/glibc symbol versioning.

September 20, 2019 C/C++ development and debugging. , , , , , , ,

Here’s a little introduction to Linux/glibc symbol versioning.  The sources, linker version scripts, and makefile from this post can be found here in github.

The definitive reference for this topic is Ulrich Drepper’s dsohowto document.

Linux symbol versioning is a very logical way to construct a definitive API for your product.  If you are careful, you can have a single version of your shared library that is binary compatible with all previous versions of code that have linked it, and additionally hide all symbols that you don’t want your API consumer to be able to use (as if all non-exported symbols have a “static” like scope within the library itself).

That visibility hiding can be done in other ways (such as using static, or special compiler options), but utilizing a symbol version linker script to do so also has all the additional binary compatibility related benefits.

Suppose that we have a “v1.1” API with the following implementation:

#include <stdio.h>

void internal_function_is_not_visible()
{
   printf( "You can't call me directly.\n" );
}

void foo( int x )
{
   internal_function_is_not_visible();

   printf( "foo@@V1: %d\n", x );
}

and we build this into a shared library like so:

$ make libfoo.1.so
rm -f libfoo.1.so
cc foo.c -g -Wl,--version-script=symver.map -fpic -o libfoo.1.so -Wl,-soname,libfoo.so -shared
rm -f libfoo.so
ln -s libfoo.1.so libfoo.so

Everything here is standard for building a shared library except for the –version-script option that is passed into the linker with -Wl,. That version script file has the following contents:

$ cat symver.map
MYSTUFF_1.1 {
	  global:
foo;
	  local: *;
	};

This defines a “V1.1” API where all the symbols that are exported with a symbol version @@MYSTUFF_1.1. Note that internal_function_is_not_visible is not in that list. It’s covered in the local: catch-all portion of the symbol version file. Code that calls foo does not look out of the ordinary:

void foo(int);

int main()
{
   foo(1);

   return 0;
}

Compiling and linking that code is also business as usual:

$ make caller1
cc caller.c -g -Wl,-rpath,`pwd` -o caller1 -lfoo -L.

However, look at the foo symbol reference that we have for this program:

$ nm caller1 | grep foo
                 U foo@@MYSTUFF_1.1

If we run this, we get:

$ ./caller1
You can't call me directly.
foo@@V1: 1

If you add in a call to internal_function_is_not_visible() you’ll see that compilation fails:

void foo(int);
void internal_function_is_not_visible();

int main()
{
   foo(1);
   internal_function_is_not_visible();

   return 0;
}

$ make caller1
cc caller.c -g -Wl,-rpath,`pwd` -o caller1 -lfoo -L.
/run/user/1002/ccqEPYcu.o: In function `main':
/home/pjoot/symbolversioning/caller.c:7: undefined reference to `internal_function_is_not_visible'
collect2: error: ld returned 1 exit status
make: *** [caller1] Error 1

This is because internal_function_is_not_visible is not a visible symbol. Cool. We now have versioned symbols and symbol hiding. Suppose that we now want to introduce a new binary incompatible change too our foo API, but want all existing binaries to still work unchanged. We can do so by introducing a symbol alias, and implementations for both the new and the OLD API.

#include <stdio.h>

void internal_function_is_not_visible()
{
   printf( "You can't call me directly.\n" );
}

void foo2( int x, int y )
{
   if ( y < 2 )
   {
      internal_function_is_not_visible();
   }
   printf( "foo@@V2: %d %d\n", x, y );
}

void foo1( int x )
{
   internal_function_is_not_visible();
   printf( "foo@@V1: %d\n", x );
}

This is all standard C up to this point, but we now add in a little bit of platform specific assembler directives (using gcc specific compiler sneaks) :

#define V_1_2 "MYSTUFF_1.2"
#define V_1_1 "MYSTUFF_1.1"

#define SYMVER( s ) \
    __asm__(".symver " s )

SYMVER( "foo1,foo@" V_1_1 );
SYMVER( "foo2,foo@@" V_1_2 );

We’ve added a symbol versioning alias for foo@@MYSTUFF_1.2 and foo@MYSTUFF_1.1. The @@ one means that it applies to new code, whereas the @MYSTUFF_1.1 is a load only function, and no new code can use that symbol. In the symbol version script we now introduce a new version stanza:

$ cat symver.2.map
MYSTUFF_1.2 {
	  global:
foo;
};
MYSTUFF_1.1 {
	  global:
foo;
	  local: *;
};

$ make libfoo.2.so
rm -f libfoo.2.so
cc foo.2.c -g -Wl,--version-script=symver.2.map -fpic -o libfoo.2.so -Wl,-soname,libfoo.so -shared
rm -f libfoo.so
ln -s libfoo.2.so libfoo.so

If we call the new V1.2 API program, like so:

void foo(int, int);
int main()
{
   foo(1, 2);
   return 0;
}

our output is now:

$ ./caller2
foo@@V2: 1 2

Our “binary incompatible changes” are two fold. We don’t call internal_function_is_not_visible if our new parameter is >= 2, and we print a different message.

If you look at the symbols that are referenced by the new binary, you’ll see that it now explicitly has a v1.2 dependency:

$ nm caller2 | grep foo
                 U foo@@MYSTUFF_1.2

peeking into relocation of function static in shared library

February 27, 2017 C/C++ development and debugging. , , , , , , , ,

Here’s GUI (TUI) output of a function with a static variable access:

B+ |0x7ffff7616800 <st32>           test   %edi,%edi                                                                                                                     |
   |0x7ffff7616802 <st32+2>         je     0x7ffff7616811 <st32+17>                                                                                                      |
   |0x7ffff7616804 <st32+4>         mov    %edi,%eax                                                                                                                     |
   |0x7ffff7616806 <st32+6>         bswap  %eax                                                                                                                          |
   |0x7ffff7616808 <st32+8>         mov    %eax,0x200852(%rip)        # 0x7ffff7817060 <st32.yst32>                                                                        |
   |0x7ffff761680e <st32+14>        mov    %edi,%eax                                                                                                                     |
   |0x7ffff7616810 <st32+16>        retq                                                                                                                                 |
  >|0x7ffff7616811 <st32+17>        mov    0x200849(%rip),%edi        # 0x7ffff7817060 <st32.yst32>                                                                        |
   |0x7ffff7616817 <st32+23>        bswap  %edi                                                                                                                          |
   |0x7ffff7616819 <st32+25>        mov    %edi,%eax                                                                                                                     |
   |0x7ffff761681b <st32+27>        retq                                                                                                                                 |
   |0x7ffff761681c <_fini>          sub    $0x8,%rsp                                                                                                                     |
   |0x7ffff7616820 <_fini+4>        add    $0x8,%rsp                                                                                                                     |
   |0x7ffff7616824 <_fini+8>        retq                                                                                                                                 |
   |0x7ffff7616825                  add    %al,(%rcx)                                                                                                                    |
   |0x7ffff7616827 <x16+1>          add    (%rcx),%al                                                                                                                    |
   +---------------------------------------------------------------------------------------------------------------------------------------------------------------------+

The associated code is:

int st32( int v ) {
    static int yst32 = 0x1a2b3c4d;

    if ( v ) {
        yst32 = v;
    }

    return yst32;
}

The object code dump (prior to relocation) just has zeros in the offset for the variable:

$ objdump -d g.bs.o | grep -A12 '<st32>'
0000000000000050 <st32>:
  50:   85 ff                   test   %edi,%edi
  52:   74 0d                   je     61 <st32+0x11>
  54:   89 f8                   mov    %edi,%eax
  56:   0f c8                   bswap  %eax
  58:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 5e <st32+0xe>
  5e:   89 f8                   mov    %edi,%eax
  60:   c3                      retq   
  61:   8b 3d 00 00 00 00       mov    0x0(%rip),%edi        # 67 <st32+0x17>
  67:   0f cf                   bswap  %edi
  69:   89 f8                   mov    %edi,%eax
  6b:   c3                      retq   

The linker has filled in the real offsets in question, and the dynamic loader has collaborated to put the data segment in the desired location.

The observant reader may notice bwsap instructions in the listings above that don’t make sense for x86_64 code. That is because this code is compiled with an LLVM pass that performs byte swapping at load and store points, making it big endian in a limited fashion.

The book Linkers and Loaders has some nice explanation of how relocation works, but I wanted to see the end result first hand in the debugger. It turned out that my naive expectation that the sum of $rip and the constant relocation factor is the address of the global variable (actually static in this case) is incorrect. Check that out in the debugger:

(gdb) p /x 0x200849+$rip
$1 = 0x7ffff781705a

(gdb) x/10 $1
0x7ffff781705a <gy+26>: 0x22110000      0x2b1a4433      0x00004d3c      0x00000000
0x7ffff781706a: 0x00000000      0x00000000      0x00000000      0x30350000
0x7ffff781707a: 0x20333236      0x64655228

My magic value 0x1a2b3c4d looks like it is 6 bytes into the $rip + 0x200849 location that the disassembly appears to point to, and that is in fact the case:

(gdb) x/10 $1+6
0x7ffff7817060 <st32.yst32>:      0x4d3c2b1a      0x00000000      0x00000000      0x00000000
0x7ffff7817070 <y32>:   0x00000000      0x00000000      0x32363035      0x52282033
0x7ffff7817080: 0x48206465      0x34207461

My guess was the mysterious offset of 6 required to actually find this global address was the number of bytes in the MOV instruction, and sure enough that MOV is 6 bytes long:

(gdb) disassemble /r
Dump of assembler code for function st32:
   0x00007ffff7616800 <+0>:     85 ff   test   %edi,%edi
   0x00007ffff7616802 <+2>:     74 0d   je     0x7ffff7616811 <st32+17>
   0x00007ffff7616804 <+4>:     89 f8   mov    %edi,%eax
   0x00007ffff7616806 <+6>:     0f c8   bswap  %eax
   0x00007ffff7616808 <+8>:     89 05 52 08 20 00       mov    %eax,0x200852(%rip)        # 0x7ffff7817060 <st32.yst32>
   0x00007ffff761680e <+14>:    89 f8   mov    %edi,%eax
   0x00007ffff7616810 <+16>:    c3      retq
=> 0x00007ffff7616811 <+17>:    8b 3d 49 08 20 00       mov    0x200849(%rip),%edi        # 0x7ffff7817060 <st32.yst32>
   0x00007ffff7616817 <+23>:    0f cf   bswap  %edi
   0x00007ffff7616819 <+25>:    89 f8   mov    %edi,%eax
   0x00007ffff761681b <+27>:    c3      retq
End of assembler dump.

So, it appears that the %rip reference in the disassembly is really the value of the instruction pointer after the instruction executes, which is curious.

Note that this 4 byte relocation requires the shared library code segment and the shared library data segment be separated by no more than 4G. The linux dynamic loader has put all the segments back to back so that this is the case. This can be seen from /proc/PID/maps for the process:

$ ps -ef | grep maindl
pjoot    17622 17582  0 10:50 pts/3    00:00:00 /home/pjoot/workspace/pass/global/maindl libglobtestbs.so

$ grep libglob /proc/17622/maps
7ffff7616000-7ffff7617000 r-xp 00000000 fc:00 2492653                    /home/pjoot/workspace/pass/global/libglobtestbs.so
7ffff7617000-7ffff7816000 ---p 00001000 fc:00 2492653                    /home/pjoot/workspace/pass/global/libglobtestbs.so
7ffff7816000-7ffff7817000 r--p 00000000 fc:00 2492653                    /home/pjoot/workspace/pass/global/libglobtestbs.so
7ffff7817000-7ffff7818000 rw-p 00001000 fc:00 2492653                    /home/pjoot/workspace/pass/global/libglobtestbs.so

We’ve got a read-execute mmap region, where the code lies, and a read-write mmap region for the data. There’s a read-only segment which I presume is for read only global variables (my shared lib has one such variable and we have one page worth of space allocated for read only memory).

I wonder what the segment that has none of the read, write, nor execute permissions set is?

New book for work: Linkers and Loaders

February 24, 2017 C/C++ development and debugging. , ,

Fresh off the press:

I got this book to get some background on relocation of ELF globals, and was surprised to find a bit on z/OS (punch card compatible!) object format layout:

… an interesting bonus that’s topical.