关键要点

  • 研究表明,Linux 动态库加载、卸载和使用的稳定性测试用例需要覆盖正常操作、边缘情况和异常情况。
  • 证据倾向于建议测试包括加载不存在的库、重复加载库、卸载正在使用的库等场景。
  • 测试可能涉及使用 dlopendlsymdlclose 函数,并需处理潜在的崩溃情况。

概述

Linux 动态库的稳定性测试是确保系统在各种条件下可靠运行的重要步骤。以下是测试用例的设计和实施方法,适合希望深入了解的普通用户。

测试用例设计

测试用例应涵盖以下主要场景:

  • 加载异常情况:尝试加载不存在的库或路径错误的库,检查是否正确返回错误。
  • 重复加载和卸载:测试同一库的多次加载和卸载,确保引用计数正确。
  • 使用中卸载:测试在库仍在使用时卸载,确保不会导致程序崩溃(可能需要子进程处理)。
  • 依赖和权限:测试带依赖关系的库加载,以及权限不足时的行为。
实施建议
  • 使用 C 语言编写测试程序,调用 dlfcn.h 中的函数。
  • 为可能导致崩溃的测试(如卸载后使用函数),使用 fork 创建子进程,父进程检查子进程是否按预期崩溃。
  • 确保测试用例独立运行,避免相互干扰。
意外细节

一个有趣的发现是,dlclose 不会立即卸载库,而是根据引用计数决定,需多次调用才能完全卸载,这可能影响测试设计。


详细报告

Linux 动态库(共享库)的加载、卸载和使用是系统运行中的关键部分,其稳定性测试对于确保软件在各种异常情况下的可靠性至关重要。以下是全面的测试用例设计和实施方法,涵盖了正常操作、边缘情况和异常场景,旨在为用户提供一个系统化的测试框架。

背景与理论基础

动态库在 Linux 中通过 dlopendlsymdlclose 函数进行管理。这些函数分别用于加载库、获取函数指针和卸载库。根据官方文档(如 dlopen(3) - Linux man page),dlopen 返回一个不透明的句柄,dlclose 根据引用计数决定是否卸载库。测试的重点是确保这些操作在各种条件下稳定运行,尤其是在异常情况下的行为。

测试用例设计

以下是覆盖各种异常情况的测试用例列表,分为多个类别:

类别

测试场景

预期行为

加载异常

加载不存在的库

返回 NULL,dlerror 报告“文件不存在”错误


加载路径错误的库

返回 NULL,dlerror 报告路径相关错误

重复加载与卸载

加载已加载的库

返回相同句柄,引用计数增加


多次 dlclose 后卸载

库在引用计数为 0 时卸载

使用中卸载

卸载仍在使用的库(函数指针在使用中)

可能导致崩溃,需通过子进程测试

依赖与版本管理

加载带依赖关系的库

自动加载依赖库,无错误


多个版本库的符号解析

按路径或环境变量(如 LD_LIBRARY_PATH)解析

权限与安全

加载权限不足的库

返回 NULL,dlerror 报告权限错误

线程安全

多个线程同时加载/卸载库

无死锁或竞争条件,正确管理引用计数

异常文件

加载损坏的库文件

返回 NULL,dlerror 报告文件格式错误

详细测试用例实现

以下是部分测试用例的实现示例,基于 C 语言,展示了如何处理异常情况:

  1. 加载不存在的库
  • 调用 dlopen("non_existent_library.so", RTLD_LAZY)
  • 预期:返回 NULL,dlerror 返回类似“文件不存在”的错误。
  • 代码:
void* handle = dlopen("non_existent_library.so", RTLD_LAZY);
if (handle == NULL) {
    const char* error = dlerror();
    if (error != NULL && strstr(error, "No such file or directory") != NULL) {
        printf("Test passed: Library not found.\n");
    } else {
        printf("Test failed: Unexpected error.\n");
    }
} else {
    printf("Test failed: Library loaded unexpectedly.\n");
}
  1. 加载已加载的库
  • 第一次调用 dlopen("", RTLD_LAZY),保存句柄。
  • 第二次调用,检查是否返回相同句柄。
  • 预期:返回相同句柄,证明引用计数增加。
  • 代码:
void* handle1 = dlopen("", RTLD_LAZY);
if (handle1 == NULL) {
    printf("Test failed: Cannot load library.\n");
    return 1;
}
void* handle2 = dlopen("", RTLD_LAZY);
if (handle2 == handle1) {
    printf("Test passed: Same handle returned.\n");
} else {
    printf("Test failed: Different handles.\n");
}
dlclose(handle1);
dlclose(handle2);
  1. 卸载仍在使用的库
  • 使用 fork 创建子进程,子进程加载库、获取函数指针、使用函数后卸载,再尝试调用。
  • 预期:子进程因调用已卸载的函数崩溃,父进程检查状态。
  • 代码:
pid_t pid = fork();
if (pid < 0) {
    printf("Test failed: Fork failed.\n");
    return 1;
} else if (pid == 0) {
    void* handle = dlopen("", RTLD_LAZY);
    if (handle == NULL) exit(1);
    int (*func)(int, int) = dlsym(handle, "add");
    if (func == NULL) exit(1);
    int result = func(1, 2);
    if (result != 3) exit(1);
    dlclose(handle);
    result = func(1, 2); // 应崩溃
    exit(0);
} else {
    int status;
    waitpid(pid, &status, 0);
    if (WIFSIGED(status)) {
        printf("Test passed: Child crashed as expected.\n");
    } else {
        printf("Test failed: Child did not crash.\n");
    }
}
测试框架建议
  • 独立性:每个测试用例应在独立的环境中运行,避免状态干扰。可以使用 fork 或单独进程。
  • 错误处理:使用 dlerror 检查所有操作的错误信息,确保报告准确。
  • 性能考虑:对于长时间运行的测试(如线程安全),设置超时机制。
相关资源与参考

测试设计参考了 glibc 的测试套件文档,特别是在 dlfcn 相关测试中。用户可进一步查阅 Testing/Testsuite - glibc wiki 获取更多细节。

结论

通过上述测试用例,覆盖了 Linux 动态库加载、卸载和使用的各种异常情况,确保系统在复杂场景下的稳定性。用户可根据实际需求扩展测试,特别是在多线程或高负载环境下的行为。

关键引用