Limiting visibility of symbols when linking shared libraries

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.


The code generated to call any exported functions or use any exported globals is less efficient than those that aren't exported. There is an extra level of indirection involved. This applies to any function that might be exported at compile time. gcc will still produce extra indirection for a function that is later un-exported by a linker script. So using the visibility attribute will produce better code than the linker script.


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:

  • http://blog.fesnel.com/blog/2009/08/19/hiding-whats-exposed-in-a-shared-library/
  • https://accu.org/index.php/journals/1372
  • https://akkadia.org/drepper/dsohowto.pdf