TL;DR: Can I use a GNU ld
linker --version-script
or some other method to promote selected symbols with hidden
visibility (due to -fvisibility=hidden
or an explicit __attribute__
) back to default
visibility, so they are available in the global symbol table of a shared library?
Is there any way I can tell a gnu ld version-script to promote the HIDDEN
symbols in the global:
version-script section to DEFAULT
visibility?
For a rather weird system I need to link a shared library such that all non-static function symbols have default visibility, but all other symbols get "hidden" visibility unless explicitly annotated with appropriate visibility attributes.
If there was a way to tell gcc
something like the (invalid imaginary syntax) -fvisibility=hidden -fvisibility-functions=default
, that'd be perfect.
Compiling with gcc's -fvisibility=hidden
will give all non-annotated symbols hidden visibility.
So I thought I'd promote the functions that have global scope and hidden visibility in the ELF objects to default (global) visibility using a linker version-script.
However it seems like GNU LD doesn't change the visibility attribute when a symbol is named in the linker version-script global
section. Symbols in the local
section get hidden, but already-hidden symbols in the global
section don't get promoted to default
.
SCCCE
Given simplified object (macros expanded, etc):
/* Public global variables are annotated */
extern int exportvar __attribute__ ((visibility ("default")));
int exportvar;
/* Internal ones are not annotated */
extern int nonexportvar;
int nonexportvar;
/* Nor are static vars obviously */
static int nonexportvar;
/* For various weird reasons we can't annotate functions with visibility information */
extern void exportfunc(void);
void exportfunc(void) {
};
static void staticfunc(void) {
};
with linker script:
Linker script snippet linker-script
:
{
global:
exportfunc;
exportvar;
local:
*;
};
built using Makefile
(de-tabified for easy copy/paste):
.RECIPEPREFIX=~
USE_LLVM?=0
VERBOSE_SYMS?=
EXTRA_CFLAGS?=
LINK_FLAGS?=
# Don't optimise so we retain the unused statics etc in this
# demo code.
EXTRA_CFLAGS+=-O0
ifneq (,$(LINKER_SCRIPT))
LINK_FLAGS+=--version-script=$(LINKER_SCRIPT)
endif
ifeq (1,$(USE_LLVM))
CC=clang -c $(EXTRA_CFLAGS)
LINK_SHARED=ld.lld -shared $(LINK_SHARED)
else
CC=gcc -c $(EXTRA_CFLAGS)
COMMA:=,
LINK_SHARED=gcc -shared $(addprefix -Wl$(COMMA),$(LINK_FLAGS))
endif
all: clean demo.so dumpsyms
clean:
~ @rm -f demo.o demo.so
demo.o: demo.c
~ $(CC) -o $@ $<
demo.so: demo.o
~ $(LINK_SHARED) -o $@ $<
# Show object file and full symbol table if VERBOSE_SYMS=1
ifneq (,$(VERBOSE_SYMS))
dumpsyms: demo.o demo.so
~ @echo
~ @echo "demo.o:"
~ @readelf --syms demo.o | egrep '(Symbol table|exportvar|nonexportvar|staticvar|exportfunc|nonexportfunc)'
~ @echo
~ @echo "demo.so:"
~ @readelf --syms demo.so | egrep '(Symbol table|exportvar|nonexportvar|staticvar|exportfunc|staticfunc)'
~ @echo
else
# Only show dynamic symbols by default
dumpsyms: demo.o demo.so
~ @echo
~ @echo "demo.so:"
~ @readelf --dyn-syms demo.so | egrep '(Symbol table|exportvar|nonexportvar|staticvar|exportfunc|nonexportfunc)'
~ @echo
endif
With default flags (no visibility, no linker script)
$ make
gcc -c -O0 -o demo.o demo.c
gcc -Wall -O0 -shared -o demo.so demo.o
demo.so:
Symbol table '.dynsym' contains 8 entries:
5: 00000000000010f9 7 FUNC GLOBAL DEFAULT 11 exportfunc
6: 0000000000004028 4 OBJECT GLOBAL DEFAULT 21 nonexportvar
7: 0000000000004024 4 OBJECT GLOBAL DEFAULT 21 exportvar
With linker script and default visibility
make LINKER_SCRIPT=linker-script
gcc -c -O0 -o demo.o demo.c
gcc -Wall -O0 -Wl,--version-script=linker-script -shared -o demo.so demo.o
demo.so:
Symbol table '.dynsym' contains 7 entries:
5: 0000000000004024 4 OBJECT GLOBAL DEFAULT 21 exportvar
6: 00000000000010f9 7 FUNC GLOBAL DEFAULT 11 exportfunc
nonexportvar
has vanished from the dynamic symbol table as expected.
With -fvisibility=hidden
and no linker script
$ make EXTRA_CFLAGS="-fvisibility=hidden"
gcc -c -fvisibility=hidden -o demo.o demo.c
gcc -Wall -fvisibility=hidden -shared -o demo.so demo.o
demo.so:
Symbol table '.dynsym' contains 6 entries:
5: 0000000000004024 4 OBJECT GLOBAL DEFAULT 21 exportvar
exportfunc
is not visible in the export symbol table. That's expected, since no linker script would override visibility.
With -fvisibility=hidden
and linker script
Can we use the linker script to make the hidden function symbols visible?
$ make EXTRA_CFLAGS="-fvisibility=hidden" LINKER_SCRIPT=linker-script
gcc -c -fvisibility=hidden -o demo.o demo.c
gcc -Wall -fvisibility=hidden -Wl,--version-script=linker-script -shared -o demo.so demo.o
demo.so:
Symbol table '.dynsym' contains 6 entries:
5: 0000000000004024 4 OBJECT GLOBAL DEFAULT 21 exportvar
... it seems not.
Why?
$ make EXTRA_CFLAGS="-fvisibility=hidden" LINKER_SCRIPT=linker-script VERBOSE_SYMS=1
gcc -c -fvisibility=hidden -o demo.o demo.c
gcc -Wall -fvisibility=hidden -Wl,--version-script=linker-script -shared -o demo.so demo.o
demo.o:
Symbol table '.symtab' contains 13 entries:
5: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 staticvar
10: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 exportvar
11: 0000000000000004 4 OBJECT GLOBAL HIDDEN 3 nonexportvar
12: 0000000000000000 7 FUNC GLOBAL HIDDEN 1 exportfunc
demo.so:
Symbol table '.dynsym' contains 6 entries:
5: 0000000000004024 4 OBJECT GLOBAL DEFAULT 21 exportvar
Symbol table '.symtab' contains 52 entries:
33: 000000000000402c 4 OBJECT LOCAL DEFAULT 21 staticvar
39: 00000000000010f9 7 FUNC LOCAL DEFAULT 11 exportfunc
42: 0000000000004028 4 OBJECT LOCAL DEFAULT 21 nonexportvar
50: 0000000000004024 4 OBJECT GLOBAL DEFAULT 21 exportvar
It looks like GNU ld
turned GLOBAL HIDDEN
symbols in the .o
into LOCAL DEFAULT
symbols in the .so
.
The linker script appears to have no effect here; the result is the same with or without it.
Is there any way I can tell the linker version-script to promote the HIDDEN
symbols in the global
section to DEFAULT
visibility?
Things I can't do
I cannot unfortunately drop the requirement for functions to be visible by default, while all other symbols should be visible only when explicitly annotated as such. I need to match the behaviour of a Windows build that uses a generated .def
file to export all functions while using __declspec__("dllexport")
to export only selected other symbols.
Due to codebase size, complexity, shared control, etc I can't go through and annotate every function with a suitable attribute macro.
I can't enumerate all symbols to be exported manually and maintain a handwritten linker script; the codebase has too many different configurations, versions and build options.
I could use a different widely-available compiler and toolchain if that'd help though, and toolchain versions are not a problem. I've tried using LLVM's clang
and ld.lld
with identical results.
Help? Ideas?
__attribute__(section(".globals")))
to move the explicitly-exported vars to a separate ELF section, but that doesn't work because both initialized and uninitialized, const and non-const vars get tagged. So gcc chokes withvar1 causes a section type conflict with var2
. It'd be a pain to collate anyway. – Aggravate