linux上对于so库的调试——包含通过vs2019远程ssh调试so库

一、背景

对于执行程序的调试,想比大家或多或少用gdb已经操作得比较熟练了,但是往往在项目中,动态库的使用非常普遍,一旦出问题通过看代码分析或者加日志分析有时候并不高效,这篇博客里,在第二章,我们讲动态库如何通过gdb进行调试,在第三章,我们讲如何使用vs2019进行远程ssh登录来调试动态库(关于vs2019进行)。

在之前的 vs2019进行远程linux用户态调试_vs2019远程调试linux-CSDN博客 博客,我们介绍了通过vs2019进行远程linux调试的方法,但是主要是针对执行程序的调试,而对于库的调试的细节,我们在这篇博客里介绍。

二、通过gdb调试动态库

在这篇博客里,我们调试的动态库是libjemalloc.so,该动态库通过LD_PRELOAD方式,替换的是glibc里的malloc实现让程序使用libjemalloc.so里的malloc实现。

这里我先把测试的源码给出,然后在 2.1 里讲通过gdb设置库里的断点的方法,然后在 2.2 里说明增加库源码部分,让断点可以展示对应源码部分。

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>

#define NUM_THREADS 8         // 线程数量
#define TOTAL_ALLOCATIONS 800000 // 总的内存分配次数(所有线程的总和)
#define ONCE_TIMES  100
#define SMALL_ALLOC_SIZE 64   // 小的内存分配大小
#define MEDIUM_ALLOC_SIZE 512  // 中的内存分配大小
#define LARGE_ALLOC_SIZE 4096  // 大的内存分配大小

// 统计内存分配和释放时间的全局变量
double total_alloc_time = 0.0;
double total_free_time = 0.0;
pthread_mutex_t alloc_time_mutex;

// 计算时间差函数
unsigned long time_diff(struct timespec start, struct timespec end) {
    return (end.tv_sec - start.tv_sec) * 1000000000ull + (end.tv_nsec - start.tv_nsec);
}

// 线程函数
void *thread_function(void *arg) {
    struct timespec start, end;
    void *ptr[ONCE_TIMES];
    int alloc_count = 0;
    int allocations_per_thread = TOTAL_ALLOCATIONS / NUM_THREADS;

    while (alloc_count < allocations_per_thread) {
        // 依次分配小的、中等的和大的内存
        size_t alloc_size;
        if (alloc_count % 3 == 0) {
            alloc_size = SMALL_ALLOC_SIZE;  // 小的内存
        } else if (alloc_count % 3 == 1) {
            alloc_size = MEDIUM_ALLOC_SIZE; // 中的内存
        } else {
            alloc_size = LARGE_ALLOC_SIZE;  // 大的内存
        }

        clock_gettime(CLOCK_MONOTONIC, &start);  // 开始计时

        // 分配内存
        for (int i = 0; i < ONCE_TIMES; i++) {
            ptr[i] = malloc(alloc_size);
            if (ptr[i] == NULL) {
                perror("Failed to allocate memory");
                pthread_exit(NULL);
            }
        }

        clock_gettime(CLOCK_MONOTONIC, &end);  // 结束计时
        pthread_mutex_lock(&alloc_time_mutex);
        total_alloc_time += time_diff(start, end); // 统计分配时间
        pthread_mutex_unlock(&alloc_time_mutex);

        // 模拟一些工作
	    for (int i = 0; i < alloc_size; i++) *((char*)ptr[0] + i) = 0;
        usleep(100); // 100微秒

        clock_gettime(CLOCK_MONOTONIC, &start);  // 开始计时释放内存

        // 释放内存
        for (int i = 0; i < ONCE_TIMES; i++) {
            free(ptr[i]);
        }

        clock_gettime(CLOCK_MONOTONIC, &end);  // 结束计时
        pthread_mutex_lock(&alloc_time_mutex);
        total_free_time += time_diff(start, end); // 统计释放时间
        pthread_mutex_unlock(&alloc_time_mutex);

        alloc_count++;  // 增加分配计数
    }
    return NULL;
}

int main() {

    pthread_t threads[NUM_THREADS];
    pthread_mutex_init(&alloc_time_mutex, NULL);

    // 创建线程
    for (int i = 0; i < NUM_THREADS; i++) {
        if (pthread_create(&threads[i], NULL, thread_function, NULL) != 0) {
            perror("Failed to create thread");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // 打印总的内存分配和释放时间
    printf("Total memory allocation time: %.9f seconds\n", total_alloc_time / 1000000000.0f);
    printf("Total memory free time: %.9f seconds\n", total_free_time / 1000000000.0f);

    // 清理
    pthread_mutex_destroy(&alloc_time_mutex);
    return 0;
}

gcc testmalloc.c -o testmalloc后

执行前,先export一下LD_PRELOAD,libjemalloc.so这种非原始系统的库一般放在/usr/local/下

export LD_PRELOAD=/usr/local/lib/libjemalloc.so后

通过gdb执行程序testmalloc:

gdb ./testmalloc

ps aux | grep testmalloc后

得到pid,在cat /proc/<pid>/maps来确认是否加载了要调试的libjemalloc的库

可以如下图看到成功加载了libjemalloc的库:

2.1 gdb设置库里断点的方法

找断点,如break在一个函数里,需要找一个no inline的函数,如下图,如果是找imalloc_fastpath的符号是找不到的:

而找下图里的malloc_default的符号,则是可以找到的,但是符号可能并不和原始的一样:

在gdb的环境里,输入b je_malloc_default来函数的断点:

它会弹出如下框,输入y即可:

然后输入run继续运行:

可以发现能断点下来,但是找不到源文件:

2.2 增加库源码部分,让断点可以展示对应源码

其实很简单,只需要把库的源码包放到执行程序的目录下即可:

重新执行 2.1 里的步骤,到最后一步就可以看到能显示出对应的代码内容:

和源码的里的内容是一致的:

三、使用vs2019进行远程ssh登录来调试动态库

关于vs2019进行远程ssh登录来调试程序的基础操作见之前的博客 vs2019进行远程linux用户态调试_vs2019远程调试linux-CSDN博客 ,这里不再赘述。这一章主要是讲如何远程调试动态库的差异部分。

在 3.1 一节里,我们会讲通过vs2019新建个项目编译出的bin来执行单步调试(编译出bin使用了要调试的so库),在 3.2 一节里我们介绍远程ssh附加到进程的方式,来调试程序所依赖的动态库。

3.1 通过vs2019编译出的bin来调试动态链接的so库

为了单步调试方便,我们把第二章里说的程序稍微改一下,由8个线程改为1个线程:

具体vs2019添加项目,包含源文件,配置ssh登录等细节操作,见之前的博客 vs2019进行远程linux用户态调试_vs2019远程调试linux-CSDN博客

我们在vs2019远程启动程序前,设置如下的断点,断在第一次使用jemalloc进行malloc分配的地方:

另外在执行前需要配置一下vs2019里的项目的属性页:

在项目位置,点击右键:

然后如下图,在调试里的启动前命令里增加我们调试libjemalloc.so需要的配置LD_PRELOAD的参数:

bash 复制代码
export LD_PRELOAD=/usr/local/lib/libjemalloc.so

然后运行起来以后,我们打开反汇编窗口:

鼠标光标点击一下上图右边红色框的反汇编窗口里,单步执行(F11),一直单步,它会弹出一个要你选择jemalloc所用到的jemalloc.c源码的对话框,选择你放jemalloc源码的位置即可(由于vs2019会保存调试的一些信息,所以我这边不方便再去抓之前的这个弹框截图了)

可能在调试时出不来源码,可以如下图,在要查看源码的函数位置条目的地方,点击右键:

它会如下图跳转到对应源码部分:

然后,你可以在源码里加断点了,方便后面debug:

有时候,有多个线程,可以通过调试窗口里的线程对话框,看对应线程的调用栈,然后进行想要的操作:

3.2 通过远程ssh附加到进程方式来调试程序所依赖的动态库

这一节,我们介绍,在远端linux上已经运行了一个程序,通过vs2019进行ssh附加到进程进行调试(即使用gdb的attach功能),因为vs2019的可视化调试比较方便灵活,所以肯定调试效率上要比直接gdb要高一些。

为了方便演示,我们修改了一下第二章里的测试代码,增加了文件的操作,每次程序运行是覆盖创建一个名字为tag.txt的文件,写入0,等到tag.txt文件上的内容变成1后,再继续malloc的测试内容,为了方便attach进行调试。

改动的部分如下:

完成的测试代码如下:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>

#define NUM_THREADS 8         // 线程数量
#define TOTAL_ALLOCATIONS 800000 // 总的内存分配次数(所有线程的总和)
#define ONCE_TIMES  100
#define SMALL_ALLOC_SIZE 64   // 小的内存分配大小
#define MEDIUM_ALLOC_SIZE 512  // 中的内存分配大小
#define LARGE_ALLOC_SIZE 4096  // 大的内存分配大小

// 统计内存分配和释放时间的全局变量
double total_alloc_time = 0.0;
double total_free_time = 0.0;
pthread_mutex_t alloc_time_mutex;

// 计算时间差函数
unsigned long time_diff(struct timespec start, struct timespec end) {
    return (end.tv_sec - start.tv_sec) * 1000000000ull + (end.tv_nsec - start.tv_nsec);
}

// 线程函数
void *thread_function(void *arg) {
    struct timespec start, end;
    void *ptr[ONCE_TIMES];
    int alloc_count = 0;
    int allocations_per_thread = TOTAL_ALLOCATIONS / NUM_THREADS;

    while (alloc_count < allocations_per_thread) {
        // 依次分配小的、中等的和大的内存
        size_t alloc_size;
        if (alloc_count % 3 == 0) {
            alloc_size = SMALL_ALLOC_SIZE;  // 小的内存
        } else if (alloc_count % 3 == 1) {
            alloc_size = MEDIUM_ALLOC_SIZE; // 中的内存
        } else {
            alloc_size = LARGE_ALLOC_SIZE;  // 大的内存
        }

        clock_gettime(CLOCK_MONOTONIC, &start);  // 开始计时

        // 分配内存
        for (int i = 0; i < ONCE_TIMES; i++) {
            ptr[i] = malloc(alloc_size);
            if (ptr[i] == NULL) {
                perror("Failed to allocate memory");
                pthread_exit(NULL);
            }
        }

        clock_gettime(CLOCK_MONOTONIC, &end);  // 结束计时
        pthread_mutex_lock(&alloc_time_mutex);
        total_alloc_time += time_diff(start, end); // 统计分配时间
        pthread_mutex_unlock(&alloc_time_mutex);

        // 模拟一些工作
	    for (int i = 0; i < alloc_size; i++) *((char*)ptr[0] + i) = 0;
        usleep(100); // 100微秒

        clock_gettime(CLOCK_MONOTONIC, &start);  // 开始计时释放内存

        // 释放内存
        for (int i = 0; i < ONCE_TIMES; i++) {
            free(ptr[i]);
        }

        clock_gettime(CLOCK_MONOTONIC, &end);  // 结束计时
        pthread_mutex_lock(&alloc_time_mutex);
        total_free_time += time_diff(start, end); // 统计释放时间
        pthread_mutex_unlock(&alloc_time_mutex);

        alloc_count++;  // 增加分配计数
    }
    return NULL;
}

int main() {

    const char *filename = "tag.txt";

    // 新建文件并写入初始内容 0
    FILE *file = fopen(filename, "w");
    if (file == NULL) {
        perror("无法打开文件进行写入");
        return 1;
    }
    fprintf(file, "0");
    fclose(file);

    while (1) {
        // 每隔一秒读取文件内容
        sleep(1);

        file = fopen(filename, "r");
        if (file == NULL) {
            perror("无法打开文件进行读取");
            return 1;
        }

        char content[2]; // 假设内容只有一个字符 + 结束符
        fgets(content, sizeof(content), file);
        fclose(file);

        // 检查内容是否变成 1
        if (strcmp(content, "1") == 0) {
            printf("文件内容已变成 1,退出循环\n");
            break;
        }
    }

    pthread_t threads[NUM_THREADS];
    pthread_mutex_init(&alloc_time_mutex, NULL);

    // 创建线程
    for (int i = 0; i < NUM_THREADS; i++) {
        if (pthread_create(&threads[i], NULL, thread_function, NULL) != 0) {
            perror("Failed to create thread");
            exit(EXIT_FAILURE);
        }
    }

    // 等待线程结束
    for (int i = 0; i < NUM_THREADS; i++) {
        pthread_join(threads[i], NULL);
    }

    // 打印总的内存分配和释放时间
    printf("Total memory allocation time: %.9f seconds\n", total_alloc_time / 1000000000.0f);
    printf("Total memory free time: %.9f seconds\n", total_free_time / 1000000000.0f);

    // 清理
    pthread_mutex_destroy(&alloc_time_mutex);
    return 0;
}

在远端先运行该程序:

启动vs2019时,如下图选择"继续但无需代码":

打开jemalloc的源码的文件夹为了后面附加到进程后的jemalloc的调试:

然后,如下图,选择调试里的附加到进程功能:

如下图,选择ssh的连接类型,填入远程的机器ip,输入要附加到进程的进程名,确认无误后,点击附加:

如下图,勾选gdb后,确定:

然后就可以如下图里,点击暂停键把程序暂停下来:

可以看到如下图显示,程序在sleep:

和源码是匹配的:

调出解决方案资源管理器,如下图搜索刚才加过断点的文件,打开该文件:

如下图,添加断点:

然后,在远端机器上,写入tag.txt来触发程序运行测试的代码:

echo 1 > tag.txt

继续vs2019里附加到进程的程序:

可以如下图,看到能进入到刚加的断点:

相关推荐
开源优测1 小时前
这些年 devops 和自动化测试项目实践工具链集合
运维·devops
Bytebase2 小时前
MySQL 如何赶上 PostgreSQL 的势头?
运维·数据库·dba·开发者·数据库管理·devops
MonkeyKing_sunyuhua4 小时前
在 Ubuntu 22.04 上从 Wayland 切换到 X11的详细步骤
linux·运维·ubuntu
xchenhao4 小时前
Linux 环境(Ubuntu)部署 Hadoop 环境
大数据·linux·hadoop·ubuntu·hdfs·环境·dfs
凡人的AI工具箱4 小时前
每天40分玩转Django:Django DevOps实践指南
运维·后端·python·django·devops
running thunderbolt4 小时前
Linux : Linux环境开发工具vim / gcc / makefile / gdb / git的使用
linux·git·vim
高 朗4 小时前
【GO基础学习】项目日志zap Logger使用
服务器·学习·golang·日志·zap
鼾声鼾语5 小时前
thingsboard通过mqtt设备连接及数据交互---记录一次问题--1883端口没开,到服务器控制面板中打开安全组1883端口
运维·服务器·安全
alex88866 小时前
万界星空科技质量管理QMS系统具体功能介绍
运维·经验分享·科技·5g·能源·制造·流量运营
伊织code6 小时前
n8n - AI自动化工作流
运维·人工智能·自动化·agent·workflow·工作流·n8n