LD_PRELOAD 是否可能只影响主可执行文件?
Is it possible for an LD_PRELOAD to only affect the main executable?
实际问题
我有一个默认情况下使用 EGL 和 SDL 1.2 分别处理图形和用户输入的可执行文件。使用 LD_PRELOAD
,我用 GLFW 替换了两者。
除非用户安装了 Wayland 版本的 GLFW,否则这将正常工作,这取决于 EGL 本身。因为所有 EGL 调用要么被存根不执行任何操作,要么调用 GLFW 等效项,因此它不起作用(即 eglSwapBuffers
调用 glfwSwapBuffers
,后者调用 eglSwapBuffers
等等)。我无法删除 EGL 存根,因为那样它会同时调用 EGL 和 GLFW,并且主要可执行文件是封闭源代码,所以我无法修改它。
有什么方法可以使 LD_PRELOAD
影响主可执行文件而不影响 GLFW?或任何其他获得相同效果的解决方案?
简化的问题
我做了一个简单的例子来演示这个问题。
主要可执行文件:
#include <stdio.h>
extern void do_something();
int main() {
do_something();
fputs("testing B\n", stderr);
}
共享库:
#include <stdio.h>
void do_something() {
fputs("testing A\n", stderr);
}
预加载库:
#include <stdio.h>
int fputs(const char *str, FILE *file) {
// Do Nothing
return 0;
}
不使用预加载库时,输出为:
testing A
testing B
使用时,输出为空
我正在寻找一种方法让预加载的库只影响主要的可执行文件,输出将是:
testing A
谢谢!
您可以检查 return 地址是否在可执行文件或库中,然后调用“真实”函数或执行存根代码,如下所示:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdio.h>
#include <stdlib.h>
static struct {
ElfW(Addr) start, end;
} *segments;
static int n;
static int (*real_fputs)(const char *, FILE *);
static int callback(struct dl_phdr_info *info, size_t size, void *data) {
n = info->dlpi_phnum;
segments = malloc(n * sizeof *segments);
for(int i = 0; i < n; ++i) {
segments[i].start = info->dlpi_addr + info->dlpi_phdr[i].p_vaddr;
segments[i].end = info->dlpi_addr + info->dlpi_phdr[i].p_vaddr + info->dlpi_phdr[i].p_memsz;
}
return 1;
}
__attribute__((__constructor__))
static void setup(void) {
real_fputs = dlsym(RTLD_NEXT, "fputs");
dl_iterate_phdr(callback, NULL);
}
__attribute__((__destructor__))
static void teardown(void) {
free(segments);
}
__attribute__((__noinline__))
int fputs(const char *str, FILE *file) {
ElfW(Addr) addr = (ElfW(Addr))__builtin_extract_return_addr(__builtin_return_address(0));
for(int i = 0; i < n; ++i) {
if(addr >= segments[i].start && addr < segments[i].end) {
// Do Nothing
return 0;
}
}
return real_fputs(str, file);
}
不过,这有一些注意事项。例如,如果您的可执行文件调用一个库函数,该库函数尾调用您正在挂钩的函数,那么这将错误地认为该库调用是可执行文件调用。 (您也可以通过为那些库函数添加包装器来缓解这个问题,无条件转发到“真实”函数,并使用 -fno-optimize-sibling-calls
编译包装器代码。)此外,无法区分匿名可执行内存(例如,JITted 代码)最初来自可执行文件或库。
要对此进行测试,请将我的代码保存为 hook_fputs.c
,将您的主要可执行文件保存为 main.c
,并将您的共享库保存为 libfoo.c
。然后 运行 这些命令:
clang -fPIC -shared hook_fputs.c -ldl -o hook_fputs.so
clang -fPIC -shared libfoo.c -o libfoo.so
clang main.c ./libfoo.so
LD_PRELOAD=./hook_fputs.so ./a.out
针对这两种情况分别实现插入库。
创建一个包装器脚本或程序,使用 ldd
找出确切的 EGL 库版本及其目标二进制文件动态链接的路径;然后,在 GLFW 库上使用 ldd
来查明它是否链接到 EGL。最后,让它在 LD_PRELOAD
环境变量中使用适当的插入库的路径执行目标二进制文件。
实际问题
我有一个默认情况下使用 EGL 和 SDL 1.2 分别处理图形和用户输入的可执行文件。使用 LD_PRELOAD
,我用 GLFW 替换了两者。
除非用户安装了 Wayland 版本的 GLFW,否则这将正常工作,这取决于 EGL 本身。因为所有 EGL 调用要么被存根不执行任何操作,要么调用 GLFW 等效项,因此它不起作用(即 eglSwapBuffers
调用 glfwSwapBuffers
,后者调用 eglSwapBuffers
等等)。我无法删除 EGL 存根,因为那样它会同时调用 EGL 和 GLFW,并且主要可执行文件是封闭源代码,所以我无法修改它。
有什么方法可以使 LD_PRELOAD
影响主可执行文件而不影响 GLFW?或任何其他获得相同效果的解决方案?
简化的问题
我做了一个简单的例子来演示这个问题。
主要可执行文件:
#include <stdio.h>
extern void do_something();
int main() {
do_something();
fputs("testing B\n", stderr);
}
共享库:
#include <stdio.h>
void do_something() {
fputs("testing A\n", stderr);
}
预加载库:
#include <stdio.h>
int fputs(const char *str, FILE *file) {
// Do Nothing
return 0;
}
不使用预加载库时,输出为:
testing A
testing B
使用时,输出为空
我正在寻找一种方法让预加载的库只影响主要的可执行文件,输出将是:
testing A
谢谢!
您可以检查 return 地址是否在可执行文件或库中,然后调用“真实”函数或执行存根代码,如下所示:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <link.h>
#include <stdio.h>
#include <stdlib.h>
static struct {
ElfW(Addr) start, end;
} *segments;
static int n;
static int (*real_fputs)(const char *, FILE *);
static int callback(struct dl_phdr_info *info, size_t size, void *data) {
n = info->dlpi_phnum;
segments = malloc(n * sizeof *segments);
for(int i = 0; i < n; ++i) {
segments[i].start = info->dlpi_addr + info->dlpi_phdr[i].p_vaddr;
segments[i].end = info->dlpi_addr + info->dlpi_phdr[i].p_vaddr + info->dlpi_phdr[i].p_memsz;
}
return 1;
}
__attribute__((__constructor__))
static void setup(void) {
real_fputs = dlsym(RTLD_NEXT, "fputs");
dl_iterate_phdr(callback, NULL);
}
__attribute__((__destructor__))
static void teardown(void) {
free(segments);
}
__attribute__((__noinline__))
int fputs(const char *str, FILE *file) {
ElfW(Addr) addr = (ElfW(Addr))__builtin_extract_return_addr(__builtin_return_address(0));
for(int i = 0; i < n; ++i) {
if(addr >= segments[i].start && addr < segments[i].end) {
// Do Nothing
return 0;
}
}
return real_fputs(str, file);
}
不过,这有一些注意事项。例如,如果您的可执行文件调用一个库函数,该库函数尾调用您正在挂钩的函数,那么这将错误地认为该库调用是可执行文件调用。 (您也可以通过为那些库函数添加包装器来缓解这个问题,无条件转发到“真实”函数,并使用 -fno-optimize-sibling-calls
编译包装器代码。)此外,无法区分匿名可执行内存(例如,JITted 代码)最初来自可执行文件或库。
要对此进行测试,请将我的代码保存为 hook_fputs.c
,将您的主要可执行文件保存为 main.c
,并将您的共享库保存为 libfoo.c
。然后 运行 这些命令:
clang -fPIC -shared hook_fputs.c -ldl -o hook_fputs.so
clang -fPIC -shared libfoo.c -o libfoo.so
clang main.c ./libfoo.so
LD_PRELOAD=./hook_fputs.so ./a.out
针对这两种情况分别实现插入库。
创建一个包装器脚本或程序,使用 ldd
找出确切的 EGL 库版本及其目标二进制文件动态链接的路径;然后,在 GLFW 库上使用 ldd
来查明它是否链接到 EGL。最后,让它在 LD_PRELOAD
环境变量中使用适当的插入库的路径执行目标二进制文件。