Limiting visibility of symbols when linking shared libraries
Asked Answered
A

5

64

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?

Aerodyne answered 12/1, 2009 at 13:5 Comment(0)
R
90

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
Remora answered 17/1, 2009 at 7:51 Comment(2)
non-exported symbols would be instead listed with a lowercase t.Notwithstanding
Version scripts do not allow compiler to optimize code as well as -fvisibility=hidden.Nitza
T
51

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.

Tarrant answered 12/1, 2009 at 23:56 Comment(2)
This is what we do in Firefox, for example.Heddy
unless that is undocumented, it should be: __attribute__((visibility("default"))) You should consider revising your answer to reflect this. Also, your link is broken.Whitewall
E
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.

Eastlake answered 12/10, 2009 at 23:4 Comment(1)
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.Kalmick
C
7

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:

Claire answered 23/10, 2019 at 16:39 Comment(4)
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".Vasili
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.Vasili
@Vasili - Good points. (Luckily my project exposes only a C API so it doesn't face these issues.)Claire
Thanks, I just wanted to save people who read your great post the disappointment that comes from applying it to C++ :-)Vasili
M
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).

Melitamelitopol answered 27/2, 2017 at 22:14 Comment(1)
Same comment as in EmployedRussian's response - this generates suboptimal code compared to -fvisibility=hidden.Nitza

© 2022 - 2024 — McMap. All rights reserved.