写一个可执行的动态库

6 篇文章 0 订阅

动态库是否能执行?

linux下某些动态库是可以运行的,例如libc.so,如下:

动态库可执行,输出一些静态信息非常方便。linux上很多系统库都有这个功能,比如ld.so,libpthread.so等。

动态库为什么可以执行?

linux上运行一个程序,首先要加载程序。程序的加载是通过执行exec(3)系统调用实现的。执行这个系统调用后,陷入操作系统内核,由操作系统负责加载该程序文件。在操作系 统确认相关参数后,然后通过内存映射方式加载进内存。然后操作系统要进行一个非常重要的判断,程序是否依赖其他共享库:

  1. 依赖其他共享库:那么就需要动态链接器来加载其他动态库。而动态链接器则由程序头中的PT_INTERP指定。操作系统则将该动态连接器映射进内存,并准备好相应的环境,将控制权转移给动态连接器。
  2. 不依赖其他共享库:操作系统准备好环境后直接转移到ELF文件的入口点开始执行。

细节就不一一展开了。动态库和可执行文件都是ELF文件,一个ELF文件需要运行需要满足以下两点:

  1. 入口点(entry point)指向你的函数。使用gcc 编译时,通过-e参数指定入口函数。
  2. 如果你的函数依赖其他动态库(非系统调用),需要指定一个解释器,加载时由解释器去加载对应的库。

实践

版本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

可以看到,这是可行的。不过,需要注意几点:

  1. 入口函数不能有参数,不能有返回值。
  2. 入口函数执行完成之后,需要退出程序。
  3. 由于我是c++编译器,所以需要使用extern "C" 声明为C风格代码,否则入口函数就找不到。
  4. 需要指定解释器。

但是,程序有个缺点,指定解释器写的是硬编码,在一些环境中可能不能运行。那么,能不能不依赖解释器呢?

版本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上要让动态库可执行,总共需要以下几步:

  1. 写一个C风格的入口函数,无参数、无返回值。
  2. 入口函数只依赖系统调用,结尾调用exit退出程序。不要依赖其他动态库,哪怕是自己,即不依赖自己导出的函数。
  3. 编译时,使用-e 指定入口函数。编译时使用-fPIC参数。
  • 21
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值