代码思路

  1. 测试程序

编写两个简单的测试程序,分别调用库函数和自定义函数:

    [main.c]
#include <stdio.h>
#include <stdlib.h>

int main() {
  int **a = (int **)malloc(sizeof(int *) * 3);
  for (int i = 0; i < 3; ++i)
    a[i] = (int *)malloc(sizeof(int) * 5);
  return 0;
}
    [foo.c]
#include "stdio.h"

void foo(){};

void bar(int i) {
  printf("%d\n", i);
  foo();
}

int main() {
  for (int i = 0; i < 3; ++i)
    bar(i);
  return 0;
}

注意部分功能需使用 #define _GNU_SOURCE 启用。

  1. -finstrument-functions

使用 -finstrument-functions 参数,编译器会自动在函数出入口增加:

void __cyg_profile_func_enter (void *this_fn,
                               void *call_site);
void __cyg_profile_func_exit  (void *this_fn,
                               void *call_site);

从而起到注入作用。

  [cyg.c]
#include <dlfcn.h>
#include <stdio.h>

void __cyg_profile_func_enter(void *this_fn, void *call_site) {
  Dl_info dl_func, dl_caller;
  if (dladdr(this_fn, &dl_func) && dladdr(call_site, &dl_caller))
    printf("Entering func: %s, caller: %s\n", dl_func.dli_sname,
           dl_caller.dli_sname);
}

void __cyg_profile_func_exit(void *this_fn, void *call_site) {
  Dl_info dl_func, dl_caller;
  if (dladdr(this_fn, &dl_func) && dladdr(call_site, &dl_caller))
    printf("Exiting func: %s, caller: %s\n", dl_func.dli_sname,
           dl_caller.dli_sname);
}

编译方法:

gcc cyg.c -c
gcc main.c cyg.o -finstrument-functions -ldl -rdynamic

这种方法应用于所有函数,适用于 debug 等场景,但需注意无法在库函数使用。

  1. -Wl,--wrap

使用 --wrap=foo 参数,链接器会把 __real_foo 解析为 foo,并增加 __wrap_foo 供注入使用。

    [wrap.c]
#include <stdio.h>

void *__real_malloc(size_t size);

void *__wrap_malloc(size_t size) {
  fprintf(stderr, "malloc(%ld) = ", size);
  void *ret = __real_malloc(size);
  fprintf(stderr, "%p\n", ret);
  return ret;
}

编译方法:

gcc main.c wrap.c -Wl,--wrap=malloc
  1. LD_PRELOAD

LD_PRELOAD 环境变量用于加载动态库,优先级最高,可以用于注入。

    [preload.c]
#include <dlfcn.h>
#include <stdio.h>

static void *(*real_malloc)(size_t);

static void init() {
  if ((real_malloc = dlsym(RTLD_NEXT, "malloc")) == NULL)
    fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
}

void *malloc(size_t size) {
  if (real_malloc == NULL)
    init();
  fprintf(stderr, "malloc(%ld) = ", size);
  void *ret = real_malloc(size);
  fprintf(stderr, "%p\n", ret);
  return ret;
}

编译方法:

gcc -fPIC -shared preload.c -ldl
LD_PRELOAD=./preload.so ./main

这种方法无需重新编译,也无需源代码,需要函数为动态链接。

实验结果

  1. 测试程序

使用 readwrite 编写测试程序:

#include <fcntl.h>
#include <unistd.h>

int main() {
  char buf[1024];
  int fn = open("rand.txt", O_RDONLY);
  size_t nread;
  if (fn != -1) {
    while ((nread = read(fn, buf, sizeof(buf))) > 0)
      write(1, buf, nread);
    close(fn);
  }
  return 0;
}

调用函数详见 read(2)write(2),避免重复以 read 为例。

  1. LD_PRELOAD

-Wl,--wrapLD_PRELOAD 本质相同,使用后者直接注入可执行文件。

共用部分:

#define _GNU_SOURCE

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

static ssize_t (*real_read)(int, void *, size_t);

支持 profile 功能:

static size_t read_time = 0;
static size_t read_count = 0;
static size_t read_size = 0;

void read_status() {
  fprintf(stderr, "read: %ld, %ld, %ld\n", read_time, read_count, read_size);
}

static void read_init() {
  if ((real_read = dlsym(RTLD_NEXT, "read")) == NULL)
    fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
  atexit(read_status);
}

ssize_t read(int fd, void *buf, size_t count) {
  if (real_read == NULL)
    read_init();
  clock_t tic = clock();
  ssize_t ret = real_read(fd, buf, count);
  clock_t toc = clock();
  read_time += toc - tic;
  ++read_count;
  read_size += ret;
  return ret;
}

支持 trace 功能:

static void read_init() {
  if ((real_read = dlsym(RTLD_NEXT, "read")) == NULL)
    fprintf(stderr, "Error in `dlsym`: %s\n", dlerror());
}

ssize_t read(int fd, void *buf, size_t count) {
  if (real_read == NULL)
    read_init();
  fprintf(stderr, "read(%d, %p, %ld) = ", fd, buf, count);
  ssize_t ret = real_read(fd, buf, count);
  fprintf(stderr, "%ld\n", ret);
  return ret;
}

可以使用 -D 等方式合并为同一程序,经测试与 iotrace 结果相同。

开销结果

反编译结果如下:

read(int, void*, unsigned long):
    push    rbp
    mov     rbp, rsp
    sub     rsp, 64
    mov     DWORD PTR [rbp-36], edi
    mov     QWORD PTR [rbp-48], rsi
    mov     QWORD PTR [rbp-56], rdx
    mov     rax, QWORD PTR real_read[rip]
    test    rax, rax
    jne     .L5
    call    read_init()

注入开销与对应函数参数有关,read 包含 9 个额外指令。

思考总结

常见注入有三种形式:

  1. -finstrument-functions 作用于所有用户函数;
  2. -Wl,--wrap 在链接时注入;
  3. LD_PRELOAD 在执行时注入,需要为动态链接函数。

注入开销不大,但仍有优化空间,如 Google 工具达到了较高速度,可以参考学习。