一個std::sort 自定義比較排序函數 crash的分析過程


    兩年未寫總結博客,今天先來練練手,總結最近遇到的一個crash case。
 注意:以下的分析都基於GCC4.4.6

一、解決crash

    我們有一個復雜的排序,涉及到很多個因子,使用自定義排序函數的std::sort做排序。Compare函數類似下文的偽代碼:

bool compare(const FakeObj& left, const FakeObj& right) {
    if (left.a != right.a) {
        return left.a > right.a;
    }
    if (left.b != right.b) {
        return left.b > right.b;
    }
     ....
}    

    后來,我們給排序函數加了更多的復雜邏輯:

bool compare(const FakeObj& left, const FakeObj& right) {
    if (left.a != right.a) {
        return left.a > right.a;
    }
    if (left.b != right.b) {
        return left.b > right.b;
    }
    if (left.c != 0 && right.c != 0 && left.c != right.c) {
        // 當C屬性都存在的時候使用C屬性做比較
        return left.c > right.c;
    }
    if (left.d != right.d) {
        return left.d > right.d;
    }
    ....
}   

    服務發布之后,進程就開始出現偶現的crash,使用gdb查看,調用堆棧如下:

/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_algo.h:5260
/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_algo.h:2194
/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_algo.h:2161
/usr/lib/gcc/x86_64-redhat-linux/4.4.6/../../../../include/c++/4.4.6/bits/stl_algo.h:2084

    crash發生位置:在標准庫調用compare函數執行比較的時候出現了越界:

 

    這時候,開始懷疑compare函數沒有按照標准庫的規范實現,查看相關資源:

  https://stackoverflow.com/questions/41488093/why-do-i-get-runtime-error-when-comparison-function-in-stdsort-always-return-t

  https://en.cppreference.com/w/cpp/named_req/Compare

       仔細看官方的文檔可以發現:

 

    我們的compare函數對c屬性的判斷,沒有嚴格遵守可傳遞性:if comp(a,b)==true and comp(b,c)==true then comp(a,c)==true。假設存在A、B、C三個對象,

1、A、B對象有屬性c,且A.c > B.c,按照我們的比較函數,這時候A>B;

2、C對象沒有c屬性,且C.d>A.d,這時候C>A;

3、C對象沒有c屬性,且B.d < C.d,這時候B>C

綜上,A>B 且 B>C,但是C>A,這就違反了strict weak ordering的transitivity。

    到這里,我們的case就解決了,但實際上,基於以下幾個原因,這個case花費了很長的時間:

1、  我們的compare函數的代碼不是逐步添加的,而是一次性寫完,導致沒有立即懷疑c屬性的比較有bug;

2、  對官方文檔不夠重視,只關注到了非對稱性:comp(a,b) ==true then comp(b,a)==false,忽略了可傳遞性;

輾轉了很久才注意到傳遞性要求。后續在解決問題時,應該更細致,不放過每一個細節。

二、crash更深層的原因

    業務上的crash問題已經解決,但crash的直接原因是什么還是未知的,需要繼續探索。

    找到std::sort的源碼:

https://github.com/gcc-mirror/gcc/blob/gcc-4_4-branch/libstdc%2B%2B-v3/include/bits/stl_algo.h

    再結合其他人分析std::sort源碼的總結:

https://www.cnblogs.com/AlvinZH/p/8682992.html

https://liam.page/2018/09/18/std-sort-in-STL/

    簡單的總結:std::sort為了提高效率,綜合了快排、堆排序、插入排序,可以分為兩階段:

1、  快排+堆排序(__introsort_loop),對於元素個數大於_S_threshold的序列,執行快排,當快排的遞歸深入到一定層次(__depth_limit)時,不再遞歸深入,對待排序元素執行堆排序;對於元素個數小於_S_threshold的序列則不處理,交給后面的插入排序。

2、  插入排序(__final_insertion_sort),當元素個數小於_S_threshold時,執行普通的插入排序(__insertion_sort);當大於_S_threshold時,執行兩批次的插入排序,首先是普通的插入排序排[0, _S_threshold);然后是無保護的插入排序(__unguarded_insertion_sort),從_S_threshold位置開始排,直到end,注意這里可能還會處理到_S_threshold之前的元素(因為這個函數只用比較結果來判斷是否停止,而不強制要求在某個位置點上停止)。

    我們的crash發生在__unguarded_insertion_sort階段,也就是無保護的插入排序。看下這塊的代碼:

/// This is a helper function for the sort routine.
template<typename _RandomAccessIterator, typename _Compare>
inline void __unguarded_insertion_sort(_RandomAccessIterator __first,
               _RandomAccessIterator __last, _Compare __comp)
{
    typedef typename iterator_traits<_RandomAccessIterator>::value_type _ValueType;
    for (_RandomAccessIterator __i = __first; __i != __last; ++__i)
        std::__unguarded_linear_insert(__i, _ValueType(*__i), __comp);
    }


/// This is a helper function for the sort routine.
template<typename _RandomAccessIterator, typename _Tp, typename _Compare>
void __unguarded_linear_insert(_RandomAccessIterator __last, _Tp __val,
              _Compare __comp) {
    _RandomAccessIterator __next = __last;
    --__next;
    while (__comp(__val, *__next)) {
        *__last = *__next;
        __last = __next;
        --__next;
    }
    *__last = __val;
}

    可以看到,__unguarded_linear_insert 函數比較的終止條件是compare函數返回false,否則就一直排序下去,這里之所以可以這么做,是因為之前的快排+堆排代碼保證了[0,X)序列的元素肯定大於(假設是遞減排序)[X, end),其中0<X<=_S_threshol,一旦無法保證,則會導致--__next越界,最終導致crash。

    再回到我們的crash case,因為compare函數不滿足傳遞性,雖然[0,X)區間的所有元素都大於X,且(X,end]區間的所有元素都小於X,但是並不能保證(X,end]的元素都小於[0,X)區間的元素,在__unguarded_linear_insert函數里,對(X,end]區間的元素執行插入排序時, 某元素大於[0,X)區間的所有元素,這時候就發生了越界crash。

    這里使用__unguarded_insertion_sort而不是僅使用__insertion_sort的好處是可以節省邊界判斷。相關討論:https://bytes.com/topic/c/answers/819473-questions-about-stl-sort


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM