4

I am building a shared (.so) library that is composed of several .a files and a thin API layer that invokes them. I only want my API and external dependencies to be visible, so I build my code using the "hidden" visibility offered by GCC (-fvisibility=hidden).

However, one of the libraries is a proprietary third party .a file (which we have paid to use) and I only have access to its binary. When I link it statically into my .so file, its symbols are visible in my .so's dynamic symbol table. I'm guessing that this is because the library was not built with the hidden visibility options. I'd rather keep these functions hidden as the manage a sensitive part of our software and I don't want third parties linking to those symbols.

Is there any way that I can mark these symbols as "hidden" after the fact so that they do not appear in my .so file's symbol list? I have looked at objdump and objcopy but I'm having a hard time with the terminology.

Other things I have tried:

BareMetalCoder
  • 569
  • 5
  • 17
  • How are you compiling the final `.so` file precisely? You usually want to `strip` it after compiling. – Marco Bonelli May 04 '20 at 17:41
  • Yes, I run strip, but it does not remove symbols that are in the dynamic symbols table. As for how we build the .so file, it's hidden under a few layers of CMake. g++ / gcc definitely have -fvisibility=hidden when the individual files are compiled. Not sure if the .so file needs anything, but it seems to work for the code I compile. It's just this third party library that is giving me a hard time. – BareMetalCoder May 04 '20 at 20:58

1 Answers1

10

Here is a worked example of how to solve your problem.

This is the source for the proprietary static library that you can't recompile:

$ cat tpa.c
int tpa(void)
{
    return 2;
}
$ cat tpb.c
int tpb(void)
{
    return 3;
}

The library, libtp.a, must have been built essentially like this1:

$ gcc -fPIC -c -O1 tpa.c tpb.c
$ ar rcs libtp.a tpa.o tpb.o

The symbol tables of tpa.o and tpb.o are:-

$ readelf -s libtp.a

File: libtp.a(tpa.o)

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS tpa.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     9: 0000000000000000    10 FUNC    GLOBAL DEFAULT    1 tpa

File: libtp.a(tpb.o)

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS tpb.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     9: 0000000000000000    10 FUNC    GLOBAL DEFAULT    1 tpb

where you see that both of the function symbols tpa and tpb are GLOBAL ( = available for linkage) and have DEFAULT dynamic visibility, not HIDDEN.

Now here's the source code for your own static library, libus.a

$ cat usa.c
int usa(void)
{
    return 5;
}
$ cat usb.c
int usb(void)
{
    return 7;
}

Which you build like this:

$ gcc -fPIC -c -O1 -fvisibility=hidden usa.c usb.c
$ ar rcs libus.a usa.o usb.o

The function symbols in libus.a are also GLOBAL but their dynamic visibility is HIDDEN:-

$ readelf -s libus.a

File: libus.a(usa.o)

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS usa.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     9: 0000000000000000    10 FUNC    GLOBAL HIDDEN     1 usa

File: libus.a(usb.o)

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS usb.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    2 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    6 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     8: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     9: 0000000000000000    10 FUNC    GLOBAL HIDDEN     1 usb

Here's the source code for your shared library:

$ cat usc.c
extern int tpa(void);
extern int tpb(void);
extern int usa(void);
extern int usb(void);

int usc(void)
{
    return tpa() * tpb() * usa() * usb();
}

Which you compile:-

$ gcc -fPIC -c -O1 usc.c

Now you want to link usc.o, libtp.a and libus.a in your shared library libsus.so. If you do it the ordinary way:

$ gcc -shared -o libsus.so usc.o -L. -ltp -lus

then you find:

$ readelf --dyn-syms libsus.so

Symbol table '.dynsym' contains 8 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __cxa_finalize
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 0000000000001139    38 FUNC    GLOBAL DEFAULT   12 usc
     6: 000000000000115f    10 FUNC    GLOBAL DEFAULT   12 tpa
     7: 0000000000001169    10 FUNC    GLOBAL DEFAULT   12 tpb

that the HIDDEN visibility symbols from libus.a are absent from the dynamic symbol table, but the DEFAULT visibility symbols from libtp.a are included, which you don't want.

To exclude the latter as well, link your shared library as follows:

$ gcc -shared -o libsus.so usc.o -L. -ltp -lus -Wl,--exclude-libs=libtp.a

Then the dynamic symbol table becomes:

$ readelf --dyn-syms libsus.so

Symbol table '.dynsym' contains 6 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __cxa_finalize
     2: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     4: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
     5: 00000000000010f9    38 FUNC    GLOBAL DEFAULT   10 usc

as you want.

The linker option --exclude-libs is documented:

--exclude-libs lib,lib,...

Specifies a list of archive libraries from which symbols should not be automatically exported. The library names may be delimited by commas or colons. Specifying --exclude-libs ALL excludes symbols in all archive libraries from automatic export. ... For ELF targeted ports, symbols affected by this option will be treated as hidden.

For reassurance that the the tp* symbol definitions have been linked, you can still see them in the full symbol table of the shared library:

$ readelf -s libsus.so | egrep 'FUNC.*(us|tp)(a|b|c)' 
     5: 00000000000010f9    38 FUNC    GLOBAL DEFAULT   10 usc
    41: 0000000000001133    10 FUNC    LOCAL  DEFAULT   10 usa
    44: 000000000000111f    10 FUNC    LOCAL  DEFAULT   10 tpa
    46: 000000000000113d    10 FUNC    LOCAL  DEFAULT   10 usb
    48: 0000000000001129    10 FUNC    LOCAL  DEFAULT   10 tpb
    50: 00000000000010f9    38 FUNC    GLOBAL DEFAULT   10 usc

Just like the explicity hidden us* symbols, they become LOCAL, not available for further linkage. (You see usc twice in the grep because it is listed as both a global and a dynamic symbol).

And as you can infer from this, we need not have troubled to compile our own us* code with -fvisibility=hidden, as long as we were going to archive it in libus.a for further linkage. We could have linked the shared library like:

$ gcc -shared -o libsus.so usc.o -L. -ltp -lus -Wl,--exclude-libs=libtp.a,libus.a

with the same effect.


[1] I specify -fPIC explicitly to be sure of generating position-independent object code that I can link in a DSO, but this has been the GCC default since GCC 6.
Mike Kinghan
  • 55,740
  • 12
  • 153
  • 182
  • Beautifully crafted response, and works perfectly. I needed the --exclude-libs statement. Thanks! – BareMetalCoder May 05 '20 at 13:16
  • @mike-kinghan wow! This is an amazing answer. I have related question. I notice that you used "extern" to make the compiler aware of libtp and libus. Most real static libraries would provide their own header files that you include. Those may not use "extern". Any clue how to deal with that scenario? Perhaps I'll use your answer here to make a test case. – CraigDavid Mar 29 '22 at 20:49
  • 1
    I tested this example with `#include "tp.h"` instead of using the extern declarations. It made no difference. The concepts in this answer apply the same. – CraigDavid Mar 29 '22 at 21:15