58

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?

Die in Sente
  • 9,546
  • 3
  • 35
  • 41
rafl
  • 11,980
  • 2
  • 55
  • 77

5 Answers5

84

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
Employed Russian
  • 199,314
  • 34
  • 295
  • 362
49

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.

a3f
  • 8,517
  • 1
  • 41
  • 46
jpalecek
  • 47,058
  • 7
  • 102
  • 144
8

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.

  • Are you sure that this is true for calling functions? In my experience, I get identical code for calling hidden and exported functions. Just regular calls with a `R_X86_64_PLT32` relocation. But for accessing exported variables, there is a difference in that it has to look it up in the GOT first. – knatten Apr 21 '23 at 07:20
6

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:

codesniffer
  • 1,033
  • 9
  • 22
  • 1
    An important point is that version scripts are hard to get right for C++. You need to identify all the necessary compiler-generated exception-related symbols yourself, and symbol name matching happens on the level of mangled names which means you will have to use a fragile set of wildcards. This is exacerbated by the fact that the documentation gives no hints at all with respect to correct usage for C++. After shipping one library with a version script, our conclusion was "never again". – tobi_s Aug 21 '20 at 06:20
  • 2
    Let me add: header-only C++ libraries can wreak total havoc with the version script approach: the unix dynamic linker allows symbols in a later-loaded dynamic library override symbols in an earlier loaded one. Now imagine that you have two libraries use different versions of the same header-only library and the earlier one accidentally exposed a symbol or two while the second didn't bother hiding them at all. You will get crashes with amazing backtraces going back and forth between the two .so files as soon as your code hits a function from the header-only library that wasn't inlined. – tobi_s Aug 21 '20 at 06:25
  • @tobi_s - Good points. (Luckily my project exposes only a C API so it doesn't face these issues.) – codesniffer Aug 21 '20 at 09:42
  • Thanks, I just wanted to save people who read your great post the disappointment that comes from applying it to C++ :-) – tobi_s Aug 22 '20 at 10:07
1

If you are using libtool, there is another option much like Employed Russian's answer.

Using his example, it would be something like:

cat export.sym
bar
baz

Then run libtool with the following option:

libtool -export-symbols export.sym ...

Note that when using -export-symbols all symbols are NOT exported by default, and only those in export.sym are exported (so the "local: *" line in libfoo.version is actually implicit in this approach).

aaa90210
  • 11,295
  • 13
  • 51
  • 88
  • 1
    Same comment as in EmployedRussian's response - this generates suboptimal code compared to `-fvisibility=hidden`. – yugr Apr 23 '18 at 21:41