-
高级调试和 Address Sanitizer
探索 UI 调试,以及如何利用高级断点操作来快速浏览和修复您的 app。了解新的 Address Sanitizer 功能如何查找缓冲区溢出、“释放后使用”错误,以及运行时的其他内存损坏错误。
资源
-
搜索此视频…
高级调试和地址消毒剂
早上好 欢迎来参加《高级调试和 地址消毒剂》讲座 我是麦克... 大家怎么样呢? 好吗?准备好啦? 激动吗? 好 现在就开始
那我先来介绍 Xcode中加入的新功能 还有一些大家 可能不知道的 新旧技巧和诀窍 先来说说视图调试器 来看怎样才能 获得更多洞察力 关于你的app UI及其用户...
界面元素 以及它们 在运行时间如何表现 我们会调试 AutoLayout Constraint问题 至少对我而言 我需要一些帮助 理解AutoLayout在 运行时间做些什么
接下来 我们将详细分析用 Advanced Breakpoints 调试代码 我想给大家看看 如何设置自定义操作和条件 快速分析例外情况 在一定条件下打印值 而不会因NSLogs和 打印把代码搞乱
之后 我的同事安娜 会来介绍 最新最兴奋的调试功能 这就是我们新加入 Xcodes调试工具箱的 Address Sanitizer 她会详细介绍 其工作方式 可以捕捉到哪些问题 以及今天就用它 来整理代码 为了这两个话题 我觉得先用 演示设备 给大家看看
这里是个应用 名为Jogr
这个健身应用 可以为跑步计时 记录跑步路线 沿途会为照片加标签 几年来我们都用Jogr 来做示范app 今年我们添加了 一些使用Swift的新功能 并转换了Objective-C 的若干类 这是个非常混合的应用 在故事板中也用了尺寸类 并完全采用了AutoLayout
因为今年我们改变并增加了 所有这些新代码 可想而知其中会有程序错误 来找找看 先来点击这里的计时器 第一个非常明显的问题是 计时器周围的圆环 不是很整齐 在我提供的最初设计中 并不是这样 这可能是Jogr是在iPhone5 的屏幕上运行有关的 而当初是在 iPhone 6上开发的 可能是我没有完全测试 来看布局是否在小号的 屏幕上也适用 为了想了解 到底出了什么问题 我在下面的Debug条上 点击了DebugView Hierarchy按钮 现在 获得了所有 视图的快照 并在Xcode中装载了 关于视图 会如何互动的运行时间信息
我点击并拖动画布 你会看到我可以 怎样扭动场景 显示所有的不同视图 以及它们如何在 彼此上方分层 我甚至还点击具体视图来选择 我们可以在对象检测器中看到 所有细节
现在有许多内容在继续 有导航条和背景所有这些 可我只想关注大家在意的一点 也就是我放在中心的内容部分
只要双击视图就可以做到 万一你忘记了 我给大家看看怎么失焦 就是在画布上双击即可 这里可以继续只关注这一点
你会注意到在这里 在Debug Navigator UI堆栈视图上的
所有层次都已经省略 因为我们最近 在关注UI堆栈视图
如果我点击修剪后的图像 我可以用这里的尺寸检测器 检测平衡和约束条件 尺寸检测器不仅是显示x y 和这个的矩形坐标 还显示了 影响视图运行时间的约束 约束并非是现在在发挥作用 它影响了大小 边界 或是x y 在这里用灰色显示 那么这就有点奇怪 我看到高度约束是249点 这实际是这个 图像内容的实际尺寸 在运行时间 却没有真正运行 如果我来看约束 表示父视图应该和 图像同等大小 我们看到它在运行 看似是有其他部分在约束 父视图的大小 我们来看视图层次 了解一下父视图发生了些什么
这里 我们看到与其他视图 是四分之三 或.75的关系 我知道另一个视图 包括下面的起始按钮 在调试导航器上 我们还看到 所有的同样约束 可以像这样 打开
我们看到 约束按钮的视图上 有着相同的 四分之三约束 所有这些 看上去都很正常 上下关系 都是有条理的 顶部高于底部 底部和父视图底部相连 没别的了 为什么我们 不能把视图层次再提高一层
我们可以看出 这里有些奇怪 我们有个中心 y-约束 它想把整个堆栈视图中心 垂直于容器中 但是我们把这个自身顶部 与父视图顶部相连 在50点 这也有些奇怪
我觉得50点的 约束不应该在这里 我觉得在我开始 打开视图到尺寸类时 它可能就已经加上了 我告知IB 加入所有丢掉的约束 之后我设置了垂直中心对齐 我可能是忘记删除了 我们在故事板上做修改
来看是不是问题所在
我可以选择堆栈视图 就在这里 这是50点约束 我会删掉它 重新运行
不错! 现在圆形没问题了 没有缺失 视图也完全可以修改
我们再来运行计时器
好的看似有异常
我会打开 Debug Navigator 查看异常的线程 但是看似不是很有用 应该是主函数出了问题 如果从控制台来看 可以看到 是抛出了部分异常 但是现在对我来说帮助不大 因为我想停止程序 在抛出异常的时刻 对它进行调试
为此 我可以进入 Breakpoint Navigator 点击下面的加号 添加异常断点 我接下来要配置这个断点 来停止Objective-C异常
像这样 我们从新运行应用
希望我们在问题发生时 能准确找到问题
非常好 但是...
如果我去看控制台 却什么都看不到 没有信息描述 异常是什么 这里有个技巧 是我从LLVM团队的 朋友那里学来的 导航进入 Ob-C Exception Throw函数 在Ob-C的运行时间中 打印函数的首个参数 实际上是异常对象本身 我说 打印对象 arg1 这就是 异常信息本身 因此我会调整异常断点 为所出现的异常 始终这么做 这样会很有帮助 我可以在这里添加一个操作 输入的内容和 输入控制台的完全一样 打印对象 arg1
现在 如果我再次运行应用
运行计时器
现在我仍然停在 异常被抛出的准确时刻 我实际上在控制台 有异常信息本身 现在 当我看着异常 被抛出的代码行 可以看到我构建的范围 范围超出了边界 估计是用了 索引1开头 而不是索引0 并且在设定字体属性时 执行了 字符串的全长 我们重新运行
再看看效果 看似计时器 现在工作正常
我们成功发现了 两处程序错误 我再看第三个 现在来看 来这里之前 我今天早上跑步的路线 看似没问题
但如果我们返回 则有些不对劲 我不是从莫斯科尼出发 跑到码头大街 然后跳到了水里 这是在太蠢了 我们再试一遍
这里出现了异常 我不知道 看似是数据模型出现了问题 所以我想找出 放在地图上的各点 出现覆盖时 我们在做什么
现在进入类 是这里 使路线出现了覆盖 我们在这里 用了一束数据点 从字典里拿出 生成并构造了代表我的...
跑步路线的多段线
我会设置一个断点 来看获得的数据值是什么
我会打开 Debug Console 来看变量视图 在下面 可以看到 我所感兴趣的点 它有着这些值 我会向前推进 这很有趣 但是有些麻烦 这些值看似相似 我在这里的处理 会让进程快些 来看所有这些值 我会编辑这个断点 打印出Struct点
实际上在变量视图上 我检测过这里 然后我会说 评价过操作后 自动继续 其实 这不是条件 是个操作 就这样
我们继续运行 我们可以看到 所有的点都看似一样 但是还会在 不知什么地方 会出现瞬移 但是所有的值看似合理 我不太确定问题是什么 也许从别人那里...
能得到更多帮助
我们回到幻灯片
总结一下看到的内容 首先在Jogr的计时器视图 深入了解 约束问题 我想给大家看看 怎样双击视图 关注具体部件 我们检测部分约束 发现当我们在其他屏幕 尺寸上运行时 需要将之删除 然后 我们在运行 计时器时 出现异常 并使用异常断点 来停在 出现异常的准确时刻 并打印异常信息 最后 我们可以 添加打印并继续添加了 日志的断点 所有这些都不会因NSLogs 或打印语句搞乱代码 现在来解决下一个谜题 为什么今天早上的 跑步会有那么 奇怪的结果 有请安娜
谢谢 麦克 大家好 那么回到 麦克刚刚在演示中 给大家看的那条路线 在测试演示app时 大家会看到 所有的路线 而且包括正确路线 我们非常希望麦克刚刚没有 找出正确的跑步路线 因为这样的话 我们的讲座 就没什么可说的了
这种不可预见的行为通常是 内存出错而导致 例如 分配给一个对象的内存 可能被其他对象所重写 或者也许由于某些计算错误 你用了并不属于你的内存
相信大家之前也遇过类似情况 代码中出现随机异常 也许在测试app的 某个侧面时 每次会出现这个问题 或者只是当你周五晚上 准备回家时出现这个问题!
最糟糕的用例场景 是用户看到这些 不稳定性或随机异常 而你却无法把它们重现眼前
内存出错是出了名的很难 一致性复现 那么也就很难 发现这一异常的根本原因
那该怎么办呢? 最好的办法就是尽量减少 面对内存出错的情况 要避免内存操控 例如使用Swift这类 语言勾选并 自动参考计数 这才会长久
即便内存出错 在技术上仍然可能 由于编码而产生这类问题 则较为不可能
另一方面 如果代码会直接操控内存 通过调用动态分配运算 或者代码可以 与C和C++ API可以互操作 你所在的风险组 会非常需要帮助
Address Sanitizer 是基于C语言的LLVM工具 这与Guard Malloc 起到相同的作用 因为它可以在运行时间 发现内存错误 并且较其他工具 有更多好处 它的运行时间 开销会少的很多 还会产生 综合详细的诊断分析 可以直接 整合到Xcode UI 还有一点很重要 只有这种工具 可以在iOS设备上运行 这些是Address Sanitizer 可以捕捉到的常见错误清单 例如 它可以很好地 捕捉到缓冲区溢出 这是个很常见的错误 因为与安全性攻击的关联 而臭名昭著
正如大家所见 它发现了部分工具 通过 Valgrind 和 Guard Malloc 发现的错误 但是它也发现了 新种类的程序错误 是其他工具没有注意的
我们回到演示 来看怎样才能 把这些用于项目中
我接着麦克讲到的内容继续 来看我们看看是否 Address Sanitizer 可以帮我们找出路线问题 为了启动 Address Sanitizer 进入Edit Scheme
进入Diagnostics选项卡 勾选 Enable Address Sanitizer 与其他内存管理工具不同 Address Sanitizer 需要重新编译 选中复选框后 Xcode就会知道 要打开 Address Sanitizer 重建应用 它会启动特殊模式 允许Address Sanitizer 在运行时间更多地 干预进程
我们继续重建 并重新运行app
来看像刚才的路线会怎样
现在Address Sanitizer 发现了问题 它的诊断直接整合到了 Xcode Debugger UI 这与出现异常时 情况非常相似 但是与使用Sec的 情况不同 这次可以对情况有更好的诊断 这里它告诉我们 查出堆缓冲区溢出 还可以看到堆栈轨迹 在这里出现了内存错误
如大家所见 我们称之为 Poly Line With Points 和Map Kit的常用方法 我们在缓冲区做测试 通过缓冲区的长度 来进行 计算点的数量 再用每个点的大小 来相除 看上去可以 随着应用在 Address Sanitizer 中执行 它会收集过程中 堆对象的重要信息 例如 分配 堆分配 和取消分配事件 当发现内存出错 它会使用启发法 来关联错误地址 到有效的堆对象 在内存项下信息都呈现在这里 这里它告诉我们 错误地址是在 在2240字节堆区域后的 一个字节 还告诉我们堆区域 所分配的位置
即使这不是激活线程 而是在分配事件发生时 进程执行的历史快照 我们可以把流 视作激活线程
它会把我们带到 内存分配的点的位置 好的 我们来看缓冲区的大小 是用每个点的大小 乘以点的数量得出 我们使用 MK Map Point 作为点的代表 这是带双精度的Struct
那么问题是什么? 我们回到Use站点
你传递到这里的类型是不同的 我们传递了MK Map Point星级
但是我们知道 刚分配了Struct的缓冲区 而不是指针的缓冲区
因为每个 指针的大小小于 结构体的大小 包括两个双精度 这里获得的计数 要大于缓冲区 包括的元素数量 这就会解释 为什么在地图上 我们会有那些额外的点
为解决问题 我们需要删掉星级 如大家所见 这是个人们非常容易犯的错误 只看代码是非常难发现的 在本例中 这个信息已经 足以诊断和解决问题 不过 如果觉得 想更多地看看这个报告 可以前往Memory View 来看哪个内存是有效的 哪个内存是被视为无效 从Address Sanitizer 的视角来看 前往Memory View 可以点击这个地址 这里可以看到所有变灰的内容 都是无效内存 所有黑色内存都被视为有效
我们返回并重新运行app 来看删掉星号 是否可以解决问题
我来继续返回Route视图
啊 这就是麦克 今天早上的跑步路线 还有早上 Bash之后的部分 我很受感动 我们返回 打开幻灯片 大家已经看到 打开Address Sanitizer很容易 进入Scheme Editor 打开Diagnostics选项卡 勾选Enable Address Sanitizer复选框 接着可以构建 并运行自己的项目
同样 正如大家在演示app所见 使用Address Sanitizer的 开销几乎很难注意到 较低的运行时间开销 允许你使用Address Sanitizer 不仅是在调试 部分内存出错问题的时候 而且是在进行UI驱动的测试 这里要手动测试 app的不同方面 再进一步 我们推荐 在连续整合过程中 使用Address Sanitizer 因为这是运行时间 程序错误发现工具 它仅会捕捉已执行 代码中的程序错误 那么应该提供尽可能多的覆盖 以实现最好结果
在Xcode或Xcode Server 的任务中启动 Address Sanitizer 进入Edit Scheme选择Task 然后再次进入 Diagnostics选项卡 勾选Enable Address Sanitizer复选框 构建并测试你的app 还可以用命令行启动 通过向Xcode Build 传递额外的参数
我们建议在Debug构建 使用Address Sanitizer 并将编译器优化关闭 但是它也得到 Fast优化水平的支持
是与01编辑器旗标相对应 需要牢记一点 当在这两个优化水平之间 进行决定时
如果有任何编译优化 是启动状态 调试体验就不会十分流畅
现在我们进入 讲座的最激动环节 我会告诉大家一个 驱动这个工具的很酷的技术
传统而言Xcode 使用clang编译器来 编译源代码 可以生成可执行的二进制代码
为使用 Address Sanitizer Xcode将特殊的旗标 传递给clang 它会生成工具二进制代码 其中包括 更多的内存检测
在运行时间 这个二进制代码 与asan运行时间dylib连接 其中包括更多检测 而且dylib是工具所要求的
但是这些内存检测 如何发挥作用? Address Sanitizer 检测进程中的所有位置 如果这是进程内存
Address Sanitizer会成为 所谓的影子内存 可以跟踪真实内存中的 每个字节
它有着字节是否为可访问 地址的相关信息
无效内存的字节就是红区 或者说内存中毒
当使用Address Sanitizer 编译程序时 它会影响每个内存访问 并在前缀加上检测 如果内存中毒 Address Sanitizer 就会跟踪程序 并生成诊断报告 否则 它会允许你继续
我们再来仔细看看 假设p为指针 然后IsPoisoned函数会 检测影子内存中的相关字节 在本例中 内存是有效的 因此程序可以编写那个 内存位置
不过 如果它并未指向有效内存 条件将会为真 程序将卡在无效内存 就是访问可能要发生的地方 这就是Address Sanitizer生成报告的方法 并将这个问题报告给用户
查找影子内存需要非常快才行
为实现这点 我们保留了查找表 在影子内存中每8个字节 就会有1个字节被跟踪
这是个很大的查找表 我们并未真正分配 而是在 进程启动的时候保存 在需要的时候使用
这样我们就可以查找地址 通过将原始指针的值除以8 再添加常数补偿 就是在内存影子的 位置上添加 即便计算地址的字节为非零 我们知道内存中毒了
现在 我们聊聊堆的内容 捕获溢出和 堆中的其他程序错误 Address Sanitizer会提供 自定义分配器 以替代默认的Malloc执行
默认分配器 可以用不同方式组织对象 例如 它可以逐个排布对象 这对于优化内存消耗十分有利 但是这对捕捉程序错误不太好 因为一个对象的溢出会落到 另一个对象上 因此无法与有效的内存访问区别开来
为解决这个问题 ASan的分配器 会让对象彼此间距更大 这些对象之间未用的内存 在影子部分会标记为中毒
当对象被取消分配 我们将对象在影子部分 标记为中毒
总之 自定Malloc执行 在有效的分配周围 插入中毒的红区 以捕捉堆下溢和溢出
这会延迟用户释放的内存 使得Address Sanitizer更有效地捕捉 用户释放的和双重释放的错误
它还会为分配和解除分配 搜集Sect痕迹 允许它提供这些综合详细的诊断 这些我们在演示中都见过 能立即明白问题在何处 和花费大量时间去调试 再找出具体发生了什么 这二者是完全不同的效果
现在 我们来谈谈堆栈
与之相似 对于堆内存而言 红区是放在独立的 堆栈变量之间
假设我们有个数组和整数 作为本地变量 那么在Address Sanitizer 编译时 这些变量之间 会插入额外的红区 这样我们可以发现 堆栈变量的任何溢出
在运行时间进入函数时 堆栈红区会中毒在运行时间 退出函数时它们会解毒
处理全局变量也非常相似 在编译期间 检测全局变量 额外的红区会插入它们四周
现在堆栈和全局编译器检测 是Address Sanitizer 非常有用的功能 这会允许它发现其他工具 无法捕获的程序错误
Address Sanitizer 还能找到其他类型的独特程序错误 在座的Avid C++开发 人员会对此特别感兴趣
我们有个C++容器矢量 即便所有的内存 都给了v.begin v.begin加容量 都已经分配 访问内存过去 v.end是个错误
检测Leap C++ 向Address Sanitizer 提供更多信息 这样就会像这里一样 发现错误
正如我们所见 我们谈到的所有检测 都需要编译器检测 但是 我们知道部分错误会出现在 即便代码没有 重新编译的时候也会触发 例如我们调用 内存拷贝函数的时候
Address Sanitizer 会使用一个技术名为BYOD函数 插补用运行时间的自身版本 来替代数十种标准库函数 因为这是一个运行技术 这些检测甚至会触发 未经重新编译的代码
这里是个内存拷贝包裹示例 正如你所预期 在转发原始内存拷贝执行前 首先会检查源和目的地 缓冲区是否有效
所有这些额外检查意味着 会有运行时间开销 而且你可能会考虑这是什么
这些细节很大程度上 取决于各自的程序 Address Sanitizer 通常会导致CPU减速2倍左右 但是我们看到在一些边缘情况下 曾高达5倍 而内存开销从2倍到3倍
需要注意的一点是 这些开销要比 可以找到相似问题的 其他工具要小很多
通过运行时间技术 编译编译器检测 是令Address Sanitizer 更为有效和 可扩展的关键所在 例如 我们在Address Sanitizer 运行并测试Safari 这是个大app
这是Xcode 7 新增的Address Sanitizer
谢谢
我们调整一下关注点 来仔细看看 我们平台上可用的 其他内存管理工具 它们可以做什么 你何时应该使用
那我们先从 Guard Malloc开始 像Address Sanitizer一样 它也可以发现同样问题 使用Guard Malloc的 主要优势是 它不需要进行重新编译
另一方面 它还有其他局限性 Guard Malloc不能在 iOS设备上运行 而且不能发现 Address Sanitizer 发现的所有问题 例如 因为它使用保护页面 它将无法捕捉到 所有的单字节缓冲区溢出 这是个常见错误
再选择两者间该使用哪个时 要考虑其他的权衡因素
可供大家考虑的 还有NSZombie 它善于捕捉Objective-C 对象过度释放 可以通过发送信息时 困住的僵尸对象 替代取消分配对象 以发挥作用
这个基本功能 可以从Xcode中的 相同Diagnostics选项卡 来启动 但是 如果希望 获得该功能的全部效用 还是要使用 Zombies Instrument
Malloc Scribble会帮助 调查未初始化的内存问题 它可以通过预设常数 填入分配和 取消分配的内存 更能预见这些错误的出现
最后 泄露Instrument会帮助你 发现保留周期 以及导致更高内存 消耗的放弃内存
总之 我们见识了三种不同的技术 可以帮助我们 更深入地理解程序 首先 使用View Debugger来发现 并解决布局的约束问题 第二 设置断点操作 以便自动评估和 打印任何LLVD表达式 并使用异常断点 让程序调试恰好停在 异常出现的位置 最后 第三 使用Address Sanitizer 整理应用 清除那些难以捉摸的 内存出错问题
关于今天谈到的问题 还有其他的资源 帮助大家了解更多内容 本周早些时候 有几次讲座介绍了 LLDB连续整合和测试问题 大会结束后 大家就可以立即观看 非常感谢 祝大家今天过得愉快
-