我的程序最近在运行时遇到了一个奇怪的段错误。 我想知道是否有人以前遇到过这个错误以及如何修复它。 这是更多信息:
基本信息:
- CentOS 5.2,内核版本为 2.6.18
- g++ (GCC) 4.1.2 20080704 (红帽 4.1.2-50)
- CPU:英特尔 x86 系列
- libstdc++.so.6.0.8
- 我的程序将启动多个线程来处理数据。段错误发生在其中一个线程中。
- 虽然它是一个多线程程序,但段错误似乎发生在本地 std::string 对象上。稍后我将在代码片段中展示这一点。
- 该程序使用-g、-Wall 和-fPIC 编译,没有-O2 或其他优化选项。
核心转储信息:
Core was generated by `./myprog'.
Program terminated with signal 11, Segmentation fault.
#0 0x06f6d919 in __gnu_cxx::__exchange_and_add(int volatile*, int) () from /usr/lib/libstdc++.so.6
(gdb) bt
#0 0x06f6d919 in __gnu_cxx::__exchange_and_add(int volatile*, int) () from /usr/lib/libstdc++.so.6
#1 0x06f507c3 in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::assign(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) () from /usr/lib/libstdc++.so.6
#2 0x06f50834 in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator=(std::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) () from /usr/lib/libstdc++.so.6
#3 0x081402fc in Q_gdw::ProcessData (this=0xb2f79f60) at ../../../myprog/src/Q_gdw/Q_gdw.cpp:798
#4 0x08117d3a in DataParser::Parse (this=0x8222720) at ../../../myprog/src/DataParser.cpp:367
#5 0x08119160 in DataParser::run (this=0x8222720) at ../../../myprog/src/DataParser.cpp:338
#6 0x080852ed in Utility::__dispatch (arg=0x8222720) at ../../../common/thread/Thread.cpp:603
#7 0x0052c832 in start_thread () from /lib/libpthread.so.0
#8 0x00ca845e in clone () from /lib/libc.so.6
请注意,段错误从basic_string::operator=()开始。
相关代码:( 我展示了比可能需要的更多的代码,请暂时忽略编码风格的东西。)
int Q_gdw::ProcessData()
{
char tmpTime[10+1] = {0};
char A01Time[12+1] = {0};
std::string tmpTimeStamp;
// Get the timestamp from TP
if((m_BackFrameBuff[11] & 0x80) >> 7)
{
for (i = 0; i < 12; i++)
{
A01Time[i] = (char)A15Result[i];
}
tmpTimeStamp = FormatTimeStamp(A01Time, 12); // Segfault occurs on this line
这是这个 FormatTimeStamp 方法的原型:
std::string FormatTimeStamp(const char *time, int len)
我认为这样的字符串赋值操作应该是一种常用的,但我就是不明白为什么会出现段错误。
我调查的内容:
我在网上搜索了答案。我看着这里。回复说尝试使用定义的 _GLIBCXX_FULLY_DYNAMIC_STRING 宏重新编译程序。我试过了,但崩溃仍然发生。
我也看过这里。它还说用_GLIBCXX_FULLY_DYNAMIC_STRING重新编译程序,但作者似乎正在处理我的另一个问题,因此我认为他的解决方案不适合我。
更新于 2011 年 8 月 15 日
这是这个 FormatTimeStamp 的原始代码。我知道编码看起来不太好(例如,幻数太多..),但让我们首先关注崩溃问题。
string Q_gdw::FormatTimeStamp(const char *time, int len)
{
string timeStamp;
string tmpstring;
if (time) // It is guaranteed that "time" is correctly zero-terminated, so don't worry about any overflow here.
tmpstring = time;
// Get the current time point.
int year, month, day, hour, minute, second;
#ifndef _WIN32
struct timeval timeVal;
struct tm *p;
gettimeofday(&timeVal, NULL);
p = localtime(&(timeVal.tv_sec));
year = p->tm_year + 1900;
month = p->tm_mon + 1;
day = p->tm_mday;
hour = p->tm_hour;
minute = p->tm_min;
second = p->tm_sec;
#else
SYSTEMTIME sys;
GetLocalTime(&sys);
year = sys.wYear;
month = sys.wMonth;
day = sys.wDay;
hour = sys.wHour;
minute = sys.wMinute;
second = sys.wSecond;
#endif
if (0 == len)
{
// The "time" doesn't specify any time so we just use the current time
char tmpTime[30];
memset(tmpTime, 0, 30);
sprintf(tmpTime, "%d-%d-%d %d:%d:%d.000", year, month, day, hour, minute, second);
timeStamp = tmpTime;
}
else if (6 == len)
{
// The "time" specifies "day-month-year" with each being 2-digit.
// For example: "150811" means "August 15th, 2011".
timeStamp = "20";
timeStamp = timeStamp + tmpstring.substr(4, 2) + "-" + tmpstring.substr(2, 2) + "-" +
tmpstring.substr(0, 2);
}
else if (8 == len)
{
// The "time" specifies "minute-hour-day-month" with each being 2-digit.
// For example: "51151508" means "August 15th, 15:51".
// As the year is not specified, the current year will be used.
string strYear;
stringstream sstream;
sstream << year;
sstream >> strYear;
sstream.clear();
timeStamp = strYear + "-" + tmpstring.substr(6, 2) + "-" + tmpstring.substr(4, 2) + " " +
tmpstring.substr(2, 2) + ":" + tmpstring.substr(0, 2) + ":00.000";
}
else if (10 == len)
{
// The "time" specifies "minute-hour-day-month-year" with each being 2-digit.
// For example: "5115150811" means "August 15th, 2011, 15:51".
timeStamp = "20";
timeStamp = timeStamp + tmpstring.substr(8, 2) + "-" + tmpstring.substr(6, 2) + "-" + tmpstring.substr(4, 2) + " " +
tmpstring.substr(2, 2) + ":" + tmpstring.substr(0, 2) + ":00.000";
}
else if (12 == len)
{
// The "time" specifies "second-minute-hour-day-month-year" with each being 2-digit.
// For example: "305115150811" means "August 15th, 2011, 15:51:30".
timeStamp = "20";
timeStamp = timeStamp + tmpstring.substr(10, 2) + "-" + tmpstring.substr(8, 2) + "-" + tmpstring.substr(6, 2) + " " +
tmpstring.substr(4, 2) + ":" + tmpstring.substr(2, 2) + ":" + tmpstring.substr(0, 2) + ".000";
}
return timeStamp;
}
更新于 2011 年 8 月 19 日
这个问题终于得到解决和解决。事实上,FormatTimeStamp() 函数与根本原因无关。段错误是由本地字符缓冲区的写入溢出引起的。
可以使用以下更简单的程序重现此问题(请暂时忽略某些变量的错误命名):
(用“g++ -Wall -g main.cpp”编译)
#include <string>
#include <iostream>
void overflow_it(char * A15, char * A15Result)
{
int m;
int t = 0,i = 0;
char temp[3];
for (m = 0; m < 6; m++)
{
t = ((*A15 & 0xf0) >> 4) *10 ;
t += *A15 & 0x0f;
A15 ++;
std::cout << "m = " << m << "; t = " << t << "; i = " << i << std::endl;
memset(temp, 0, sizeof(temp));
sprintf((char *)temp, "%02d", t); // The buggy code: temp is not big enough when t is a 3-digit integer.
A15Result[i++] = temp[0];
A15Result[i++] = temp[1];
}
}
int main(int argc, char * argv[])
{
std::string str;
{
char tpTime[6] = {0};
char A15Result[12] = {0};
// Initialize tpTime
for(int i = 0; i < 6; i++)
tpTime[i] = char(154); // 154 would result in a 3-digit t in overflow_it().
overflow_it(tpTime, A15Result);
str.assign(A15Result);
}
std::cout << "str says: " << str << std::endl;
return 0;
}
在继续之前,我们应该记住以下两个事实:1)。我的机器是 Intel x86 机器,所以它使用 Little Endian 规则。因此,对于一个 int 类型的变量“m”,例如,其值为 10,它的内存布局可能是这样的:
Starting addr:0xbf89bebc: m(byte#1): 10
0xbf89bebd: m(byte#2): 0
0xbf89bebe: m(byte#3): 0
0xbf89bebf: m(byte#4): 0
2)。上面的程序在主线程中运行。说到overflow_it()函数,线程栈中的变量布局是这样的(只显示了重要的变量):
0xbfc609e9 : temp[0]
0xbfc609ea : temp[1]
0xbfc609eb : temp[2]
0xbfc609ec : m(byte#1) <-- Note that m follows temp immediately. m(byte#1) happens to be the byte temp[3].
0xbfc609ed : m(byte#2)
0xbfc609ee : m(byte#3)
0xbfc609ef : m(byte#4)
0xbfc609f0 : t
...(3 bytes)
0xbfc609f4 : i
...(3 bytes)
...(etc. etc. etc...)
0xbfc60a26 : A15Result <-- Data would be written to this buffer in overflow_it()
...(11 bytes)
0xbfc60a32 : tpTime
...(5 bytes)
0xbfc60a38 : str <-- Note the str takes up 4 bytes. Its starting address is **16 bytes** behind A15Result.
我的分析:
1)。m 是 overflow_it() 中的一个计数器,其值在每个 for 循环中递增 1,并且其最大值不应大于 6。因此它的值可以完全存储在 m(byte#1) 中(记住它是 Little Endian)恰好是 temp 3。
2)。在错误行中:当 t 是 3 位整数时,例如 109,则 sprintf() 调用将导致缓冲区溢出,因为将数字 109 序列化为字符串“109”实际上需要 4 个字节:'1' , '0', '9' 和终止符 '\0'。因为 temp[] 只分配了 3 个字节,所以最终的 '\0' 肯定会被写入 temp 3,这只是 m(byte#1),不幸的是它存储了 m 的值。因此,m 的值每次都重置为 0。
3)。然而,程序员的期望是,overflow_it() 中的 for 循环只会执行 6 次,每次 m 加 1。因为 m 总是重置为 0,所以实际循环时间远不止 6 次。
4)。我们看一下overflow_it()中的变量i:每次执行for循环,i的值加2,访问A15Result[i]。但是,如果您编译并运行该程序,您会看到 i 值最终加起来为 24,这意味着 overflow_it() 将数据写入从 A15Result[0] 到 A15Result[23] 的字节。请注意,对象 str 仅落后于 A15Result[0] 16 个字节,因此 overflow_it() 已“扫过” str 并破坏了它的正确内存布局。
5)。我认为 std::string 的正确使用,因为它是一个非 POD 数据结构,取决于实例化的 std::string 对象必须具有正确的内部状态。但是在这个程序中,str的内部布局已经被外部强行改变了。这应该就是为什么 assign() 方法调用最终会导致段错误的原因。
2011 年 8 月 26 日更新
在我之前 2011 年 8 月 19 日的更新中,我说过段错误是由对本地 std::string 对象的方法调用引起的,该对象的内存布局已被破坏,因此成为“被破坏”的对象。这不是一个“总是”真实的故事。考虑下面的 C++ 程序:
//C++
class A {
public:
void Hello(const std::string& name) {
std::cout << "hello " << name;
}
};
int main(int argc, char** argv)
{
A* pa = NULL; //!!
pa->Hello("world");
return 0;
}
Hello() 调用会成功。即使您为 pa 分配了一个明显错误的指针,它也会成功。原因是:根据 C++ 对象模型,类的非虚拟方法不驻留在对象的内存布局中。C++ 编译器将 A::Hello() 方法转换为诸如 A_Hello_xxx(A * const this, ...) 之类的东西,它可能是一个全局函数。因此,只要您不对“this”指针进行操作,事情就会顺利进行。
这一事实表明,“坏”对象不是导致 SIGSEGV 段错误的根本原因。assign() 方法在 std::string 中不是虚拟的,因此“坏”的 std::string 对象不会导致段错误。一定有其他原因最终导致了段错误。
我注意到段错误来自 __gnu_cxx::__exchange_and_add() 函数,所以我在这个网页中查看了它的源代码:
00046 static inline _Atomic_word
00047 __exchange_and_add(volatile _Atomic_word* __mem, int __val)
00048 { return __sync_fetch_and_add(__mem, __val); }
__exchange_and_add() 最终调用了 __sync_fetch_and_add()。根据这个网页, __sync_fetch_and_add() 是一个 GCC 内置函数,其行为如下:
type __sync_fetch_and_add (type *ptr, type value, ...)
{
tmp = *ptr;
*ptr op= value; // Here the "op=" means "+=" as this function is "_and_add".
return tmp;
}
就在那里!传入的 ptr 指针在这里被取消引用。在 08/19/2011 程序中,ptr 实际上是 assign() 方法中“坏”std::string 对象的“this”指针。正是此时的取消引用实际上导致了 SIGSEGV 分段错误。
我们可以使用以下程序对此进行测试:
#include <bits/atomicity.h>
int main(int argc, char * argv[])
{
__sync_fetch_and_add((_Atomic_word *)0, 10); // Would result in a segfault.
return 0;
}