创作日期:2012年2月8日;最后编辑日期:2014年6月27日
以下内容适用于非windows操作系统。这不是为胆小的人准备的,需要一些c级的熟悉度。
对于那些更好的视觉学习,看视频的生物导体校友关于使用gdb调试带有本机代码的R包.
的好处diagnose-a-crash而且案例研究实例是将所有的步骤和逻辑都写出来;人们不需要倒带视频来回顾步骤。
第一步,也是最重要的一步,是编写一个简短的脚本,可靠且快速地再现错误。调用这个脚本车。R
.
要在C/ c++级别调试包代码,通常从安装包开始,而不进行任何编译器优化,例如
RShowDoc(“R-admin”)
6.3.3节。设置为例
CFLAGS = - ggdb o0
在r / Makevars。看到相关的包指南部分更多示例和信息。
Valgrind是一套用于低级程序分析的成熟工具。Valgrind的内存错误检查器(Memcheck)是诊断C/ c++内存错误的主要工具。
Valgrind可用于发现内存访问问题,这是C/ c++代码中段错误的常见来源。当病毒被隔离并且很容易产生时车。R
,开始R
:
R -d valgrind -f buggy。R
这运行得相当慢,并且会标记无效的内存读写位置。前者通常会导致糟糕的数据,后者会导致内存损坏和严重的故障。需要熟悉C语言才能解释输出。在没有编译器优化的情况下,使用已安装的包运行有bug的代码是很有帮助的。见第4.3条RShowDoc(“R-exts”)
及相关包指南部分.
如果你从未使用过命令行调试器,在web上有许多很好的快速入门指南;它并不像看起来那样令人生畏。
在Linux上首选的调试器是广东发展银行,但lldb是Mavericks平台的默认值。接口是类似的,但是如果您习惯使用gdb,请参阅GDB到LLDB命令映射.
开始R
使用c级调试器,如gdb。
R -d gdb -f buggy。R
您将在gdb提示符处结束
(gdb)
一个典型的操作是(r)un或(c) oncontinue执行
(gdb) r
buggy.R运行。当出现段错误时,您将返回到C中,或者按cntrl-C (C ^
,或者当你在某个c级函数中插入了一个(b)点,而你怀疑这个点有bug时,例如,
> ^C (gdb) b some_buggy_fun (gdb
当你最终回到调试器中,你可以打印C变量或R变量的C表示(如果R没有被这一点搞糊涂的话)
调用Rf_PrintValue(some_R_variable)
您还可以查看调用堆栈的(b)ack(t)race,导航(u)p和(d)拥有调用堆栈,等等。看到
(gdb)帮助
以及我们共同的朋友谷歌获取更多信息。
也许调试器最有用的功能是提供导致有bug的程序崩溃的例程的面包屑追踪(“回溯”)。有了这些知识,我们可以将查询范围缩小到影响与崩溃时间相关的程序状态部分的代码。
值得重申的是,这是事实至关重要的如果希望有一个富有成效的调试会话,就应该关闭优化,并指示编译器包含调试符号。看到相关的包指南部分.
尽管与其他计算环境的输出相比,示例中的调试器输出可能略有不同,但底层技术适用于在任何平台上诊断程序崩溃。看到案例研究这是一个结合使用Valgrind和gdb的真实示例。
我们将使用一个虚构的示例来演示如何识别代码中可能导致崩溃的地方。您应该能够完全按照示例文件的样子使用它们。为了简洁起见,省略了一些无关的输出。
c++文件buggy.cpp
:
#include
编译和R CMD SHLIB buggy.cpp -o buggy.so
.
源()
正在执行此文件(车。R
)在一个R
会话(或在R
Session)将导致程序崩溃:
dyn.load(“buggy.so”)打电话给(“buggy_function”)
不幸的是R
的诊断并不是很有启发性:
> source("buggy.R") *** caught segfault *** address 0x2, cause 'memory not mapped' Traceback: 1: .Call("buggy_function") 2: eval(expr, envir, enclos) 3: eval(ei, envir) 4: withVisible(eval(ei, envir)) 5: source("buggy.R")
现在我们转向调试器。开始R
与lldb
调试器(或您的平台的等效工具):
R -d lldb (lldb) run ## R startup messages elded ## now in R session > source("buggy.R")
在这一点上R
崩溃时,LLDB产生一些输出,然后我们回到LLDB提示符。lldb的输出如下所示(显示了调用堆栈中发生崩溃的帧(#0)):
进程21657停止*线程#1:tid = 0xbcb4ab, 0x00000001028fcbb0 buggy。所以'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。主线程',停止原因= EXC_BAD_ACCESS (code=1, address=0x2)帧#0:0x00000001028fcbb0错误。所以'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}
它看起来像是调试器告诉我们在获取树节点时发生了内存访问错误。(树是标准库的通用底层数据结构地图
).它的输出是大量的,看起来令人困惑,但现在只有主旨是重要的。
仍然在相同的lldb会话中,输入英国电信
命令(用于“回溯”),我们就会看到崩溃之前的所有堆栈帧(和函数调用)。帧按升序排列,从发生崩溃的帧开始。(注意这里的第0帧与上面给出的第0帧相同。)这意味着在诊断崩溃时,通常从编号较低的帧开始,然后向上进行。
(lldb) bt *线程#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。主线程',停止原因= EXC_BAD_ACCESS (code=1, address=0x2) *帧#0:0x00000001028fcbb0错误。所以'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帧# 1:0 x00000001028fcbb0车。所以'buggy_function(内联)std:: __1: __tree_node_base < void * > * std:: __1: __tree_next < std:: __1: __tree_node_base < void * > * > (std:: __1: __tree_node_base < void * > *) + 20 __tree: 158帧# 2:0 x00000001028fcb9c车。因此'buggy_function[内联]std::__1::__tree_const_iterator, std::__1::__tree_node, void*>*, long>::operator++() at __tree:747 frame# 3: 0x00000001028fcb9c buggy。所以'buggy_function(内联)std:: __1: __map_const_iterator < std:: __1: __tree_const_iterator < std:: __1: __value_type < int, int >, std:: __1:: __tree_node < std:: __1: __value_type < int, int >, void * > *,长> >::操作符+ +()在地图:750帧# 4:0 x00000001028fcb9c车。所以'buggy_function + 188 at buggy.cpp:17帧#5:0x0000000100073a13 libR。Dylib 'do_dotcall (call=, op=, args=, env=) + 323 at dotcode.c:578
# 5帧提到do_dotcall
中的本机函数R
库)对应的打电话给(“buggy_function”)
行车。R
我们称之为C入口点。我们可以合理地得出结论,我们的bug的有用信息可能在第0-4帧中。
下面是一条可能的思路,可以得出正确的结论:
0-2帧看起来像是在处理树/地图内部;暂时忽略它。
第3帧表示我们可能正在讨论在buggy.cpp(第14行声明的map const_iterator变量。Std::map
).
第4帧是关键:它告诉我们第17行buggy.cpp
文件(+ +;
)是从c++代码开始执行的我们写入产生错误的映射迭代器内部。
尤里卡!通过仔细阅读代码buggy.cpp
我们意识到,在插入映射的大小之后米
是2。这意味着在迭代器增量之后它
在第16行(+ +;
的值它
是特殊的结束之后价值。将迭代器增量大于结束之后(第三+ +;
在第17行)未定义的行为!
如果我们修改buggy.cpp
不增加它
除了结束之后通过移除第三个+ +;
这个程序运行起来毫无怨言。问题解决了!
如您所见,调试器不能立即告诉我们为什么程序崩溃了在哪里程序崩溃了。我们使用有关崩溃发生位置的信息来锁定在崩溃发生时影响程序状态的代码部分。显然,这个例子是人为的;在现实场景中,洞察相关程序状态所提供的额外帮助是无价的。
作为一个案例研究,一位同事报告说,他们的复杂程序会在一台特定的计算机上产生分割错误或停止响应。同样的一系列操作不会在其他计算机上引起问题。这听起来像一个经典的记忆问题,有段错误和复制困难。
第一个建议是开发一个简单的脚本来再现这个问题:原始报告有太多移动的部分。一个重要的发现是,可以通过运行使用RCurl的部分代码,然后调用垃圾收集器来产生错误,gc ()
.垃圾收集器的角色再次表明了某种类型的内存破坏,特别是可能RCurl正在分配(在C级别)一个R对象,但没有正确地保护它免受垃圾收集。我们怀疑是RCurl而不是R或libcurl(其他可能的播放器),因为它是最少经过测试的代码。当然,我们也有可能是错的……经过多次迭代之后,我的同事得到了buggy24。接待员:
library(RCurl) foo <- function() {url <- "https://google.com" curl <- getCurlHandle() opts <- list(followlocation=NULL, ssl.verifypeer=TRUE) d <- debugGatherer() getURL(url,customrequest="GET",curl=curl,debugfunction=d$update,.opts=opts)} execute <- function() {foo() gc()} execute()
这非常简单,不需要访问任何特殊的资源(比如最初被查询的服务器)。这个脚本在所有系统上运行时不会导致段错误,但是运行valgrind(已经安装了RCurl,没有任何优化)显示…
> R -d valgrind -f buggy24。R……10859 = = = =条件转移或移动取决于uninitialised值(s) = = 10859 = = 0 x11bf00f6: getCurlPointerForData (curl.c: 798) = = 10859 = = 0 x11bf0e80: R_curl_easy_setopt (curl.c: 164) = = 10859 = = 0 x11bf17ad: R_curl_easy_perform (curl.c: 89) = = 10859 = = 0 x4ed5499: do_dotcall (dotcode.c: 588) = = 10859 = = 0 x4f1caa4: Rf_eval (eval.c: 593) = = 10859 = = 0 x4f2bd5c: do_set (eval.c: 1828) = = 10859 = = 0 x4f1c8b7: Rf_eval (eval.c: 567) = = 10859 = = 0 x4f2b957: do_begin (eval.c: 1514) = = 10859 = = 0 x4f1c8b7:Rf_eval (eval.c:567) ==10859== by 0x4F297E9: Rf_applyClosure (eval.c:960) ==10859== by 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 Breakpoint 1, getCurlPointerForData (el=0x79e038, option=CURLOPT_WRITEFUNCTION, isProtected=FALSE, curl=0x1d9bdc0) at curl.c:798 798 curl.c:没有这样的文件或目录。(gdb)
“没有这样的文件”意味着gdb不知道在哪里找到RCurl包src/目录,所以告诉它(l)ist上下文,(p)打印C变量的值isProtected
,这似乎是valgrind警告的来源
/tmp/RCurl/src (gdb) l 793} 794} 795} 796 break;797 case CLOSXP: 798 (gdb) l 793} 794} 795} 796 break;797 case cloxp: 798 if(!isProtected) {799 r_reserveobject (el);800} 801 PTR = (void *) el;802年打破;(gdb) p isProtected $5 = FALSE
isProtected
有一个值(它必须!),并且进一步的值FALSE结果在PROTECT 'ing对象埃尔
通过C调用(这是什么R_PreserveObject
做)。这非常有趣,因为我们知道垃圾收集触发了段错误。瓦尔磨告诉我们isProtected
实际上不是赋值的结果,它可能是访问超出边界的数组的结果。让我们向上查看调用堆栈,看看这个值来自哪里
(gdb) up #1 0x00007ffff426e273 in R_curl_easy_setopt (handle=0x15d9600, values=0x1445788, opts=0xf3d418, isProtected=0xb7d308, encoding=0x776db0) at curl.c:164 164 val = getCurlPointerForData(el, opt, LOGICAL(isProtected)[i % n], obj);l 159 /*循环所有我们设置的选项。*/ 160 for(i = 0;我< n;i++) {161 opt = INTEGER(opts)[i];162 el = VECTOR_ELT(values, i);163 /*将R值转换为我们可以在libcurl中使用的值。*/ 164 val = getCurlPointerForData(el, opt, LOGICAL(isProtected)[i % n], obj);165 166 if(opt == CURLOPT_WRITEFUNCTION && TYPEOF(el) == CLOSXP) {167 data->fun = val;useData + +; 168 status = curl_easy_setopt(obj, CURLOPT_WRITEFUNCTION, &R_curl_write_data); (gdb)
我们进入这个函数getCurlPointerForData
与价值逻辑(isProtected)[i % n]
.在这里,isProtected
现在是一个R对象,而不是C变量。看看周围的代码我% n
看起来不对,可能是用来回收的isProtected
在这种情况下,提供的逻辑变量比需要保护的元素的向量更短,但的值n
是不是一定是长度的isProtected
.让我们看看我们得到了什么,使用c级R函数Rf_PrintValue
以R方式打印R值(SEXP)
(gdb) p isProtected $1 = (SEXP) 0xaad8a0 (gdb) call Rf_PrintValue(isProtected) [1] FALSE
isProtected
长度为1的逻辑向量。
(gdb) p I $7 = 1 (gdb) p n $8 = 6 (gdb) p I % n $9 = 1
我们正在尝试访问其中的元素1。但是R向量的C表示是从零开始的,所以索引的唯一有效值是0——我们越界了!这很可能是我们的bug,是时候尝试修复它了(天真地,逻辑(isProtected)[i % LENGTH(isProtected)]
)确认诊断,或向packageDescription (RCurl) $维护者
他们可能对代码的整体结构和意图有更好的理解。