翻译: iOS调试(iOS Debugging Magic)

iOS包含许多“秘密”调试工具,包括环境变量、偏好、GCB的常规调用,等等。本技术说明描述了这些工具。如果你开发iOS,你应该通过这个列表查看你错过的可以使你更轻松的工具。

简介

所有苹果系统包括苹果工程团队添加的调试工具都是用来帮助开发和调试特定子系统。这些工具属于已发布系统软件,可以使用它们来调试代码。本技术手册描述一些广泛使用的工具。调试工具记录在另一个地方,有个简短的工具概述并链接到现有文档。

重要:这不是一个详尽的列表:并没有记录所有的调试工具。

本技术说明中的许多细节在不同的平台和版本不一样。因此,你可能遇到平台间的小差异以及新旧系统的差异。已知的重大变化在文章中列出。

本技术说明是参照iOS4.1写的。

警告:本技术说明中描述的调试工具是无根据的。苹果有权依据iOS的演变改变或消除每个工具;这在过去已经发生,并且在未来也有可能发生。这些工具只用于调试:不能发布一个依赖本技术说明中存在或描述工具功能的产品

除了兼容二进制这个技术问题,记住,iOS应用程序必须遵守各种法律协议和应用商店审查指南。

注意:如果你也开发Mac OS X,你可能希望阅读Technical Note TN2124, 'Mac OS X Debugging Magic'.

本技术说明包括先进的调试技术。如果你是刚刚开始,你应该先阅读如下材料“

  • GDB是系统的主要调试工具。查看GDB完整描述,可参阅GDB调试(Debugging with GDB)。

  • Xcode是平跟的集成开发环境(IDE)。它包括一个复杂的图形调试器,作为GDB的包装。关于Xcode的更多信息,可参阅苹果开发者参考库中的“语言和工具”章节。

本技术说明不包括性能调试。如果你想调试性能问下,最好从开始使用性能(Getting Started with Performance )文档开始

基础知识

本技术说明中后面的部分将详细描述调试工具。许多工具使用类似的技术来启用或禁用工具,并看到输出。本章节描述这些常见的技术。

启用调试设备

一些调试工具默认是启用的。然而,大多数工具必须使用下面章节描述的技术来启用。

环境变量

在许多情况下,你可以通过设置一个特定的环境变量来启用调试工具。你可以通过Xcode中的可执行检查器来实现,如图1所示。

图1 在Xcode中设置环境变量

偏好设置

一些调试工具可以通过设置特殊的偏好来启用。你可以通过配置Xcode中命令行参数来设置调试偏好,如图2所示。

图2 在Xcode中设置命令行参数

可调用的程序

许多系统框架包括打印调试信息到stderr的程序。专门设计这些程序用来调用GDB,或者他们可能是调试时非常有用的API程序。列表1展示了如何调用GDB调试程序的例子,特别的,它调用CFBundleGetAllBundles 获取应用中加载的所有bundles 的列表,通过调用CFShow 程序来打印列表。

列表1 调用GDB调试程序
<pre><code>
(gdb) call (void)CFShow((void *)CFBundleGetAllBundles())

<CFArray 0x10025c010 [0x7fff701faf20]>{type = mutable-small, count = 59, values = (

0 : CFBundle 0x100234d00 </System/Library/Frameworks/CoreData.framework> …

[…]

12 : CFBundle 0x100237790 </System/Library/Frameworks/Security.framework> …

[…]

23 : CFBundle 0x100194eb0 </System/Library/Frameworks/CoreFoundation.framework> …

[…]

)}
</pre></code>

注意:当调用像这样的程序,GDB必须知道程序的返回类型(因为一些返回类型可以影响参数传递的方式)。如果调用一个没有调试标识的程序,你必须告诉GDB添加一个返回类型。例如,列表1传递CFBundleGetAllBundles 返回类型为 (void \*)CFShow 返回类型为 (void)

类似的,如果你调用一个采用非标准参数的程序,你需要传递参数确保GDB能正确的传递。

如果你看不到程序的输出,你可能需要查看控制台日志,如调试输出章节所述。

重要:如果在代码中使用这种技术,它并不总是为静态程序公正。编译器程序间的优化坑内会引起一个静态程序偏离标准函数调用ABI。这种情况下,通过GDB调用不可靠。

在实践中,这只会影响使用英特尔32位代码的iPhone模拟器。

配置概要文件

一些iOS子系统可以通过安装一个特殊的配置概要文件,启用调试工具。查看推送通知可查看这样的例子。通常以这种方式安装配置概要文件:

把它放到一个web服务器上,然后使用设备上的Safari下载

附加到email,寄到设备上的账户,然后运行mail并打开配置文件的附近

了解更多关于配置概要文件,可阅读iPhone商业网址上的各种文档。

查看调试输出

生成调试输出的程序通常使用下列机制:

  • NSLog

  • 打印到stderr

  • 系统日志

NSLog是一个高级的API用于日志,在Objective-C代码中广泛使用。NSLog确切行为非常复杂,并且随着时间的推移在不断的改变,这已超出了本文的范围。然而,我们可以了解nslog打印到stderr或者记录到系统日志或者两者。因此,如果你了解这两种机制,可以通过NSLog查看任何记录。

打印到stderr是最常用的输出机制。鉴于这个主题的重要性,在下一节将深度覆盖。

查看系统日志最简单的方法是在Xcode的Organizer 窗口中的控制台tab。如果用户不行安装Xcode,可以使用iPhone配置utility(iPhone Configuration Utility)查看系统日志。

控制台输出

许多程序,甚至许多系统框架,打印调试信息到stderr。输出的目的地最终是程序控制:它可以将stderr重定向到任何选择的目的地。然而,在大多数情况下,程序一般不重定向stderr,所以输出到默认的目的地,该目的地继承自启动环境程序。通常是下列情况之一:

  • 如果你启动一个GUI应用程序,同时这个应用程序也可能被普通用户启动,系统将任何打印到stderr的信息重定向到系统日志。你可以使用前面描述的技术来查看这些信息。

  • 如果你在Xcode中运行一个程序,你可以在Xcode的调试器控制台窗口(在运行菜单中选择控制台菜单项便可以看到这个菜单)看到stderr输出.

附加到运行程序(使用Xcode中的附加到程序菜单或者GDB中附加命令)不会自动将程序的stderr连接到你的GDB窗口。你可以使用Technical Note TN2030, 'GDB for MacsBug Veterans'中“附加后查看stdout和stderr”章节中描述的技巧来完成。

一些汇编要求

现在不太可能需要编写大量的汇编语言代码,但对这有个基本的了解很有用。尤其是当你在调试,特别是当你调试库或框架崩溃,并且没有源代码。本节讨论一些基本的技巧有利于汇编级调试程序。具体的说,它描述了如何在所有支持的架构中设置断点,访问参数以及访问返回地址。

重要的区别是在本技术说明中汇编级别例子来自运行在iPhone 3GS的armv7。然而,在大多数情况下差异并不显著,例子可能来自不同的架构,甚至来自Mac。很容易改编这样的例子到其他架构。最显著的差异是:

  • 访问参数

  • 获取返回地址

这些将在以下章节讨论。

重要提示:以下特定架构章节包含经验法则。如果程序有任何非标准参数或非标准函数结果,这些法则并不适用,你应阅读文档了解相关细节。

在这种情况下,标准参数是interger(一个寄存器),枚举和指针(包括数组指针和函数指针)。非标准参数是浮点数,向量,结构,比一个寄存器大的interger以及在程序最后固定参数后的任何参数,该程序参数数量可变。

了解所有iOS设备的调用约定详情,可查看iOS ABI函数调用指南(iOS ABI Function Call Guide.)。使用英特尔32位架构的iPhone模拟器在Max OS X ABI函数调用指南(Mac OS X ABI Function Call Guide.)。

在你阅读以下章节前,了解GDB的精妙处是非常重要的。因为GDB本质上是一个源代码级别的调试器,当你在程序设置一个断点,GDB不会在程序的第一个指令处设置断点;相反,它在程序prologue后第一个指令处设置断点。从源代码级别调试来看,这样非常完美。在源代码级别调试器中,你不会想要单步调试程序prologue。然而,当在汇编级别调试中,非常容易在prologue运行前访问参数。这是因为程序第一条指令的参数的位置是由调用ABI函数决定的,但运行prologue自行决定搅乱周围的事情。此外,每个prologue可以以稍微不同的方式在做这件事。所以唯一访问已执行prologue后参数的方法是反编译prologue,搞清楚一切。这是典型但不总是很简单,但它仍然是额外的工作。

告诉GDB在程序的第一个指令处设置断点最好的办法是在程序名字上价格前缀“*”。列表2展示了这样的例子。

列表2 prologue前后
<pre><code>
(gdb) b CFStringCreateWithFormat

Breakpoint 1 at 0x34427ff8

(gdb) info break

Num Type Disp Enb Address What

1 breakpoint keep n 0x34427ff8 <CFStringCreateWithFormat+8>

-- The breakpoint is not at the first instruction.

-- Disassembling the routine shows that GDB has skipped the prologue.

(gdb) x/5i CFStringCreateWithFormat

0x34427ff0 <CFStringCreateWithFormat>: push {r2, r3}

0x34427ff2 <CFStringCreateWithFormat+2>: push {r7, lr}

0x34427ff4 <CFStringCreateWithFormat+4>: add r7, sp, #0

0x34427ff6 <CFStringCreateWithFormat+6>: sub sp, #4

0x34427ff8 <CFStringCreateWithFormat+8>: add r3, sp, #12

-- So we use a "*" prefix to disable GDB's 'smarts'.

(gdb) b *CFStringCreateWithFormat

Breakpoint 2 at 0x34427ff0

(gdb) info break

Num Type Disp Enb Address What

1 breakpoint keep n 0x34427ff8 <CFStringCreateWithFormat+8>

2 breakpoint keep n 0x34427ff0 <CFStringCreateWithFormat>

</pre></code>

重要:因为iOS无法从命令行运行程序,像列表2中的iOS特殊列表来自于Xcode控制台窗口。由于控制台窗口不支持“#”字符注释,所以注释用‘--’来表示。如果你想自己重复这些例子,你不能在控制台窗口输入--。

相比之下,在本文档中其他列表是在Mac OS X上创建的,使用传统GDB注释。

最后,如果你正在寻找关于具体说明信息,Shark(包括Xcode开发工具)中的帮助菜单中有一个ARM,英特尔和PowerPC 架构的指令集引用。

ARM

在ARM程序中,前面四个参数放在寄存器中。返回地址在寄存器LR中。表1展示了当你停在函数的第一个指令时,如何从GDB访问这些值。

表1 ARM的访问参数
What GDB Syntax
return address $lr
first parameter $r0
second parameter $r1
third parameter $r2
fourth parameter $r3

函数返回的结果在R0 ($r0)寄存器中。

因为参数传递到寄存器中,在prologue之后没有简单的方法来访问参数。

列表3展示了如何使用这些信息访问GDB参数。
<pre><code>
-- We have to start the program from Xcode. Before we do that, we go to

-- Xcode's Breakpoints window and set a symbolic breakpoint on

-- CFStringCreateWithFormat.

GNU gdb 6.3.50-20050815 […]

-- We've stopped after the prologue.

(gdb) p/a $pc

$1 = 0x34427ff8 <CFStringCreateWithFormat+8>

-- Let's see what the prologue has done to the registers

-- holding our parameters.

(gdb) x/5i $pc-8

0x34427ff0 <CFStringCreateWithFormat>: push {r2, r3}

0x34427ff2 <CFStringCreateWithFormat+2>: push {r7, lr}

0x34427ff4 <CFStringCreateWithFormat+4>: add r7, sp, #0

0x34427ff6 <CFStringCreateWithFormat+6>: sub sp, #4

0x34427ff8 <CFStringCreateWithFormat+8>: add r3, sp, #12

-- Hey, the prologue hasn't modified R0..R3, so we're OK.

--

-- first parameter is "alloc"

(gdb) p/a $r0

$2 = 0x3e801d60 <__kCFAllocatorSystemDefault>

-- second parameter is "formatOptions"

(gdb) p/a $r1

$3 = 0x0

-- third parameter is "format"

(gdb) call (void)CFShow($r2)

managed/%@/%@

-- It turns out the prologue has left LR intact as well.

-- So we can get our return address.

--

-- IMPORTANT: Bit zero of the return address indicates that it lies

-- in a Thumb routine. So when using a return address in LR or on

-- the stack, always mask off bit zero.

(gdb) p/a $lr & 0xfffffffe

$4 = 0x34427e18 <__CFXPreferencesGetManagedSourceForBundleIDAndUser+48>

-- Now clear the breakpoint and set a new one before the prologue.

(gdb) del 1

(gdb) b *CFStringCreateWithFormat

Breakpoint 2 at 0x34427ff0

(gdb) c

Continuing.

Breakpoint 2, 0x34427ff0 in CFStringCreateWithFormat ()

-- We're at the first instruction. The parameters are guaranteed

-- to be in the right registers.

(gdb) p/a $pc

$5 = 0x34427ff0 <CFStringCreateWithFormat>

-- first parameter is "alloc"

(gdb) p/a $r0

$6 = 0x3e801d60 <__kCFAllocatorSystemDefault>

-- second parameter is "formatOptions"

(gdb) p/a $r1

$7 = 0x0

-- third parameter is "format"

(gdb) call (void)CFShow($r2)

%@/%@.plist

-- return address is in LR; again, we mask off bit zero

(gdb) p/a $lr & 0xfffffffe

$8 = 0x34427e7c <__CFXPreferencesGetManagedSourceForBundleIDAndUser+148>

(gdb) b *0x34427e7c

Breakpoint 3 at 0x34427e7c

-- Delete the other breakpoint to avoid thread-based confusion.

(gdb) del 2

-- Continue to the return address.

(gdb) c

Continuing.

Breakpoint 3, 0x34427e7c in __CFXPreferencesGetManagedSourceForBundleIDAndUser ()

-- function result

(gdb) p/a $r0

$9 = 0x1062d0

(gdb) call (void)CFShow($r0)

mobile/.GlobalPreferences.plist

</pre></code>

列表3 ARM参数

英特尔32位

iPhone模拟器使用的是英特尔32位架构。

在英特尔32位程序中,参数传递到堆栈中。程序的第一个指令在栈顶,包含返回地址,下个字节包含第一个(最左边)参数,下个字节包含第二个参数,等等。表2展示了如何访问GDB的这些值。

表2 英特尔32位的可访问参数
What GDB Syntax
return address (int)$esp
first parameter (int)($esp+4)
second parameter (int)($esp+8)
... and so on

在程序prologue之后,可以访问帧指针参数(寄存器EBP)。表3展示了语法。

表3 prologue之后可访问参数
What GDB Syntax
previous frame (int)$ebp
return address (int)($ebp+4)
first parameter (int)($ebp+8)
second parameter (int)($ebp+12)
... and so on

函数返回的结果在寄存器EAX ($eax)。

列表4展示了如何使用信息访问GDB参数。
<pre><code>
$ # Use the -arch i386 argument to GDB to get it to run the

$ # 32-bit Intel binary.

$ gdb -arch i386 /Applications/TextEdit.app

GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]

(gdb) fb CFStringCreateWithFormat

Breakpoint 1 at 0x31ec6d6

(gdb) r

Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit

Reading symbols for shared libraries […]

Breakpoint 1, 0x940e36d6 in CFStringCreateWithFormat ()

(gdb) # We've stopped after the prologue.

(gdb) p/a $pc

$1 = 0x940e36d6 <CFStringCreateWithFormat+6>

(gdb) # However, for 32-bit Intel we don't need to inspect

(gdb) # the prologue because the parameters are on the stack.

(gdb) # We can access them relative to EBP.

(gdb) #

(gdb) # first parameter is "alloc"

(gdb) p/a (int)($ebp+8)

$2 = 0xa0473ee0 <__kCFAllocatorSystemDefault>

(gdb) # second parameter is "formatOptions"

(gdb) p/a (int)($ebp+12)

$3 = 0x0

(gdb) # third parameter is "format"

(gdb) call (void)CFShow((int)($ebp+16))

%@

(gdb) # return address is at EBP+4

(gdb) p/a (int)($ebp+4)

$4 = 0x940f59fb <__CFXPreferencesGetNamedVolatileSourceForBundleID+59>

(gdb) # Now clear the breakpoint and set a new one before the prologue.

(gdb) del 1

(gdb) b *CFStringCreateWithFormat

Breakpoint 2 at 0x940e36d0

(gdb) c

Continuing.

Breakpoint 2, 0x940e36d0 in CFStringCreateWithFormat ()

(gdb) # We're at the first instruction. We must access

(gdb) # the parameters relative to ESP.

(gdb) p/a $pc

$6 = 0x940e36d0 <CFStringCreateWithFormat>

(gdb) # first parameter is "alloc"

(gdb) p/a (int)($esp+4)

$7 = 0xa0473ee0 <__kCFAllocatorSystemDefault>

(gdb) # second parameter is "formatOptions"

(gdb) p/a (int)($esp+8)

$8 = 0x0

(gdb) # third parameter is "format"
(gdb) call (void)CFShow((int)($esp+12))

managed/%@/%@

(gdb) # return address is on the top of the stack

(gdb) p/a (int)$esp

$9 = 0x940f52cc <__CFXPreferencesGetManagedSourceForBundleIDAndUser+76>

(gdb) # Set a breakpoint on the return address.

(gdb) b *0x940f52cc

Breakpoint 3 at 0x940f52cc

(gdb) c

Continuing.

Breakpoint 3, 0x940f52cc in __CFXPreferencesGetManagedSourceForBundleIDAndUser ()

(gdb) # function result

(gdb) p/a $eax

$10 = 0x1079d0

(gdb) call (void)CFShow($eax)

managed/com.apple.TextEdit/kCFPreferencesCurrentUser

</pre></code>

列表4 英特尔32位参数

架构陷阱

下面章节描述在汇编级调试时坑内个遇到的几个陷阱

额外参数

在查看汇编级参数时,要记住以下几点:

如果程序是C++成员函数,第一个隐式参数是this。

如果程序是Objective-C方法,有两个隐式参数(有关详细信息,请参阅Objective-C)。

如果编译器可以找到函数(通常是声明为static的函数)的所有调用者,它可以以非标准方式选择传递参数给函数。很少有高效基于寄存器ABI的架构,但是对于英特尔32位程序是非常常见的。因此,如果你在一个英特尔32位程序的静态函数上设置断点,小心这令人费解的行为。

字节顺序和单位大小

当检查GDB内存事,如果使用正确的单位大小,事情会更加顺利。表4是GDB支持的单位大小的总结。

表4 GDB单位大小
Size C Type GDB Unit Mnemonic
1 byte char b byte
2 bytes short h half word
4 bytes int w word
8 bytes long or long long g giant

当在低位优先系统(所有iOS设备和模拟器)上调试时这非常重要,如果你使用了错误的单位大小,你会得到一个令人困惑的结果。列表5展示了一个例子(来自Max,但在iOS设备上结果是相同的)。CFStringCreateWithCharacters``的第二个和第三个参数指定Unicode字符数组。每个元素都是一个UniChar,是本地优先格式的16位数字。当在低位优先系统运行时,必须使用正确的单位大小转储数组,否则看起来一团糟。

列表5 使用正确的单位大小
<pre><code>
$ gdb

GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]

(gdb) attach Finder

Attaching to process 4732.

Reading symbols for shared libraries . done

Reading symbols for shared libraries […]

0x00007fff81963e3a in mach_msg_trap ()

(gdb) b *CFStringCreateWithCharacters

Breakpoint 1 at 0x7fff86070520

(gdb) c

Continuing.

Breakpoint 1, 0x00007fff86070520 in CFStringCreateWithCharacters ()

(gdb) # The third parameter is the number of UniChars

(gdb) # in the buffer pointed to by the first parameter.

(gdb) p (int)$rdx

$1 = 18

(gdb) # Dump the buffer as shorts. Everything makes sense.

(gdb) # This is the string "Auto-Save Recovery".

(gdb) x/18xh $rsi

0x10b7df292: 0x0041 0x0075 0x0074 0x006f 0x002d 0x0053 0x0061 0x0076

0x10b7df2a2: 0x0065 0x0020 0x0052 0x0065 0x0063 0x006f 0x0076 0x0065

0x10b7df2b2: 0x0072 0x0079

(gdb) # Now dump the buffer as words. Most confusing.

(gdb) # It looks like "uAotS-va eeRocevyr"!

(gdb) x/9xw $rsi

0x10b7df292: 0x00750041 0x006f0074 0x0053002d 0x00760061

0x10b7df2a2: 0x00200065 0x00650052 0x006f0063 0x00650076

0x10b7df2b2: 0x00790072

(gdb) # Now dump the buffer as bytes. This is a little less

(gdb) # confusing, but you still have to remember that it's big

(gdb) # endian data.

(gdb) x/36xb $rsi

0x10b7df292: 0x41 0x00 0x75 0x00 0x74 0x00 0x6f 0x00

0x10b7df29a: 0x2d 0x00 0x53 0x00 0x61 0x00 0x76 0x00

0x10b7df2a2: 0x65 0x00 0x20 0x00 0x52 0x00 0x65 0x00

0x10b7df2aa: 0x63 0x00 0x6f 0x00 0x76 0x00 0x65 0x00

0x10b7df2b2: 0x72 0x00 0x79 0x00

</pre></code>

控制的崩溃

在某些情况下,以可控的方式控制程序崩溃。一个常见的方法是终止调用。另一个选择是使用__builtin_trap内在函数,产生特定指令。列表6展示了如何实现。

列表6 通过__builtin_trap崩溃
<pre><code>
int main(int argc, char **argv) {

__builtin_trap();

return 1;

}

</pre></code>

注意: __builtin_trap 内在函数在PowerPC 和ARM上生成一个trap指令,在英特尔生成一个ud2a指令。

如果你在这个调试器上运行程序,必须在调用__builtin_trap之前停止。否则程序将崩溃并生成崩溃报告。

警告:我们建议你在调试版本使用这个技术,发布版本应该使用abort``。

需注意:iOS应用程序声明周期由用户控制,这意味着iOS应用程序不应该退出。只有当程序崩溃的情况下,你发布的版本需调用abort,调用abort可以防止破坏用户数据或者让你诊断问题更容易。

工具

工具是一个动态跟踪和分析代码的应用程序。它运行在Mac OS X 上,允许你的目标程序运行在Mac OS X,iOS设备,iPhone模拟器。

而工具主要集中在性能调试,你也可以使用它来调试错误。例如,ObjectAlloc 工具可以帮助你追踪bug。

工具的一个特别好的功能是让你可以访问死机的电脑,有关详情和其他工具功能,请参阅工具用户指南( Instruments User Guide)。

崩溃报告

崩溃报告是一个非常重要的调试功能,可以记录程序崩溃的信息。它一直处于启用状态,你只需要查看它的输出。

崩溃报告的详情在“理解和分析iPhone OS应用程序崩溃报告”(Technical Note TN2151, 'Understanding and Analyzing iPhone OS Application Crash Reports')。

BSD

BSD子系统实现过程、内存、文件和网络基础结构,从而对系统上所有应用程序至关重要。BSD实现一些方便的调试工具,这些工具你都可以使用。

内存分配器

默认的内存分配器中包含大量的调试工具,你可以通过环境变量启用。这些都记录在指南页面(manual page)。表5列出了一些更有用的环境变量。

表5 一些有用的内存分配器环境变量
变量 概要
MallocScribble 先填充到分配的内存0xAA ,释放内存到0x55
MallocGuardEdges 在大内存分配之前或之后添加保护页面
MallocStackLogging 为每个内存block记录回溯以协助内存调试工具;如果block被分配并立即释放,两条记录将从日志中删除,这有助于减小日志大小。
MallocStackLoggingNoCompact 和MallocStackLogging 一样,但是保存所有日志记录

如果默认的内存分配器检查到某些常见的编程问题,将记录日志。例如,如果你两次释放block的内存或者你释放未分配的内存,free将打印列表7中列出的消息。括号里的数字是进程id

列表7 free打印的公共消息
<pre><code>
DummyPhone(1839) malloc:

*** error for object 0x826600:

double free *** set a breakpoint in malloc_error_break to debug
</pre></code>

你可以在GDB上运行你的程序并在malloc_error_break设置一个断点,来调试这类问题。一旦你点击断点,你可以使用GDB的 backtrace 命令决定调用者。

最后,你可以使用malloc_zone_check(来自``)以编程的方式检查堆的一致性。

标准C++库

标准C++库支持大量的调试特性:

  • 设置_GLIBCXX_DEBUG编译时变量启用标准C++库中的调试模式。

GCC4.2及更高版本不支持,在这种情况下不能使用。

  • GCC4.0之前的版本,将环境变量GLIBCPP_FORCE_NEW设置为1并在标准C++库中禁用内存缓存。运行你使用其他内存调试工具调试C++内存分配。

在GCC4.0 及之后版本这是默认的。

动态链接(dyld)

动态链接(dyld) 支持大量的调试工具,你可以通过环境变量启用。这些都记录在指南页面(manual page)。表6列出了一些更有用的环境变量。

表6 动态链接环境变量
变量 概述
DYLD_IMAGE_SUFFIX 先用后缀搜索库
DYLD_PRINT_LIBRARIES 加载日志库
DYLD_PRINT_LIBRARIES_POST_LAUNCH 同上,但只在main运行后
DYLD_PRINT_OPTS [1] 打印启动命令参数
DYLD_PRINT_ENV [1] 打印启动环境变量
DYLD_PRINT_APIS [1] 记录dyld API调用(例如,dlopen)
DYLD_PRINT_BINDINGS [1] 记录符号绑定
DYLD_PRINT_INITIALIZERS [1] 记录图像初始化打印
DYLD_PRINT_SEGMENTS [1] 记录分段映射
DYLD_PRINT_STATISTICS [1] 打印开始性能统计数据

注意:只支持Mac OS X 10.4及之后版本。不过,支持所有版本的iOS。

虽然这些环境变量在iOS上实现,因为受限于系统环境,其中只有有限的用处。

核心服务

核心基础

核心基础(CF)框架导出CFShow程序,该程序输出任何CF对象的描述到stderr。你可以在自己的代码中调用,然而,从GDB中调用时特别有用。列表8中展示了这样的一个例子。

列表8 从GDB调用CFShow
<pre><code>
$ gdb /Applications/TextEdit.app

GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]

(gdb) fb CFRunLoopAddSource

Breakpoint 1 at 0x624dd2f195cfa8

(gdb) r

Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit

Reading symbols for shared libraries […]

Breakpoint 1, 0x00007fff8609bfa8 in CFRunLoopAddSource ()

(gdb) # Check that the prologue hasn't changed $rdi.

(gdb) p/a 0x00007fff8609bfa8

$1 = 0x7fff8609bfa8 <CFRunLoopAddSource+24>

(gdb) p/a $pc

$2 = 0x7fff8609bfa8 <CFRunLoopAddSource+24>

(gdb) x/8i $pc-24

0x7fff8609bf90 <CFRunLoopAddSource>: push %rbp

0x7fff8609bf91 <CFRunLoopAddSource+1>: mov %rsp,%rbp

0x7fff8609bf94 <CFRunLoopAddSource+4>: mov %rbx,-0x20(%rbp)

0x7fff8609bf98 <CFRunLoopAddSource+8>: mov %r12,-0x18(%rbp)

0x7fff8609bf9c <CFRunLoopAddSource+12>: mov %r13,-0x10(%rbp)

0x7fff8609bfa0 <CFRunLoopAddSource+16>: mov %r14,-0x8(%rbp)

0x7fff8609bfa4 <CFRunLoopAddSource+20>: sub $0x40,%rsp

0x7fff8609bfa8 <CFRunLoopAddSource+24>: mov %rdi,%r12

(gdb) # Nope. Go ahead and CFShow it.

(gdb) call (void)CFShow($rdi)

<CFRunLoop 0x100115540 [0x7fff70b8bf20]>{

locked = false, 

wakeup port = 0x1e07, 

stopped = false,

current mode = (none),

common modes = <CFBasicHash 0x1001155a0 [0x7fff70b8bf20]>{

    type = mutable set, 

    count = 1,

    entries =>

        2 : <CFString 0x7fff70b693d0 [0x7fff70b8bf20]>{

            contents = "kCFRunLoopDefaultMode"

        }

},

common mode items = (null),

modes = <CFBasicHash 0x1001155d0 [0x7fff70b8bf20]>{

    type = mutable set, 

    count = 1,

    entries =>

        0 : <CFRunLoopMode 0x100115670 [0x7fff70b8bf20]>{

            name = kCFRunLoopDefaultMode, 

            locked = false, 

            port set = 0x1f03,

            sources = (null),

            observers = (null),

            timers = (null)

    },

}

}
</pre></code>

重要:如果你没有看到CFShow输出任何东西,很可能是发送到控制台。如何查看输出,可参阅“查看调试输出信息”

注意:在列表8中,CFShow的输出已经格式化,更容易阅读。

从GCB调用有很多其他CF程序,你会发现非常有用,包括CFGetRetainCount, CFBundleGetMainBundle,和 CFRunLoopGetCurrent

zombie

重要:如果你用Objective-C编程,你可能对NSZombieEnabled更感兴趣,如更多zombie所述。

核心基础支持叫做CFZombieLevel的环境变量。该变量为包含一组标志位的正式。表7描述了当前定义的bit。可以帮助你追踪各种CF内存管理问题。

表7 CFZombieLevel 环境变量的bit定义
Bit Action
0 释放CF内存
1 当释放CF内存,不要乱想对象头(CFRuntimeBase)
4 没有空闲内存用于保存CF对象
7 如果设置,使用bit8..15释放内存,否则使用0xFC
8..15 如果设置bit 7,使用这个值来释放内存
16 分配CF内存
23 如果设置,使用bit 24..31分配内存,否则使用0xCF
24..31 如果设置为23bit,使用这个值分配内存

应用程序服务

核心动画

核心动画工具可以衡量应用程序的帧速率,查看各种类型的绘画。详情查看工具用户指南(Instruments User Guide)。

Cocoa and Cocoa Touch

所有Cocoa对象(一切均源自NSObject)支持description 方法,该方法返回一个NSString 描述对象。访问这个描述最方便的方法是通过Xcode打印描述到控制台。或者,如果你是个命令行迷,你可以使用GDB的print-object 命令,如列表9所示。

列表9 使用GDB的命令
<pre><code>
$ gdb /Applications/TextEdit.app

GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]

(gdb) fb -[NSCFDictionary copyWithZone:]

Breakpoint 1 at 0x83126e97675259

(gdb) r

Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit

Reading symbols for shared libraries […]

Breakpoint 1, 0x00007fff837aa259 in -[NSCFDictionary copyWithZone:] ()

(gdb) po $rdi

{

AddExtensionToNewPlainTextFiles = 1;

AutosaveDelay = 30;

CheckGrammarWithSpelling = 0;

CheckSpellingWhileTyping = 1;

[…]

}

</pre></code>

Objective-C

在一个Objective-C异常处中断,不管它如何抛出异常,在objc_exception_throw设置一个象征性的断点。最简单的方法是在Xcode中设置断点,停止在Objective-C异常菜单命令。

注意:比在-[NSException raise]``处设置断点更好,因为即使使用@throw抛出异常,断点仍会触发。

Objective-C汇编级程序调试

在汇编级别调试Cocoa代码时,请记住Objective-C运行时如下特性:

  • Objective-C编译器为每个方法增加了两个隐式参数,第一个参数是一个指向调用对象(self)。

  • 第二个隐式参数是方法选择器(_cmd)。在Objective-C中这是SEL类型,在GDB中你可以打印C字符串一样打印它。

  • Objective-C运行时调度方法时通过C函数的一种。最常见的是objc_msgSend,但有一些架构使用objc_msgSend_stret方法返回结构,另一些架构使用objc_msgSend_fpret方法返回浮点值。调用super(objc_msgSendSuper等等)都是等效函数。

  • Objective-C对象的第一个单词(isa字段)是一个指向对象类的指针。

注意:如果你对 Objective-C消息调度有兴趣,可以查看这篇文章。

表8 总结了如何从GDB访问self和_cmd,如果你停在方法的第一个指令。详情,可阅读‘Some Assembly Required’。

表8 访问self和_cmd
Architecture self _cmd
ARM $r0 $r1
Intel 32-bit (int)($esp+4) (int)($esp+8)

列表10 展示了如何从使用GDB信息的例子

重要:列表10和列表11都是在Mac OS X上创建,但核心技术在iOS上同样有用。有关如何转化这些技术到iOS,可以阅读‘Some Assembly Required for information ’。

列表10 Objective-C运行时‘秘密’
<pre><code>
$ gdb /Applications/TextEdit.app

GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]

(gdb) # Give the runtime a chance to start up.

(gdb) fb NSApplicationMain

Breakpoint 1 at 0x9374bc69df7307

(gdb) r

Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit

Reading symbols for shared libraries […]

Breakpoint 1, 0x00007fff841a0307 in NSApplicationMain ()

(gdb) # Set a breakpoint on -retain.

(gdb) b *'-[NSObject(NSObject) retain]'

Breakpoint 2 at 0x7fff8608a860

(gdb) c

Continuing.

Breakpoint 2, 0x00007fff8608a860 in -[NSObject(NSObject) retain] ()

(gdb) # Hit the breakpoint; dump the first 4 words of the object

(gdb) x/4xg $rdi

0x1001138f0: 0x00007fff7055e6d8 0x0041002f01a00000

0x100113900: 0x0069006c00700070 0x0069007400610063

(gdb) # Now print the selector

(gdb) x/s $rsi

0x7fff848d73d8: "retain"

(gdb) # Want to 'po' object; must disable the breakpoint first

(gdb) dis

(gdb) po $rdi

/Applications/TextEdit.app

(gdb) # Print the 'isa' pointer, which is a Class object.

(gdb) po 0xa02d9740

NSPathStore2

</pre></code>

无标识调试时,你可以使用Objective-C运行时函数来辅助你的调试工作,表9中展示的程序特别有用

表9 有用的Objective-C运行时函数
Function Summary
id objc_getClass(const char *name); 获取给定类名的Objective-C类对象
SEL sel_getUid(const char *str); 获取给定方法名的Objective-C SEL
IMP class_getMethodImplementation(Class cls, SEL name); 获取指向给定类给定方法的实现代码的指针

列表11 展示了TextEdit的-[DocumentController openUntitledDocumentAndDisplay:error:] 方法的调试,尽管TextEdit没有标识。

列表11 使用Objective-C运行时来无标识调试
<pre><code>
$ gdb -arch x86_64 /Applications/TextEdit.app

GNU gdb 6.3.50-20050815 (Apple version gdb-1346) […]

(gdb) r

Starting program: /Applications/TextEdit.app/Contents/MacOS/TextEdit

Reading symbols for shared libraries […]

^C

(gdb) # Try to find the

(gdb) # -[DocumentController openUntitledDocumentAndDisplay:error:]

(gdb) # symbol.

(gdb) info func openUntitledDocumentAndDisplay

All functions matching regular expression "openUntitledDocumentAndDisplay":

Non-debugging symbols:

0x00007fff843ac083 -[NSDocumentController openUntitledDocumentAndDisplay:error:]

(gdb) # These are not the droids we're looking for. It turns out that

(gdb) # TextEdit ships with its symbols stripped, so we'll have to do

(gdb) # this the hard way.

(gdb) #

(gdb) # Get the Class object for the DocumentController class.

(gdb) set $class=(void *)objc_getClass("DocumentController")

(gdb) # Get the SEL object for the "openUntitledDocumentAndDisplay:error:" method.

(gdb) set $sel=(void *)sel_getUid("openUntitledDocumentAndDisplay:error:")

(gdb) # Get a pointer to the method implementation.

(gdb) call (void*)class_getMethodImplementation($class, $sel)

$1 = (void *) 0x100001966

(gdb) # Confirm that this is sensible. Looks like a method prologue to me.

(gdb) x/4i 0x00009aa5

0x100001966: push %rbp

0x100001967: mov %rsp,%rbp

0x10000196a: push %r12

0x10000196c: push %rbx

(gdb) # Set a breakpoint on the method.

(gdb) b *0x100001966

Breakpoint 1 at 0x100001966

(gdb) # Resume execution, and then create a new, untitled document.

(gdb) c

Continuing.

[…]

Breakpoint 1, 0x0000000100001966 in ?? ()

(gdb) # We've hit our breakpoint; print the parameters, starting with

(gdb) # the implicit "self" and "SEL" parameters that are common to all

(gdb) # methods, followed by the method-specific "display" and

(gdb) # "error" parameters.

(gdb) po $rdi

<DocumentController: 0x100227a50>

(gdb) x/s $rsi

0x7fff848e4e04: "openUntitledDocumentAndDisplay:error:"

(gdb) p (int)$rdx

$2 = 1

(gdb) x/xg $rcx

0x7fff5fbff108: 0x00000001001238f0

</pre></code>

你可以通过/usr/include/objc/的头,了解更多关于Objective-C 运行时函数与数据结构

警告:Objective-C 运行时允许你发现更多系统类的私有实现细节。你在最终的产品中不能使用任何有关信息。

Foundation

Foundation有大量的调试工具,这些工具可以通过环境变量启用。表10中突出显示了这些变量。

表10 Foundation 环境变量
Name Default Action
NSZombieEnabled NO 如果设置为YES,已释放对象是'zombified' ,在你发送消息到已释放的对象时可以快速的调试问题。详情查阅更多Zombies
NSDeallocateZombies NO 如果设置为YES,已'zombified' 的对象的内存实际上是释放掉的
NSUnbufferedIO NO 如果设置为YES,Foundation 将为stdout(stderr默认无缓冲)使用无缓冲 I/O

重要:启用或禁用基础调试工具,你必须设置环境变量值为“是”或“否”,而非1或0,其他系统组件也一样。

Retain 计数

你可以使用retain计数来获取当前对象的retain计数。虽然有时候这种方法是有用的调试助手,当你解释结果时要非常小心。列表12展示了一个混乱的潜在来源。

列表12 混乱的retain计数
<pre><code>
(gdb) set $s=(void *)[NSClassFromString(@"NSString") string]

(gdb) p (int)[$s retainCount]

$4 = 2147483647

(gdb) p/x 2147483647

$5 = 0x7fffffff

(gdb) # The system maintains a set of singleton strings for commonly

(gdb) # used values, like the empty string. The retain count for these

(gdb) # strings is a special value indicating that the object can't be

(gdb) # released.
</pre></code>

另一个常见混淆的来源时自动释放机制。如果一个对象呗自动释放,它的retain数可能比你想象的要高。在未来的某个时间点自动释放池将释放它。

你可以决定哪些对象在哪个自动释放池中,通过调用 _CFAutoreleasePoolPrintPools 可以打印所有在指定释放池堆栈上的自动释放池的内容。

列表13 打印自动释放池堆栈
<pre><code>
(gdb) call (void)_CFAutoreleasePoolPrintPools()

  • -- ---- -------- Autorelease Pools -------- ---- -- -

==== top of stack ================

0x327890 (NSCFDictionary)
0x32cf30 (NSCFNumber)

[…]

==== top of pool, 10 objects ================

0x306160 (__NSArray0)

0x127020 (NSEvent)

0x127f60 (NSEvent)

==== top of pool, 3 objects ================


</pre></code>

注意:_CFAutoreleasePoolPrintPools只适用于iOS4.0及更高版本。

在早期系统,你可以使用函数来确定一个对象呗条件到自动释放池的次数。例如,见列表14.

列表14 调用NSAutoreleasePoolCountForObject
<pre><code>
(gdb) # d is an NSDictionary created with -[NSDictionary dictionaryWithObjectsAndKeys:].

(gdb) p d

$1 = (NSDictionary *) 0x12d620

(gdb) po d

{

test = 12345;

}

(gdb) p (int)[d retainCount]

$2 = 1

(gdb) p (int)NSAutoreleasePoolCountForObject(d)

$3 = 1

</pre></code>

更多的zombie!

用Cocoa编程时常见的bug类型是过度释放对象。这通常会导致应用程序崩溃,但当崩溃发生时最后一个引用计数会释放(当你试图给已释放对象发消息),通常会从原bug移除。对于调试这样的问题,NSZombieEnabled 是最好的选择。它将展示任何试图与释放对象交互。

启用zombie最好的方法是通过工具。然而,你可以可以通过一个环境变量来启用zombie。列表15展示了在这种情况下你会看到的消息类型。

列表15 NSZombie的效果
<pre><code>
$ NSZombieEnabled=YES build/Debug/DummyMac.app/Contents/MacOS/DummyMac

[…] -[AppDelegate testAction:]

[…] *** -[CFNumber release]:

message sent to deallocated instance 0x3737c0 Trace/BPT trap
</pre></code>

如列表15所示,如果系统检测到zombie,系统将执行一个断点指令。如果你在GDB下运行,程序将停止,你可以查看backtrace 来找出触发zombie检查器的调用链。

NSZombieEnabled 也影响核心基础对象以及Objective-C对象。然而,只有当你从Objective-C访问一个zombie核心基础对象时你会被通知,而不是当你通过一个CF API来访问它。

其他Foundation

如果你使用键值观察并且你想知道谁在观察一个特定的对象,你可以使用GDB打印对象命令来获取对象的观察信息并将它打印。列表16给出了一个这样的例子。

列表16 显示键值观察者
<pre><code>
(gdb) # self is some Objective-C object.

(gdb) po self

<ZoneInfoManager: 0x48340d0>

(gdb) # Let's see who's observing what key paths.

(gdb) po [self observationInfo]

<NSKeyValueObservationInfo 0x48702d0> (

<NSKeyValueObservance 0x4825490: Observer: 0x48436e0, \

Key path: zones, Options: <New: NO, Old: NO, Prior: NO> \

Context: 0x0, Property: 0x483a820>

)
</pre></code>

你可以设置NSShowNonLocalizedStrings 的偏好来找到该被本地化但没有的字符串。一旦启用,如果你请求一个本地字符串同时在字符串文件中没找到该字符串,系统将返回大写的字符串和日志消息到控制台。这是一个很好的方法来发现过期本地化问题。

这个设置会影响NSLocalizedString、所有变形以及包括CFCopyLocalizedString的底层基础结构。

UIKit

UIView实现了一个很有用的description 方法。此外,它还实现了recursiveDescription 方法,你可以调用这个方法获取整个视图层次。

列表17 UIView的description 和recursiveDescription 方法
<pre><code>
-- The following assumes that you're stopped in some view controller method.

(gdb) po [self view]

<UIView: 0x6a107c0; frame = (0 20; 320 460); autoresize = W+H; layer = […]

Current language: auto; currently objective-c

(gdb) po [[self view] recursiveDescription]

<UIView: 0x6a107c0; frame = (0 20; 320 460); autoresize = W+H; layer = […]

| <UIRoundedRectButton: 0x6a103e0; frame = (124 196; 72 37); opaque = NO; […]

| | <UIButtonLabel: 0x6a117b0; frame = (19 8; 34 21); text = 'Test'; […]

</pre></code>

网络

调试网络代码最重要的工具是packet trace。“获取到一个packet trace”( Technical Q&A QA1176, 'Getting a Packet Trace' )中讨论了如何在 Mac OS X上获取一个packet trace。

尽管iOS没有内置packet trace工具,通常你可以通过Mac来获取packet trace。

电源

能源诊断工具是理解你的应用程序如何影响电池寿命的好方法。详情查看工具用户指南(Instruments User Guide )。

推送通知

“推送通知故障排除”(Technical Note TN2265, 'Troubleshooting Push Notifications' )是调试推送通知问题的全面指南。它还包括一个配置概要文件(APNsLogging.mobileconfig),你可以安装这个文件启用更多的调试日志。

iPhone模拟器

iPhone模拟器让你可以在Mac OS X上构建和运行iOS代码。因为iPhone模拟器是一个Mac OS X应用程序,你可以使用各种各样的Mac OS X调试工具。例如,如果你有个问题只能在DTrace (它在iOS上不可用)上解决,你可以在模拟器运行你的程序,并将DTrace 指向它。

重要:模拟器很是和调试,但最终还是真正的设备决定是否在iOS可用。在做性能测试与调试的时候,一定要牢记这一点。

关于Mac OS X 调试工具的更多信息,参见“Mac OS X 调试”(Technical Note TN2124, 'Mac OS X Debugging Magic')。

文件修订历史

Date Notes
2011-01-22 描述大量的iOS调试提示与技巧的新文档

官方原文地址:

https://developer.apple.com/library/ios/technotes/tn2239/_index.html

推荐阅读更多精彩内容