【问题标题】:C - Externs - Safe way to monitor value in LD_PRELOAD'ed libraryC - Externs - 监控 LD_PRELOAD 库中值的安全方法
【发布时间】:2016-09-30 14:03:02
【问题描述】:

背景

我帮助维护了一个简单的命令行工具diskmanager,用于监控较差的磁盘性能,主要是由于太多操作/用户同时使用同一个磁盘。我的工作涉及维护一个库 libdisksupervisor.so,它偶尔用于通过以下方式启动磁盘管理器程序来“监督”它:

LD_PRELOAD=/public/libdisksupervisor.so /sbin/diskmanager

我们这样做的原因是因为库和应用程序有非常不同的发布时间表,由于跨 NDA 无法共享源等。为了让我们的生活更轻松,diskmanager 的维护者创建了应用程序中的一些 extern 变量,并添加了对与 diskmanager 捆绑的库 (libdonothing.so) 中的“虚拟”函数的一些调用。

当调用int dummy(void)(通常在libdonothing.so中找到,但我们通过LD_PRELOAD'inglibdisksupervisor.so拦截它,其中也包含相同的函数原型),我们知道diskmanager是处于我们可以从我们自己的库中安全地读取extern int internalStatus(位于diskimager)的状态。 dummy() 的代码很简单:

# In source for diskmanager
int internalStatus = (-1);

# In libdummy.so
int dummy(void) { return 0; }

# In libdisksupervisor.so
extern int internalStatus;
int dummy(void) { syslog(LOG_ERR, "State:%d", internalStatus);

问题

到目前为止,一切都很好。几个月前,diskmanager 的维护者之一做了一些愚蠢的事情,并从diskmanager 中删除了int internalStatus,导致我们的库在执行LD_PRELOAD=/public/libdisksupervisor.so diskmanager 时导致分段错误。当一名初级工程师摸索 GCC 隐藏属性并将某些值更改为 static 并再次导致段错误时,出现了类似的问题。


问题

有什么方法可以在libdisksupervisor.so 中的代码中测试这些extern(从我们的库的角度来看)变量的存在,然后再继续,可能是通过一些神秘的链接器或GCC 魔法?我知道我可以将nmobjdump 作为预验证脚本的一部分扔给它,但我们需要单独在 库中完成此操作。

谢谢。

【问题讨论】:

  • 很明显,您将从diskmanager 的测试套件中受益,包括测试libdisksupervisor 访问的外部符号的可访问性。但这当然不能回答提出的问题。
  • @JohnBollinger 确实如此。仅将非静态全局变量设置到位并公开它以在适当的法律约束下使用是很困难的。

标签: c c gcc ld extern ld-preload


【解决方案1】:

在我们的 libdisksupervisor.so 代码中,有什么方法可以在继续之前测试这些外部变量(从我们的库的角度来看)是否存在,可能是通过一些神秘的链接器或 GCC 魔法?

您在这里遇到了时间问题。实际上,您不需要做任何特殊的事情来测试这些符号的存在和可见性在编译时在您链接的diskmanager 版本中 .当您尝试将 libdisksupervisor.so 与运行时不兼容的 diskmanager 版本一起使用时,就会出现问题。

我知道我可以将 nm 或 objdump 作为预验证脚本的一部分扔给它,但我们需要单独在我们的 c 库中完成此操作。

我不知道有任何方法可以与您运行程序的方式配合使用,并且不会轻易被diskmanager维护意外挫败。

但也许有一种方法可以改变你运行程序的方式。如果你现在调用的libdisksupervisor.so 提供了一个程序入口点(即main())并且你直接运行它,它可以dlopen()diskmanager 并通过dlsym() 检查是否存在所需的符号。然后它可以将控制权转移到diskmanagermain()(也可以通过dlsym() 访问)。您可以将其视为在系统的动态链接器和diskmanager 之间插入一个垫片。


更新:

好消息是我有一个概念证明证明它是可以做到的(见下文)。坏消息是,将主可执行文件作为共享库加载需要特殊的构建选项,听起来让对方使用这些选项进行构建可能会很麻烦。另一方面,这种方法可以让他们准确控制和记录哪些符号暴露在你的身边,也许这将成为一个合适的胡萝卜。

总之,POC 由三个 C 源文件、两个辅助文件和一个 Makefile 组成:

dummy.c

int dummy(void) {
    return 0;
}

main.c

#include <stdio.h>

int dummy(void);

#ifndef BREAKME
int internalStatus = 42;
#endif

int main(int argc, char *argv[]) {
    printf("dummy() returns %d\n", dummy());
    return 0;
}

shim.c

#include <stdlib.h>
#include <stdio.h>
#include <dlfcn.h>
#include <assert.h>

#define TARGET_PATH "./mainprog"
#define NOT_FOUND_STATUS 127
#define MISSING_SYM_STATUS 126

typedef int (*main_type)(int, char **);

static int *internalStatus_p;
#define internalStatus (*internalStatus_p);

int dummy(void) {
    return internalStatus;
}

#define LOAD_SYM(dso, name, var) do { \
    char *e_; \
    var = dlsym(dso, name); \
    e_ = dlerror(); \
    if (e_) { \
        fprintf(stderr, "%s\n", e_); \
        return MISSING_SYM_STATUS; \
    } \
} while (0)

int main(int argc, char *argv[]) {
    void *diskmanager_bin = dlopen(TARGET_PATH, RTLD_LAZY | RTLD_GLOBAL);
    char *error;
    main_type main_p;

    if (!diskmanager_bin) {
        fprintf(stderr, "Could not load " TARGET_PATH ": %s\naborting\n", dlerror());
        return NOT_FOUND_STATUS;
    } else {
        error = dlerror();
        assert(!error);
    }

    LOAD_SYM(diskmanager_bin, "internalStatus", internalStatus_p);
    LOAD_SYM(diskmanager_bin, "main", main_p);

    return main_p(argc, argv);
}

#undef LOAD_SYM

mainprog_dynamic

{
    main; internalStatus;
};

shim_dynamic

{
    dummy;
};

生成文件

# sources contributing to a shared library must be built with -fpic or -fPIC
CFLAGS = -fPIC -std=c99
LDFLAGS = 

SHLIB_LDFLAGS = -shared
SHLIB_EXTRALIBS = -lc

# Sources contributing to the main program should be built with -fpie or -fPIE
SHMAIN_CFLAGS = -fpie
# The main program must be linked with -pie
SHMAIN_LDFLAGS = -pie

DL_EXTRALIBS = -ldl

LIBDUMMY_SO_VER = 0
LIBDUMMY = libdummy.so.$(LIBDUMMY_SO_VER)

all: mainprog shim

mainprog: main.o $(LIBDUMMY) mainprog_dynamic
    $(CC) $(CFLAGS) $(SHMAIN_CFLAGS) $(LDFLAGS) $(SHMAIN_LDFLAGS) -Wl,--dynamic-list=mainprog_dynamic -o $@ $< $(LIBDUMMY) $(SHLIB_EXTRALIBS)

main.o: main.c
    $(CC) $(CPPFLAGS) $(CFLAGS) $(SHMAIN_CFLAGS) -c -o $@ $<

libdummy.so.$(LIBDUMMY_SO_VER): libdummy.so
    ln -sf $< $@

libdummy.so: dummy.o
    $(CC) -o $@ $(CFLAGS) $(LDFLAGS) $(SHLIB_LDFLAGS) -Wl,-soname,libdummy.so.$(LIBDUMMY_SO_VER) $^ $(SHLIB_EXTRALIBS)

shim: shim.o shim_dynamic
    $(CC) $(CFLAGS) $(LDFLAGS) -Wl,--dynamic-list=shim_dynamic -o $@ $< $(DL_EXTRALIBS)

test: all
    @echo "LD_LIBRARY_PATH=`pwd` ./mainprog :"
    @LD_LIBRARY_PATH=`pwd` ./mainprog
    @echo "LD_LIBRARY_PATH=`pwd` ./shim :"
    @LD_LIBRARY_PATH=`pwd` ./shim

clean:
    rm -f *.o *.so *.so.* mainprog shim

这模拟了您描述的情况,您要覆盖的函数位于单独的共享库中。它假定使用 GNU 工具链。成功构建示例 (make all) 后,您可以 make test 进行演示:

$ make test
LD_LIBRARY_PATH=/tmp/dl ./mainprog :
dummy() returns 0
LD_LIBRARY_PATH=/tmp/dl ./shim :
dummy() returns 42

*_dynamic 文件告诉链接器两个可执行文件中应包含在导出(动态)符号中的符号,即使链接中没有任何内容引用它们。

这种方法不允许 shim 直接引用主程序的internalStatus 变量,因为这样 shim 需要将主程序作为库链接,并且当 shim 时动态链接器会自动加载它运行。对变量的引用总是被立即绑定,所以如果internalStatus 消失了,就会导致动态链接器出错,这超出了 shim 的控制。

【讨论】:

  • 这听起来很合理。请问我可以请求一个小代码sn-p吗?谢谢!
  • @DevNull,更新了概念验证代码和附加评论。 TL;DR:它可以工作,但你需要对方的一些合作。
  • 这太棒了。我唯一的遗憾是被测代码不是作为 PIE 构建的。不过,这很棒。谢谢!
猜你喜欢
  • 1970-01-01
  • 2011-03-25
  • 1970-01-01
  • 2011-02-11
  • 2020-06-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多