一般来说一个动态链接库是不可以直接执行的,但是也有一些例外,比如linux下libpthread,以此为例看一下动态链接库是如何做到可执行的。
至于这有什么用处,可能也就是方便输出一些版本信息编译信息吧。

centos8.5下2.28版本libpthread
1
2
3
4
5
6
7
8
9
10
[huyu@centos852111 2]$ ll /usr/lib64/libpthread.*
lrwxrwxrwx. 1 root root 27 Aug 25 2021 /usr/lib64/libpthread.so -> ../../lib64/libpthread.so.0
lrwxrwxrwx. 1 root root 18 Aug 25 2021 /usr/lib64/libpthread.so.0 -> libpthread-2.28.so
[huyu@centos852111 2]$ /usr/lib64/libpthread.so
Native POSIX Threads Library
Copyright (C) 2018 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.
Forced unwind support included.

动态链接库例子

先做一个普通的使用动态链接库的例子

libtest.c
1
2
3
4
5
6
7
#include <stdio.h>
#include <stdlib.h>

extern int lib_func(const char *p) {
printf("in libfun, param %s\n", p);
return 2;
}
main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <dlfcn.h> // 动态加载库相关头文件

int main() {
void *handle; // 动态库句柄
int (*lib_func)(const char *); // 函数指针
int n;

// 1. 加载动态库
handle = dlopen("./libtest.so", RTLD_NOW);
if (!handle) {
fprintf(stderr, "加载动态库失败: %s\n", dlerror());
return 1;
}

// 2. 清除之前的错误信息
dlerror();

// 3. 获取函数地址
lib_func = dlsym(handle, "lib_func");
if (lib_func == NULL) {
fprintf(stderr, "查找符号失败: %s\n", dlerror());
dlclose(handle);
return 1;
}

// 4. 调用目标函数
printf("准备调用动态库函数...\n");
n = lib_func("main");
printf("lib_func return %d\n", n);

// 5. 关闭动态库
dlclose(handle);
return 0;
}
Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.PHONY: all
all: libtest.so main

libtest.so: libtest.o
gcc -Wall -g -shared $^ -o $@

main: main.o
gcc -Wall -g -ldl $^ -o $@

%.o: %.c
gcc -Wall -g -fPIC $^ -c -o $@

clean:
$(RM) libtest.so main *.o
执行一下,没有问题
1
2
3
4
5
6
7
8
9
10
[huyu@centos852111 2]$ make
gcc -Wall -g -fPIC libtest.c -c -o libtest.o
gcc -Wall -g -shared libtest.o -o libtest.so
gcc -Wall -g -fPIC main.c -c -o main.o
gcc -Wall -g -ldl main.o -o main
[huyu@centos852111 2]$ ./main
准备调用动态库函数...
in libfun, param main
lib_func return 2
[huyu@centos852111 2]$

libpthread中做了什么

找一个libpthread 2.28版本源码,比如
https://github.com/bminor/glibc/tree/glibc-2.28/nptl

找到字符串”Native POSIX Threads Library”
https://github.com/bminor/glibc/blob/glibc-2.28/nptl/version.c

nptl/version.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <unistd.h>
#include <sysdep.h>


static const char banner[] =
"Native POSIX Threads Library\n\
Copyright (C) 2018 Free Software Foundation, Inc.\n\
This is free software; see the source for copying conditions.\n\
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\n\
PARTICULAR PURPOSE.\n"
"Forced unwind support included.\n"
;


/* This is made the e_entry of libpthread.so by LDFLAGS-pthread.so. */
__attribute__ ((noreturn))
void
__nptl_main (void)
{
__libc_write (STDOUT_FILENO, banner, sizeof banner - 1);
_exit (0);
}

可以看到__nptl_main函数将banner字符串输出到标准输出,这个函数名字看起来就是找对了。

下一步看__nptl_main函数是如何被调用的

nptl/Makefile
1
2
3
4
5
6
7
8
LDFLAGS-pthread.so = -Wl,--enable-new-dtags,-z,nodelete,-z,initfirst

# bla bla bla

# Give libpthread.so an entry point and make it directly runnable itself.
LDFLAGS-pthread.so += -e __nptl_main
# pt-interp.c exists just to get the runtime linker path into libpthread.so.
$(objpfx)pt-interp.os: $(common-objpfx)runtime-linker.h

可以看到Makefile中通过链接参数-e __nptl_main设置程序运行起始位置,可以参考man ld

还提到pt_interp.c是为了将运行时链接器路径设置给libpthread.so,那么看一下这个文件

nptl/pt-interp.c
1
#include <elf/interp.c>
elf/interp.c
1
2
3
4
#include <runtime-linker.h>

const char __invoke_dynamic_linker__[] __attribute__ ((section (".interp")))
= RUNTIME_LINKER;

头文件runtime-linker.h和RUNTIME_LINKER分别是什么呢?

文件Makerules中有这样一段

Makerules
1
2
3
4
5
6
7
8
9
ifeq (yes,$(build-shared))
$(common-objpfx)runtime-linker.h: $(common-objpfx)runtime-linker.stamp; @:
$(common-objpfx)runtime-linker.stamp: $(common-objpfx)config.make
$(make-target-directory)
echo '#define RUNTIME_LINKER "$(rtlddir)/$(rtld-installed-name)"' \
> ${@:stamp=T}
$(move-if-change) ${@:stamp=T} ${@:stamp=h}
touch $@
endif

看起来runtime-linker.h是编译时生成的,里面定义了宏RUNTIME_LINKER

RUNTIME_LINKER具体定义不找了,直接看一下/usr/lib64/libpthread-2.28.so中的链接器路径吧

1
2
3
[huyu@centos852111 executable_so]$ file /usr/lib64/libpthread-2.28.so
/usr/lib64/libpthread-2.28.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7a60d9380cc2284fe162f9900b28f4dbdb7029fd, for GNU/Linux 3.2.0, not stripped
[huyu@centos852111 executable_so]$

看起来路径就是/lib64/ld-linux-x86-64.so.2

修改后的例子

修改后的libtest.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdlib.h>

// 如果不指定动态链接器,则需要调用链接器执行本程序,比如 /lib64/ld-2.28.so SOFILE
#ifdef __i386__ // 只考虑了x86的32位,不考虑其他架构32位
const char service_interp[] __attribute__((section(".interp"))) = "/lib/ld-linux.so.2";
#elif defined(__x86_64__)
const char service_interp[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
#else // 视为__aarch64__
//const char service_interp[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
#endif

// 参考glibc中libpthread.so
// nptl/version.c 中函数__nptl_main
// nptl/Makefile 中指定了__nptl_main作为entry point,注释中还提到了pt-interp.c
// nptl/pt-interp.c中include了<elf/interp.c>
// include/elf/interp.c中代码为生成的动态链接库文件添加了动态链接器的信息,也就是上面的".interp"段

extern int lib_func(const char *p) {
printf("in libfun, param %s\n", p);
return 2;
}

// 默认入口名为_start
int _my_main() {
int n;
printf("in so %s\n", __FUNCTION__);

n = lib_func(p: "test1");
printf("lib_func return %d\n", n);
exit(111);
}
修改后的Makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.PHONY: all
all: libtest.so main

libtest.so: libtest.o
gcc -Wall -g -shared $^ -o $@ -Wl,-e,_my_main

main: main.o
gcc -Wall -g -ldl $^ -o $@

%.o: %.c
gcc -Wall -g -fPIC $^ -c -o $@

clean:
$(RM) libtest.so main *.o
执行一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[huyu@centos852111 executable_so]$ make clean
rm -f libtest.so main *.o
[huyu@centos852111 executable_so]$
[huyu@centos852111 executable_so]$ make
gcc -Wall -g -fPIC libtest.c -c -o libtest.o
gcc -Wall -g -shared libtest.o -o libtest.so -Wl,-e,_my_main
gcc -Wall -g -fPIC main.c -c -o main.o
gcc -Wall -g -ldl main.o -o main
[huyu@centos852111 executable_so]$
[huyu@centos852111 executable_so]$ ./main
准备调用动态库函数...
in libfun, param main
lib_func return 2
[huyu@centos852111 executable_so]$
[huyu@centos852111 executable_so]$ ./libtest.so
in so _my_main
in libfun, param test1
lib_func return 2
[huyu@centos852111 executable_so]$ echo $?
111
[huyu@centos852111 executable_so]$

看起来libtest.so作为动态链接库和作为可执行程序都满足了。

这种情况下,由于手动指定了程序入口,就不会有正常c程序的预处理和退出处理,需要自己调用exit函数了。至于如果不调用exit会发生什么,试一下就知道了。