签到

05月06日
尚未签到

共有回帖数 0

    空城旧事

    等级:
    直到现在我仍然经常被(现实生活中)问及这样的问题:〔我的程序不出现象,是怎么回事?〕——而且第一个分句的原话往往就是这样。这往往是个有点让人觉得奇怪的问题,尽管提问者可能没有意识到〔这其实并不是一个组织良好的问题,而是一个单纯表达出了自己愿望的疑问句而已〕。esr所谓提问的艺术已经被引用得到处都是,但考虑到各种因素,或许可以尝试从另一个角度作一次引导,而不是简单地抑制不经思考的问题:换言之,我试图在这里简单地整理出一些有关调试中小程序和进行简单单元测试的新手向建议。

    这是一篇个人色彩比较多的note, 我并不会逐步给出使用某种工具进行调试的guide.


    现在回到一开始给出的情景里提问者的问题上。提问者期待的现象并没有发生,而且没能第一时间发现错误所在,于是他提出了这个问题。实际上遇到错误时的第一反应也往往如此,但这样的念头只适合作为一个起点存在一瞬间。既然提出了一个问题,最好对它做一次澄清:

    1. 我期望产生什么现象?期望的现象确实是正确的输出吗?
    2. 这个错误现象是否源于我的理解错误或者观察失误?

    这是两个模糊的问题,这里也没法提出一般性的有关如何自我解答的建议。只能勉强指出,第一个问题的基础上,提问者或许需要重审他的测试计划(即使不是完整的计划,也应该有测试样例、样例输出和有关的演算/结果产生过程),——但第二个问题并不好解答,先搁置在这里。
    假设确认了期望的现象正确,并有理由认定观察并没有失误,提问者应该可以相信他的实现出了问题。下一个迫切的问题是

    3. 程序中的哪一部分导致了错误发生?

    这可能是个很麻烦的问题(往往如此),所以我只讨论几个简单的方面,它们可能提供了一些解答的起点。


    设计。一个设计恶劣的程序很可能不值得调试。不幸的是,一部分令人反感的错误很可能来自恶劣的设计,比如(这些例子并不是无交集的)

    * 数据的变换和数据的输出混杂在一起,而数据的输出依赖某个硬编码的量,增加了分别测试变换和输出的难度。(实际上,确定错误位置的一种试探是确认错误结果究竟发生在变换部分还是输出部分。很容易硬编码一系列临时的测试输出以确定输出部分是否工作正常。)
    * 更一般地,某个例程试图完成大量工作,诸如物理输入到逻辑输入的变换,以及在变换输入的过程中在线对数据进行演算和输出,而输出过程往往需要对某些数据对象进行进一步的变换(比如序列化或是整理成某种硬件期望的输入表示)。(不要执念于调试这样的代码,必要时考虑重构或者推翻重写。或许给出这种设计的人期望把肮脏的细节一次性埋葬在某个例程深处,但确认这些细节是否基本上不会乍尸就是一件痛苦而缺乏长远意义的工作。对于某些不必要立刻演算的输入——比如小规模的数据,或者采样所得的数据——可以推迟其处理时间,并划分出数据输入和数据变换的边界。)
    * 未能复用的例程。比如某个固定延时一段时间T的例程被硬编码十次,以实现10*T时间的延时。(重写这样的例程,提供一系列可以控制执行量度的参数。这或许是一个并不影响正确性推理的细节,但多量重复的硬编码会增加修改程序的难度,或者增加修改时偶然发生错误的可能。)
    * 试图在时序没有保证的敏感区域中进行相对耗时的操作。比如在某个中断回调例程中完成一系列演算和外设输出工作,但没有考虑可能的嵌套和屏蔽。(是非之地最好不要久留。可以考虑推迟演算和输出的时机,仅仅让这个敏感的事件声明自身的发生,期望订阅者们会稍后作出反应。)

    或许提问者自认为确实能够跟上自己的设计,但如果他对设计的描述仅仅局限于对混乱流程本身的重述,即使这个描述并不难理解,也不值得花太多力气基于这样的描述进行调试。或者说,提问者并没能确认问题发生的位置就是一个小小的提醒:大概不应该太珍惜目前的成果。
    工具。假设提问者确实理解了自己的设计,不至于对主要算法、结构甚至使用的功能、设备和其他术语缺乏了解。(实际上的反面例子并不少见。我曾经被一个正编写某单片机定时器中断程序的提问者问及〔定时器是什么〕和〔TMOD是不是定时器〕。)

    * 假如开发平台比较容易获得输出(比如有显示屏或是有通信功能),在怀疑有问题的部位给出测试输出。比如在某次变换产生结果之后,立刻输出相关信息——这可能是某个关键的变量值,某个分支的执行路径或是执行任务的进程/线程ID等等。这些输出可能会显示出异常现象发生的大体位置和直接原因。
    * 一旦确定了大体位置,往往需要一种调试器以便直接观察相关的控制流和数据。设置一个在猜测位置附近的断点,配合适当的watch/单步跟踪观察涉及到的数据,藉此确认错误数据/控制流和期望的差异。(一种可能的情况是程序得到了正确的数据,但没能正确选择变换方式——比如拿到了某棵子树的根节点,却误认为拿到的是此根节点的某个子节点。至于数据错误的情况,请逐层追溯各变换算法的实现。)
    * 如果在平台A上开发用于平台B的程序,挑选一种具有调试功能的仿真器或许能减少一些外围劳作,尽管仿真器不一定能模拟实际的输入(比如试图仿真某个模-数转换的输入量)。如果开发机和测试机可以通信,也请尽量利用,这时往往可以在能检测到真实输入的情况下进行一定程度的调试。
    * 如果在开发独立环境下的程序,除了同样需要考虑仿真器之外,也许在出错时进行一次core dump会给出更直接的数据供解读。

    * 对某些语言的初学者而言,一种典型的情况是发生了访存违例。实际上这种情况的具体位置确定可以直接参考调试器汇报的崩溃位置。内存泄漏也往往有方便的工具进行检测(比如valgrind)。


    即使找出了错误的位置,也不一定能改正错误(比如某些竞态条件引发的内存错误)。不过至少作出了这些努力之后,提问者已经大体明确了自己的期望,并能对发生的问题和问题的相关细节作出有条理的描述——换言之,他现在提出的问题会更明确而有章法,获得解答的可能性也更大。


    写到这里,突然发现实际上这坨东西绕了很大一个圈子仍然不得要领。
    当提问者单单表达了他希望对一个含混的问题获得解答之愿望时,他缺乏的或许是知识和经验,但或许更多地是解决问题的欲望。缺乏知识和经验的入门者只是新手而已,不意味着他是一个缺乏在不熟悉的领域中试图搜集资料、分析问题和解决问题能力的〔小白〕,或者动辄faint的旁观者。
    所以请不要旁观问题。提问者始终是问题的核心,无论他的发问对象是自己还是别人,无论最终成功解答了问题的人是自己还是别人。

    楼主 2015-11-05 13:57 回复

共有回帖数 0
  • 回 帖
  • 表情 图片 视频
  • 发表

登录直线网账号

Copyright © 2010~2015 直线网 版权所有,All Rights Reserved.沪ICP备10039589号 意见反馈 | 关于直线 | 版权声明 | 会员须知