System function overloading in nested shared library - c

Simple scenario
Application uses write function from libc and links to shared library.
Shared library defines write function.
Original write function from libc will be overloaded by its version from the shared library.
Nested scenario
Application uses write function from libc.
Shared library 1 does not defines its own write but depends on shared library 2.
Shared library 2 defines write function.
No. write function will NOT be replaced with a version from the second shared library.
I do want to understand Why so ? How to make it work with nested shared library dependencies.
Here is exact code examples:
main.c
#include <unistd.h>
int main() {
write(1,"Hello\n",6);
return 0;
}
shared-lib-1.c
#include <unistd.h>
__attribute__((constructor))
void shared_lib_1_constructor(void) {
char str[] = "shared-lib-1-constructor\n";
write(1, str, sizeof(str));
}
shared-lib-2.c
#include <stdlib.h>
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count) {
exit(1);
return 0;
}
__attribute__((constructor))
void shared_lib_2_constructor(void) {
char str[] = "shared-lib-2-constructor\n";
write(1, str, sizeof(str));
}
build.sh
#!/usr/bin/env sh
set -v
set -e
gcc -g -fPIC -shared shared-lib-2.c -o libshared-lib-2.so
gcc -g -fPIC -shared -Wl,-rpath . -L`pwd` -lshared-lib-2 shared-lib-1.c -o libshared-lib-1.so
gcc -g -L`pwd` -Wl,-rpath . -lshared-lib-1 main.c
Execution of the a.out gives:
[smirnov#localhost tmp]$ ./a.out
shared-lib-2-constructor
shared-lib-1-constructor
Hello
write was not overwritten by shared-lib-2. It was completely ignored even from shared-lib-2 code.
How does it work with nested libraries ? Moving write definition to shared-lib-1 does work, it overloads glibc version and exits from application.
Running application with LD_DEBUG=all shows order of resolving:
36441: Initial object scopes
36441: object=./a.out [0]
36441: scope 0: ./a.out ./libshared-lib-1.so /lib64/libc.so.6 ./libshared-lib-2.so /lib64/ld-linux-x86-64.so.2
...
36441: calling init: ./libshared-lib-2.so
36441:
36441: symbol=write; lookup in file=./a.out [0]
36441: symbol=write; lookup in file=./libshared-lib-1.so [0]
36441: symbol=write; lookup in file=/lib64/libc.so.6 [0]
36441: binding file ./libshared-lib-2.so [0] to /lib64/libc.so.6 [0]: normal symbol `write'
Why libc placed in between shared-lib-1 and shared-lib-2 ?
It seems that ld resolves in the following order:
all dependencies of a.out, but not recursivelly, only first level
all subdependencies, but not recursivelly
so on...
The only solution I've found is to use
LD_DYNAMIC_WEAK=1 ./a.out
Is there a way to fix behavior in advance ?

write function will NOT be replaced with a version from the second shared library.
This is not what I expected, and it took me a while to figure out why it's happening.
The issue is that libc.so.6 doesn't define write function (if it did, everything would have worked the same whether the dependency is direct or not).
Instead, libc.so.6 defines a versioned write##GLIBC_2.2.5 symbol. It is that symbol versioning (a GNU extension) which interferes with your desired outcome.
Compare:
gcc main.c ./libshared-lib-1.so && nm -D a.out | grep ' write'
U write#GLIBC_2.2.5
gcc main.c ./libshared-lib-2.so && nm -D a.out | grep ' write'
U write
When performing symbol resolution, the loader looks up symbol version in the search scope (and a.out is at the front). With libshared-lib-1.so, the loader finds that it should be looking for write version GLIBC_2.2.5 (which can only be found in libc.so.6), and ignores the un-versioned definition in libshared-lib-2.so.
Is there a way to fix behavior in advance ?
If you can control the application link line, it's best to link a.out with libshared-lib-2.so:
gcc main.c ./libshared-lib-1.so ./libshared-lib-2.so
./a.out
# no output as expected.
If you can't control the application link line, you should be able to provide a versioned write#GLIBC_2.2.5 using the linker --version-script flag and appropriate version script. But my initial attempt at doing that failed, and I am not sure this approach can be made to work.

Related

Unable to access own custom global function of shared library in linux from other module in C

I have downloaded libgcrypt library source code and
added my own customize function inside one particular file.
Although compilation/build process of customized shared library is successful, and both nm and objdump show
the customized function is global, it nonetheless shows an error (undefined reference) at linking time.
Here is what I have done:
inside /src/visibility.c file, I have added my custom function,
void __attribute__((visibility("default"))) MyFunction(void)
{
printf("This is added just for testing purpose");
}
build process
./configure --prefix=/usr/local --disable-ld-version-script
sudo make install
nm and objdump command find this custom function as global inside shared library.
nm /usr/local/lib/libgcrypt.so | grep MyFunction
000000000000fbf0 T MyFunction
objdump -t /usr/local/lib/libgcrypt.so | grep MyFunction
000000000000fbf0 g F .text 0000000000000013 MyFunction
Here is my sample code to access my custom function.
//gcrypt_example_test.c
#include <stdio.h>
#include <gcrypt.h>
#include <assert.h>
int main()
{
MyFunction();
return 0;
}
export LD_RUN_PATH=/usr/local/lib
gcc gcrypt_example_test.c -o test -lgcrypt
/tmp/ccA0qgAB.o: In function `main':
gcrypt_example_test.c:(.text+0x3a2): undefined reference to `MyFunction'
collect2: error: ld returned 1 exit status
Edit 1:
I tried all possible way to include function prototype declaration inside header file (/src/gcrypt.h) as follows:
void __attribute__((visibility("default"))) MyFunction(void);
... or:
extern void __attribute__((visibility("default"))) MyFunction(void);
... or:
extern void MyFunction(void);
... or:
void MyFunction(void);
I am still getting the same error (undefined reference) although no build error results in all above cases.
Why is this happening, and what mistake am I making?
Although other global functions which are part of standard shared library and defined inside visibility.c (nm also shows T for those functions) are accessible, why is my customized global function (MyFunction) of the shared library still inaccessible? Thanks!
Any link or explanation to resolve this error will be highly appreciable.
From the GCC documentation (emphasis mine):
Some linkers allow you to specify the path to the library by setting LD_RUN_PATH in your environment when linking.
But, from the GNU ld man page:
-rpath=dir
Add a directory to the runtime library search path. This is used
when linking an ELF executable with shared objects. All -rpath
arguments are concatenated and passed to the runtime linker,
which uses them to locate shared objects at runtime. The -rpath
option is also used when locating shared objects which are needed
by shared objects explicitly included in the link; see the
description of the -rpath-link option. If -rpath is not used
when linking an ELF executable, the contents of the environment
variable "LD_RUN_PATH" will be used if it is defined.
Note that there is no mention at all of the link time library search path.
You need to compile/link with /usr/local/lib in the link time library search path:
gcc gcrypt_example_test.c -o test -L/usr/local/lib -lgcrypt
most likely cause of the problem:
The header file for the library has not been updated to include the prototype for the new function
I don't understand the reason behind why it is working now, but not before. Anyway, I found the way to make the code working after adding customized function inside standard library. This post may help others in future.
I first locate libgcrypt.so and then remove all versions of libgcrypt.so
locate libgcrypt.so
sudo rm /usr/local/lib/libgcrypt.so
sudo rm /usr/local/lib/libgcrypt.so.20
sudo rm /usr/local/lib/libgcrypt.so.20.2.2
then I delete the libgcrypt folder (which I had extracted for building library) to start fresh.
Again, I follow these steps
Step 0 : extract libgcrypt source code
Step 1 : add my custom function, inside /src/visibility.c file
void __attribute__((visibility("default"))) MyFunction(void)
{
printf("This is added just for testing purpose");
}
Step 2 : build library
export LD_RUN_PATH=/usr/local/lib
./configure --prefix=/usr/local --disable-ld-version-script
sudo make install
Step 3: Open another terminal to compile
export LD_RUN_PATH=/usr/local/lib
gcc gcrypt_example_test.c -o test -lgcrypt
Step 4 : run
./test
This is added just for testing purpose
This is working fine now as expected.
What I noticed that __attribute__((visibility("default"))) in function definition and --disable-ld-version-script during build process is very important to make the customized function global, elimination of any makes the customized function local inside shared library(.so) file.
Below changes are working at my end
visibility.h
#include <cstdio>
void __attribute__((visibility("default"))) MyFunction(void);
visibility.cpp
#include "visibility.h"
void MyFunction(void)
{
printf("This is added just for testing purpose");
}
library build command
gcc -shared -o libtest.so -Wall -Werror -fpic -I. visibility.cpp
test.cpp
#include <stdio.h>
#include <gcrypt.h>
#include <assert.h>
#include "visibility.h"
extern void MyFunction();
int main()
{
MyFunction();
return 0;
}
exe build command
gcc test.cpp -o test -I. -L. -ltest -lstdc++
My gcc version is 4.4.7
And of-course I did not try and install the lib under /usr/local/lib but kept it local for quick testing.

-rdynamic for select symbols only?

Scenario:
Executable loads shared object at run time via dlopen.
The shared object references some symbol (a function) that is actually compiled into the main executable.
This works fine if I add -rdynamic to gcc when linking the executable.
-rdynamic exports all non-static symbols of the executable. My shared object only needs a select few.
Question: Is there a way to achieve the effect of -rdynamic, but restricted the the few select symbols that I know are needed by my shared object?
Edit:
At least two people misunderstood the question, so I try to clarify:
This question is about exporting a symbol from the main executable.
This question is not about exporting a symbol from a dynamic library.
Here is a minimal example:
func.h, the common header file
#include <stdio.h>
void func(void);
main.c, the main executable code:
#include <dlfcn.h>
#include "func.h"
// this function is later called by plugin
void func(void) {
printf("func\n");
}
int main() {
void * plugin_lib = dlopen("./plugin.so", RTLD_NOW);
printf("dlopen -> %p, error: %s\n", plugin_lib, dlerror());
// find and call function "plugin" in plugin.so
void (*p)(void); // declares p as pointer to function
p = dlsym(plugin_lib, "plugin");
p();
return 0;
}
plugin.c, code for the plugin that is loaded at runtime:
#include "func.h"
void plugin()
{
printf("plugin\n");
func();
}
If I compile with
$ gcc -o main main.c -ldl
$ gcc -shared -fPIC -o plugin.so plugin.c
Then plugin.so cannot be loaded, because it references the symbol func, which cannot be resolved:
$ ./main
dlopen -> (nil), error: ./plugin.so: undefined symbol: func
Segmentation fault (core dumped)
I can convince the main executable to export all its global symbols by compiling with -rdynamic:
$ gcc -rdynamic -o main main.c -ldl
$ ./main
dlopen -> 0x75e030, error: (null)
plugin
func
But this fills the dynamic symbol table unnecessarily with all symbols.
(This dynamic symbol table can be inspected with nm -D main.)
The question is, how can I add only "func" to the dynamic symbol table of the main executable, and not everything.
Unfortunately it's harder to achieve this for executables. You need to generate a list of symbols that you want to export and then add -Wl,--dynamic-list=symfile.txt to LDFLAGS.
Here's example of how it's done in Clang (and here's the script they use to generate the symbols file).
You could do it with the visibility attribute of GCC.
Declare the function you need to export with __attribute__ ((visibility ("default"))) flag. Then compile your whole library passing -fvisibility=hidden argument to GCC.
For full explanation on this, refer to the following GCC documentation page.

How to make a linux shared object (library) runnable on its own?

Noticing that gcc -shared creates an executable file, I just got the weird idea to check what happens when I try to run it ... well the result was a segfault for my own lib. So, being curious about that, I tried to "run" the glibc (/lib/x86_64-linux-gnu/libc.so.6 on my system). Sure enough, it didn't crash but provided me some output:
GNU C Library (Debian GLIBC 2.19-18) stable release version 2.19, by Roland McGrath et al.
Copyright (C) 2014 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
Compiled by GNU CC version 4.8.4.
Compiled on a Linux 3.16.7 system on 2015-04-14.
Available extensions:
crypt add-on version 2.1 by Michael Glad and others
GNU Libidn by Simon Josefsson
Native POSIX Threads Library by Ulrich Drepper et al
BIND-8.2.3-T5B
libc ABIs: UNIQUE IFUNC
For bug reporting instructions, please see:
<http://www.debian.org/Bugs/>.
So my question here is: what is the magic behind this? I can't just define a main symbol in a library -- or can I?
I wrote a blog post on this subject where I go more in depth because I found it intriguing. You can find my original answer below.
You can specify a custom entry point to the linker with the -Wl,-e,entry_point option to gcc, where entry_point is the name of the library's "main" function.
void entry_point()
{
printf("Hello, world!\n");
}
The linker doesn't expect something linked with -shared to be run as an executable, and must be given some more information for the program to be runnable. If you try to run the library now, you will encounter a segmentation fault.
The .interp section is a part of the resulting binary that is needed by the OS to run the application. It's set automatically by the linker if -shared is not used. You must set this section manually in the C code if building a shared library that you want to execute by itself. See this question.
The interpreter's job is to find and load the shared libraries needed by a program, prepare the program to run, and then run it. For the ELF format (ubiquitous for modern *nix) on Linux, the ld-linux.so program is used. See it's man page for more info.
The line below puts a string in the .interp section using GCC attributes. Put this in the global scope of your library to explicitly tell the linker that you want to include a dynamic linker path in your binary.
const char interp_section[] __attribute__((section(".interp"))) = "/path/to/ld-linux";
The easiest way to find the path to ld-linux.so is to run ldd on any normal application. Sample output from my system:
jacwah#jacob-mint17 ~ $ ldd $(which gcc)
linux-vdso.so.1 => (0x00007fff259fe000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007faec5939000)
/lib64/ld-linux-x86-64.so.2 (0x00007faec5d23000)
Once you've specified the interpreter your library should be executable! There's just one slight flaw: it will segfault when entry_point returns.
When you compile a program with main, it's not the first function to be called when executing it. main is actually called by another function called _start. This function is responsible for setting up argv and argc and other initialisation. It then calls main. When main returns, _start calls exit with the return value of main.
There's no return address on stack in _start as it's the first function to be called. If it tries to return, an invalid read occurs (ultimately causing a segmentation fault). This is exactly what is happening in our entry point function. Add a call to exit as the last line of your entry function to properly clean up and not crash.
example.c
#include <stdio.h>
#include <stdlib.h>
const char interp_section[] __attribute__((section(".interp"))) = "/path/to/ld-linux";
void entry_point()
{
printf("Hello, world!\n");
exit(0);
}
Compile with gcc example.c -shared -fPIC -Wl,-e,entry_point.
While linking with -shared gcc strips start files, and some objects (like cout) will not be initialized. So, std::cout << "Abc" << std::endl will cause SEGFAULT.
Approach 1
(simplest way to create executable library)
To fix it change linker options. The simplest way - run gcc to build executable with -v option (verbose) and see the linker command line. In this command line you should remove -z now, -pie (if present) and add -shared. The sources must be anyway compiled with -fPIC (not -fPIE).
Let's try. For example we have the following x.cpp:
#include <iostream>
// The next line is required, while building executable gcc will
// anyway include full path to ld-linux-x86-64.so.2:
extern "C" const char interp_section[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
// some "library" function
extern "C" __attribute__((visibility("default"))) int aaa() {
std::cout << "AAA" << std::endl;
return 1234;
}
// use main in a common way
int main() {
std::cout << "Abc" << std::endl;
}
Firstly compile this file via g++ -c x.cpp -fPIC. Then will link it dumping command-line via g++ x.o -o x -v.
We will get correct executable, which can't be dynamically loaded as a shared library. Check this by python script check_x.py:
import ctypes
d = ctypes.cdll.LoadLibrary('./x')
print(d.aaa())
Running $ ./x will be successful. Running $ python check_x.py will fail with OSError: ./x: cannot dynamically load position-independent executable.
While linking g++ calls collect2 linker wraper which calls ld. You can see command-line for collect2 in the output of last g++ command like this:
/usr/lib/gcc/x86_64-linux-gnu/11/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/11/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper -plugin-opt=-fresolution=/tmp/ccqDN9Df.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o x /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/11 -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/11/../../.. x.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/11/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crtn.o
Find there -pie -z now and replace with -shared. After running this command you will get new x executable, which will wonderfully work as an executable and as a shared library:
$ ./x
Abc
$ python3 check_x.py
AAA
1234
This approach has disadvantages: it is hard to do replacement automatically. Also before calling collect2 GCC will create a temporary file for LTO plugin (link-time optimization). This temporary file will be missing while you running the command manually.
Approach 2
(applicable way to create executable library)
The idea is to change linker for GCC to own wrapper which will correct arguments for collect2. We will use the following Python script collect3.py as linker:
#!/usr/bin/python3
import subprocess, sys, os
marker = '--_wrapper_make_runnable_so'
def sublist_index(haystack, needle):
for i in range(len(haystack) - len(needle)):
if haystack[i:i+len(needle)] == needle: return i
def remove_sublist(haystack, needle):
idx = sublist_index(haystack, needle)
if idx is None: return haystack
return haystack[:idx] + haystack[idx+len(needle):]
def fix_args(args):
#print("!!BEFORE REPLACE ", *args)
if marker not in args:
return args
args = remove_sublist(args, [marker])
args = remove_sublist(args, ['-z', 'now'])
args = remove_sublist(args, ['-pie'])
args.append('-shared')
#print("!!AFTER REPLACE ", *args)
return args
# get search paths for linker directly from gcc
def findPaths(prefix = "programs: ="):
for line in subprocess.run(['gcc', '-print-search-dirs'], stdout=subprocess.PIPE).stdout.decode('utf-8').split('\n'):
if line.startswith(prefix): return line[len(prefix):].split(':')
# get search paths for linker directly from gcc
def findLinker(linker_name = 'collect2'):
for p in findPaths():
candidate = os.path.join(p, linker_name)
#print("!!CHECKING LINKER ", candidate)
if os.path.exists(candidate) : return candidate
if __name__=='__main__':
args = sys.argv[1:]
args = fix_args(args)
exit(subprocess.call([findLinker(), *args]))
This script will replace arguments and call true linker. To switch linker we will create the file specs.txt with the following content:
*linker:
<full path to>/collect3.py
To tell our fake linker that we want to correct arguments we will use the additional argument --_wrapper_make_runnable_so. So, the complete command line will be the following:
g++ -specs=specs.txt -Wl,--_wrapper_make_runnable_so x.o -o x
(we suppose that you want to link existing x.o).
After this you can both run the target x and use it as dynamic library.

Detecting unresolved symbols in an ELF executable

Let's say I have two files:
// shared.c (will be compiled to 'shared.so')
#include <stdio.h>
int f() { printf("hello\n"); }
and
// exe.c (will be compiled to 'exe')
#include <stdio.h>
int f();
int main() {
int i;
scanf("%d", &i);
if (i == 5) f();
}
I compile both files as following:
gcc -shared shared.c -o libshared.so
gcc exe.c -o exe -lshared -L.
When I run exe and type 5, it will call f and then exit. However, if I delete f from shared.c and recompile it I will get a runtime symbol lookup error only if I type 5. Is there a way that I can check that exe has all its symbols that will work independent of user input in this case? Preferrably without running it.
You can use ldd -r exe command to list the shared library dependencies.
Here is my output for your example without the f function:
$ LD_LIBRARY_PATH=. ldd -r ./exe
linux-vdso.so.1 (0x00007ffcfa7c3000)
libshared.so => ./libshared.so (0x00007f303a02e000)
libc.so.6 => /lib64/libc.so.6 (0x0000003e26c00000)
/lib64/ld-linux-x86-64.so.2 (0x0000003e26400000)
undefined symbol: f (./exe)
(Don't mind the LD_LIBRARY_PATH=. part. It is used to tell to look for shared libraries in the current directory)
#tohava
When you compile the executable and link it with the shared object, ld (linker) checks if all referenced symbols are available in the list of shared objects your executable is dependent on and will throw an error if any symbol was unresolved.
So, I am not sure how you managed to get a runtime error when you removed f() from the shared library and rebuilt the executable. (I did the exercise myself and got the linker error).

when dlopen one so, it's symbol is not covered by main symbol, why?

libp2.c
#include <stdio.h>
void pixman()
{
printf("pixman in libp1\n");
}
libc2.c
#include <stdio.h>
void pixman();
void cairo()
{
printf("cairo2\n");
pixman();
}
main.c
#include <stdio.h>
#include <dlfcn.h>
void pixman()
{
printf("pixman in main\n");
}
int main()
{
pixman();
void* handle=NULL;
void (*callfun)();
handle=dlopen("/home/zpeng/test/so_test/libc2.so",RTLD_LAZY);
callfun = (void(*)())dlsym(handle, "cairo");
callfun();
...
}
compile
gcc -c libp2.c -fPIC -olibp2.o
rm libp2.a
ar -rs libp2.a libp2.o
gcc -shared -fPIC libc2.c ./libp2.a -o libc2.so
gcc main.c -ldl -L. -g
the result:
pixman in main
cairo2
pixman: libp2
why the last is not "pixman in main"?
I see the symbols processing(LD_DEBUG=symbols), it begins with :
21180: symbol=pixman; lookup in file=./a.out
21180: symbol=pixman; lookup in file=/lib64/libdl.so.2
21180: symbol=pixman; lookup in file=/lib64/tls/libc.so.6
21180: symbol=pixman; lookup in file=/lib64/ld-linux-x86-64.so.2
21180: symbol=pixman; lookup in file=/home/zpeng/test/so_test/libc2.so
if I add -lc2 or -rdynamic to gcc main cmd , it will generate:
pixman in main
cairo2
pixman in main
My questions:
why lookup symbol in a.out but not get the result and continue to search libc2.so when without -rdynamic and -lc2 ?
Why the last is not "pixman in main"?
That's because shared libraries have their own global offset table or GOT. When you use the cairo function in libc2.so, the pixman function that will be called is the same function that was resolved when compiling the .so file in the first place.
That is:
# creates object file only -- contains first pixman implementation
gcc -c libp2.c -fPIC -olibp2.o
# just turns the object file into an archive
ar -rs libp2.a libp2.o
# creates the .so file -- all symbols in libc2.c are resolved here
# and you passed in the .a file for that purpose. The .a file containing the
# first pixman implementation gets put in libc2.so.
gcc -shared -fPIC libc2.c ./libp2.a -o libc2.so
After this, anyone using libc2.so will get the copy stored in libc2.so. The lookup order you post is for a.out I believe and it's right. It looks for pixman in a.out, then libc2.so, and so on.
Why lookup symbol in a.out but not get the result and continue to search libc2.so when without -rdynamic and -lc2?
The rdynamic option loads ALL symbols to the dynamic symbol table -- not just the ones it thinks are used (lc2 has the same effect). When you load all those symbols you have a conflict -- the pixman function. The main.c implementation is used in this case. As others have pointed out, this will probably generate a warning.
You need to compile the sources that get archived into the .a file with -fvisibility=hidden, to indicate that, although they are global functions, they are not meant to be used outside the resulting library but are instead meant to resolve symbols inside the library. That will cause the symbols in the .a file to appear with the qualifier " t " in nm -a instead of " T ", which is used for symbols available to other libraries.
It just auto binded to LOCAL symbol,
Since there not __attribute__((visibility("default"))) explicit in libp2.c, the compiler auto bind this function calling to LOCAL .symtab, instead of .dynsym
appendix1: more about ELF header: readelf -s xxx.lib
appendix2: keyword of ld argument -Bsymbolic-functions

Resources