Linux C/C++調試之一:利用LD_PRELOAD機制監控程序IO操作

#引言
有時,我們爲了分析程序的某些問題(例如性能問題),需要對程序的IO操作的頻率耗時等數據進行監控。就以經常調用的read和write函數爲例,如何監控程序中所有上述兩種操作,同時進行計時呢?使用gdb打斷點自然是個方法,這樣可以很容易發現程序中所有的調用,但是這樣就沒辦法計時了。如果在程序中所有read/write函數前後打上時間戳,就可以實現計時了,但是這會有另一個問題,我們可能調用了某些三方庫的接口,可能沒辦法爲三方庫的read/write函數打時間戳計時。那麼有沒有辦法既能監控所有read/write函數調用,同時進行計時呢?自然是有的,例如本文下面要講的——利用LD_PRELOAD機制實現上述目標。
關於LD_PRELOAD的概述,在之前一篇博文我已經進行過描述:CSAPP第三版運行時打樁Segmentation fault。這裏貼上其部分內容:

CSAPP第三版7.13.3節提到了運行時打樁機制,它可以在運行時將程序中對共享庫函數的調用進行截獲,替換爲執行自己的代碼。這個機制基於動態鏈接器的LD_PRELOAD環境變量。如果LD_PRELOAD環境變量被設置爲一個共享路徑名的列表(以空格或分號分隔),那麼當加載和執行一個程序,需要解析未定義的引用時,動態鏈接器(ld-linux.so)會先搜索LD_PRELOAD庫,然後才搜索任何其他的庫。有了這個機制,當加載和執行任意可執行文件時,可以對共享庫中的任何函數打樁,包括libc.so

我們要做的就是對C標準庫的read/write函數進行包裝,實現計時功能。同時在運行時利用LD_PRELOAD將C標準庫的read/write函數替換成我們的實現。
#包裝read/write函數
直接上代碼:

#ifndef _GNU_SOURCE
# define _GNU_SOURCE
#endif

#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <unistd.h>
#include <sys/time.h>

// 計時函數,返回值是一個毫秒值
static double getMs()
{
    struct timeval tv;
    gettimeofday(&tv, NULL);
    return tv.tv_sec * 1000 + tv.tv_usec * 0.001;
}

ssize_t read(int fd, void *buf, size_t count)
{
    // 爲什麼要有print_times這個變量,請查看上面我貼出來的那篇博文
    static __thread int print_times = 0;
    print_times++;
    // 用於保存真正的read函數
    ssize_t (*readp)(int, void *, size_t);
    char *error;
    // 獲取真正的read函數
    readp = dlsym(RTLD_NEXT, "read");
    if ((error = dlerror()) != NULL)
    {
        fputs(error, stderr);
        exit(1);
    }
    
    double start = getMs();
    // 調用真正的read函數
    ssize_t result = readp(fd, buf, count);
    double stop = getMs();

    if (print_times == 1)
    {
        // 輸出調用read時的實參和耗時
        printf("read(%d, %p, %lu) %lf\n", fd, buf, count, stop - start);
    }
    print_times--;
    return result;
}

ssize_t write(int fd, const void *buf, size_t count)
{
    static __thread int print_times = 0;
    print_times++;

    ssize_t (*writep)(int, const void *, size_t);
    char *error;

    writep = dlsym(RTLD_NEXT, "write");
    if ((error = dlerror()) != NULL)
    {
        fputs(error, stderr);
        exit(1);
    }
    
    double start = getMs();
    ssize_t result = writep(fd, buf, count);
    double stop = getMs();

    if (print_times == 1)
    {
        printf("write(%d, %p, %lu) %lf\n", fd, buf, count, stop - start);
    }
    print_times--;
    return result;
}

上面的代碼即能夠對read/write函數進行計時,並將計時結果輸出到標準輸出。
將上面的代碼編譯成庫:

$ gcc -shared -fpic -Wall -Wextra -o libwrapper.so wrapper.c -ldl

這樣,我們就得到了一個libwrapper.so文件。
#替換read/write函數
用於測試的demo代碼如下:

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

int main()
{
    int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);
    if (fd == -1)
    {
        exit(1);
    }

    write(fd, "test", 4);
    lseek(fd, 0, SEEK_SET);
    char buf[5] = {0};
    read(fd, buf, 4);
    close(fd);

    printf("%s\n", buf);
    return 0;
}

編譯成可執行文件:

$ g++ main.c

那麼如何使用前面生成的libwrapper.so文件呢?就像下面這樣:

$ LD_PRELOAD=./libwrapper.so ./a.out

其輸出爲:

write(3, 0x55a5dd6c29ad, 4) 0.023926
read(3, 0x7ffcf0a92fa3, 4) 0.010010
test

這樣,我們就達到了監控read/write函數的目標,對於其他IO庫函數,你也可以根據需要進行包裝替換。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章