Reputation: 2397
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).
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
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.
$ 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 libc
s, 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__)
.
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.
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.
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!?
$ 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
.
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.
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
.
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]
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