调试C / c++代码

创建日期:2012年2月8日;最后编辑日期:2014年6月27日

以下内容适用于非Windows操作系统。这不是为了胆小的胆小,需要一些C级熟悉程度。

对于那些在视觉上更好地学习的人,请通过Biocumon Alum查看视频使用gdb用本地代码调试R包

的好处diagnose-a-crash案例研究例子是所有步骤和逻辑都写出;一个不需要重新推出视频来查看这些步骤。

设置

第一步(也是最重要的一步)是编写一个简短的脚本,以可靠、快速地再现错误。调用这个脚本buggy.R

对于在C/ c++级别调试包代码,通常首先安装包而不进行任何编译器优化,例如,通过以下方式

rshowdoc(“r-admin”)

6.3.3节。设置为例

CFLAGS = - ggdb o0

在r / Makevars。看到相关的套餐指南部分有关更多示例和信息。

检测内存错误(Valgrind)

Valgrind是一个成熟的低级计划分析工具套件。valgrind内存错误检查器(MEMCHECK)是诊断C / C ++内存错误的首要工具。

Valgrind可用于点发现内存访问问题,这是C / C ++代码中的SEGFAULTS的通用来源。当错误被隔离并且容易产生buggy.R,开始R.有:

R -d valgrind -f bug .R

这会慢慢运行,并将标记无效的内存读写位置。前者通常会有助于坏数据,后者到内存损坏和壮观的失败。输出需要C熟悉解释。使用未在没有编译器优化的未安装的包运行WATGY代码是有帮助的。见第4.3节rshowdoc(“r-exts”)和相关的套餐指南部分

交互式调试(GDB或LLDB)

如果您从未使用过命令行调试器,则网络上有许多精细的快速启动指南;它看起来不是令人生畏的。

在Linux上,首选的调试器是广东发展银行,但是LLDB.默认为mavericks平台。接口是相似的,但如果您习惯于GDB,请参阅GDB到LLDB命令地图

开始R.使用C级调试器,如GDB。

R -d gdb -f bug .R

你将在GDB提示下结束

(GDB)

一个典型的操作是(r)un或(c)继续执行

(GDB)r

运行buggy.r。当有赛格虚拟物时,您将在C中返回C,或者按CNTRL-C(C ^,或者当你在某个c级函数上插入了一个(b)reakpoint,而你怀疑它有bug时,例如,

> ^ c(gdb)b some_buggy_fun(gdb)c

当您返回到调试器中时,您可以打印C变量或R变量的C表示(前提是R没有被这一点弄糊涂)

(gdb)调用r_printvalue (some_R_variable)

您还可以查看(b)呼叫堆栈的ACK(T)Race,导航(U)P和(D)拥有呼叫堆栈等

(GDB)帮助

和我们共同的朋友谷歌获取更多信息。

寻找程序崩溃的原因线索

也许调试器最有用的功能是提供一个面包屑(“回溯”)导致了导致错误程序崩溃的程序。有了这种知识,我们可以缩小我们对影响崩溃时相关计划状态部分的代码的询问。

值得重申它是必要如果希望有一个富有成效的调试会话,就关闭优化,并指示编译器包含调试符号。看到相关的套餐指南部分

尽管本例中的调试器输出可能与其他计算环境的输出稍有不同,但底层技术适用于诊断任何平台上的程序崩溃。看到案例研究对于使用Valgrind和GDB的真实界限。

我们将使用一个创新的榜样来演示如何识别我们代码中导致崩溃的潜在位置。您应该能够完全按照它们的显示使用示例文件。对于简洁起见,已经省略了一些无关输出。

c++文件buggy.cpp

#include  #include <实用程序> #ifdef __cplusplus #define r_no_remap #endif #include  extern“c”sexp butgy_function();sexp butgy_function(){std :: map  m;M.Insert(STD :: Make_Pair(5,7));M.Insert(STD :: Make_Pair(9,42));std :: map  :: const_iterator它= m.begin();++它;++它;++它;返回r_nilvalue;}

编译r cmd shlib buggy.cpp -o buggy.so

资源()这个文件(buggy.R)在A中R.会话(或输入命令R.session)将导致程序崩溃:

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 buggy.so`buggy_function [内联] 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*) at __tree:134 131 _NodePtr 132 __tree_min(_NodePtr __x) _NOEXCEPT 133 { -> 134 while (__x->__left_ != nullptr) 135 __x = __x->__left_; 136 return __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> >>do_dotcall(call = <不可用>,op = <不可用>,args = <不可用>,dotcode.c:578的erv = <不可用>)+ 323

框架#5提到do_dotcall,这是本机函数(在R.图书馆)对应于.call(“buggy_function”)线路buggy.R我们呼叫我们的C入口点。我们可以合理地结束我们的错误的有用信息可能在框架#0-4中。

以下是一个可能的思想链,导致正确的结论:

  1. 框架#0-2看起来像他们正在处理树/地图内部;忽略这一刻。

  2. 框架#3表示我们可能正在谈论我们的地图Const_iterator变量在Buggy.cpp中声明std :: map :: const_iterator它= m.begin();)。

  3. 框架#4是关键:它告诉我们线路(#17)buggy.cpp文件(++它;)是从c++代码开始执行的地方我们写入产生错误的地图迭代器内部。

  4. 尤里卡!通过仔细阅读代码buggy.cpp我们意识到在插入地图的大小之后m是2。这意味着迭代器加1之后在第16行++它;), 的价值是个特别过去的值。递增迭代器以外过去的(第三++它;在第17行)是未定义的行为!!

如果我们修改buggy.cpp不要增加除了过去的通过去除第三个++它;这个程序毫无问题地运行着。问题解决了!

正如您所看到的,调试器无法立即告诉我们为什么该计划崩溃了,只是哪里该计划崩溃了。我们使用了关于在我们的代码的部件中遇到的崩溃所发生的信息的信息,这些部分在崩溃时受到影响的程序状态。显然这个例子是有价值的;在真实世界的情景中,有关相关节目状态的洞察力提供的额外帮助是宝贵的。

案例研究

作为一个案例研究,同事报告称,他们的复杂计划将在一个特定的计算机上产生分割错误或只是停止响应。同一系列行动不会导致其他计算机上的问题。这听起来像经典的记忆问题,赛格虚B和再现的难度。

第一个建议是开发一个复制问题的简单脚本:原始报告有太多的移动部件。大见解是,可以通过运行使用rcurl的代码,然后呼叫垃圾收集器来制作这个错误,GC()。垃圾收集器的作用再次介绍某种类型的内存损坏,特别是rcurl正在分配(在c电平)中,但没有正确保护它从垃圾收集。我们怀疑rcurl而不是r或libcurl(其他可能的玩家),因为它是代码的最少测试。当然,我们可能是错的......经过许多迭代,我的同事们到达Buggy24.r:

库(RCurl) foo < url -函数(){< -“https://google.com”旋度< getCurlHandle()选择< -列表(followlocation = NULL, ssl.verifypeer = TRUE) d < - debugGatherer () getURL (url, customrequest =“获得”,旋度=卷发,debugfunction = d美元更新.opts =选择)}执行< -()函数执行gc ()} {foo () ()

这非常简单,不需要访问任何特殊的资源(比如最初被查询的服务器)。这个脚本在所有系统上运行时不会导致segfault,但是运行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 Breakpoint 1, getCurlPointerForData (el=0x79e038, option=CURLOPT_WRITEFUNCTION, isProtected=FALSE, curl=0x1d9bdc0) at curl. getCurlPointerForData (el=0x79e038, option=CURLOPT_WRITEFUNCTION, isProtected=FALSE,c: 798 798卷。c:没有这样的文件或目录。(GDB)

“没有这样的文件”意味着GDB不知道在哪里找到rcurl包src /目录,所以告诉它和(l)是上下文,(p)rint c变量的值是保护的这似乎是valgrind警告的来源

(gdb) dir ~/tmp/RCurl/src (gdb) l 793} 794} 795} 796 break;797 case closexp: 798 (gdb) l 793} 794} 795} 796 break;797 case closexp: 798 if(!isProtected) {799 R_PreserveObject(el);800} 801 ptr = (void *) el;802年打破;(gdb) p isProtected $5 = FALSE

是保护的有一个值(它必须有!),而且FALSE的值会导致保护对象el跨越C电话(这是什么R_PreserveObject做)。这很有趣,因为我们知道垃圾收集会触发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.与价值逻辑(isProtected)[i % n]。在这里,是保护的现在是R对象,而不是C变量。看看周围的代码我% n看起来不太对,可能是用来回收的是保护的在这种情况下,提供的逻辑变量比需要保护的元素的向量短,但是N长度不一定是是保护的。让我们来看看我们使用的是我们拥有的内容,使用C级R功能Rf_PrintValue以r时尚打印r值(sexp)

(GDB)P isProtectated $ 1 =(性别)0xAad8a0(gdb)调用rf_printvalue(被认为)[1] false

是保护的是长度为1的逻辑向量。

(gdb) p i % n $9 = 1 (gdb) p n $8 = 6 (gdb) p i % n $9 = 1

我们试图访问其中的1号元素但是R向量的C表示是基于0的,所以索引的唯一有效值是0——我们越界了!这很可能是我们的错误,是时候尝试修复它了(天真地,逻辑(ISPRoted)[I%长度(ISPRoted)])以确认我们的诊断,或向PackageSescription(“rcurl”)$维护者他们可能对代码的整体结构和意图有更好的理解。