提问人: 提问时间:7/18/2015 更新时间:5/13/2022 访问量:4024
如何使 linux 共享对象(库)可自行运行?
How to make a linux shared object (library) runnable on its own?
问:
注意到创建了一个可执行文件,我只是有一个奇怪的想法来检查当我尝试运行它时会发生什么......好吧,结果是我自己的库的段错误。因此,出于好奇,我尝试“运行”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
答:
我写了一篇关于这个主题的博客文章,我更深入地研究,因为我发现它很有趣。你可以在下面找到我的原始答案。
可以使用 gcc 选项指定链接器的自定义入口点,其中 是库的“main”函数的名称。-Wl,-e,entry_point
entry_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.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
例子.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
评论
void *res
struct stackframe
rbp
void *
与 gcc 条带链接时启动文件,并且某些对象(如 )将不会初始化。所以,会导致SEGFAULT。-shared
cout
std::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 -fPIC
g++ x.o -o x -v
我们将得到正确的可执行文件,它不能作为共享库动态加载。通过python脚本check_x.py检查一下:
import ctypes
d = ctypes.cdll.LoadLibrary('./x')
print(d.aaa())
运行将成功。运行将失败,并显示 。$ ./x
$ python check_x.py
OSError: ./x: cannot dynamically load position-independent executable
链接调用时,链接器包装器调用 .您可以在最后一个命令的输出中看到命令行,如下所示:g++
collect2
ld
collect2
g++
/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
-shared
x
$ ./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
评论
glibc
main
glibc
readelf
readelf -l stub
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]