C++如何捕获悬空引用?

15

假设以下代码段:

struct S {
    S(int & value): value_(value) {}
    int & value_;
};

S function() {
    int value = 0;
    return S(value);   // implicitly returning reference to local value
}

如果编译器不产生警告(-Wall),这种错误可能很难发现。

有哪些工具可以帮助发现这些问题?


那甚至不应该编译。你正在将引用绑定到整数字面量。你使用的是哪个编译器? - jalf
@jalf 抱歉,应该是 S(value)。 - Anycorn
1
为什么需要工具来检测这个?只需要不编写明显错误的代码就可以了。一个带引用成员的类正确的可能性非常非常小。 - anon
1
@Neil 这是来自第三方库,我无法控制。 我花了相当长的时间试图弄清楚发生了什么,因为我忽略了构造函数参数不作为引用存储的假设。 - Anycorn
你对代码的哪一部分有控制权?是 function() 部分还是 struct S {} 部分?如果是 S 部分,你能否暂时添加/更改代码? - Nordic Mainframe
7个回答

12

有基于运行时的解决方案可以对代码进行插桩以检查无效指针访问。到目前为止,我只用过mudflap(自GCC 4.0版本以来已集成)。mudflap试图跟踪代码中的每个指针(和引用),并检查每个访问是否指向其基本类型的一个活动对象。以下是一个示例:

#include <stdio.h>
struct S {
    S(int & value): value_(value) {}
    int & value_;
};

S function() {
    int value = 0;
    return S(value);   // implicitly returning reference to local value
}
int main()
{
    S x=function();
    printf("%s\n",x.value_); //<-oh noes!
}

启用mudflap编译这个程序:

g++ -fmudflap s.cc -lmudflap

运行后得到:

$ ./a.out
*******
mudflap violation 1 (check/read): time=1279282951.939061 ptr=0x7fff141aeb8c size=4
pc=0x7f53f4047391 location=`s.cc:14:24 (main)'
      /opt/gcc-4.5.0/lib64/libmudflap.so.0(__mf_check+0x41) [0x7f53f4047391]
      ./a.out(main+0x7f) [0x400c06]
      /lib64/libc.so.6(__libc_start_main+0xfd) [0x7f53f358aa7d]
Nearby object 1: checked region begins 332B before and ends 329B before
mudflap object 0x703430: name=`argv[]'
bounds=[0x7fff141aecd8,0x7fff141aece7] size=16 area=static check=0r/0w liveness=0
alloc time=1279282951.939012 pc=0x7f53f4046791
Nearby object 2: checked region begins 348B before and ends 345B before
mudflap object 0x708530: name=`environ[]'
bounds=[0x7fff141aece8,0x7fff141af03f] size=856 area=static check=0r/0w liveness=0
alloc time=1279282951.939049 pc=0x7f53f4046791
Nearby object 3: checked region begins 0B into and ends 3B into
mudflap dead object 0x7089e0: name=`s.cc:8:9 (function) int value'
bounds=[0x7fff141aeb8c,0x7fff141aeb8f] size=4 area=stack check=0r/0w liveness=0
alloc time=1279282951.939053 pc=0x7f53f4046791
dealloc time=1279282951.939059 pc=0x7f53f4046346
number of nearby objects: 3
Segmentation fault

需要考虑以下几点:

  1. mudflap可以进行精细调整,具体需要检查和执行的操作详见http://gcc.gnu.org/wiki/Mudflap_Pointer_Debugging
  2. 默认行为是在违规时引发SIGSEGV信号,这意味着您可以在调试器中找到违规情况。
  3. mudflap可能会出现问题,特别是当您与未编译mudflap支持的库进行交互时。
  4. 它不会在创建悬空引用的位置(return S(value))发出警告,只有在引用被解引用时才会发出警告。如果需要此功能,则需要使用静态分析工具。

P.S. 一个需要考虑的事情是,在S()的复制构造函数中添加非可移植的检查,断言value_没有绑定到寿命更短的整数上(例如,如果*this位于比它绑定的整数“旧”的堆栈槽上)。这是高度依赖于机器的,可能很棘手,但只要仅用于调试就应该没问题。


我会尝试一下。我之前不知道这个工具。谢谢。 - Anycorn

5

我认为这不可能全部捕获,尽管某些编译器在某些情况下可能会发出警告。

需要记住的是,引用在底层实际上是指针,许多指针自我“射脚”的情况仍然存在。

为了澄清我的意思,以下是两个类。一个使用引用,另一个使用指针。

class Ref
{
  int &ref;
public:
  Ref(int &r) : ref(r) {};
  int get() { return ref; };
};

class Ptr
{
  int *ptr;
public:
  Ptr(int *p) : ptr(p) {};
  int get() { return *ptr; };
};

现在,比较这两个生成的代码。
@@Ref@$bctr$qri proc    near  // Ref::Ref(int &ref)
    push      ebp
    mov       ebp,esp
    mov       eax,dword ptr [ebp+8]
    mov       edx,dword ptr [ebp+12]
    mov       dword ptr [eax],edx
    pop       ebp
    ret 

@@Ptr@$bctr$qpi proc    near  // Ptr::Ptr(int *ptr)
    push      ebp
    mov       ebp,esp
    mov       eax,dword ptr [ebp+8]
    mov       edx,dword ptr [ebp+12]
    mov       dword ptr [eax],edx
    pop       ebp
    ret 

@@Ref@get$qv    proc    near // int Ref:get()
    push      ebp
    mov       ebp,esp
    mov       eax,dword ptr [ebp+8]
    mov       eax,dword ptr [eax]
    mov       eax,dword ptr [eax]
    pop       ebp
    ret 

@@Ptr@get$qv    proc    near // int Ptr::get()
    push      ebp
    mov       ebp,esp
    mov       eax,dword ptr [ebp+8]
    mov       eax,dword ptr [eax]
    mov       eax,dword ptr [eax]
    pop       ebp
    ret 

看出区别了吗?其实没有。


它们在底层绝对不是指针,它们没有位置(它们不是间接的),它们是一个别名。 - user322610
4
@Steven - 该看看引擎盖下面了!我已经更新了答案。 - Roddy
你说得对,我学到了一些东西,所以谢谢你。但是引用Effective C++第三版第89页的话,“如果你看一下C++编译器的内部实现,你会发现引用通常被实现为指针”。有人能详细解释一下吗? - user322610
Roddy,能否请你分享一下你是如何生成汇编代码的呢?你可以给出一些基本指针,告诉我在哪里可以学习如何做到这一点。(这将帮助我深入了解我所写的每一行C++代码) - bits
1
@bits。这将取决于您的工具链。通常编译器会有一个命令行开关来“编译为汇编语言”。我认为gcc的选项是“-S”。 - Roddy
@bits:你也可以使用反汇编工具/汇编级调试器,如ollydbg或IDA来完成相同的事情,由于它们的标记和对象扫描功能,它们甚至可能表现更好。在MSVC下,您可以使用“Assembly,Machine Code and Source (/FAcs)”编译器选项生成汇编转储。 - Necrolis

2
你需要使用基于编译时插装的技术。虽然valgrind可以在运行时检查所有函数调用(如malloc,free),但它无法仅检查代码。
根据你的架构,IBM PurifyPlus可以找到其中一些问题。因此,你应该找到一个有效的许可证(或使用公司的许可证)来使用它,或者试用试用版。

1

我认为没有任何静态工具可以捕捉到这个问题,但是如果你使用Valgrind并结合一些单元测试或者崩溃的代码(段错误),你可以轻松地找到内存被引用的位置以及最初分配内存的位置。


1
虽然valgrind非常棒,但在这种情况下,由于所有内容都分配在堆栈上,valgrind无法捕获问题。 - Michael Anderson

1

在遭受过这种情况的打击后,我遵循一个准则:

当一个类有引用成员(或指向生命周期不受控制的内容的指针)时,请将对象设置为不可复制。

这样可以降低逃逸带有悬空引用的作用域的风险。


很遗憾,我无法控制那个库中的类。 我希望能找到一个静态分析工具。 - Anycorn

0

你的代码甚至不应该编译通过。我知道的编译器要么无法编译代码,要么至少会抛出一个警告。

如果你的意思是return S(value),那么拜托复制粘贴你在这里发布的代码

重写并引入拼写错误只会使我们无法真正猜测你正在询问哪些错误,哪些是我们应该忽略的意外。

当你在互联网上的任何地方发布问题时,如果该问题包含代码,请发布完全相同的代码

现在,假设这实际上是一个拼写错误,那么代码是完全合法的,没有任何工具应该警告你。

只要你不尝试解除悬空引用,代码就是完全安全的。

有些静态分析工具(例如Valgrind或带有/analyze的MSVC)可能会警告您,但似乎没有太大意义,因为您没有做错任何事情。 您正在返回一个包含悬空引用的对象。 您没有直接返回对局部对象的引用(编译器通常会发出警告),而是返回一个具有行为的高级对象,该行为可能使其完全安全使用,即使它包含对已超出范围的局部对象的引用。


0

这是完全有效的代码。

如果您调用函数并将临时变量绑定到const引用,则作用域会延长。

const S& s1 = function(); // valid

S& s2 = function(); // invalid

这在C++标准中是明确允许的。

参见12.2.4:

有两种情况下,临时对象的销毁时间不同于完整表达式的结束时间。

和12.2.5:

第二种情况是当引用绑定到临时对象时。除非:引用绑定的临时对象或作为引用绑定的子对象的完整对象在引用的生命周期内持久存在[...]。


2
S 可能会保持在作用域中,但是 S 引用的对象(在本例中为 int 类型)将超出作用域。 - MSN

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接