问题 如何让linux共享对象(库)自行运行?


注意到了 gcc -shared 创建一个可执行文件,我只是有一个奇怪的想法,检查当我尝试运行它时会发生什么...好结果是一个 段错误 为了我自己的lib。因此,对此感到好奇,我试图“运行”glibc(/lib/x86_64-linux-gnu/libc.so.6 在我的系统上)。果然,它没有崩溃,但为我提供了一些输出:

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/>.

所以我的问题是:这背后的魔力是什么?我不能只定义一个 main 图书馆里的符号 - 或者我可以吗?


11854
2017-07-17 21:11


起源

libc是开源的。要了解其作者如何将其制作成甜点浇头和地板蜡,您可以查看来源。 - bmargulies
@bmargulies我有时会在这里竭尽全力帮助IFF IFF我觉得这个问题很有意思...我确定我可以挖掘所有的 glibc 来源,寻找那个文本等...仍然会是一个 批量 如果有人知道答案会更容易,并且(理想情况下)可以指向某些人 文件。它可能只会使用谷歌服务其他人。所以,让我们看看;)
也许真正的问题是“如何创建一个拥有自己入口点的共享库?” 这个问题 可能是相关的。 - Octopus
@Octopus这是一个有用的链接!似乎有些人认为,的确如此 main 定义于 glibc。我只是试一试...... [忘记了,显然不那么容易]
在Solaris和Linux上有一个名为的实用程序 readelf。如果您正在编写automake文件,则可以编译存根文件并使用readelf检查编译器使用的解释器。这样的命令将是 readelf -l stub 你会看到像这样的一条线 [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]。我不知道任何其他平台的类似效用。 - alvits


答案:


我写了 一篇博文 在这个问题上,我更深入,因为我发现它很有趣。您可以在下面找到我的原始答案。


您可以使用指定链接器的自定义入口点 -Wl,-e,entry_point gcc的选项,在哪里 entry_point 是库的“主要”功能的名称。

void entry_point()
{
    printf("Hello, world!\n");
}

链接器不期望链接的东西 -shared 要作为可执行文件运行,并且必须为程序提供更多信息才能运行。如果您现在尝试运行库,则会遇到分段错误。

.interp部分是操作系统运行应用程序所需的结果二进制文件的一部分。它是由链接器自动设置的 -shared 未使用。如果要构建要自行执行的共享库,则必须在C代码中手动设置此部分。看到 这个问题

解释器的工作是查找并加载程序所需的共享库,准备程序运行,然后运行它。对于Linux上的ELF格式(普遍适用于现代* nix), ld-linux.so 使用程序。看到了 手册页 了解更多信息。

下面的行使用.interp部分中的字符串 GCC属性。将它放在库的全局范围内,以明确告诉链接器您希望在二进制文件中包含动态链接器路径。

const char interp_section[] __attribute__((section(".interp"))) = "/path/to/ld-linux";

找到路径的最简单方法 ld-linux.so 是跑 ldd 在任何正常的应用程序我系统的示例输出:

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)

一旦你指定了解释器,你的库应该是可执行的!只有一个轻微的缺陷:它会在何时发生 entry_point 回报。

使用时编译程序 main,它不是执行它时调用的第一个函数。 main 实际上是被另一个叫做的函数调用 _start。此功能负责设置 argv 和 argc 和其他初始化。然后它打电话 main。什么时候 main 回报, _start 电话 exit 具有返回值 main

堆栈中没有返回地址 _start 因为它是第一个被调用的函数。如果它尝试返回,则发生无效读取(最终导致分段错误)。这正是我们的入口点函数中发生的事情。添加电话 exit 作为您的输入功能的最后一行,以正确清理而不是崩溃。

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);
}

编译 gcc example.c -shared -fPIC -Wl,-e,entry_point


11
2017-07-21 23:11



接受这一点,因为尽管MOST细节已经在评论中,但这是一个非常全面的答案,我想是 出口处的段错误 问题会咬我;)
@FelixPalmen我认为这个问题应该有一个正确的答案,因为它是一个非常有趣的:) - jacwah
不妨编写一个常规的main函数,而不必指定一个特殊的entry_point - PSkocik
@jacwah在堆栈中玩了一下,我想出了以下内容 集会自由 检索命令行参数: coliru.stacked-crooked.com/a/0f8dc99a1e164ff8  - 当然取决于堆栈布局,因此完全基于平台/实现。令我感到困惑的是为什么要离开 void *res 在 struct stackframe 让它破裂......
@FelixPalmen我做了一些研究,开始意识到推动 rbp 堆栈是标准函数调用序列的一部分。 GCC只是将它添加到每个函数的开头,即使在这种情况下不需要它。这是什么的 void * 在堆栈的顶部是。 - jacwah