吴章金: 如何创建一个*可执行*的共享库

license: "cc-by-nc-nd-4.0" description: "本文手把手指导如何创建一个可以执行的共享目标文件"

前言

前段时间,有多位同学在“泰晓原创团队”微信群聊到 C 语言相关的两个问题:

  • 如何让共享库文件也可以直接执行
  • 如何在可执行文件中用 dlopen 解析自身的函数

这两个需求汇总起来,可以大体理解为如何让一个程序既可以作为共享库,又能够直接运行。

这类需求在 Linux 下面其实很常见,比如 ld-linux.so 和 libc.so:

$ file /lib/i386-linux-gnu/ld-linux.so.2
/lib/i386-linux-gnu/ld-linux.so.2: symbolic link to ld-2.23.so
$ file /lib/i386-linux-gnu/ld-2.23.so
/lib/i386-linux-gnu/ld-2.23.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked
$ /lib/i386-linux-gnu/ld-2.23.so
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]

$ file /lib/i386-linux-gnu/libc.so.6
/lib/i386-linux-gnu/libc.so.6: symbolic link to libc-2.23.so
$ file /lib/i386-linux-gnu/libc-2.23.so
/lib/i386-linux-gnu/libc-2.23.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked

$ /lib/i386-linux-gnu/libc.so.6
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu11) stable release version 2.23, by Roland McGrath et al.

那如何做到的呢?

先来看看两类文件的区别

当前 Linux 下面的二进制程序标准格式是 ELF,这类格式可以用来表示 4 种不同类型的文件:

  • 可重定位目标文件(.o),用于静态链接
  • 可执行文件格式,用于运行时创建进程映像
  • 共享目标文件(.so,共享库),协同可执行文件创建进程映像
  • Core dump(core),运行过程中崩溃时自动生成,用于调试

我们来看中间两类:

  • 可执行文件
    • 如果不引用外部库函数,那么所有符号地址是确定的,执行加载后可直接运行
  • 共享库
    • 如果可执行文件用到外部库函数,那么需要通过动态链接器加载引用到的共享库并在运行时解析用到的相应符号

所以,前者和后者通常情况下是独立存在的,是联合行动的,两者差异明显:

  • 可执行文件有标准的 C 语言程序执行入口 main,而共享库则并没有这类强制要求
  • 后者为了确保可以灵活被多个可执行文件共享,所以,符号地址在链接时是相对的,在装载时动态分配和计算符号地址

接下来做个实验具体看看两者的区别,准备一个“烂大街”的 hello.c 先:

#include <stdio.h>

int main(void)
{
    printf("hello\n");

    return 0;
}

先来编译为可执行文件(-m32 用来生成采用 i386 指令集的代码):

$ gcc -m32 -o hello hello.c
$ file hello
hello: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-
$ ./hello
hello

再来编译为共享目标文件,并尝试直接执行它:

$ gcc -m32 -shared -fpic -o libhello.so hello.c
$ file libhello.so
libhello.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked
$ ./libhello.so
Segmentation fault (core dumped)

直接执行失败,再试试如何生成一个可执行文件来加载运行它,这个是引用共享库的通常做法:

$ gcc -m32 -o hello.noc -L./ -lhello
$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./ ./hello.noc
hello

通过实验,可以确认“正常”创建出来的共享库并不能够直接运行,而是需要链接到其他可执行文件中。

上述编译选项简介:

-shared Create a shared library. -fpic Generate position-independent code (PIC) suitable for use in a shared library

让可执行文件可共享

接下来,好好研究一番。

先来看一个 gcc 直接支持的方式:

$ gcc -m32 -pie -fpie -rdynamic -o libhello.so hello.c
$ file libhello.so
libhello.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked

$ ./libhello.so
hello

$ gcc -m32 -o hello.noc -L./ -lhello
$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./ ./hello.noc
hello

确实可以执行,而且可以作为共享库链接到其他可执行文件中。

上述编译选项简介:

-pie Produce a position independent executable on targets that support it. -fpie These options are similar to -fpic and -fPIC, but generated position independent code can be only linked into executables. -rdynamic Pass the flag -export-dynamic to the ELF linker, on targets that support it. This instructs the linker to add all symbols, not only used ones, to the dynamic symbol table.

-rdynamic 等价于 -Wl,-E / -Wl,--export-dynamic,确保所有“库”中的符号都 export 到动态符号表,包括当前未用到的那些符号。

举个例子,如果 hello.c 有一个独立的 hello() 函数,没有别的函数(这里是指 main)调用到,但是其他用到该库的可执行文件希望用到它,那么 -rdynamic 就是必须的。

$ cat hello.c
#include <stdio.h>

void hello(void)
{
    printf("hello...\n");
}

int main(void)
{
    printf("hello\n");

    return 0;
}
$ cat main.c

$ gcc -m32 -pie -fpie -rdynamic -o libhello.so hello.c
$ readelf --dyn-syms libhello.so  | grep hello
    19: 00000662    43 FUNC    GLOBAL DEFAULT   14 hello

$ gcc -m32 -o hello.main main.c -L./ -lhello
$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./ ./hello.main
hello...

如果没有 -rdynamic,链接时就没法使用。

$ gcc -m32 -o hello.main main.c -L./ -lhello
main.c:(.text+0x7): undefined reference to `hello'

同理,dlopen 自解析时也需要 -rdynamic

#include <stdio.h>
#include <stdlib.h>
#define _GNU_SOURCE
#include <dlfcn.h>

void hello(void)
{
    printf("hello...\n");
}

int main(void)
{
    void *handle;
        void (*func)(void);
        char *error;

        handle = dlopen(NULL, RTLD_LAZY);
        if (!handle) {
            fprintf(stderr, "%s\n", dlerror());
            return EXIT_FAILURE;
        }

        dlerror();    /* Clear any existing error */

        func = (void (*)(void)) dlsym(handle, "hello");

        error = dlerror();
        if (error != NULL) {
            fprintf(stderr, "%s\n", error);
            return EXIT_FAILURE;
        }

    func();
        dlclose(handle);

    return 0;
}

实测效果:

$ gcc -m32 -pie -fpie -o libhello.so hello.c -ldl
$ ./libhello.so
./libhello.so: undefined symbol: hello

$ gcc -m32 -pie -fpie -rdynamic -o libhello.so hello.c -ldl
$ ./libhello.so
hello...

让共享库可执行

下面来探讨另外一种方式,在生成共享库的基础上,来研究怎么让它可以执行。

先来回顾一下共享库,在本文第 2 节直接执行的时候马上出段错误,基本原因是共享库没有强制提供一个标准的 C 程序入口。

即使是我们提供了 main()(把标准 hello.c 编译为 libhello.so),程序的入口并没有指向它。

$ readelf -h libhello.so | grep "Entry point"
  Entry point address:               0x3d0
$ objdump -d libhello.so | grep 3d0 | head -2
 380:    e8 4b 00 00 00          call   3d0 <__x86.get_pc_thunk.bx>
000003d0 <__x86.get_pc_thunk.bx>:

那么,先解决入口的问题并运行,同样出错了:

$ gcc -m32 -shared -fpic -o libhello.so hello.c -Wl,-emain
$ readelf -h libhello.so | grep "Entry point"
  Entry point address:               0x4b9
$ objdump -d libhello.so | grep 4b9 | head -2
000004b9 <main>:
 4b9:    8d 4c 24 04             lea    0x4(%esp),%ecx

$ ./libhello.so
Segmentation fault

加上 -g 编译用 gdb 来看看原因:

$ gcc -m32 -g -shared -fpic -o libhello.so hello.c -Wl,-emain
$ objdump -d libhello.so | grep 4b9 | head -2
000004b9 <main>:
 4b9:    8d 4c 24 04             lea    0x4(%esp),%ecx

$ ulimit -c unlimited
$ ./libhello.so
Segmentation fault (core dumped)

$ gdb ./libhello.so core
Core was generated by `./libhello.so'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x000003a6 in ?? ()
(gdb) bt
#0  0x000003a6 in ?? ()
#1  0xf77344e3 in main () at hello.c:5
(gdb) l hello.c:5
1    #include <stdio.h>
2
3    int main(void)
4    {
5        printf("hello\n");
6
7        return 0;
8    }
(gdb)

可以看到是执行 printf 的时候出错,说明库函数的解析出了问题,主动用动态连接器跑一下看看:

$ /lib/i386-linux-gnu/ld-2.23.so ./libhello.so
hello
Segmentation fault (core dumped)

哇哦,可以解析符号并打印了,不过最后还是崩溃了?

如果去分析 glibc 的 __libc_start_main 不难发现,我们还少调用一个标准退出函数,改造过后:

$ cat hello.c
#include <stdio.h>
#include <unistd.h>

void main(void)
{
    printf("hello\n");

    _exit(0);
}

再编译运行就没段错误了。再进一步,同样是分析 glibc,发现实际的入口函数并非 main(),而是 _start

$ cat hello.c
#include <stdio.h>
#include <unistd.h>

int main(void)
{
    printf("hello\n");

    return 0;
}

void _start(void)
{
    int ret;

    ret = main();
    _exit(ret);
}

编译时连入口都不用指定了:

$ gcc -m32 -g -shared -fpic -o libhello.so hello.c
$ /lib/i386-linux-gnu/ld-2.23.so ./libhello.so
hello

也可以当共享库使用:

$ gcc -m32 -o hello.noc -L./ -lhello
$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./ ./hello.noc
hello

最后还有一点遗憾,怎么样才能“动态”链接,而不是手动指定动态链接器呢?我们在程序中主动加入一个 .interp 节区来指定动态链接器吧。

$ cat hello.c
#include <stdio.h>
#include <unistd.h>

asm(".pushsection .interp,\"a\"\n"
    "        .string \"/lib/i386-linux-gnu/ld-linux.so.2\"\n"
    ".popsection");

int main(void)
{
    printf("hello\n");

    return 0;
}

void _start(void)
{
    int ret;

    ret = main();
    _exit(ret);
}

再试试,完美运行:

$ gcc -m32 -shared -fpic -o libhello.so hello.c
$ ./libhello.so
hello

最后,稍后整理一下:

$ cat hello.c
#include <stdio.h>

#ifdef EXEC_SHARED
#include <unistd.h>

asm(".pushsection .interp,\"a\"\n"
    "        .string \"/lib/i386-linux-gnu/ld-linux.so.2\"\n"
    ".popsection");

int entry(void)
{
    printf("%s %d: %s(): the real entry of shared library here.\n", __FILE__, __LINE__, __func__);

    /* do whatever */

    return 0;
}

int main(void)
{
    return entry();

    return 0;
}

void _start(void)
{
    int ret;

    ret = main();
    _exit(ret);
}
#endif

void hello(void)
{
    printf("hello...\n");
}

当普通共享库使用,默认编译即可,要能够执行的话,实现一下 entry(),编译时打开 EXEC_SHARED 即可:

$ gcc -m32 -shared -fpic -o libhello.so hello.c -DEXEC_SHARED
$ ./libhello.so
hello.c 12: entry(): the real entry of shared library here.

$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./ ./hello.main
hello...

小结

本文详细讲解了如何像 libc.so 和 ld-linux.so 一样,既可以当共享库使用,还能直接执行,并且讲述了两种方法。

两种方法都可以达成目标,第一种方法用起来简单方便,第二种方法揭示了很多背后的工作逻辑。

本文分享自微信公众号 - Linux阅码场(LinuxDev)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-11-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏nginx遇上redis

pfring模块安装使用

PF_RING是Luca Deri发明的提高内核处理数据包效率,并兼顾应用程序的补丁,如Libpcap和TCPDUMP等,以及一些辅助性程序(如ntop查看并分...

19850
来自专栏DevOps持续集成

关于提交和合并流水线的实践

泽阳,运维工程师,实际工作经验4-5年,经历了传统运维到自动化运维整个过程。整理分享DevOps、CICD、编程开发、监控、日志等相关技术实践!定期更新,来吧一...

10330
来自专栏iOS小生活

Runtime再理解

Objective-C、Java、Swift等高级语言,其可读性很强,但是并不能直接被机器识别,所以就需要将这些源代码编译成相对应的机器语言(比如汇编语言),最...

4920
来自专栏趣谈前端

基于create-react-app打包编译自己的第三方UI组件库

这篇文章主要是总结一下我们在工作中如何为公司开发内部的第三方UI组件,并通过npm install的方式安装的一些步骤和思路。在学习完这套发布方法后大家也可以快...

11680
来自专栏码洞

《快学 Go 语言》第 16 课 —— 包管理 GOPATH 和 Vendor

到目前位置我们一直在编写单文件代码,只有一个 main.go 文件。本节我们要开始朝完整的项目结构迈进,需要使用 Go 语言的模块管理功能来组织很多的代码文件。

11840
来自专栏跟Qt君学编程

C++替代关键词(and,or,not)

10430
来自专栏深度学习和计算机视觉

【PCL入门系列之一】点云库PCL简介

本系列文章首先介绍什么是PCL以及PCL的功能。之后将讲解如何在Linux上安装PCL,为下一步测试、编程、开发做准备。后续的文章将对PCL官网...

22340
来自专栏代码男人

Android Gradle 多渠道打包

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

17550
来自专栏along的开发之旅

Linux下如何解压tar.gz和tar.bz2和zip

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

12650
来自专栏移动端周边技术扩展

vue加载优化(全)

8920

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励