46、共享对象调试与内存问题排查

共享对象调试与内存问题排查

1. 创建共享对象

从概念上讲,共享对象和程序的唯一区别通常在于共享对象一般没有 main 函数,但这并非硬性要求。你可以创建既能像可执行文件一样被调用,又能动态链接到更大程序中的共享对象,例如动态链接器本身就是这样的共享对象,它被之前提到的 ldd 命令所使用。

创建一个简单的共享对象很容易,只需像构建程序一样操作,但要使用 -shared -fpic 标志。示例命令如下:

$ cc -shared -fpic -o libmylib.so mylib1.c mylib2.c
  • -shared 标志用于告诉链接器生成共享对象而非可执行文件。
  • -fpic 标志告知编译器生成位置无关代码,这很重要,因为与传统可执行文件不同,共享对象的虚拟地址直到运行时才可知。

将程序与共享对象链接看似简单,示例命令如下:

$ cc -o myprog myprog.o -L . -lmylib

这里使用 -L 选项告知链接器共享库位于当前目录。但问题是,运行时链接器 ld-linux.so 也需要知道在哪里找到这个共享对象。当你尝试运行程序时就会发现问题:

$ ./myprog
./myprog: error while loading shared libraries: libmylib.so: cannot open shared object file: No such file or directory

问题在于系统不知道共享对象的位置。与共享对象链接的程序不包含查找共享对象位置的信息,这是有意为之,因为共享对象在每个系统上都位于特定位置。如果应用程序要在不同系统上运行,不应假设共享对象的位置。相反,每个共享对象都提供一个所谓的 soname ,动态链接器使用它来识别对象。你创建的库没有指定 soname ,这是可选的,因为如果动态链接器不识别 soname ,会回退使用文件名。

2. 定位共享对象

定位共享对象是位于 /lib 目录下的动态链接器 ldlinux.so 的工作。动态链接器总是会在标准路径 /lib /usr/lib 中搜索。如果你想将共享对象存储在其他位置,可以使用环境变量 LD_LIBRARY_PATH 。通常,系统中的共享库分布在多个位置。为避免动态链接器搜索过多路径,系统会在 /etc/ld.so.cache 中保存 soname 和共享对象位置的缓存。这个缓存由 /sbin/ldconfig 程序创建和更新,该程序会搜索 /etc/ld.so.conf 中列出的目录。

每当安装新库时,需要运行 ldconfig 程序来更新缓存。此外, ldconfig 会创建符号链接,使共享对象文件的文件名与 soname 不同。例如,在 Fedora Core 3 机器上的 hello 程序中, libc.so.6 指向 libc - 2.3.6.so 文件:

$ ls -l /lib/libc.so.6
lrwxrwxrwx  1 root root 13 Jul  2 16:03 /lib/libc.so.6 -> libc-2.3.6.so

libc.so.6 是编译器遇到的通用 soname ,而 libc - 2.3.6.so 是 GNU C 库包( glibc )使用的文件名。原则上,你可以用自己的库替代 glibc 来提供 libc.so.6 ,只要使用正确的 soname 并能在路径中找到,动态链接器就会使用它。但实际上,替换 glibc 可能会破坏所有需要 glibc 扩展的 GNU 工具。

下面是定位共享对象的流程:

graph LR
    A[启动程序] --> B{动态链接器ldlinux.so}
    B --> C{搜索标准路径 /lib 和 /usr/lib}
    C -->|未找到| D{检查 LD_LIBRARY_PATH}
    D -->|未找到| E{检查 /etc/ld.so.cache}
    E -->|未找到| F[报错]
    C -->|找到| G[加载共享对象]
    D -->|找到| G
    E -->|找到| G

3. 覆盖默认共享对象位置

非特权用户可以使用环境变量 LD_LIBRARY_PATH 告知动态链接器在哪里查找共享对象。例如,要让之前的 myprog 程序运行,可以这样做:

$ LD_LIBRARY_PATH=./ ./myprog

这告诉动态链接器在当前目录查找共享对象,即 libmylib.so 所在的位置。对于有 soname 的对象,可以使用 LD_PRELOAD 环境变量来简化输入,示例如下:

$ LD_PRELOAD=libc.so.6 ./hello-world

libc.so.6 是系统上 glibc soname 。这会告诉动态链接器在链接 hello - world 的其他部分之前先链接 libc 。如果你的程序链接了一个重新实现了 libc 中重要函数的库,这种技术会很有用。 LD_PRELOAD 仅适用于 /etc/ld.so.cache 中列出 soname 的库。

这两种技术对于调试共享对象非常有用,因为它们允许你在私有目录中创建共享对象,在那里链接共享对象不会干扰可能正在使用同一共享对象已安装版本的其他进程,你可以放心地调试新的共享对象版本而不用担心导致其他进程崩溃。

4. 共享对象的安全问题

虽然共享对象有很多好处,但如果使用不当,也会带来严重的安全风险。一些共享对象被许多程序共享,例如 libc ,几乎每个系统命令都会使用它。这些对象被许多程序使用,包括许多以 root 权限运行的程序。如果恶意程序员能够破坏一个常用的共享对象,就可能破坏整个系统。

假设你创建了一个具有 setuid root 权限的程序,允许普通用户执行一些日常维护任务。当普通用户运行这个程序时,进程将以 root 权限执行。如果这个程序使用的共享对象位于不安全的位置,恶意程序员理论上可以用恶意软件替换该共享对象。原始程序不会被修改,但在调用这个被劫持的共享对象中的某个函数时,会在不知不觉中破坏系统。

因此,动态链接器会尽力确保此类程序引入的共享对象是安全的。例如,只允许使用标准路径中的对象(忽略 LD_LIBRARY_PATH ),并且所有共享对象必须由 root 拥有且具有只读权限。

5. 处理共享对象的工具

Linux 动态链接器本身就是一个命令行工具,由于历史原因,其手册页列在 ld.so(8) 下,尽管当前发行版中实际使用的程序名是 ld - linux.so.2 。从命令行调用时,链接器接受多个选项并识别许多环境变量,你可以使用这些选项更好地理解程序。这些选项在 ld.so(8) 手册页中有描述,但大多数情况下首选工具是 ldd ,它实际上是一个调用 ld - linux.so.2 的包装脚本,并且有一些更用户友好的选项。

5.1 列出可执行文件所需的共享对象

不使用任何选项时, ldd 会显示可执行文件所需的所有共享对象,示例如下:

$ ldd hello
linux-gate.so.1 =>  (0xffffe000)
libm.so.6 => /lib/libm.so.6 (0xb7f1b000)
libpthread.so.0 => /lib/libpthread.so.0 (0xb7f09000)
libc.so.6 => /lib/libc.so.6 (0xb7de0000)
/lib/ld-linux.so.2 (0xb7f4e000)

严格来说,这是文件链接的对象列表,不一定是可执行文件实际需要的对象。例如,这里故意将 Hello World 程序与数学库和 pthread 库链接,但实际上并不需要这两个库:

$ gcc -o hello hello.c -lm -lpthread

与静态库不同,链接器不会从可执行文件中移除共享对象代码。静态库只是一个存档,链接器使用存档提取所需的目标文件,从而能够从可执行文件中消除不需要的目标文件。而当你在命令行指定共享对象时,无论是否必要,链接器都会将其包含在可执行文件中。

可以使用 ldd 命令的 -u 选项查看未使用的依赖项,示例如下:

$ ldd -u ./hello
Unused direct dependencies:
/lib/libm.so.6
/lib/libpthread.so.0

你需要自己思考这些共享对象是如何进入可执行文件的。了解库的命名约定有助于反向推导出命令行选项。许多开源项目使用 pkg - config 工具来创建链接项目的命令行选项,在某些情况下,这些规则可能会引入额外的共享对象。

5.2 为何要关注未使用的共享对象

对于程序链接的每个共享对象,动态链接器必须在对象中搜索未解析的引用并调用初始化例程,这会增加程序启动时间。在快速桌面机器上,未使用的共享对象可能不会增加太多额外时间,但如果数量很多,可能会导致显著延迟。

未使用的共享对象还会消耗资源。无论是否使用,共享对象可能会分配和初始化大量物理内存。如果初始化后未使用,这些内存最终会进入交换分区。不消耗物理内存的对象可能会消耗虚拟内存,即已分配但未初始化的内存。如果从未使用,它不会被交换,也不会消耗物理 RAM,但会限制程序可用的虚拟地址数量。通常,这只在 32 位架构上需要大量数据集(如数 GB 的 RAM)的应用程序中才会成为问题。

一般来说,避免不必要的共享对象是个好主意。特别是在 CPU 速度慢或 RAM 有限的系统上,要格外小心,不要链接不需要的共享库。在现代服务器或桌面系统上,这些问题单独来看可能不是严重问题,但当许多应用程序使用大量不需要的共享对象时,整个系统会受到影响。

5.3 在共享对象中查找符号

有时你可能会下载一些能编译但链接失败的源代码,原因是缺少符号。 nm objdump readelf 命令可用于查看程序符号表。

查找共享对象的 soname

如果你有一个共享对象文件,想在安装前知道它的 soname ,可以使用 objdump readelf 命令查看所谓的 DYNAMIC 部分,示例如下:

$ objdump -x some-obj-1.0.so | grep SONAME
SONAME      libmylib.so
$ readelf  -a libmylib.so |grep SONAME
0x0000000e (SONAME)                     Library soname: [libmylib.so]
查找未解析的符号

当链接器抱怨未解析的符号时,可能是由于缺少库或尝试链接错误版本的库。在 C++ 中,问题可能是函数签名更改或拼写错误导致的。

nm 命令可能是最容易使用的,要在目标代码中查找对特定符号的引用,可以使用以下命令:

$ nm -uA *.o | grep foo
  • -u 选项将输出限制为每个目标文件中的未解析符号。
  • -A 选项显示每个符号的文件名信息,以便在将输出通过管道传递给 grep 命令时,能看到包含该符号的目标文件。

对于 C++ 代码,还可以使用 -C 选项来解析符号,示例如下:

$ nm -gCA lib*.a | grep foo
libFoolib.a:somefile.o:00000000 T foo(char)
libFoolib.a:somefile.o:00000016 T foo(unsigned char)

objdump readelf 命令也可以完成相同的任务, objdump 等效命令为:

$ objdump -t

objdump 也有 -C 选项来解析符号名。 readelf 等效命令为:

$ readelf -s

截至版本 2.15.94, readelf 没有解析符号名的选项。这三个工具都包含在 binutils 包中。

6. 内存问题排查

6.1 双重释放

两次释放指针是一个容易犯的错误,但后果可能很严重。直到最近, glibc 都不会检查指针,会盲目接受你提供的任何指针。释放指向无效虚拟地址的指针会在该操作发生时引发 SIGSEGV ,这种情况容易发现。而释放指向有效虚拟地址的指针则更难发现。

大多数情况下,被释放的无效指针是之前通过 malloc 调用初始化,但已经通过 free 调用释放过的指针。两次释放指针可能会破坏 glibc 用于跟踪动态内存分配的空闲列表。发生这种情况时,会得到 SIGSEGV ,但可能直到下一次 free malloc 调用时才会出现,这就更难查找问题了。

glibc 的一些特性使查找此类错误更加困难。例如,超过一定大小的内存块使用 mmap 调用分配,而不是传统的堆。传统上,堆是一个随着进程需求增长和收缩的大内存池。 glibc 使用匿名 mmap 分配大块内存,使用传统堆分配小块内存,这会导致不同大小的内存块有不同的失败模式,可能难以解释。

最近版本的 glibc 会检查无效的释放指针,无论在什么情况下,都会导致程序以核心转储的方式终止。

6.2 内存泄漏

当进程分配一块内存,丢弃它,然后忽略释放它时,就会发生内存泄漏。通常,小的泄漏可能无害,程序可以继续正常运行。但随着时间推移,即使是小的泄漏也可能变成问题。一个简单的短期运行的实用程序可以容忍小的泄漏,因为它在退出后会丢弃堆。但一个可能运行数月的守护进程不能容忍任何泄漏,因为泄漏会随着时间累积。

内存泄漏的影响是进程的内存占用会持续增长。当分配的内存未被访问时,泄漏的内存可能只消耗虚拟地址。只要程序没有耗尽用户空间的虚拟地址(在 32 位机器上通常为 3GB),就不会看到任何不良影响。在大多数情况下,泄漏的内存已被程序修改,因此这些页面必须占用物理存储(RAM 或交换分区)。随着未使用的内存页面老化和系统内存需求增加,这些页面会被交换到磁盘。

交换可能是内存泄漏最隐蔽的副作用,因为一个泄漏内存的程序可能会使整个系统变慢。幸运的是,内存泄漏并不难发现。

6.3 缓冲区溢出

当应用程序写入超过内存块末尾的数据,覆盖可能用于其他目的的内存时,就会发生缓冲区溢出。溢出可能导致写入未映射或只读内存,从而引发 SIGSEGV 。缓冲区溢出是一种常见的错误,可能发生在任何类型的内存中:栈、动态或静态。有几种工具可以检测动态内存中的溢出,但检测静态内存和局部变量中的溢出更困难。

处理缓冲区溢出的最佳建议是避免它们。某些标准库函数会增加缓冲区溢出的风险,应避免使用。在大多数情况下,有更安全的替代方案。 flawfinder 是一个很好的工具,它是一个 Python 脚本,可以解析你的源代码,找出危险函数并报告给你。

栈缓冲区溢出

栈缓冲区溢出是一种安全风险。一些攻击者利用商业软件中的溢出漏洞,在原本安全的系统上植入恶意软件。

典型的漏洞涉及一个使用没有溢出检查的函数定义为局部变量的文本输入字段。当输入为纯文本或垃圾数据时,程序可能会崩溃。但聪明的攻击者可以将二进制机器代码输入到文本字段中,使输入缓冲区溢出。通过一些试验和错误,攻击者可以找到合适的字节来让程序运行他的代码并接管进程。

栈缓冲区溢出发生时通常很难查找。典型的特征是 SIGSEGV 后跟一个没有有用回溯信息的核心转储。发生这种情况时,直接进行代码审查,查找已知的问题函数可能比尝试调试更快。

堆缓冲区溢出

当程序使堆缓冲区溢出时,后果并不总是立即显现。当 malloc 使用 mmap 调用分配内存块(用于大块内存)时,会填充请求的块大小,使其成为页面大小的倍数。因此,如果请求的块大小不是页面的整数倍,分配的块空间会包含额外的字节。你的代码可能会超出这些额外字节,但不一定会立即引发错误。

下面是缓冲区溢出的影响及处理方式总结:
| 缓冲区类型 | 溢出后果 | 检测难度 | 处理建议 |
| ---- | ---- | ---- | ---- |
| 栈缓冲区 | 可能导致程序崩溃,存在安全风险 | 较难,典型特征为 SIGSEGV 加无有用回溯的核心转储 | 代码审查查找已知问题函数 |
| 堆缓冲区 | 后果不一定立即显现 | 较难,可能需要复杂的调试工具 | 避免使用易导致溢出的标准库函数,使用 flawfinder 检测 |

7. 总结与建议

7.1 共享对象方面

  • 创建与使用 :创建共享对象时,要正确使用 -shared -fpic 标志,以确保生成位置无关的共享对象。在链接程序时,使用 -L 选项指定共享库的位置,但要注意运行时链接器的查找问题。可通过 LD_LIBRARY_PATH LD_PRELOAD 环境变量来覆盖默认的共享对象位置,方便调试。
  • 安全问题 :要高度重视共享对象的安全问题,确保共享对象位于安全位置,由 root 拥有且具有只读权限,避免恶意攻击。
  • 工具使用 :利用 ldd 工具查看可执行文件所需的共享对象,使用 -u 选项找出未使用的共享对象,尽量避免不必要的共享对象链接,以减少程序启动时间和资源消耗。使用 nm objdump readelf 工具查找共享对象的 soname 和未解析的符号。

7.2 内存问题方面

  • 双重释放 :要避免双重释放指针的错误,利用最近版本 glibc 的检查机制,及时发现并解决此类问题。
  • 内存泄漏 :对于长期运行的程序,要特别注意内存泄漏问题,及时发现并修复泄漏点,避免内存占用不断增长。
  • 缓冲区溢出 :尽量避免使用易导致缓冲区溢出的标准库函数,使用 flawfinder 工具检测源代码中的危险函数,对于栈缓冲区溢出要及时进行代码审查。

为了更清晰地展示这些问题的处理流程,下面给出一个综合的流程图:

graph LR
    A[程序开发与运行] --> B{是否涉及共享对象}
    B -->|是| C{创建共享对象}
    C --> D[使用 -shared 和 -fpic 标志]
    D --> E[链接程序,使用 -L 选项]
    E --> F{运行时链接问题}
    F -->|有| G[使用 LD_LIBRARY_PATH 或 LD_PRELOAD 调试]
    F -->|无| H[正常运行]
    B -->|否| I{是否有内存问题}
    I -->|是| J{双重释放问题}
    J -->|有| K[利用 glibc 检查机制]
    J -->|无| L{内存泄漏问题}
    L -->|有| M[及时发现并修复泄漏点]
    L -->|无| N{缓冲区溢出问题}
    N -->|有| O[使用 flawfinder 检测,栈溢出进行代码审查]
    N -->|无| H
    I -->|否| H

7.3 实际操作建议

以下是一些在实际开发和维护中可以遵循的操作步骤:
1. 共享对象创建与链接
- 编写共享对象代码,如 mylib1.c mylib2.c
- 使用以下命令创建共享对象:

$ cc -shared -fpic -o libmylib.so mylib1.c mylib2.c
- 编写主程序代码 `myprog.c` 并编译:
$ cc -c myprog.c
- 链接主程序与共享对象:
$ cc -o myprog myprog.o -L . -lmylib
- 若运行时出现共享对象找不到的问题,使用 `LD_LIBRARY_PATH` 解决:
$ LD_LIBRARY_PATH=./ ./myprog
  1. 共享对象安全管理
    • 确保共享对象位于标准路径 /lib /usr/lib 中。
    • 检查共享对象的所有权和权限:
$ chown root:root /path/to/shared_object.so
$ chmod 444 /path/to/shared_object.so
  1. 内存问题排查
    • 对于双重释放问题,使用最新版本的 glibc 进行检查。
    • 对于内存泄漏问题,可使用工具如 valgrind 进行检测:
$ valgrind --leak-check=full ./your_program
- 对于缓冲区溢出问题,使用 `flawfinder` 检查源代码:
$ flawfinder /path/to/your/source/code

通过以上对共享对象和内存问题的详细分析及相应的处理建议,希望能帮助开发者更好地开发和维护程序,避免常见的错误和安全风险。在实际应用中,要根据具体情况灵活运用这些方法和工具,确保程序的稳定性和安全性。

基于径向基函数神经网络RBFNN的自适应滑模控制学习(Matlab代码实现)内容概要:本文介绍了基于径向基函数神经网络(RBFNN)的自适应滑模控制方法,并提供了相应的Matlab代码实现。该方法结合了RBF神经网络的非线性逼近能力和滑模控制的强鲁棒性,用于解决复杂系统的控制问题,尤其适用于存在不确定性和外部干扰的动态系统。文中详细阐述了控制算法的设计思路、RBFNN的结构权重更新机制、滑模面的构建以及自适应律的推导过程,并通过Matlab仿真验证了所提方法的有效性和稳定性。此外,文档还列举了大量相关的科研方向和技术应用,涵盖智能优化算法、机器学习、电力系统、路径规划等多个领域,展示了该技术的广泛应用前景。; 适合人群:具备一定自动控制理论基础和Matlab编程能力的研究生、科研人员及工程技术人员,特别是从事智能控制、非线性系统控制及相关领域的研究人员; 使用场景及目标:①学习和掌握RBF神经网络滑模控制相结合的自适应控制策略设计方法;②应用于电机控制、机器人轨迹跟踪、电力电子系统等存在模型不确定性或外界扰动的实际控制系统中,提升控制精度鲁棒性; 阅读建议:建议读者结合提供的Matlab代码进行仿真实践,深入理解算法实现细节,同时可参考文中提及的相关技术方向拓展研究思路,注重理论分析仿真验证相结合。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值