动态库是否能执行?
linux下某些动态库是可以运行的,例如libc.so,如下:
动态库可执行,输出一些静态信息非常方便。linux上很多系统库都有这个功能,比如ld.so,libpthread.so等。
动态库为什么可以执行?
linux上运行一个程序,首先要加载程序。程序的加载是通过执行exec(3)系统调用实现的。执行这个系统调用后,陷入操作系统内核,由操作系统负责加载该程序文件。在操作系 统确认相关参数后,然后通过内存映射方式加载进内存。然后操作系统要进行一个非常重要的判断,程序是否依赖其他共享库:
- 依赖其他共享库:那么就需要动态链接器来加载其他动态库。而动态链接器则由程序头中的PT_INTERP指定。操作系统则将该动态连接器映射进内存,并准备好相应的环境,将控制权转移给动态连接器。
- 不依赖其他共享库:操作系统准备好环境后直接转移到ELF文件的入口点开始执行。
细节就不一一展开了。动态库和可执行文件都是ELF文件,一个ELF文件需要运行需要满足以下两点:
- 入口点(entry point)指向你的函数。使用gcc 编译时,通过-e参数指定入口函数。
- 如果你的函数依赖其他动态库(非系统调用),需要指定一个解释器,加载时由解释器去加载对应的库。
实践
版本1
首先,写一个test.cpp:
#include <stdio.h>
#include <stdlib.h>
//指定解释器
extern const char elf_interpreter[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
//不能有参数,不能有返回值
extern "C" void func()
{
printf("Hello so\n");
//执行完了就退出,否则程序会挂掉
exit(0);
}
然后,编译:
gcc -fPIC -shared -e func test.cpp -o libtest.so
运行:
[compile@localhost executable_so]$ ./libtest.so
Hello so
可以看到,这是可行的。不过,需要注意几点:
- 入口函数不能有参数,不能有返回值。
- 入口函数执行完成之后,需要退出程序。
- 由于我是c++编译器,所以需要使用extern "C" 声明为C风格代码,否则入口函数就找不到。
- 需要指定解释器。
但是,程序有个缺点,指定解释器写的是硬编码,在一些环境中可能不能运行。那么,能不能不依赖解释器呢?
版本2
版本2的目标是去掉对解释器的依赖。首先,为什么会依赖解释器?是因为printf和exit两个函数是在libc.so中实现的,需要解释器来加载libc.so.所以,我们的目标就很清晰了,不要用这两个函数,只使用系统调用。
#include <sys/uio.h>
#include <linux/unistd.h>
#include <unistd.h>
//先注释掉,根据分析,不需要解释器
//指定解释器
//extern const char elf_interpreter[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
int my_do_syscall3(int num, long a1, long a2, long a3)
{
int rc;
__asm__ __volatile__(
"syscall"
: "=a" (rc)
: "0"((long)num), "D"(a1), "S"(a2), "d"(a3)
: "r11","rcx","memory"
);
return rc;
}
#define my_syscall3(call, a1, a2, a3) \
my_do_syscall3(__NR_##call, (a1), (a2), (a3))
extern "C" void func()
{
struct iovec v[1];
static const char msg0[] = "Hello so\n";
v[0].iov_base = (void*) msg0;
v[0].iov_len = sizeof(msg0)-1;
my_syscall3(writev, STDOUT_FILENO, (long) v, 1);
my_syscall3(exit, 0, 0, 0);
}
编译,运行:
[compile@localhost executable_so]$ ./libtest.so
段错误(吐核)
程序竟然挂掉了。
加上解释器,再编译,运行,程序又成功了。为什么需要解释器呢?使用nm -S libtest.so:
原来my_do_syscall3这个函数被导出了。那么一个函数被导出和不被导出有什么分别呢?其实这个函数被导出了,它就是一个动态库。刚才说到,只调用系统函数,不依赖其他动态库,也包含不依赖自己。因为一旦需要加载动态符号表,就需要使用到加载器。
了解这个原理之后,我们不导出my_do_syscall3试试,很简单,加上static修饰符即可。
#include <sys/uio.h>
#include <linux/unistd.h>
#include <unistd.h>
//指定解释器
//extern const char elf_interpreter[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
static int my_do_syscall3(int num, long a1, long a2, long a3)
{
int rc;
__asm__ __volatile__(
"syscall"
: "=a" (rc)
: "0"((long)num), "D"(a1), "S"(a2), "d"(a3)
: "r11","rcx","memory"
);
return rc;
}
#define my_syscall3(call, a1, a2, a3) \
my_do_syscall3(__NR_##call, (a1), (a2), (a3))
extern "C" void func()
{
struct iovec v[1];
static const char msg0[] = "Hello so\n";
v[0].iov_base = (void*) msg0;
v[0].iov_len = sizeof(msg0)-1;
my_syscall3(writev, STDOUT_FILENO, (long) v, 1);
my_syscall3(exit, 0, 0, 0);
}
编译,运行,正常。
总结
linux上要让动态库可执行,总共需要以下几步:
- 写一个C风格的入口函数,无参数、无返回值。
- 入口函数只依赖系统调用,结尾调用exit退出程序。不要依赖其他动态库,哪怕是自己,即不依赖自己导出的函数。
- 编译时,使用-e 指定入口函数。编译时使用-fPIC参数。