如何检测 C++ 中对超出范围的堆栈变量的引用?
How to detect references to out-of-scope stack variables in C++?
Valgrind 可用于检测对堆上已释放对象的延迟引用。但是,它似乎没有这个特性来延迟对堆栈上范围外变量的引用。例如:
#include <iostream>
struct CharHolder {
const char ch;
CharHolder(char _ch) : ch(_ch) {}
};
struct Printer {
const CharHolder& ref;
Printer(const CharHolder& _ref) : ref(_ref) {}
void print() {
std::cout << &ref << ": " << ref.ch << std::endl;
}
};
int main() {
// g++ -O0: prints 'x'
// g++ -O3: prints undefined character
Printer p1(CharHolder('x'));
p1.print();
// g++: prints undefined character
CharHolder* h = new CharHolder('x');
Printer p2(*h);
delete h;
p2.print();
}
第一个例子,p1
,打印机持有对范围外堆栈变量的引用,因为 CharHolder('x')
在构造 p1
完成。
第二个示例 p2
是打印机保存对堆变量的引用的示例,在 p2
尝试在 print()
中引用它之前该变量是空闲的.
Valgrind 抱怨第二个例子:
==82331== Invalid read of size 1
==82331== at 0x400A8E: Printer::print()
==82331== by 0x400967: main
==82331== Address 0x5a1c040 is 0 bytes inside a block of size 1 free'd
==82331== at 0x4C2C2BC: operator delete(void*)
==82331== by 0x40095F: main
如何检测第一类错误,也许使用像 Valgrind 这样的工具?
没有任何静态分析工具是完美的。 valgrind
等静态分析工具在捕获常见编程错误方面有着良好的记录。
但他们无法 100% 地捕捉到它们。
我试图尽可能避免产生此类编程错误的方法是防御性编程纪律,旨在通过合同证明这些 classes 编程错误在逻辑上是不可能的.这包括:
使用智能指针代替引用和指针。您可以通过合同证明,使用智能指针会导致对超出范围的对象的引用在逻辑上变得不可能。
使用迭代器和标准库算法,而不是 class真正的 for (size_t i=0; i<container.size(); ++i)
方法。使用明确定义的开始和结束迭代器 运行 关闭数组的末尾在逻辑上是不可能的。另外,作为额外的好处,如果由于某种原因切换容器的选择,代码将需要更少的更改。
在您的情况下,仅运行时静态分析工具几乎不可能检测到这一点。最终编译的代码绝对不包含任何在运行时正式将临时标记为超出范围的内容。生成的代码分配一个堆栈帧,足以容纳自动作用域变量和作为参数传递的临时变量。构造函数调用完成后,不会生成显式调用来将临时对象标记为已销毁。我不明白 valgrind
或任何其他静态分析工具怎么可能知道这一点。
也许如果临时的 class 有一个明确的析构函数,理论上通用静态分析工具可以知道 class 实例现在被销毁了,因为它的析构函数得到了已调用。
但这表明没有完美的答案。即使是我提到的编程实践也不能 100% 地避免问题;他们有时会引入自己必须考虑的复杂性(如使用智能指针时的循环引用)。
Valgrind 可用于检测对堆上已释放对象的延迟引用。但是,它似乎没有这个特性来延迟对堆栈上范围外变量的引用。例如:
#include <iostream>
struct CharHolder {
const char ch;
CharHolder(char _ch) : ch(_ch) {}
};
struct Printer {
const CharHolder& ref;
Printer(const CharHolder& _ref) : ref(_ref) {}
void print() {
std::cout << &ref << ": " << ref.ch << std::endl;
}
};
int main() {
// g++ -O0: prints 'x'
// g++ -O3: prints undefined character
Printer p1(CharHolder('x'));
p1.print();
// g++: prints undefined character
CharHolder* h = new CharHolder('x');
Printer p2(*h);
delete h;
p2.print();
}
第一个例子,p1
,打印机持有对范围外堆栈变量的引用,因为 CharHolder('x')
在构造 p1
完成。
第二个示例 p2
是打印机保存对堆变量的引用的示例,在 p2
尝试在 print()
中引用它之前该变量是空闲的.
Valgrind 抱怨第二个例子:
==82331== Invalid read of size 1
==82331== at 0x400A8E: Printer::print()
==82331== by 0x400967: main
==82331== Address 0x5a1c040 is 0 bytes inside a block of size 1 free'd
==82331== at 0x4C2C2BC: operator delete(void*)
==82331== by 0x40095F: main
如何检测第一类错误,也许使用像 Valgrind 这样的工具?
没有任何静态分析工具是完美的。 valgrind
等静态分析工具在捕获常见编程错误方面有着良好的记录。
但他们无法 100% 地捕捉到它们。
我试图尽可能避免产生此类编程错误的方法是防御性编程纪律,旨在通过合同证明这些 classes 编程错误在逻辑上是不可能的.这包括:
使用智能指针代替引用和指针。您可以通过合同证明,使用智能指针会导致对超出范围的对象的引用在逻辑上变得不可能。
使用迭代器和标准库算法,而不是 class真正的
for (size_t i=0; i<container.size(); ++i)
方法。使用明确定义的开始和结束迭代器 运行 关闭数组的末尾在逻辑上是不可能的。另外,作为额外的好处,如果由于某种原因切换容器的选择,代码将需要更少的更改。
在您的情况下,仅运行时静态分析工具几乎不可能检测到这一点。最终编译的代码绝对不包含任何在运行时正式将临时标记为超出范围的内容。生成的代码分配一个堆栈帧,足以容纳自动作用域变量和作为参数传递的临时变量。构造函数调用完成后,不会生成显式调用来将临时对象标记为已销毁。我不明白 valgrind
或任何其他静态分析工具怎么可能知道这一点。
也许如果临时的 class 有一个明确的析构函数,理论上通用静态分析工具可以知道 class 实例现在被销毁了,因为它的析构函数得到了已调用。
但这表明没有完美的答案。即使是我提到的编程实践也不能 100% 地避免问题;他们有时会引入自己必须考虑的复杂性(如使用智能指针时的循环引用)。