面试问题 -
一旦您的代码出现问题,通常会更容易调试程序。您可以放置手表,断点等。由于调试器,生活更容易。
但是如何在没有调试器的情况下调试程序?
我知道一种可能的方法是将print语句放在代码中的任何地方,以便检查问题。
除此之外还有其他方法吗?
作为一般性问题,它不受任何特定语言的限制。请分享您对如何做到的想法?
编辑 - 在提交您的答案时,请提及有关任何概念的有用资源(如果您有的话)。例如记录
对于那些根本不了解它的人来说,这将非常有用。(这包括我,在某些情况下:)更新: Michal Sznajderhas提出了一个真正的“最佳”答案,并将其作为一个社区维基。真的值得大量投票。
答案 0 :(得分:54)
实际上你有很多可能性。重新编译源代码或不重新编译。
ToString()
或创建一些EnumToString()
函数(适合您的语言)最后的一些一般提示:
awk
,grep
或perl
)一起为您提供令人难以置信的分析包。如果您有超过32K的记录,请考虑使用Access作为数据源。一般来说,调试就像科学一样:你不会创造它而发现它。通常它喜欢在刑事案件中寻找凶手。所以,自己买a hat,永不放弃。
答案 1 :(得分:6)
首先,调试实际上做了什么?高级调试器为您提供机器挂钩以暂停执行,检查变量并可能修改正在运行的程序的状态。大多数程序不需要所有这些来调试它们。有很多方法:
跟踪:实现某种日志记录机制,或使用现有的日志记录机制,如dtrace()。通常值得实现某种类似printf的函数,它可以将通常格式化的输出输出到系统日志中。然后将状态从程序中的关键点抛出到此日志中。信不信由你,在复杂的程序中,这比使用真正的调试器的原始调试更有用更多。日志可以帮助您了解如何遇到麻烦,而陷阱崩溃的调试器假设您可以从您已经处于的任何状态对您的到达方式进行逆向工程。对于使用其他复杂库的应用程序你没有在它们中间拥有崩溃,日志往往更有用。但是在编写日志消息时需要一定的纪律。
程序/库自我意识:为了解决非常具体的崩溃事件,我经常在系统库上实现包装器,例如malloc / free / realloc,这些扩展可以执行诸如步行内存,检测双重释放,尝试等操作释放未分配的指针,检查明显的缓冲区溢出等。通常,您也可以为重要的内部数据类型执行此类操作 - 通常您可以对链接列表等内容进行自我完整性检查(它们可以循环,他们不能指向la-la land。)即使对于OS同步对象这样的事情,通常你只需要知道哪个线程,或者哪个文件和行号(可以__FILE__
捕获,{ {1}})同步对象的最后一个用户是帮助你解决竞争条件。
如果你像我一样疯了,你可以在你自己的程序中实现自己的迷你调试器。这实际上只是自反射编程语言中的一个选项,或者在具有某些OS-hook的C语言中。在Windows / DOS中编译C / C ++时,可以实现“崩溃挂钩”回调,该回调在触发任何程序错误时执行。编译程序时,您可以构建一个.map文件来确定所有公共函数的相对地址(这样您就可以通过从.map中给出的地址减去main()的地址来计算加载器的初始偏移量。文件)。因此,当发生崩溃时(例如,甚至在运行期间按下^ C,因此您可以找到无限循环),您可以获取堆栈指针并在返回地址内扫描它以获得偏移量。您通常可以查看您的寄存器,并实现一个简单的控制台,让您检查所有这些。瞧,你实现了一半真正的调试器。保持这种状态,您可以重现VxWorks的控制台调试机制。
另一种方法是逻辑演绎。这与#1有关。基本上,程序中的任何崩溃或异常行为都会在停止按预期运行时发生。您需要有一些反馈方法来了解程序何时正常运行然后异常运行。然后,您的目标是找到程序从正确行为到错误行为的确切条件。使用printf()/ logs或其他反馈(例如在嵌入式系统中启用设备 - PC有一个扬声器,但有些主板还有一个用于BIOS阶段报告的数字显示器;嵌入式系统通常会有一个COM端口,您可以使用)您可以通过程序的工具,至少推导出关于程序运行状态的良好和不良行为的二进制状态。
相关方法是关于代码版本的逻辑推理。程序通常在一个状态下完美运行,但某些后续版本不再有效。如果您使用良好的源代码控制,并且在编程团队中强制执行“树顶必须始终正常工作”的理念,那么您可以使用二进制搜索来查找发生故障的代码的确切版本。然后,您可以使用差异来推断出哪些代码更改会暴露错误。如果差异太大,那么您的任务是尝试以较小的步骤重做代码更改,以便更有效地应用二进制搜索。
答案 2 :(得分:5)
只有几个建议:
1)断言。这应该有助于您在计划的不同状态下制定一般期望。同时熟悉代码
2)单元测试。我有时使用这些来挖掘新代码并测试API
答案 3 :(得分:4)
一个字:记录。
您的程序应编写描述性调试行,其中包含基于可配置调试级别的日志文件的时间戳。读取生成的日志文件可以获得有关程序执行期间发生的情况的信息。每种常见的编程语言都有一些日志记录包可以实现这一点:
Java:log4j
Python:Python Logging
Ruby:Ruby Logger
C:log4c
答案 4 :(得分:3)
我想你只需编写细粒度单位测试。
我也想为我的数据结构编写一台漂亮的打印机。
答案 5 :(得分:3)
我认为接下来的采访可能就是这样......
候选人:所以你不为你的开发者购买调试器? 采访者:不,他们有调试器。
候选人:所以你正在寻找程序员,即使他们的工作效率较低,他们会因为受虐狂或胸膛骚扰hamartia而使事情变得复杂? 采访者:不,我只是想知道你是否知道在一个永远不会发生的情况下你会做些什么。
候选人:我想我会添加日志记录或打印语句。我可以问你一个类似的问题吗? 采访者:当然。
候选人:如果您没有任何明显的面试技巧来根据相关信息区分好的潜在客户,您会如何招募开发团队?
答案 6 :(得分:3)
同行评审。您一直在查看代码8小时,您的大脑只是向您展示您希望在代码中看到的内容。一双新鲜的眼睛可以发挥重要作用。
版本控制。特别是对于大型团队。如果有人改变了你所依赖的东西,但没有告诉你很容易找到一个特定的变更集,通过逐个滚动变更来引起你的麻烦。
答案 7 :(得分:2)
答案 8 :(得分:2)
二进制搜索及时也是一种方法:如果您的源代码存储在版本控制存储库中,并且您知道版本100有效,但版本200没有,请尝试查看版本150是否有效。如果是,则错误必须介于版本150和200之间,因此找到版本175并查看它是否有效......等等。
答案 9 :(得分:1)
答案 10 :(得分:1)
更一般地说,您可以监控程序的副作用和输出,并在程序外部触发某些事件。
Print语句并不总是合适的。您可以使用其他形式的输出,例如写入事件日志或日志文件,写入TCP套接字(我有一个很好的实用程序,可以从我的程序中侦听该类型的跟踪)等。
对于没有UI的程序,可以使用外部标志(例如文件的存在)触发要调试的行为。您可能让程序等待创建文件,然后在记录相关事件时运行您感兴趣的行为。
另一个文件的存在可能会触发程序的内部状态写入您的日志记录机制。
答案 11 :(得分:1)
答案 12 :(得分:1)
我在这里提到的另一件事我不得不在嵌入式系统上使用相当多的是串行终端。
对于地球上任何类型的设备,您都不能使用串行终端(我甚至已将其用于液压系统,发电机等嵌入式CPU)。然后你可以写到串口,看看终端上的一切。
您可以获得真正的想象力,甚至可以设置一个侦听串行终端并响应命令的线程。我也做了这个,并通过简单的9600波特RS-232串口实现了简单的命令转储列表,查看内部变量等等!
答案 13 :(得分:1)
答案 14 :(得分:1)
安德烈亚斯泽勒的好读是Delta Debugging。这就像二进制搜索调试