内存管理与GDB调试全解析
1. 内存耗尽问题
1.1 标准内存分配策略
标准的内存分配策略是过度提交(over-commit),即内核允许应用程序分配的内存超过物理内存。多数情况下,这能正常工作,因为应用程序通常会请求比实际需求更多的内存。同时,这也有助于
fork(2)
的实现,设置写时复制标志后,大程序的复制是安全的。大多数情况下,
fork
后会调用
exec
函数,取消内存共享并加载新程序。
1.2 内存耗尽情况(OOM)
特定工作负载可能导致一组进程同时兑现已分配的内存,从而需求超过实际可用内存,这就是内存耗尽情况(OOM)。此时,唯一的办法是杀死进程,直到问题解决,这由内存杀手(oom-killer)完成。
1.3 内核分配调优参数
/proc/sys/vm/overcommit_memory
可设置为以下值:
- 0:启发式过度提交(默认)
- 1:总是过度提交,从不检查
- 2:总是检查,从不过度提交
| 参数值 | 描述 | 适用场景 |
|---|---|---|
| 0 | 启发式过度提交 | 大多数情况的最佳选择 |
| 1 | 总是过度提交,从不检查 | 处理大稀疏数组且只写入小部分内存的程序 |
| 2 | 总是检查,从不过度提交 | 担心内存耗尽的关键任务或安全关键型应用 |
过度提交比率由
/proc/sys/vm/overcommit_ratio
控制,默认值为50%。例如,对于一个有512 MB系统RAM的设备,设置保守比率为25%:
# echo 25 > /proc/sys/vm/overcommit_ratio
# grep -e MemTotal -e CommitLimit /proc/meminfo
MemTotal: 509016 kB
CommitLimit: 127252 kB
由于没有交换空间,提交限制是
MemTotal
的25%。
1.4 关键变量与应对措施
/proc/meminfo
中的
Committed_AS
表示到目前为止所有分配所需的总内存。例如:
# grep -e MemTotal -e Committed_AS /proc/meminfo
MemTotal: 509016 kB
Committed_AS: 741364 kB
这意味着内核已承诺的内存超过了可用内存。将
overcommit_memory
设置为2会使所有分配失败,解决方法可能是增加一倍的RAM或减少运行进程的数量。
1.5 内存杀手(oom-killer)
内存杀手使用启发式方法为每个进程计算0到1000的不良分数,然后终止分数最高的进程,直到有足够的空闲内存。可以在日志中看到类似信息:
[44510.490320] eatmem invoked oom-killer: gfp_mask=0x200da,
order=0, oom_score_adj=0
可以通过
echo f > /proc/sysrq-trigger
强制触发OOM事件。还可以通过向
/proc/<PID>/oom_score_adj
写入调整值来影响进程的不良分数,-1000表示进程永远不会被杀死,+1000表示进程总是会被杀死。
2. 内存使用监控与调试
2.1 内存监控
虽然无法精确统计虚拟内存系统中使用的每一个字节,但可以使用
free
命令获得排除缓冲区和缓存后的可用内存总量。通过一段时间内不同工作负载的监控,可以确保内存使用在给定范围内。
2.2 详细信息资源
-
内核空间:
/proc中的meminfo、slabinfo和vmallocinfo提供最有用的信息。 -
用户空间:
smem等工具显示的Pss是获取准确测量值的最佳指标。 -
内存调试:可以使用简单的跟踪器(如
mtrace)或强大的Valgrind memcheck工具。
2.3 内存分配微调
如果担心内存耗尽的后果,可以通过
/proc/sys/vm/overcommit_memory
微调分配机制,并通过
oom_score_adj
参数控制特定进程被杀死的可能性。
3. GDB调试基础
3.1 GDB简介
GDB是用于编译语言(主要是C和C++)的源代码级调试器,也支持多种其他语言(如Go和Objective-C)。项目网站为http://www.gnu.org/software/gdb ,包含大量有用信息,如GDB用户手册。默认情况下,GDB具有命令行用户界面,也有许多前端用户界面可供选择。
3.2 调试准备
3.2.1 调试符号编译
GCC提供
-g
和
-ggdb
两个选项来编译带有调试符号的代码:
-
-g
:为目标操作系统生成合适格式的调试信息,更具可移植性。
-
-ggdb
:添加特定于GDB的调试信息。
两个选项都可以指定调试信息的级别,从0到3:
- 0:不生成调试信息,相当于省略
-g
或
-ggdb
开关。
- 1:生成最少信息,包括函数名和外部变量,足以生成回溯。
- 2:默认级别,包括局部变量和行号信息,可进行源代码级调试和单步执行。
- 3:包含额外信息,使GDB能正确处理宏展开。
多数情况下,
-g
就足够了,
-g3
或
-ggdb3
用于调试包含宏的代码有问题时。
3.2.2 代码优化级别
编译器优化会破坏源代码和机器代码之间的关系,导致单步执行源代码不可预测。遇到此类问题时,可能需要不进行优化编译(省略
-O
编译开关)或使用
-Og
,它启用不影响调试的优化。
3.2.3 栈帧指针
GDB生成函数调用回溯需要栈帧指针。在某些架构上,GCC在高级别优化(
-O2
及以上)时不会生成栈帧指针。如果必须使用
-O2
编译但仍需要回溯,可以使用
-fno-omit-frame-pointer
覆盖默认行为。同时,要注意手动优化省略帧指针的代码,可能需要临时移除相关部分。
3.3 调试应用程序方式
可以通过两种方式使用GDB调试应用程序:
- 本地调试:在桌面和服务器开发中,在同一台机器上编译和运行代码时,自然地本地运行GDB。
- 远程调试:大多数嵌入式开发使用交叉工具链,需要从交叉开发环境控制设备上运行的代码。下面主要介绍远程调试。
3.4 远程调试使用gdbserver
3.4.1 关键组件与差异
远程调试的关键组件是调试代理
gdbserver
,它运行在目标设备上,通过网络连接或串行接口与主机上的GDB副本连接。与本地调试相比,有以下差异:
- 调试会话开始时,需要在目标设备上使用
gdbserver
加载要调试的程序,在主机上单独加载GDB。
- GDB和
gdbserver
需要相互连接才能开始调试会话。
- 主机上的GDB需要知道在哪里查找调试符号和源代码,特别是共享库。
- GDB的
run
命令可能不如预期工作。
- 调试会话结束时,
gdbserver
会终止,需要重新启动才能进行下一次调试会话。
- 主机上需要调试符号和源代码,但目标设备上通常不需要,部署到目标设备前需要剥离这些信息。
-
GDB/gdbserver
组合不支持本地运行GDB的所有功能,例如
gdbserver
在
fork
后不能跟踪子进程。
- GDB和
gdbserver
版本不同或配置不同可能会出现问题,理想情况下应使用相同的源代码构建。
3.4.2 剥离调试符号
调试符号会显著增加可执行文件的大小,有时会增加10倍。可以使用交叉工具链中的
strip
工具在不重新编译的情况下移除调试符号,有以下开关:
-
--strip-all
:移除所有符号(默认)
-
--strip-unneeded
:移除重定位处理不需要的符号
-
--strip-debug
:仅移除调试符号
对于应用程序和共享库,
--strip-all
通常没问题,但对于内核模块,应使用
--strip-unneeded
。
3.5 不同开发环境设置
3.5.1 Yocto Project远程调试设置
使用Yocto Project进行远程调试需要做两件事:
- 将
gdbserver
添加到目标镜像:
- 方法一:在
conf/local.conf
中添加
IMAGE_INSTALL_append = " gdbserver"
- 方法二:在
EXTRA_IMAGE_FEATURES
中添加
tools-debug
EXTRA_IMAGE_FEATURES = "debug-tweaks tools-debug"
- 创建包含GDB和调试符号的SDK:
$ bitbake -c populate_sdk <image>
SDK包含GDB副本、目标系统的sysroot(包含所有程序和库的调试符号)以及可执行文件的源代码。
3.5.2 Buildroot远程调试设置
使用Buildroot内部工具链时,需要启用以下选项:
-
BR2_PACKAGE_HOST_GDB
:在工具链中构建主机的交叉GDB
-
BR2_PACKAGE_GDB
:在目标包中构建GDB
-
BR2_PACKAGE_GDB_SERVER
:在目标包中构建
gdbserver
-
BR2_ENABLE_DEBUG
:构建带有调试符号的可执行文件
这将在
output/host/usr/<arch>/sysroot
中创建带有调试符号的库。
3.6 开始调试
3.6.1 连接GDB和gdbserver
-
网络连接
:在目标设备上启动
gdbserver并指定监听的TCP端口:
# gdbserver :10000 ./hello-world
在主机上启动GDB并指向未剥离调试符号的程序:
$ arm-poky-linux-gnueabi-gdb hello-world
在GDB中使用
target remote
命令连接到
gdbserver
:
(gdb) target remote 192.168.1.101:10000
- 串行连接 :在目标设备上指定串行端口:
# gdbserver /dev/ttyO0 ./hello-world
可能需要预先配置端口波特率:
# stty -F /dev/ttyO0 115200
在主机上使用
target remote
命令连接,并设置波特率:
(gdb) set serial baud 115200
(gdb) target remote /dev/ttyUSB0
3.6.2 设置sysroot
GDB需要知道在哪里查找调试信息和源代码。
-
Yocto Project SDK
:
(gdb) set sysroot /opt/poky/2.2.1/sysroots/cortexa8hf-neon-poky-linux-gnueabi
- Buildroot :
(gdb) set sysroot /home/chris/buildroot/output/staging
GDB有源代码搜索路径,默认是
$cdir:$cwd
。如果目录在编译和调试之间发生了移动,可能需要调整。例如,使用Yocto Project SDK调试时:
(gdb) set sysroot /opt/poky/2.2.1/sysroots/cortexa8hf-neon-poky-linux-gnueabi
(gdb) set substitute-path /usr/src/debug /opt/poky/2.2.1/sysroots/cortexa8hf-neon-poky-linux-gnueabi/
还可以使用
set solib-search-path
指定共享库搜索路径,或使用
directory
命令添加源代码搜索路径。
graph TD;
A[开始调试] --> B{选择连接方式};
B -->|网络连接| C[目标设备启动gdbserver并指定端口];
B -->|串行连接| D[目标设备指定串行端口并配置波特率];
C --> E[主机启动GDB并指向未剥离符号的程序];
D --> E;
E --> F[GDB中使用target remote连接gdbserver];
F --> G[设置sysroot];
G --> H[根据需要调整源代码搜索路径];
通过以上步骤,可以有效管理内存并使用GDB进行调试,无论是解决内存耗尽问题还是调试应用程序,都能提供有力的支持。
4. 调试中的其他要点
4.1 即时调试(Just-in-time debugging)
即时调试允许在程序崩溃时自动启动调试器。在某些情况下,当程序遇到错误时,可以配置系统直接启动 GDB 来调试崩溃的程序。这对于处理复杂的、难以重现的崩溃问题非常有用。不过,具体的配置方式会因操作系统和开发环境而异。
4.2 调试分叉和线程
4.2.1 调试分叉(fork)
在程序使用
fork
创建子进程时,调试会变得复杂。本地运行的 GDB 可以跟踪子进程,但
gdbserver
通常无法做到这一点。如果需要调试子进程,可以考虑使用本地调试或者寻找其他替代方案。
4.2.2 调试线程
当程序使用多线程时,GDB 提供了一些命令来帮助调试。例如,可以使用
info threads
命令查看当前所有线程的信息,使用
thread <线程编号>
命令切换到指定线程进行调试。
4.3 核心文件(Core files)
当程序崩溃时,操作系统可能会生成核心文件(core files),其中包含了程序崩溃时的内存状态和寄存器信息。可以使用 GDB 来分析这些核心文件,找出程序崩溃的原因。具体步骤如下:
1. 确保系统配置允许生成核心文件。可以通过修改
/etc/security/limits.conf
文件来设置核心文件的大小限制。
2. 当程序崩溃后,会在当前目录下生成一个核心文件(通常命名为
core
或
core.<进程 ID>
)。
3. 使用 GDB 打开可执行文件和核心文件:
$ gdb <可执行文件> <核心文件>
-
在 GDB 中,可以使用
bt(backtrace)命令查看函数调用栈,找出崩溃发生的位置。
4.4 GDB 用户界面
虽然 GDB 默认使用命令行界面,但也有许多前端用户界面可供选择,以提供更友好的调试体验。以下是一些常见的 GDB 用户界面:
| 用户界面 | 特点 |
| ---- | ---- |
| DDD(Data Display Debugger) | 提供图形化界面,支持多种编程语言,可直观地查看变量和数据结构。 |
| Eclipse CDT | 集成开发环境,支持 GDB 调试,提供丰富的调试功能和可视化界面。 |
| Visual Studio Code | 轻量级代码编辑器,通过插件支持 GDB 调试,具有良好的扩展性和用户体验。 |
5. 调试内核代码
5.1 内核调试的挑战
调试内核代码比调试应用程序更加复杂,因为内核运行在特权模式下,并且涉及到硬件交互和系统级操作。此外,内核代码的调试需要特殊的配置和工具。
5.2 准备工作
5.2.1 编译内核时添加调试信息
在编译内核时,需要使用
-g
选项来添加调试符号。例如:
make menuconfig
# 在配置中选择相关调试选项
make -j$(nproc) EXTRA_CFLAGS="-g"
5.2.2 配置内核启动参数
在内核启动时,需要传递一些参数来启用调试功能。例如,可以在
/boot/grub/grub.cfg
中添加以下参数:
nokaslr kgdboc=ttyS0,115200 kgdbwait
其中,
nokaslr
禁用内核地址空间布局随机化,
kgdboc
指定调试串口和波特率,
kgdbwait
表示内核启动后等待 GDB 连接。
5.3 连接 GDB 进行内核调试
5.3.1 启动内核
启动内核时,由于设置了
kgdbwait
参数,内核会暂停并等待 GDB 连接。
5.3.2 启动 GDB
在主机上启动 GDB,并加载内核镜像:
$ gdb vmlinux
5.3.3 连接到内核
使用 GDB 的
target remote
命令连接到内核:
(gdb) target remote /dev/ttyUSB0
这里假设使用的是 USB 串口设备
/dev/ttyUSB0
。
5.3.4 开始调试
连接成功后,就可以使用 GDB 的各种命令来调试内核代码,例如设置断点、单步执行等。
graph TD;
A[准备调试内核代码] --> B[编译内核添加调试信息];
B --> C[配置内核启动参数];
C --> D[启动内核并等待 GDB 连接];
D --> E[主机启动 GDB 并加载内核镜像];
E --> F[GDB 连接到内核];
F --> G[使用 GDB 命令进行调试];
6. 总结
6.1 内存管理总结
-
标准内存分配策略的过度提交机制在多数情况下能有效利用内存,但可能导致内存耗尽问题。可以通过调整
/proc/sys/vm/overcommit_memory和/proc/sys/vm/overcommit_ratio来控制内存分配。 -
内存杀手(oom-killer)是处理内存耗尽的最后手段,可通过
oom_score_adj参数影响进程被杀死的可能性。 -
可以使用
free命令监控内存使用情况,通过/proc目录下的文件获取详细的内存信息。
6.2 GDB 调试总结
- GDB 是一个强大的调试工具,可用于调试应用程序、分析核心文件和调试内核代码。
- 调试前需要确保代码编译时带有调试符号,合理设置代码优化级别和栈帧指针。
-
远程调试使用
gdbserver时,需要注意连接、调试符号和源代码的查找等问题。 - 不同的调试场景(如分叉、线程、内核调试)需要采用不同的调试方法和技巧。
通过掌握内存管理和 GDB 调试的相关知识和技巧,可以更高效地开发和维护软件,解决各种内存和调试问题。
在实际开发中,建议根据具体的需求和场景选择合适的方法和工具,不断积累经验,提高调试效率。
超级会员免费看
562

被折叠的 条评论
为什么被折叠?



