背景
在工作中,我经常使用优化代码的核心转储进行死后调试。
对于某些类型的难以再现的故障,我希望获得额外的信息。在这种情况下,添加额外的跟踪是不可行的,因为绝大多数调用都是成功的,并且每分钟会添加数百万个"不必要"的跟踪,这将快速滚动日志文件。捕获和跟踪并不总是可行的,因为一些错误可能会破坏环境,导致跟踪失败。
由于我们的核心转储包括调用堆栈内存,我想我可以使用调用堆栈内存上的一个区域进行"跟踪"。
问题
多亏了优化编译器,像这样的代码不起作用
void process (int i)
{
int save_me = i;
// Do something else
}
其思想是通过分配给局部变量来将输入变量存储在堆栈上。这通常在调试模式下运行良好,但在优化的构建中,编译器认为该语句没有副作用并将其删除
alloca
似乎可以工作,但我们针对的是一些不支持alloca
的平台,我不确定它与C++的配合效果如何。
我做了一些实验,以下代码似乎能够使状态"停留"在堆栈上,即使在优化的构建中也是如此:
#include <cstdint>
#include <stdexcept>
#include <istream>
#include <sstream>
struct saved_state
{
saved_state ()
: head (0xAABBCCDD)
, tail (0xEEFF0000)
{
std::fill (state, state + 16, 0);
}
void push (std::int32_t input) volatile
{
for (auto i = 15U; i > 0U; --i)
{
state[i] = state[i - 1];
}
state[0] = input;
}
volatile std::uint32_t head ;
volatile std::int32_t state [16];
volatile std::uint32_t tail ;
};
void invoke (std::int32_t i)
{
if (i > 10)
{
throw std::runtime_error ("Busted");
}
}
void process (std::istream & input)
{
saved_state volatile ss;
while (!input.eof ())
{
std::int32_t i;
if (input >> i)
{
ss.push (i);
invoke (i);
}
}
}
int main()
{
std::istringstream input ("1n2n30n");
process (input);
return 0;
}
问题
我可以期望代码执行我希望它执行的操作吗?它似乎适用于我们当前的编译器集(clang&gcc(,但我能指望它继续工作吗?
有没有更好的方法来实现我想要做的事情?
我所说的"更好"是指更简单、更健壮或符合标准。
从您的问题中可以看出,您知道在代码的特定函数/区域中存在罕见/难以调试的问题?我之所以这么认为,是因为你在谈论手动仪器,我猜你不打算在任何地方都这样做,因为你预计会出现可能的问题。
如果这是您的情况,那么我认为您可能需要考虑仅禁用该函数/代码区域的优化。在Visual Studio中,您可以使用#pragma
来实现这一点,我想clang/gcc也存在类似的功能。在最坏的情况下,您可以将相关函数提取到一个单独的文件中,然后只编译该文件而不进行优化。
这可能对那些只出现在优化构建中的问题没有帮助,但当你遇到那些棘手的Heisenbug时,任何类型的添加跟踪都可能会隐藏issue或使其不那么频繁。在这种情况下,你唯一真正的办法就是真正擅长破译拆解。。。
也就是说,volatile
确实告诉编译器,它不允许优化读写,因此您的方法应该是稳健的,并且可能是解决某些类型错误的有用工具。
优化的编译可能很难调试:
你可以试试这样的东西:
在您的示例中:
void process (int i)
{
int save_me = i;
// Do something else
}
(预初始化的(形式参数和auto变量都在同一个堆栈上,只相隔几个字节。如果在"做其他事情"期间发生崩溃,优化器已经对不再使用的堆栈项进行了处理。
我运气不错的是:
void process (int i)
{
// Do something else
if (bool_that_compiler_can_not_predetermine_is_always_false)
{
std::cerr << "error: int i is " << i << std::endl;
}
}
由于编译器无法确定cerr行永远不会被执行,因此它将生成代码,并将形式参数保留在作用域中。
当然,除了cerr,你还可以选择其他动作。也许是日志条目?也许是更小的。关键是,在丢弃i的值(或者,如果您仍然需要save_me(之后,直到"进程"结束,核心转储中的故障才可能发生。
优化器也可以重新排序代码,但if子句在进程末尾的位置(我认为(迫使do的所有部分在该子句之前完成其他操作。
我有时会使用时间戳来创建cannot be true子句。(因为::time(0(非常有效(。
如果您有一个main,argc很容易使用,即(0==argc(或(argc>100(,并且多余的args很容易被忽略。