LD_PRELOAD 是否可能只影响主可执行文件?

The*_*ail 4 c linux dynamic-linking ld-preload

实际问题

我有一个可执行文件,默认情况下使用 EGL 和 SDL 1.2 分别处理图形和用户输入。使用LD_PRELOAD,我已将两者替换为 GLFW。

除非用户安装了GLFW的Wayland版本,否则这可以正常工作,这取决于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);
}
Run Code Online (Sandbox Code Playgroud)

共享库:

#include <stdio.h>

void do_something() {
    fputs("testing A\n", stderr);
}
Run Code Online (Sandbox Code Playgroud)

预加载库:

#include <stdio.h>

int fputs(const char *str, FILE *file) {
    // Do Nothing
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

当不使用预加载库时,输出为:

testing A
testing B
Run Code Online (Sandbox Code Playgroud)

使用时,输出什么也没有。

我正在寻找一种方法使预加载的库仅影响主可执行文件,输出将是:

testing A
Run Code Online (Sandbox Code Playgroud)

谢谢你!

Jos*_*ica 5

您可以检查返回地址是否在可执行文件或库中,然后调用“真实”函数或执行存根代码,如下所示:

#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);
}
Run Code Online (Sandbox Code Playgroud)

不过,这有一些警告。例如,如果您的可执行文件调用一个库函数,该库函数尾部调用您正在挂钩的函数,那么这将错误地将该库调用视为可执行调用。(您也可以通过为这些库函数添加包装器来缓解此问题,无条件转发到“真实”函数,并使用 编译包装器代码-fno-optimize-sibling-calls。)此外,无法区分是否是匿名可执行内存(例如,JITted 代码)最初来自可执行文件或库。

要测试这一点,请将我的代码另存为hook_fputs.c,将您的主要可执行文件另存为main.c,并将您的共享库另存为libfoo.c。然后运行这些命令:

#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);
}
Run Code Online (Sandbox Code Playgroud)