修改动态库,无需更改源代码

作者:Greg Kroah-Hartman

有时,您可能想确定共享库中发生了什么,而无需修改该库(您最近尝试构建 glibc 了吗?)。其他时候,您可能只想覆盖库中的几个函数,并让它们执行其他操作——强制进程到特定的 CPU,阻止发送特定的 USB 消息等等。 如果您使用 LD_PRELOAD 环境变量和放置在应用程序和库之间的一个小型 shim 程序,所有这些任务都是可能的。

例如,假设您创建一个名为 shim.so 的共享库对象,并希望它在任何其他共享库之前加载。假设您还想运行程序“test”。 这些事情可以在命令行中使用 LD_PRELOAD 来完成

	LD_PRELOAD=/home/greg/test/shim.so ./test

此命令告诉 glibc 首先从目录 /home/greg/test 加载库 shim.so,然后再加载 test 程序可能需要的任何其他共享库。

由于内核开发人员 Anton Blanchard 发送的一封旧电子邮件的帮助,创建一个 shim 库非常容易。 他的电子邮件提供了一个关于如何准确地做到这一点的例子。

例如,让我们记录一些关于 libusb 库如何被另一个程序调用的信息。 libusb 是一个程序用来从用户空间访问 USB 设备的库,从而无需为所有 USB 设备创建内核驱动程序。

从 libusb 提供的 usb_open() 函数开始,以下一小段代码用于记录对该函数的所有访问

usb_dev_handle *usb_open(struct usb_device *dev)
{
  static usb_dev_handle *(*libusb_open)
             (struct usb_device *dev) = NULL;
  void *handle;
  usb_dev_handle *usb_handle;
  char *error;

  if (!libusb_open) {
    handle = dlopen("/usr/lib/libusb.so",
                    RTLD_LAZY);
    if (!handle) {
      fputs(dlerror(), stderr);
      exit(1);
    }
    libusb_open = dlsym(handle, "usb_open");
    if ((error = dlerror()) != NULL) {
      fprintf(stderr, "%s\n", error);
      exit(1);
    }
  }

  printf("calling usb_open(%s)\n", dev->filename);
  usb_handle = libusb_open(dev);
  printf("usb_open() returned %p\n", usb_handle);
  return usb_handle;
}

要编译这段代码,请使用以下选项运行 GCC

 gcc -Wall -O2 -fpic -shared -ldl -o shim.so shim.c

此命令从 shim.c 源代码文件创建一个名为 shim.so 的共享库。 然后,如果 test 程序使用之前的 LD_PRELOAD 行运行,则 test 函数中对函数 usb_open() 的任何调用都将调用我们的库函数。 我们函数中的代码尝试使用调用 dlopen 的函数加载真正的 libusb 库

    handle = dlopen("/usr/lib/libusb.so",
                    RTLD_LAZY);

此处传递了选项 RTLD_LAZY,因为我不希望加载程序此时解析所有符号。 我希望仅当 shim 代码要求解析它们时才解析它们。

如果该函数成功,则代码会通过调用 dlsym 请求指向真正的 usb_open 函数的指针

    libusb_open = dlsym(handle, "usb_open");

如果该函数成功,则 shim 现在有一个指向真正的 libusb 函数的指针。 它可以在将一些信息记录到屏幕后随时调用真正的函数

  printf("calling usb_open(%p)\n", dev);
  usb_handle = libusb_open(dev);

这也允许代码在库函数被调用之后以及控制返回到原始程序之前执行某些操作。

运行带有此 shim 的程序的示例可能会产生以下输出

 calling usb_open(002)
 usb_open() returned 0x8061100
 calling usb_open(002)
 usb_open() returned 0x8061100
 calling usb_open(002)
 usb_open() returned 0x8061100
 calling usb_open(002)
 usb_open() returned 0x8061100
 calling usb_open(002)
 usb_open() returned 0x8061120
 calling usb_open(002)
 usb_open() returned 0x8061120

要记录一个更复杂的函数,例如 usb_control_message,需要做与 usb_open 相同的事情

int usb_control_msg(usb_dev_handle *dev, 
                    int requesttype,
                    int request,
                    int value,
                    int index,
                    char *bytes,
                    int size,
                    int timeout)
{
  static int(*libusb_control_msg)
            (usb_dev_handle *dev,
             int requesttype, int request,
             int value, int index, char *bytes,
             int size, int timeout) = NULL;
  void *handle;
  int ret, i;
  char *error;

  if (!libusb_control_msg) {
    handle = dlopen("/usr/lib/libusb.so", RTLD_LAZY);
    if (!handle) {
      fputs(dlerror(), stderr);
      exit(1);
    }
    libusb_control_msg = dlsym(handle, "usb_control_msg");
    if ((error = dlerror()) != NULL) {
      fprintf(stderr, "%s\n", error);
      exit(1);
    }
  }

  printf("calling usb_control_msg(%p, %04x, "
         "%04x, %04x, %04x, %p, %d, %d)\n"
         "\tbytes = ", dev, requesttype, 
         request, value, index, bytes, size,
         timeout);
  for (i = 0; i < size; ++i)
    printf ("%02x ", (unsigned char)bytes[i]);
  printf("\n");

  ret = libusb_control_msg(dev, requesttype, 
                           request, value, 
                           index, bytes, size,
                           timeout);

  printf("usb_control_msg(%p) returned %d\n"
         "\tbytes = ", dev, ret);
  for (i = 0; i < size; ++i)
    printf ("%02x ", (unsigned char)bytes[i]);
  printf("\n");

  return ret;
}

再次加载 shim 库运行测试程序会产生以下输出

usb_open() returned 0x8061100
calling usb_control_msg(0x8061100, 00c0, 0013, 6c7e, c41b, 0x80610a8, 8, 1000)
        bytes = c9 ea e7 73 2a 36 a6 7b 
usb_control_msg(0x8061100) returned 8
        bytes = 81 93 1a c4 85 27 a0 73 
calling usb_open(002)
usb_open() returned 0x8061120
calling usb_control_msg(0x8061120, 00c0, 0017, 9381, c413, 0x8061100, 8, 1000)
        bytes = 39 83 1d cc 85 27 a0 73 
usb_control_msg(0x8061120) returned 8
        bytes = 35 15 51 2e 26 52 93 43 
calling usb_open(002)
usb_open() returned 0x8061120
calling usb_control_msg(0x8061120, 00c0, 0017, 9389, c413, 0x8061108, 8, 1000)
        bytes = 80 92 1b c6 e3 a3 fa 9d 
usb_control_msg(0x8061120) returned 8
        bytes = 80 92 1b c6 e3 a3 fa 9d 

使用 LD_PRELOAD 环境变量,很容易将您自己的代码放置在程序和它所链接的库之间。

Greg Kroah-Hartman 目前是 Linux 内核中各种不同驱动程序子系统的维护者。 他为 IBM 工作,从事与 Linux 内核相关的工作,可以通过 greg@kroah.com 与他联系。

加载 Disqus 评论