vinipsmaker
vinipsmaker

Reputation: 2397

Meson doesn't link “redundant” shared library

I wrote a library to override functions from libc in my binary, and I want to link it to the binary.

Both the library and the binary are defined in the same meson.build.

The library should appear early in the list so its functions are preferred over libc function. The decision whether to build a static or a shared library is a decision for the packager to make (not mine), so I use Meson's library().

That's how I define the library in meson:

libemilua_libc_service = library(
    'emilua-libc-service',
    libc_service_src,
    override_options : ['b_lundef=false'],
    dependencies : [ boost ],
    include_directories : include_directories(incdir),
    implicit_include_directories : false,
    version : meson.project_version(),
    install : true,
)

libemilua_libc_service_dep = declare_dependency(
    dependencies : [ libemilua_dep ],
    link_with : [libemilua_libc_service],
)

And that's how I use libemilua_libc_service_dep:

emilua_bin = executable(
    'emilua',
    ['src/main.cpp'],
    dependencies : [
        # libc_service must be linked first to override glibc functions
        libemilua_libc_service_dep,

        libemilua_dep,
        libemilua_main_dep,
    ],
    export_dynamic : get_option('enable_plugins'),
    include_directories : include_directories(incdir),
    implicit_include_directories : false,
    install : true,
)

However if I run ldd emilua to inspect the linked libraries, I see no mention of libemilua-libc-service:

linux-vdso.so.1 (0x00007ffdf8958000)
libemilua-main.so.0 => /home/vinipsmaker/Projects/emilua/build/./libemilua-main.so.0 (0x0000752bf824c000)
libc.so.6 => /usr/lib/libc.so.6 (0x0000752bf802e000)
libemilua.so.0 => /home/vinipsmaker/Projects/emilua/build/./libemilua.so.0 (0x0000752bf7600000)
libboost_context.so.1.86.0 => /usr/lib/libboost_context.so.1.86.0 (0x0000752bf8029000)
libluajit-5.1.so.2 => /usr/lib/libluajit-5.1.so.2 (0x0000752bf756e000)
libfmt.so.11 => /usr/lib/libfmt.so.11 (0x0000752bf8001000)
libncursesw.so.6 => /usr/lib/libncursesw.so.6 (0x0000752bf74ff000)
liburing.so.2 => /usr/lib/liburing.so.2 (0x0000752bf7ffa000)
libstdc++.so.6 => /usr/lib/libstdc++.so.6 (0x0000752bf7200000)
libgcc_s.so.1 => /usr/lib/libgcc_s.so.1 (0x0000752bf74d1000)
/lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x0000752bf82cc000)
libserd-0.so.0 => /usr/lib/libserd-0.so.0 (0x0000752bf74b9000)
libsord-0.so.0 => /usr/lib/libsord-0.so.0 (0x0000752bf74b0000)
libssl.so.3 => /usr/lib/libssl.so.3 (0x0000752bf7124000)
libcrypto.so.3 => /usr/lib/libcrypto.so.3 (0x0000752bf6c00000)
libcap.so.2 => /usr/lib/libcap.so.2 (0x0000752bf74a4000)
libpsx.so.2 => /usr/lib/libpsx.so.2 (0x0000752bf7ff1000)
libm.so.6 => /usr/lib/libm.so.6 (0x0000752bf6b11000)
libzix-0.so.0 => /usr/lib/libzix-0.so.0 (0x0000752bf7495000)

When I do run my software to make tests, I verify that indeed the function is not linked (standard functions from glibc are used instead).

If I run meson configure -Ddefault_library=static and rebuild the project, then libemilua-libc-service gets linked into the binary (but not shown in the list above which is fine because the library is now static and embedded into the executable). I run my tests and I verify that its functions are used in place of glibc functions.

What's wrong here? How to fix this? Why Meson isn't linking to my library and how to force it to link it? My project is cross-platform (Windows, Linux, FreeBSD) so bear that in mind when proposing system-specific compile flags (although it's okay to not support Windows for this small feature anyways).

Context

libemilua is a runtime for Lua programs that you can embed into your own C++ programs, so it's abstracted as such... as a library. /usr/bin/emilua is linked with this library. Not every C++ programmer would be okay in using a library that replaces/overrides functions from glibc, so I abstract the behavior for such into yet another library: libemilua-libc-service. libemilua-libc-service is only used if linked against the running binary (it overrides a few symbols from libemilua too so libemilua can access these symbols and detect whether it can assume libc functions were overriden). That explains the organization of the library.

libemilua doesn't depend on any symbol from libemilua-libc-service. None.

The purpose to override functions from libc is to disable ambient authority. Ambient authority is not disabled through this mechanism, but by proper sandboxing mechanisms (e.g. seccomp on Linux, and capsicum on FreeBSD). However disabling ambient authority will make most programs (and libraries!) not work. Then I override functions from libc when the process is in sandboxed mode. If the process is running sandboxed, my replacements functions from libc will forward requests over an unnamed UNIX socket to a process running outside of the sandbox that will act as a policy manager rejecting actions or performing actions on the behalf of the sandboxed process and sending the results back.

The code already works. When I use static linking or force the dynamic library to be loaded through LD_PRELOAD, I can verify the desired behavior. It's Meson who is biting me here.

Upvotes: 2

Views: 167

Answers (1)

Mike Kinghan
Mike Kinghan

Reputation: 61137

As per comments, you can coerce your libemilua_libc_service.so functions to be interposed before their libc equivalents in the shared library build by arranging the linkage commandline to contain:

-Wl,--no-as-needed libemilua-libc-service.so.0.10.1 -Wl,--as-needed

or equally:

-Wl,--push-state,--no-as-needed libemilua-libc-service.so.0.10.1 -Wl,--pop-state

This is coercing the static linker to consider the library needed even when it really isn't. The linker will then accept definitions from this library as resolving symbol references it needs to resolve as if those definitions came from an input object file, which the linker cannot reject - even if it would otherwise reject them in a shared library and conclude that the library is not needed.

That solution leaves it wholly unexplained why you can't have your shared libraries linked in logical dependency order and have interposition just work, per textbook, because libemilua_libc_service is input before libc. If I adequately understand your scenario, you can do that, and the obstruction you're wrestling with does not come from Meson, it comes from GCC.

A minimized Meson reproduction of the problem scenario

$ tail -n +1 *.c *.cpp *.build
==> puts.c <==
#include <stdio.h>

int puts_interposed(void)
{
    static int interposed = 1;
    return interposed;
}

int puts(char const *s)
{
    (void)puts_interposed();
    return fprintf(stdout,"%s with arg [%s]\n",__FUNCTION__,s);

}

==> emilua_func.cpp <==
extern "C" {
    int puts(const char *);
    // int puts_interposed(void);
}

void emilua_func()
{
    puts(__FUNCTION__);
    // puts(puts_interposed() ? "puts interposed" : "puts not_interposed");
}

==> emilua_main.cpp <==
void emilua_func();

void emilua_main()
{
    emilua_func();
}

==> main.cpp <==
void emilua_main();

int main()
{
    emilua_main();
    return 0;
}

==> meson.build <==
project(
    'emilua', 'cpp','c',
    default_options : ['cpp_std=c++20'],
    meson_version : '>=1.2.0',
    version : '0.10.1',
)

libemilua_libc_service = library(
        'emilua-libc-service',
        ['puts.c'],
        override_options : ['b_lundef=false'],
        implicit_include_directories : false,
        version : meson.project_version(),
        install : true
    )
    
libemilua = library(
    'emilua',
    ['emilua_func.cpp'],
    implicit_include_directories : false,
    version : meson.project_version(),
    install : true
)
    
libemilua_dep = declare_dependency(
    link_whole :
        (
            get_option('default_library') == 'static'
        ) ?
        [libemilua] : [],
    link_with :
        (
            get_option('default_library') == 'static'
        ) ?
        [] : [libemilua],
)

libemilua_libc_service_dep = declare_dependency(
    dependencies : [ libemilua_dep ],
    link_with : [libemilua_libc_service],
)

libemilua_main = library(
    'emilua-main',
    ['emilua_main.cpp'],
    link_with : libemilua,
    implicit_include_directories : false,
    version : meson.project_version(),
    install : true
)

libemilua_main_dep = declare_dependency(
    dependencies : [ libemilua_dep ],
    link_with : [libemilua_main],
)

emilua_bin = executable(
    'emilua',
    ['main.cpp'],
    dependencies : [
        # libc_service must be linked first to override glibc functions
        libemilua_libc_service_dep,
        libemilua_dep,
        libemilua_main_dep,
    ],
    implicit_include_directories : false,
    install : true
)

What we want foremost is to see our own definition of the libc dynamic symbol puts interposed in the linkage of program emilua, as per file puts.c.

The program emilua depends on libraries libemilua_libc_service, libemilua and libemilua_main (in that order per the meson.build).

We would also like this implementation of puts to enable its caller to introspect that it is our interposed definition and not libcs, but that is a secondary goal that will muddy the libc interposition problem, so for now the code in emilua_func.cpp that participates in the secondary goal is commented out. The puts.c code is "introspection ready" and I'll plug it in in the end.

Here is a Makefile that facilities repeated builds and cleans of the meson project:

$ cat Makefile 
.PHONY: all clean run

Q :=
stdout := 
ifndef VERBOSE
Q := @
stdout := > /dev/null
endif

configure :=
buildlog := build-shared-libs.log
ifeq ($(LIBS),shared)
configure := meson configure -Ddefault_library=shared build
else
ifeq ($(LIBS),static)
configure := meson configure -Ddefault_library=static build
buildlog := build-static-libs.log
endif
endif

all: ./build/emilua

./build/emilua:
    $(Q)meson setup build $(stdout)
    $(Q)$(configure) $(stdout)
    $(Q)meson compile --verbose -C build > $(buildlog) # stderr goes to console 
    
run: ./build/emilua
    $<

clean:
    $(Q)rm -fr build $(stdout) build.log

The pristine project tree is:

$ tree
.
├── emilua_func.cpp
├── emilua_main.cpp
├── main.cpp
├── Makefile
├── meson.build
└── puts.c

1 directory, 6 files

The toolchain is:

$ gcc --version
gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.0
...
$ meson --version
1.3.2

Here's the default build - just for once in verbose mode - with all libraries built as shared (*.so):

$ make VERBOSE=1
meson setup build 
The Meson build system
Version: 1.3.2
Source dir: /home/imk/develop/so/scrap
Build dir: /home/imk/develop/so/scrap/build
Build type: native build
Project name: emilua
Project version: 0.10.1
C compiler for the host machine: cc (gcc 13.2.0 "cc (Ubuntu 13.2.0-23ubuntu4) 13.2.0")
C linker for the host machine: cc ld.bfd 2.42
C++ compiler for the host machine: c++ (gcc 13.2.0 "c++ (Ubuntu 13.2.0-23ubuntu4) 13.2.0")
C++ linker for the host machine: c++ ld.bfd 2.42
Host machine cpu family: x86_64
Host machine cpu: x86_64
Build targets in project: 4

Found ninja-1.11.1 at /usr/bin/ninja
meson compile --verbose -C build > build-shared-libs.log # stderr goes to console 

What have we got?

$ make run
build/emilua
emilua_func

Interposition failed :( emilua_func is the output of libc puts(__FUNCTION__).

Now let's redo it building static libraries:

$ make LIBS=static clean run
build/emilua
puts with arg [emilua_func]

Interposition succeeds :) puts with arg [emilua_func] is the output of our puts(__FUNCTION__).

Why does interposition fail in the dynamic build?

Let's look at the linkage line for the executable emilua from the saved dynamic build log.

$ cat build-shared-libs.log | grep '\-o emilua ' | fmt -w80
[11/11] c++  -o emilua emilua.p/main.cpp.o -Wl,--as-needed -Wl,--no-undefined
'-Wl,-rpath,$ORIGIN/' -Wl,-rpath-link,/home/imk/develop/so/scrap/build/
-Wl,--start-group libemilua-libc-service.so.0.10.1 libemilua.so.0.10.1
libemilua-main.so.0.10.1 -Wl,--end-group

This is the complete Meson-generated-ninja-generated recipe for linking emilua but it's not the complete linkage commandline: it's missing the automatic linkage boilerplate that c++ ( = g++) generates behind the scenes, which includes -lc near the end. c++ --verbose to see that.

Note that all 3 shared libraries are enclosed in linker options --start-group...--end-group. These options are significant for an enclosed group of static libraries. They are insignificant for shared libraries, so they're insignificant here. [See the GNU ld manual: 2.1 Command-line Options.

Note also linker option --as-needed. It applies to all subsequent shared libraries until and unless disabled by --no-as-needed. It means that a shared library will not be considered needed by the linkage unless it really is needed, i.e. it actually provides at least one definition for an unresolved symbol reference aleady in the program.

The first library input is libemilua-libc-service.so, and the linker will appraise it to find definitions of any undefined symbol references already linked into the program. The only thing so far linked into program is ./build/emilua.p/main.cpp.o. The only undefined symbol reference in that file is:

$ make clean all
$ readelf --demangle --syms --wide ./build/emilua.p/main.cpp.o | grep 'GLOBAL.*UND' 
     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND emilua_main()
     

emilua_main(). libemilua-libc-service.so does not define that dynamic symbol:

$ readelf --demangle --dyn-syms --wide ./build/libemilua-libc-service.so | egrep \(emilua_main\|puts\)
     7: 0000000000001149    67 FUNC    GLOBAL DEFAULT   14 puts
     8: 0000000000001139    16 FUNC    GLOBAL DEFAULT   14 puts_interposed

It only defines our interposed GLIBC API (puts*). So libemilua-libc-service.so is not needed by the linker on this occasion: it proceeds to the next input file and doesn't look back.

As noted, libemilua-libc-service.so could only escape being snubbed here if we coerced the linker to need it.

The next input file is libemilua.so, whose dynamic symbol table references or defines functions:

$ readelf --demangle --dyn-syms --wide ./build/libemilua.so | egrep \(Symbol\|Num\|FUNC\)
Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
     6: 0000000000001119    26 FUNC    GLOBAL DEFAULT   14 emilua_func()
 

This library does define the unresolved reference emilua_func(). So it is needed. And because it is needed it introduces into the program an unresolved reference (UND) to puts, the GLIBC symbol that libemilua-libc-service.so was supposed to interpose. But it's too late for that now. Only libraries that are yet to be input still have a shot at resolving puts, and it won't be resolved until -lc is eventually input, when the libc definition will be bound.

So that's why interposition fails in the dynamic build. But notice a peculiarity of that undefined reference to puts. Despite the fact that the symbol is an undefined reference, the static linker has already decided that is a global function (FUNC GLOBAL), and that it a GLIBC versioned symbol (puts@GLIBC_2.2.5). We'll come back to that observation.

Why does interposition succeed in the static build?

From the static buildlog, here's the linkage line for the executable emilua:

$ cat build-static-libs.log | grep '\-o emilua ' | fmt -w80
[8/8] c++  -o emilua emilua.p/main.cpp.o -Wl,--as-needed -Wl,--no-undefined
-Wl,--whole-archive -Wl,--start-group libemilua.a -Wl,--no-whole-archive
libemilua-libc-service.a libemilua-main.a -Wl,--end-group

Here we have all the static libraries rather brain-achingly interleaved with overlapping linker-option pairs. Teasing them apart, this fragment:

--whole-archive libemilua.a -no-whole-archive

means that all the object files in the archive libemilua.a will be linked into the program whether they are needed to resolve undefined references or not. That is because meson.build says link_whole when libemilua is static.

And the rest:

--start-group libemilua.a libemilua-libc-service.a libemilua-main.a --end-group

directs the linker to suspend its default only go forward policy with respect to the enclosed sequence of archives. Instead, it will iteratively appraise this sequence of archives, linking contained object files that are needed to resolve references and accruing new unresolved references until no new ones accrue. The libraries are in a different order from their order in the dynamic build, which was:

libemilua-libc-service.so libemilua.so libemilua-main.so

But the order doesn't matter now because --start-group ... --end-group is operative for static libraries.

The first library considered is libemilua.a, which references or defines symbols:

$ make LIBS=static clean all
$ readelf --demangle --syms --wide ./build/libemilua.a | egrep \(File\|Symbol\|Num\|GLOBAL\)
File: ./build/libemilua.a(emilua_func.cpp.o)
Symbol table '.symtab' contains 11 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     9: 0000000000000000    26 FUNC    GLOBAL DEFAULT    1 emilua_func()
    10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts

Once again, the only unresolved reference in the program at this point is emilua_main() from main.cpp.o, but that doesn't matter because libemilua.a enjoys --whole-archive. So libemilua.a(emilua_func.cpp.o) goes into the program, defines emilia_func and introduces a new unresolved reference to symbol puts which is of unknown type (NOTYPE) and is not a versioned symbol.

Next up is libemilua-libc-service.a:

$ readelf --demangle --syms --wide ./build/libemilua-libc-service.a | egrep \(File\|Symbol\|Num\|GLOBAL\)
File: ./build/libemilua-libc-service.a(puts.c.o)
Symbol table '.symtab' contains 17 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
    12: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 puts_interposed
    13: 0000000000000010    67 FUNC    GLOBAL DEFAULT    1 puts
    14: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND stdout
    16: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND fprintf
    

which resolves puts. That's our interposition done. New unresolved references are introduced to stdout and fprintf. They'll eventually be resolved when the linkage reaches -lc. The only reference yet to account for is that emilua_main() from the outset and it is resolved by the last library:

$ readelf --demangle --syms --wide ./build/libemilua-main.a | egrep \(File\|Symbol\|Num\|GLOBAL.*emilua_main\)
File: ./build/libemilua-main.a(emilua_main.cpp.o)
Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     8: 0000000000000000    16 FUNC    GLOBAL DEFAULT    1 emilua_main()
     

So that's why interposition succeeds in the static build.

Which seems to show that...

If the libraries were linked in the obvious order of dependency:

libemilua-main libemilua libemilua-libc-service

the linkage, with our interposed puts, should just work, with no need for the --[no-]whole-archive, --(start|end)-group rigmarole, like:

$ pushd build
$ c++ -o emilua emilua.p/main.cpp.o libemilua-main.a libemilua.a libemilua-libc-service.a
$ ./emilua
puts with arg [emilua_func]
$ popd

And so it does.

So let's try that with the dynamic build:

$ make clean all
$ pushd build
$ c++ -o emilua emilua.p/main.cpp.o libemilua-main.so libemilua.so libemilua-libc-service.so -Wl,-rpath=.
$ ./emilua
emilua_func
$ popd

Interposition failed!?

Remember this?

$ readelf --demangle --dyn-syms --wide ./build/libemilua.so | egrep \(Symbol\|Num\|puts\)
Symbol table '.dynsym' contains 7 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     

Our libemilua.so has that unresolved reference to puts which is nevertheless known to be a GLIBC versioned global function symbol.

Our libemilua-libc-service.so defines plain puts:

$ readelf --demangle --dyn-syms --wide ./build/libemilua-libc-service.so | egrep \(Symbol\|Num\|puts$\)
Symbol table '.dynsym' contains 9 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     7: 0000000000001149    67 FUNC    GLOBAL DEFAULT   14 puts
     

The static linker does not stop searching for a shared library that defines puts@GLIBC_2.2.5 because it finds one that defines puts. By default it carries on and gets a perfect match in libc. This is the actual spanner in the works of interposition in your dynamic build. Coercing the static linker to consider libemilua-libc-service.so needed even if it doesn't think so is a way of brute-forcing the interposition without extracting the spanner. Another one is to LD_PRELOAD=libemilua-libc-service.so when running emilua.

Either way, the dynamic linker will come to resolve libemilua.so's reference to puts@GLIBC_2.2.5 with libemilua-libc-service.so loaded before libc.so.6 - and it plays by different rules :) It will bind a versioned reference to an unversioned definition of puts if that is the first one it loads.

In that respect the dynamic linker plays like the static linker when the latter considers resolving a reference to puts@GLIBC_2.2.5 to a definition provided statically by an input object file (which cannot be a versioned definition). The static linker will accept that match and give us interposition, as per:

$ pushd build
$ gcc -c ../puts.c
$ readelf --syms --wide puts.o | grep 'puts$'
     8: 0000000000000010    64 FUNC    GLOBAL DEFAULT    1 puts
$ c++ -o emilua emilua.p/main.cpp.o libemilua-main.so libemilua.so puts.o -Wl,-rpath=.
$ ./emilua
puts with arg [emilua_func]
$ popd

That is the "minimally static" linkage you could do to achieve interposition, and coercing libemilua-libc-service.so to be needed has the same force: it makes the static linker accept the shared library's unversioned definition of puts.

How does the spanner get in the works?

Check the dynamic section of libemilua.so:

$ readelf --dynamic --wide ./build/libemilua.so | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 

You'll find the same entry in libemilua-main.so and libemilua-libc-service.so. They've all already been linked against libc.

The static linker treads a pay-grade boundary when it resolves a symbol reference, e.g. puts from emilua_func.o in the linkage of libemilua.so, to a definition in a shared library, e.g. libc.so.6. By the meaning of shared library, it can't statically link the symbol definition into the output image: that means it can't actually define the symbol in the output image. Instead, it outputs (non-binding) guidance for the dynamic linker to resolve and define the symbol at runtime. It encodes:

(NEEDED)             Shared library: [libc.so.6]

in the dynamic section of the output image and it encodes everything it knows about the libc.so.6 definition as an undefined symbol entry in the dynamic symbol table of the output image:

     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     

When the static linker returns for the linkage of a the program emilua that depends on libemilua.so, it reuses the dynamic linkage guidance it has previously encoded in libemilua.so to perform symbol resolution and thus shape the dynamic linkage guidance it will encode in emilua. It will read that libemilua.so needs libc.so.6 (which it can of course find by default search), and it will read that libemilua.so's reference to puts was resolved to the definition of puts@GLIBC_2.2.5. So it will persevere in trying to resolve puts@GLIBC_2.2.5 for libemilua.so, rejecting any otherwise versioned or unversioned definition of puts, until and unless it finds libc, or it is offered an unversioned definition that it can't refuse: one that comes from an input object file or one that comes from a no-as-needed shared library.

How to extract the spanner?

Just don't link your shared libraries against libc.

By default GCC silently appends -lc to the linkage of a shared library, just as it does to the linkage of a program. By default Meson let's that be. That's how libemilua's reference to puts gets preemptively resolved to puts@GLIBC_2.2.5

But a (C or C++) program has to be linked against the C runtime or it won't link, whereas it's perfectly in order and routine to link a shared library with any or all of its external references unresolved.

Here's the simple hand-rolled way to build the shared libraries and interpose our definition of puts in the program.

$ g++ -c -fPIC emilua_main.cpp emilua_func.cpp
$ gcc -c -fPIC puts.c
$ g++ -c main.cpp
$ ld -shared -o libemilua-main.so emilua_main.o
$ ld -shared -o libemilua.so emilua_func.o
$ ld -shared -o libemilua-libc-service.so puts.o
$ g++ -o emilua main.o -L. -lemilua-main -lemilua -lemilua-libc-service -Wl,-rpath=. 
$ ./emilua
puts with arg [emilua_func]

By cutting GCC out of the shared libraries' linkage, we get no libraries linked with them that we didn't ask for. More long-windedly we can make GCC link exactly the libraries we tell it to with -nostdlib -nodefaultlibs.

Now the dynamic symbol table of libemilua.so contains:

$ readelf --dyn-syms --wide libemilua.so | grep puts
     1: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts
     

with no predetermined type or version, and its dynamic section:

$ readelf --dynamic --wide libemilua.so | grep NEEDED; echo Done
Done

contains no dynamic dependencies at all. Similarly for the other shared libraries. They contain no dynamic linkage information that engages the Kafkaesque regulations of symbol versioning, and you can link them in their actual dependency order.

Needless to say, those Kafkaesque regulations as they are implicated in the default linkage of libc into shared libraries hardly ever entangle us, because we're hardly ever trying to knock out the symbols of libc.

`meson.build` redux

Here is a revised meson.build file that does the same thing.

$ cat meson.build 
project(
    'emilua', 'cpp','c',
    default_options : ['cpp_std=c++20'],
    meson_version : '>=1.2.0',
    version : '0.10.1',
)

lib_link_args_dict = {
    'gcc' : ['-nostdlib','-nodefaultlibs'],
    'clang' : ['-nostdlib','-nodefaultlibs'],
    'msvc' : ['/NODEFAULTLIB']
}

compiler = meson.get_compiler('c')

lib_link_args = lib_link_args_dict[compiler.get_id()]

libemilua_main = library(
    'emilua-main',
    ['emilua_main.cpp'],
    override_options : ['b_lundef=false'],
    link_args : lib_link_args,
    implicit_include_directories : false,
    version : meson.project_version(),
    install : true
)

libemilua = library(
    'emilua',
    ['emilua_func.cpp'],
    override_options : ['b_lundef=false'],
    link_args : lib_link_args,
    implicit_include_directories : false,
    version : meson.project_version(),
    install : true
)

libemilua_libc_service = library(
        'emilua-libc-service',
        ['puts.c'],
        override_options : ['b_lundef=false'],
        link_args : lib_link_args,
        implicit_include_directories : false,
        version : meson.project_version(),
        install : true
    )

libemilua_libc_service_dep = declare_dependency(
    dependencies : [],
    link_with : [libemilua_libc_service],
)
        
libemilua_dep = declare_dependency(
    dependencies : [],
    link_with : [libemilua]
)

libemilua_main_dep = declare_dependency(
    dependencies : [],
    link_with : [libemilua_main],
)

emilua_bin = executable(
    'emilua',
    ['main.cpp'],
    dependencies : [
        libemilua_main_dep,
        libemilua_dep,
        libemilua_libc_service_dep,
    ],
    implicit_include_directories : false,
    install : true
)

Dynamic libraries build and run:

$ make clean run
build/emilua
puts with arg [emilua_func]

is good. Likewise static libraries build and run:

$ make LIBS=static clean run
build/emilua
puts with arg [emilua_func]

And what about the introspection?

Now we can uncomment the commented-out code:

$ cat emilua_func.cpp 
extern "C" {
    int puts(const char *);
    int puts_interposed(void);
}

void emilua_func()
{
    puts(__FUNCTION__);
    puts(puts_interposed() ? "puts interposed" : "puts not_interposed");
}

which links in the obvious way with puts.c

And rebuild:

$ make clean run
build/emilua
puts with arg [emilua_func]
puts with arg [puts interposed]

$ make LIBS=static clean run
build/emilua
puts with arg [emilua_func]
puts with arg [puts interposed]

We have interposition, and libemulia.(so|a) knows we have interposition.

The linkage of the additional code presents no new problem because the new references go from libemilua to definitions in libemilua_libc_service, which is already the linkage order.

Tested with gcc/g++, clang/clang++, not msvc.

Upvotes: 1

Related Questions