Overriding HIDDEN symbol-visibility with a gnu ld linker script
Asked Answered
A

0

7

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?

Aggravate answered 5/6, 2020 at 3:49 Comment(4)
I've also tried using a __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 with var1 causes a section type conflict with var2. It'd be a pain to collate anyway.Aggravate
Related: https://mcmap.net/q/340995/-version-script-and-hidden-visibility/398670Aggravate
This may not be a good idea - by the time you try to change visibility in linker, compiler might have already performed optimizations which rely on hidden symbols (removed unused functions, inlined/cloned them, etc.) and there's no way to undo such optimizations at link time.Wrangler
@tmm1 IIRC I didn't and gave up in the endAggravate

© 2022 - 2024 — McMap. All rights reserved.