来自libstdc ++的std :: string :: assign()方法中的奇怪的SIGSEGV分段错误.so.6

yao*_*bin 17 linux string segmentation-fault assign

我的程序最近在运行时遇到了一个奇怪的段错误. 我想知道是否有人之前遇到过这个错误以及如何修复它. 这是更多信息:

基础信息:

  • CentOS 5.2,核心版本为2.6.18
  • g ++(GCC)4.1.2 20080704(Red Hat 4.1.2-50)
  • CPU:Intel x86系列
  • 的libstdc ++.so.6.0.8
  • 我的程序将启动多个线程来处理数据.段错误发生在其中一个线程中.
  • 虽然它是一个多线程程序,但segfault似乎发生在本地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
Run Code Online (Sandbox Code Playgroud)

请注意,segfault在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
Run Code Online (Sandbox Code Playgroud)

这是FormatTimeStamp方法的原型:

std::string FormatTimeStamp(const char *time, int len)
Run Code Online (Sandbox Code Playgroud)

我认为这样的字符串赋值操作应该是一种常用的操作,但我只是不明白为什么会出现段错误.

我调查的内容:

我在网上搜索了答案.我看着这里.回复说,尝试使用定义的_GLIBCXX_FULLY_DYNAMIC_STRING宏重新编译程序.我试过但崩溃仍然发生.

我也看过这里.它还说用_GLIBCXX_FULLY_DYNAMIC_STRING重新编译程序,但是作者似乎正在处理与我不同的问题,因此我不认为他的解决方案适合我.


更新于08/15/2011

大家好,这是这个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;
}
Run Code Online (Sandbox Code Playgroud)

更新于08/19/2011

这个问题终于得到了解决和解决.事实上,FormatTimeStamp()函数与根本原因无关.segfault是由本地char缓冲区的写溢出引起的.

可以使用以下更简单的程序重现此问题(请忽略现在某些变量的错误命名):

(用"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;
}
Run Code Online (Sandbox Code Playgroud)

以下是我们在继续之前应该记住的两个事实: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
Run Code Online (Sandbox Code Playgroud)

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.
Run Code Online (Sandbox Code Playgroud)

我的分析:

1).m是overflow_it()中的一个计数器,其值在每个for循环中递增1,其最大值不应大于6.因此它的值可以完全存储在m(字节#1)中(记住它的Little Endian)碰巧是临时3.

2).在有缺陷的行中:当t是3位整数(例如109)时,sprintf()调用将导致缓冲区溢出,因为将数字109序列化为字符串"109"实际上需要4个字节:'1' ,'0','9'和终止'\ 0'.因为temp []只分配3个字节,所以最后的'\ 0'肯定会被写入temp 3,这就是m(字节#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;
}
Run Code Online (Sandbox Code Playgroud)

Hello()调用会成功.即使你为pa分配一个明显错误的指针,它也会成功.原因是:根据C++对象模型,类的非虚方法不驻留在对象的内存布局中.C++编译器将A :: Hello()方法转换为类似A_Hello_xxx(A*const this,...)的方法,这可能是一个全局函数.因此,只要你不操作"this"指针,事情就会很顺利.

这一事实表明,"坏"对象不是导致SIGSEGV段错误的根本原因.assign()方法在std :: string中不是虚拟的,因此"坏"std :: string对象不会导致segfault.必须有一些其他原因最终导致段错误.

我注意到segfault来自__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); }
Run Code Online (Sandbox Code Playgroud)

__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;
}
Run Code Online (Sandbox Code Playgroud)

它就是!传入的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;
}
Run Code Online (Sandbox Code Playgroud)

Emp*_*ian 2

有两种可能的可能性:

  • 第 798 行之前的一些代码已损坏本地tmpTimeStamp 对象
  • 返回值FormatTimeStamp()有点糟糕。

_GLIBCXX_FULLY_DYNAMIC_STRING很可能是转移注意力,与问题无关。

如果您安装debuginfo软件包libstdc++(我不知道它在 CentOS 上叫什么),您将能够“查看”该代码,并且可能能够判断是左侧(LHS)还是右侧赋值运算符导致了这个问题。

如果这是不可能的,则必须在程序集级别进行调试。进入框架#2并执行操作x/4x $ebp应该会给出 previous ebp、调用者地址 ( )、LHS (应在框架中0x081402fc匹配)和 RHS。从那里开始,祝你好运!&tmpTimeStamp#3