一、背景
对于执行程序的调试,想比大家或多或少用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里附加到进程的程序:
可以如下图,看到能进入到刚加的断点: