创作日期:2012年2月8日;上次编辑日期:2014年6月27日
以下内容适用于非Windows操作系统。这不是为了胆小的胆小,需要一些C级熟悉程度。
对于那些在视觉上更好地学习的人,请通过Biocumon Alum查看视频使用GDB使用本机代码进行调试R软件包。
福利诊断 - a - 崩溃和案例分析例子是所有步骤和逻辑都写出;一个不需要重新推出视频来查看这些步骤。
第一个,必不可少的步骤是编写一个可靠且快速地再现错误的短脚本。打电话给这个脚本buggy.r.
。
对于在C / C ++级别的调试包代码,通常通过安装包而没有任何编译器优化,例如,通过以下方式开始
rshowdoc(“r-admin”)
第6.3.3节。例如设置
cflags = -ggdb -o0
在.r / mapervars。看看套餐指南部分有关更多示例和信息。
valgrind.是一个成熟的低级计划分析工具套件。valgrind内存错误检查器是诊断C / C ++内存错误的首要工具。
Valgrind可用于点发现内存访问问题,这是C / C ++代码中的SEGFAULTS的通用来源。当错误被隔离并且容易产生buggy.r.
,开始R.
和:
R -D Valgrind -f Buggy.r
这会慢慢运行,并将标记无效的内存读写位置。前者通常会有助于坏数据,后者到内存损坏和壮观的失败。输出需要C熟悉解释。使用未在没有编译器优化的未安装的包运行WATGY代码是有帮助的。见第4.3节rshowdoc(“r-exts”)
和相关的套餐指南部分。
如果您从未使用过命令行调试器,则网络上有许多精细的快速启动指南;它看起来不是令人生畏的。
在Linux上,首选调试器是GDB., 但lldb默认为mavericks平台。接口是相似的,但如果您习惯于GDB,请参阅GDB到LLDB的命令映射。
开始R.
使用C级调试器,如GDB。
r -d gdb -f buggy.r
你将在GDB提示下结束
(GDB)
典型的操作是(r)联合国或(c)on intinue执行
(GDB)r
运行buggy.r。当有赛格虚拟物时,您将在C中返回C,或者按CNTRL-C(^ C.
如下所示,或者当您在某些C级功能中插入(b)reakpoint时,您怀疑是越野车,例如,
> ^ c(gdb)b some_buggy_fun(gdb)c
当您最终返回调试器时,您可以打印C变量或R变量的C表示(所提供的R不是太困惑)
(gdb)p c_var(gdb)调用rf_printvalue(some_r_variable)
您还可以查看(b)呼叫堆栈的ACK(T)Race,导航(U)P和(D)拥有呼叫堆栈等
(GDB)帮助
和我们共同的朋友谷歌获取更多信息。
也许调试器最有用的功能是提供导致有bug程序崩溃的例程的breadcrumb跟踪(“反向跟踪”)。有了这些知识,我们可以将查询范围缩小到影响崩溃时相关程序状态部分的代码。
值得重申它是基本的关闭优化,如果希望有富有成效的调试会话,则指示编译器包含调试符号。看看套餐指南部分。
虽然该示例中的调试器输出可能与来自其他计算环境的输出相比略有不同,但是底层技术适用于诊断任何平台上的程序崩溃。看看案例分析这是一个结合使用Valgrind和gdb的实际示例。
我们将使用一个创新的榜样来演示如何识别我们代码中导致崩溃的潜在位置。您应该能够完全按照它们的显示使用示例文件。对于简洁起见,已经省略了一些无关输出。
C ++文件Buggy.cpp.
:
#include
编译r cmd shlib buggy.cpp -o buggy.so
。
来源()
这个文件(buggy.r.
)在A中R.
会话(或输入命令R.
会议)将导致计划崩溃:
dynload(“buggy.so”).call(“buggy_function”)
很遗憾R.
诊断不是很亮起:
>源(“buggy.r”)***抓住了segfault ***地址0x2,导致'内存未映射'回溯:1:.call(“buggy_function”)2:eval(exp(expr,envir,cuccos)3:eval(EI,ENVIR)4:可用性(eval(EI,ENVIR))5:来源(“BUGGY.R”)
现在我们转向调试器。开始R.
与之lldb
调试器(或对您的平台等同):
R -D LLDB(LLDB)RUN ## R启动邮件在r会话>源(“buggy.r”中)eld ##
在此刻R.
崩溃,LLDB产生一些输出,我们回到了LLDB提示。LLDB输出看起来像这样(在发生崩溃的呼叫堆栈中向我们展示帧(#0)):
进程21657停止*线程#1:tid = 0xbcb4ab, 0x00000001028fcbb0 bug所以'buggy_function(内联)std:: __1: __tree_node_base < void * > * std:: __1: __tree_min < std:: __1: __tree_node_base < void * > * > (std:: __1: __tree_node_base < void * > *) __tree: 134年,队列= ' com.apple。#0: 0x00000001028fcbb0 error . error (code=1, address=0x2)所以'buggy_function(内联)std:: __1: __tree_node_base < void * > * std:: __1: __tree_min < std:: __1: __tree_node_base < void * > * > (std:: __1: __tree_node_base < void * > *) __tree: 134 131 132 _NodePtr __tree_min (_NodePtr __x) _NOEXCEPT{133 - > 134年(__x - > __left_ ! = nullptr) 135年__x = __x - > __left_;136年返回__x;137}
它看起来像调试器正在告诉我们,在获取树节点时存在内存访问错误。(树是标准库的常见潜在数据结构地图
)。输出是巨大的,看起来很困惑,但现在只有GIST很重要。
仍然在相同的LLDB会话中,输入BT.
命令(用于LLDB提示符的“回溯”),我们看到崩溃之前的所有堆叠帧(和函数调用)。帧以升序列出,以崩溃发生的帧开始。(注意帧#0与上面给出的帧#0相同。)这意味着在诊断崩溃时,它通常是有意义的,以便以较低编号的帧开始并向上行进。
(LLDB)BT *线程#1:TID = 0xbcb4ab,0x00000001028fcbb0 buggy.so`buggy_function [inlined] std :: __ 1 :: __ tree_node_base * std :: __ 1 :: __ tree_min *>(std :: __ 1 :: __ tree_node_base *)在__tree:134,queue ='com.apple.main-thread',停止原因= exc_bad_access(code = 1,地址= 0x2)*框架#0:0x00000001028fcbb0 buggy.so`buggy_function [Inlined] std :: __ 1 :: __ tree_node_base * std :: __ 1 :: __ tree_min *>(std ::__1 :: __ tree_node_base *)在__tree:134帧#1:0x00000001028fcbb0 buggy.so`buggy_function [inlined] std :: __ 1 :: __ tree_node_base * std :: __ 1 :: __ tree_next *>(std :: __ 1 :: __ tree_node_base *)+ 20在__tree:158帧#2:0x00000001028fcb9c buggy.so`buggy_function [内联] std :: __ 1 :: __ tree_const_iterator,std :: __ 1 :: __ tree_node ,void *> *,long> ::运算符++()__tree:747帧#3:0x00000001028fcb9c越野车。So`buggy_function [内联] std :: __ 1 :: __ map_const_iterator ,std :: __ 1 :: __ tree_node ,void *> *,long >> ::运算符++()在地图:750帧#4:0x00000001028fcb9c buggy.so在buggy.cpp:17帧#5:0x0000000100073a13 libl.dylib`do_dotcall(call = <不可用>,op = <不可用>,args = <不可用>,dotcode.c:578的erv = <不可用>)+ 323
框架#5提到do_dotcall.
,这是本机函数(在R.
图书馆)对应于.call(“buggy_function”)
线路buggy.r.
我们呼叫我们的C入口点。我们可以合理地结束我们的错误的有用信息可能在框架#0-4中。
以下是一个可能的思想链,导致正确的结论:
框架#0-2看起来像他们正在处理树/地图内部;忽略这一刻。
框架#3表示我们可能正在谈论我们的地图Const_iterator变量在Buggy.cpp中声明std :: map
)。
框架#4是关键:它告诉我们线路(#17)Buggy.cpp.
文件 (++它;
)执行的是来自C ++代码的位置我们写入产生错误的地图迭代器内部。
尤里卡!通过仔细阅读代码Buggy.cpp.
我们意识到在插入地图的大小之后m
是2.递增迭代器后意味着它
在第16行(++它;
), 的价值它
是特别的过去的价值。递增迭代器以外过去的(第三++它;
在第17行)是未定义的行为!!
如果我们修改Buggy.cpp.
不要增加它
超过过去的通过去除第三个++它;
该计划在没有投诉的情况下运行。问题解决了!
正如您所看到的,调试器无法立即告诉我们为什么该计划崩溃了,只是在哪里该计划崩溃了。我们使用了关于在我们的代码的部件中遇到的崩溃所发生的信息的信息,这些部分在崩溃时受到影响的程序状态。显然这个例子是有价值的;在真实世界的情景中,有关相关节目状态的洞察力提供的额外帮助是宝贵的。
作为一个案例研究,一位同事报告说,他们的复杂程序会在一台特定的计算机上产生分割故障或停止响应。同样的一系列操作不会在其他计算机上造成问题。这听起来像是一个典型的记忆问题,带有片段错误和再现困难。
第一个建议是开发一个复制问题的简单脚本:原始报告有太多的移动部件。大见解是,可以通过运行使用rcurl的代码,然后呼叫垃圾收集器来制作这个错误,GC()
。垃圾收集器的作用再次介绍某种类型的内存损坏,特别是rcurl正在分配(在c电平)中,但没有正确保护它从垃圾收集。我们怀疑rcurl而不是r或libcurl(其他可能的玩家),因为它是代码的最少测试。当然,我们可能是错的......经过许多迭代,我的同事们到达Buggy24.r:
库(rcurl)foo < - function(){url < - “https://google.com”curl < - getcurlhandle()opts < - list(powerlocation = null,ssl.verifypeer = true)d < - deguggatherer()geturl(URL,customrequest =“get”,curl = curl,debugfunction = d $更新,.opts = opts)} execute < - function(){foo()gc()} execute()
这非常简单,不需要访问任何特殊资源(如最初查询的服务器)。在所有系统上运行时,此脚本不会导致赛格虚拟器,但运行Valgrind(未在没有任何优化的情况下安装RCurl)显示...
> R -D Valgrind -f Buggy24.r ... == 10859 ==条件跳转或移动取决于未初版值(s)== 10859 ==在0x11bf00f6:getcurlpointerfordata(curl.c:798)== 10859 ==逐0x11bf0e80:r_curl_easy_setopt(curl.c:164)== 10859 == by 0x11bf17ad:r_curl_easy_perform(curl.c:89)== 10859 == by 0x4ed5499:do_dotcall(dotcode.c:588)== 10859 == by 0x4f1caa4:rf_eval(eval.c:593)== 10859 ==×0x4f2bd5c:do_set(eval.c:1828)== 10859 == by 0x4f1c8b7:rf_eval(eval.c:567)== 10859 == 0x4f2b957:do_begin(eval.c:1514)== 10859 == by 0x4f1c8b7:rf_eval(eval.c:567)== 10859 == by 0x4f297e9:rf_apply closure(eval.c:960)== 10859 == 0x4f1cba5:rf_eval(eval.c:611)== 10859 == by 0x4f2bd5c:do_set(eval.c:1828)
查看RCurl的Curl.c中的C源代码,如回溯,只是为了面向面向。然后做
R -d gdb -f buggy24.r
在GDB下运行脚本。运行我们的测试脚本
(GDB)r
没有错误。不要放弃,设置一个断点
(GDB)B CURL.C:798
再次运行
(GDB)R断点1,GetCurlPointerFordata(EL = 0x79E038,选项= Curlopt_WriteFunction,ISProtected = False,Curl = 0x1d9bdc0)在curl.c:798 798 curl.c:没有这样的文件或目录。(GDB)
“没有这样的文件”意味着GDB不知道在哪里找到rcurl包src /目录,所以告诉它和(l)是上下文,(p)rint c变量的值是保护的
,这似乎是Valgrind警告的来源
(GDB)Dir〜/ TMP / RCurl / SRC(GDB)L 793} 794} 795} 796休息;797案例Closxp:798(GDB)L 793} 794} 796} 796突破;797案例Closxp:798 IF(!ISPRoted){799 R_PRESEWEROBJECT(EL);800} 801 ptr =(void *)el;802休息;(GDB)p被保护$ 5 = false
是保护的
有一个值(它必须!),此外,虚假导致保护对象的值el
跨越C电话(这是什么r_prepservehject.
做)。这非常有趣,因为我们意识到垃圾收集触发了SEGFault。Valgrind告诉我们的价值是保护的
实际上并不是分配的结果,它可能是访问界限的数组的结果。让我们向上呼叫堆栈,看看这个值来自哪里
(GDB)向上#1 0x00007FFFF426E273在r_curl_easy_setopt(lavel = 0x15d9600,值= 0x1445788,opts = 0xf3d418,incotected = 0xb7d308),rucl.c:164 164 Val = GetCurlPointerFordata(EL,OPT,逻辑(ISPRoted)[I%n],obj);(gdb)l 159 / *循环我们设置的所有选项。* / 160 for(i = 0; i fun = val;Urderata ++;168 status = curl_easy_setopt(obj,curlopt_writefunction,&r_curl_write_data); (gdb)
我们正在进入该功能getcurlpointerfordata.
有价值逻辑(ISPRoted)[I%n]
。这里,是保护的
现在是一个r对象,而不是c变量。看着周围的代码,那在
看起来不对 - 这可能意味着回收是保护的
在提供比需要保护的元素的向量的较短逻辑变量的情况下,但是值N
不一定是长度是保护的
。让我们来看看我们使用的是我们拥有的内容,使用C级R功能rf_printvalue.
以r时尚打印r值(sexp)
(GDB)P isProtectated $ 1 =(性别)0xAad8a0(gdb)调用rf_printvalue(被认为)[1] false
是保护的
是长度为1的逻辑向量。
(GDB)P i $ 7 = 1(gdb)p n $ 8 = 6(gdb)p i%n $ 9 = 1
......我们正在尝试访问它的元素1。但是R向量的C表示是基于零的,因此索引的唯一有效值是0 - 我们超出了界限!这很可能是我们的错误,是时候尝试修复它(天真,逻辑(ISPRoted)[I%长度(ISPRoted)]
)确认我们的诊断或报告PackageSescription(“rcurl”)$维护者
谁可能有更好的守则的整体结构和意图。