进程跟踪和调试
DTrace
DTrace中的“D”指的是D语言。这是一门完整的跟踪语言,通过这门语言可以创建专门的跟踪器(tracer)或者探测器(probe)
druss 工具是一个底层为DTrac的工具,允许跟踪系统调用时打印出C风格的形式,显示系统调用、参数及返回值。druss支持3种使用模式:
- 通过druss 允许一个进程:在druss 的参数后面指定命令参数
- 附加到某个正在允许的进程示例:在druss -p 参数指定进程的PID
- 附加到命名的进程:在druss -n 参数指定进程的名字
druss 的另一个有用的特性是能够自动锁定子进程(指定 -f 参数)
druss 还可以同时当场跟踪器和剖析器使用
进程信息
除了DTrace 之外,OS X 还提供了两个关键机制可以查看详细的进程信息
- sysctl:sysctl 提供了一些显示进程统计数据的变量
- proc_info:OS X 和 iOS 都提供了proc_info 系统调用。通过proc_info 可以查询进程和线程的很多信息
进程和系统快照
除了 DTrace 和 Instruments 之外,在 OS X 中还有一些工具能够获得系统或进程状态的“快照”(snapshot)
- system_profiler( ):图形工具
- sysdiagnose( ):是一个一站式的完美诊断工具
- allmemory( ):捕获用户进程的所有内存使用情况
- stackshot( ):可以获得进程执行状态的快照
- *stack_sanpshot系统调用:这个系统调用的参数是pid、一个返回快照的缓冲区、缓冲区大小以及一些参数。这个快照机制的实现细节会深入到Mach微内核中
kdebug
kdebug 是內建内核跟踪设施。默认是禁用的
OS X 提供了3个利用kdebug 设施的工具
- sc_usage:显示每一个进程的系统调用信息
- fs_usage:显示系统调用,但是显示的是与文件、套接字和目录相关的应用。可以显示系统范围内的跟踪(除非调用时提供了PID 或 命令参数)
- latency:显示中断和调度的延迟值。这个工具展示落在阈值内的上下文切换和中断处理程序技术,这两个分别可以通过-st 和 -it 参数设置
应用程序崩溃
大部分应用程序都可能会崩溃。在UNIX 中,崩溃和一个信号有关。崩溃的真正原因来自于内核,内核发现进程无法继续执行时,生成这个信号作为最后的补救方法
- 核心转储:当一个进程崩溃时,可以选择是否生成核心转储文件。这取决于进程的资源限制RLIMIT_CORE的设置。默认情况下是禁止的
- Crash Reporter:iOS 和 OS X 都没有选择创建巨大的核心转储文件,而是包含一个Crash Reporter,当进程异常终止(崩溃)时自动触发Crash Reporter生成详细的崩溃日志,保存在用户的Library/Logs/CrashReporter目录下或者系统的Library/Logs/CrashReporter目录下
- 修改Crash Repoerter 选项:如果安装了Xcode,那么可以在 /Developer/Application/Utilities 目录下找到一个名为CrashReporter的小程序。启动程序
- defaults( ):将DialogType 属性修改为basic、developer 或 server
内存破坏的bug
内存破坏是程序中常见的bug来源。应用程序崩溃的主要原因就是缓冲区溢出(既包含栈也包含堆)和堆内存的破坏。问题在于,在很多情况下,导致问题的代码和出现问题的代码相隔甚远,因此从出现bug到引发崩溃之间肯呢个会间隔数分钟甚至更长时间
- LibC中的内存保护:OS X 的LibC库高度可配置,malloc( ) 的手册页记录了可以控制内存分配行为的环境变量,如下表
环境变量 | 用途 |
---|---|
MallocLogFile | 设置malloc调试日志文件 |
MallocCheckHeapStart MallocCheckHeapEach MallocCheckHeapSleep/Abort |
在MallocCheckHeapStart 次分配后,每隔MallocCheckHeapEach 次分配之后检查堆的一致性。如果发现堆不一致的情况,要么进入睡眠(允许调试),要么调用abort( )(通过SIGABRT 崩溃) |
MallocErrorAbort MallocCorruptionAbort |
发送任何错误时调用abort( )(即发送 SIGABRT 信号),或只有内存破坏时调用abort( ) |
MallocGuardEdges MallocDoNotProtectPrelude MallocDoNotProtectPostlude |
在分配的大内存块之前(如果没有设置MallocDoNotProtectPrelude)和之后(如果没有设置MallocDoNotProtectPostlude)添加守护页 |
MallocScribble | 在分配的内存中填满0xAA,在释放的内存中填满0x55 |
MallocStackLogging MallocStackLoggingNoCompact MallocStackLoggingDirectory |
将malloc操作时所用的栈跟踪记录到/tmp(或 MallocStackLoggingDirectory 指定的目录)中。然后可以调用 leaks( ) 和 malloc_history( ) 之类的程序,后者要求设置MallocStackLoggingNoCompact |
LibGMalloc:OS X 还提供了一个特殊的库libgmalloc.dylib,这个库可以截获并调试分配内存。这个强大的库的工作原理是截获LibSystem 中的分配函数,一旦挂上了目标函数,就可以很容易地将其替换为更复杂的替代品,从而对内存分配施加更多的限制,以期能够捕捉到导致崩溃的蛛丝马迹
具体来说,libgmalloc库提供了以下的技术
- 给么一个分配的内存块加上自定义的数据头,其中包含了关于分配详情的调试信息:这个数据头记录了分配时的线程ID 和 栈回溯,还带有一个常量值(魔数)0xDEADBEEF,这个常量值可以用于检测同一个缓冲区分配和解除分配的错误。
-
将内存块分配在自己专有的页面上,将相邻的页面设置为不可写(如果设置了MALLOC_ALLOW_READS),或者设置为完全不可访问:分配的内存块也放在其所在页面的尾部(除非设置了MALLOC_PROTECT_BEFORE)。这样做的结果就是任何读/写操作如果越过了缓冲区的尾部就会导致读写操作越过页边界,从而导致越过未处理的页错误,使得进程收到总线错误的信号(SIGBUS)而崩溃。设置MALLOC_PROTECT_BEFORE 环境变量会将这个保护行为翻转过来,即保护缓冲区之前不能被访问,而不是保护缓冲区后面
-释放内存块时接触分配页面:在free( ) 释放内存块同时解除分配其所在页,这样的话,如果在释放的缓冲区上进行读写操作会导致总线错误
自动发生总线错误表示存在页面处理的 bug,一旦发生,就使得调试变得相对简单。将 gdb 附加到进程上,可以定位崩溃的位置,然后检查这个自定义的数据头,判断分配相关的问题,最后修复问题:要么修改缓冲区分配的参数。要么移除出错的操作
内存泄露
另外一个常见的bug 就是内存泄露。当程序员分配了内存或某个对象,但是忘记调用free( )释放内存或调用delete 删除对象时,就会发生能吃泄露。内存泄露的问题很难查找,因为内存泄露不会导致致命的bug 。而是慢慢地填满进程内存空间,因为一旦一个指针丢失了,在程序运行时,就再也无法回收这个指针指向的内存了。
Xcode 的 Instruments 工具提供了针对跟着内存分配和泄露的工具(如下图)
- heap( ):列出给定进程的堆中所有分配的缓冲区,使用很简单,只要传入PID或部分进程的名字即可。这个工具可以识别类的名字,所以对Objective-C 编译的二进制文件和依赖 CoreFundation 的库非常有用
- leaks( ):遍历进程的堆,检查可疑的内存泄露,对进程进行采样,生成一个分配但是没有释放的指针的报告
- malloc_history( ):malloc_history( ) 工具提供了进程中发生的每一次内存分配的详细数据,包括dyld( ) 进行的那些初始内存分配,要求设置 MallocStackLogging 和 MallocStackLoggingNoCompact 环境变量
标准 UNIX 工具
- **ps( ) **:可以显示进程列表
- top( ) 系统全局视图:获取当前系统运行状况的关键工具
- 通过lsof( ) 和 fuser( ) 进行文件诊断:lsof( ) 显示一个进程所有文件描述符(包括套接字)的映射; fuser( ) 提供一个反向的映射,从文件到用于这个文件的进程。主要作用的诊断文件锁定或者“文件被占用”的问题