如何使 linux 共享对象(库)可自行运行?

How to make a linux shared object (library) runnable on its own?

提问人: 提问时间:7/18/2015 更新时间:5/13/2022 访问量:4024

问:

注意到创建了一个可执行文件,我只是有一个奇怪的想法来检查当我尝试运行它时会发生什么......好吧,结果是我自己的库的段错误。因此,出于好奇,我尝试“运行”glibc(在我的系统上)。果然,它没有崩溃,但为我提供了一些输出:gcc -shared/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

C Linux的

评论

1赞 bmargulies 7/18/2015
libc 是开源的。要了解它的作者如何将其制成甜点浇头和地板蜡,您可以查看来源。
2赞 7/18/2015
@bmargulies我有时会不遗余力地帮助 ppl IFF,但我认为这个问题很有趣......当然我可以挖掘所有来源,寻找该文本等......不过,如果有人知道答案并且可以(理想情况下)向我指出一些文档,那会容易得多。而且它可能会为其他人服务,只是使用谷歌。因此,让我们看看;)glibc
1赞 Octopus 7/18/2015
也许这里真正的问题是“我怎样才能创建一个有自己的入口点的共享库?这个问题可能相关。
0赞 7/18/2015
@Octopus这是一个有用的链接!似乎有人认为,确实,在 .我就试一试......[算了,这显然不是那么容易]mainglibc
1赞 alvits 7/18/2015
在 Solaris 和 Linux 上,有一个名为 .如果你正在编写一个 automake 文件,你可以编译一个存根文件,并使用 readelf 来检查编译器使用的解释器。这样的命令将是,你会看到一行,如.我不知道任何其他平台都有类似的实用程序。readelfreadelf -l stub[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

答:

24赞 jacwah 7/22/2015 #1

我写了一篇关于这个主题的博客文章,我更深入地研究,因为我发现它很有趣。你可以在下面找到我的原始答案。


可以使用 gcc 选项指定链接器的自定义入口点,其中 是库的“main”函数的名称。-Wl,-e,entry_pointentry_point

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

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

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

解释器的工作是查找并加载程序所需的共享库,准备要运行的程序,然后运行它。对于 Linux 上的 ELF 格式(在现代 *nix 中无处不在),使用该程序。有关详细信息,请参见其手册页ld-linux.so

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

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

查找路径的最简单方法是在任何普通应用程序上运行。我的系统的示例输出:ld-linux.soldd

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

当您使用 编译程序时,它不是执行程序时要调用的第一个函数。 实际上是由另一个名为 的函数调用的。此函数负责设置和其他初始化。然后调用 .返回时,使用返回值 进行调用。mainmain_startargvargcmainmain_startexitmain

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

例子.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

评论

1赞 7/22/2015
接受这一点,因为尽管大多数细节已经在评论中,但这是一个非常全面的答案,我想退出问题时的段错误会让我咬;)
0赞 jacwah 7/22/2015
@FelixPalmen我认为这个问题应该有一个正确的答案,因为它是一个非常有趣的问题:)
0赞 Petr Skocik 7/22/2015
还不如写一个常规的 main 函数,而不必指定特殊的 entry_point
1赞 7/31/2015
@jacwah对堆栈进行了一些尝试,我想出了以下用于无汇编检索命令行参数的方法: coliru.stacked-crooked.com/a/0f8dc99a1e164ff8 -- 当然取决于堆栈布局,因此完全特定于平台/实现。让我感到困惑的是,为什么省略 in 会让它破裂......void *resstruct stackframe
1赞 jacwah 8/1/2015
@FelixPalmen,我做了一些研究,并意识到推送到堆栈是标准函数调用序列的一部分。GCC 只是将其添加到每个函数的开头,即使在这种情况下不需要它。这就是堆栈顶部的内容。rbpvoid *
0赞 xjossy 5/13/2022 #2

与 gcc 条带链接时启动文件,并且某些对象(如 )将不会初始化。所以,会导致SEGFAULT。-sharedcoutstd::cout << "Abc" << std::endl

方法 1

(创建可执行库的最简单方法)

若要修复此问题,请更改链接器选项。最简单的方法 - 运行 gcc 以使用选项(详细)构建可执行文件并查看链接器命令行。在此命令行中,应删除 、(如果存在)并添加 .无论如何,源代码必须使用 (not ) 进行编译。-v-z now-pie-shared-fPIC-fPIE

让我们试试吧。例如,我们有以下 x.cpp

#include <iostream>

// The next line is required, while building executable gcc will
// anyway include full path to ld-linux-x86-64.so.2:
extern "C" const char interp_section[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";

// some "library" function
extern "C"  __attribute__((visibility("default"))) int aaa() {
    std::cout << "AAA" << std::endl;
    return 1234;
}

// use main in a common way
int main() {
    std::cout << "Abc" << std::endl;
}

首先通过 编译此文件。然后将通过 .g++ -c x.cpp -fPICg++ x.o -o x -v

我们将得到正确的可执行文件,它不能作为共享库动态加载。通过python脚本check_x.py检查一下:

import ctypes
d = ctypes.cdll.LoadLibrary('./x')
print(d.aaa())

运行将成功。运行将失败,并显示 。$ ./x$ python check_x.pyOSError: ./x: cannot dynamically load position-independent executable

链接调用时,链接器包装器调用 .您可以在最后一个命令的输出中看到命令行,如下所示:g++collect2ldcollect2g++

/usr/lib/gcc/x86_64-linux-gnu/11/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/11/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper -plugin-opt=-fresolution=/tmp/ccqDN9Df.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o x /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/11 -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/11/../../.. x.o -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/11/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crtn.o

找到那里并替换为 .运行此命令后,您将获得新的可执行文件,它将很好地用作可执行文件和共享库:-pie -z now-sharedx

$ ./x
Abc
$ python3 check_x.py
AAA
1234

这种方法有缺点:很难自动进行替换。此外,在调用之前,GCC 将为 LTO 插件创建一个临时文件(链接时间优化)。手动运行命令时,此临时文件将丢失。collect2

方法 2

(创建可执行库的适用方式)

这个想法是将 GCC 的链接器更改为自己的包装器,这将纠正 .我们将使用以下 Python 脚本 collect3.py 作为链接器:collect2

#!/usr/bin/python3
import subprocess, sys, os

marker = '--_wrapper_make_runnable_so'

def sublist_index(haystack, needle):
    for i in range(len(haystack) - len(needle)):
        if haystack[i:i+len(needle)] == needle: return i

def remove_sublist(haystack, needle):
    idx = sublist_index(haystack, needle)
    if idx is None: return haystack

    return haystack[:idx] + haystack[idx+len(needle):]

def fix_args(args):
    #print("!!BEFORE REPLACE ", *args)
    if marker not in args:
         return args

    args = remove_sublist(args, [marker])
    args = remove_sublist(args, ['-z', 'now'])
    args = remove_sublist(args, ['-pie'])

    args.append('-shared')
    #print("!!AFTER REPLACE ", *args)

    return args

# get search paths for linker directly from gcc
def findPaths(prefix = "programs: ="):
    for line in subprocess.run(['gcc', '-print-search-dirs'], stdout=subprocess.PIPE).stdout.decode('utf-8').split('\n'):
        if line.startswith(prefix): return line[len(prefix):].split(':')

# get search paths for linker directly from gcc
def findLinker(linker_name = 'collect2'):
    for p in findPaths():
        candidate = os.path.join(p, linker_name)
        #print("!!CHECKING LINKER ", candidate)
        if os.path.exists(candidate) : return candidate

if __name__=='__main__':
    args = sys.argv[1:]
    args = fix_args(args)
    exit(subprocess.call([findLinker(), *args]))

此脚本将替换参数并调用 true 链接器。为了切换链接器,我们将创建包含以下内容的文件规范 .txt

*linker:
<full path to>/collect3.py

为了告诉我们的假链接器我们想要更正参数,我们将使用附加参数。因此,完整的命令行如下:--_wrapper_make_runnable_so

g++ -specs=specs.txt -Wl,--_wrapper_make_runnable_so x.o -o x

(我们假设您要链接现有的 X.O)。

在此之后,您既可以运行目标,也可以将其用作动态库。x