Limiting visibility of symbols when linking shared libraries

Viewed 34100

Some platforms mandate that you provide a list of a shared library's external symbols to the linker. However, on most unixish systems that's not necessary: all non-static symbols will be available by default.

My understanding is that the GNU toolchain can optionally restrict visibility just to symbols explicitly declared. How can that be achieved using GNU ld?

5 Answers

GNU ld can do that on ELF platforms.

Here is how to do it with a linker version script:

/* foo.c */
int foo() { return 42; }
int bar() { return foo() + 1; }
int baz() { return bar() - 1; }

gcc -fPIC -shared -o libfoo.so foo.c && nm -D libfoo.so | grep ' T '

By default, all symbols are exported:

0000000000000718 T _fini
00000000000005b8 T _init
00000000000006b7 T bar
00000000000006c9 T baz
00000000000006ac T foo

Let's say you want to export only bar() and baz(). Create a "version script" libfoo.version:

FOO {
  global: bar; baz; # explicitly list symbols to be exported
  local: *;         # hide everything else
};

Pass it to the linker:

gcc -fPIC -shared -o libfoo.so foo.c -Wl,--version-script=libfoo.version

Observe exported symbols:

nm -D libfoo.so | grep ' T '
00000000000005f7 T bar
0000000000000609 T baz

I think the easiest way of doing that is adding the -fvisibility=hidden to gcc options and explicitly make visibility of some symbols public in the code (by __attribute__((visibility("default")))). See the documentation here.

There may be a way to accomplish that by ld linker scripts, but I don't know much about it.

Seems there's several ways to manage exported symbols on GNU/Linux. From my reading these are the 3 methods:

  • Source code annotation/decoration:
    • Method 1: -fvisibility=hidden along with __attribute__((visibility("default")))
    • Method 2 (since GCC 4): #pragma GCC visibility
  • Version Script:
    • Method 3: Version script (aka "symbol maps") passed to the linker (eg. -Wl,--version-script=<version script file>)

I won't get into examples here since they're mostly covered by other answers, but here's some notes, pros & cons to the different approaches off the top of my head:

  • Using the annotated approach allows the compiler to optimize the code a bit (one less indirection).
  • If using the annotated approach, then consider also using strip --strip-all --discard-all.
  • The annotated approach can add more work for internal function-level unit tests since the unit tests may not have access to the symbols. This might require building separate files: one for internal development & testing, and another for production. (This approach is generally non-optimal from a unit test purist perspective.)
  • Using a version script loses the optimization but allows symbol versioning which seems to not be available with the annotated approach.
  • Using a version script allows for unit testing assuming code is first built into an archive (.a) file and then linked into a DSO (.so). The unit tests would link with the .a.
  • Version scripts are not supported on Mac (at least not if using the linker provided by Mac, even if using GCC for the compiler), so if Mac is needed use the annotated approach.

I'm sure there are others.

Here's some references (with examples) that I've found helpful:

Related