成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

關于C++內存問題排查攻略

開發
雖然使用現代C++能夠有效解決大部分問題,但掌握常用的內存問題排查方法仍然十分必要,特別是在維護一些歷史系統時。

作者 | johncchen

C++因其高性能仍然是許多關鍵應用的首選語言,但其復雜的內存管理也帶來了諸多挑戰。雖然使用現代C++能夠有效解決大部分問題,但掌握常用的內存問題排查方法仍然十分必要,特別是在維護一些歷史系統時。本文分為上下兩篇:上篇(15)按照問題分類介紹和比較常用工具,下篇(67)通過兩個具體案例展示這些工具的組合使用,希望能為讀者帶來有益的啟發。筆者個人水平有限,文中難免存在疏漏之處,歡迎大家批評指正。

一、棧溢出(stack-overflow):查看coredump文件為主,動態檢測為輔

棧溢出的定位方法主要有靜態分析、動態檢測、查看coredump文件三種。

1. 靜態分析

(1) 原理

GCC提供了-fstack-usage選項,能輸出每個函數棧的最大使用量。開啟后,為每個編譯目標創建.su文件,每行包括函數名、字節數、修飾符(static/dynamic/bounded)中的一個或多個。修飾符的含義如下:

  • static: 堆棧使用量在編譯時是已知的,不依賴于任何運行時條件。
  • dynamic: 堆棧使用量依賴于運行時條件,例如遞歸調用或基于輸入數據的條件分支。
  • bounded: 堆棧使用量雖然依賴于運行時條件,但有一個可預知的上限。

(2) 舉個栗子

void static_stack_usage() { int static_array[5]; }
void dynamic_stack_usage(int n) { int val[n]; }
int main() {
  static_stack_usage();
  int n = 10;
  dynamic_stack_usage(n);
  return 0;
}

g++ ./stack_test.cc -o stack_test -fstack-usage 

./stack_test.cc:2:6:void static_stack_usage() 16 static
./stack_test.cc:4:6:void dynamic_stack_usage(int) 48 dynamic
./stack_test.cc:6:5:int main() 32 static

疑問:看到這里,估計有小伙伴會問了:既然dynamic是不確定的,靜態分析還有意義嗎?其實,實際代碼的.su一般是下面這種,dynamic和bounded組合在一起,雖然動態但有上限,因此可以計算出“最大”的棧用量。

xxbuild.cpp:277:5:int XXBuild::BuildPage() 528 dynamic,bounded

每個函數的棧使用量有了,如果知道函數的調用鏈就可以得出棧的最大使用量了。調用鏈可以從二進制文件中反匯編得到。

(3) 工具

靜態分析常用于資源有限的嵌入式系統,常常集成在它們的開發工具中。但非嵌入式系統的這類工具比較少。開源的有 checkStackUsage等,收費的有stackanalyzer等。

注意事項:

若使用bazel編譯,默認的沙箱模式會刪除.su文件,因此編譯時需要增加--spawn_strategy=standalone選項(非沙箱模式)

2. 動態檢測

(1) 通過proc文件系統

pmap或查看/proc/pid/maps中的stack,缺點是進程退出后就看不到了。

(2) 捕捉操作系統信號

原理:

  • 在 Unix-like 系統中,當程序執行非法內存訪問時,操作系統會向該程序發送 SIGSEGV 信號(段錯誤)。默認情況下,接收到此信號的程序會終止。
  • 如果通過注冊一個自定義的信號處理函數來攔截 SIGSEGV信號,處理函數會收到一個 siginfo_t 結構體,其中包含錯誤的地址和寄存器狀態等上下文信息,可以判斷是否發生了棧溢出。

工具:

libsigsegv-devel,可以定義自己的處理函數來響應內存訪問錯誤,例如嘗試恢復、記錄錯誤信息或者優雅地關閉程序。

注意事項:

libsigsegv是GPL協議

3. 查看coredump文件

重點關注:

  • 層級是否過多,是否遞歸調用
  • 棧變量是否過大

修改棧(以及線程堆棧、協程堆棧)大小后測試。

二、棧緩沖區溢出(stack-buffer-overflow):GCC -fstack-protector/C11 Annex K/AddressSanitizer

棧緩沖區溢出原因中很大一部分是數組索引/指針越界。在我看來,在項目中停止使用C風格的指針、使用STL容器能解決大部分問題。當然,一些項目處于維護狀態,大規模改造未必合算,可以考慮使用以下工具。

1. GCC -fstack-protector

-fstack-protector的原理:

  • 函數調用時,編譯器在棧上分配一個隨機生成的 canary 值(guard值),通常被放置在局部變量和控制數據(如返回地址)之間。
  • 函數執行過程中,所有的局部變量操作都應當保持 canary 值不變。如果有緩沖區溢出,超出局部變量的數據可能會覆蓋到 canary 值。
  • 如果 canary 值被修改,程序會認為發生了棧溢出攻擊,通常會立即終止,例如通過調用 __stack_chk_fail() 函數。

有不同的保護強度-fstack-protector/-fstack-protector-all/-fstack-protector-strong/-fstack-protector-explicit,一般-fstack-protector-strong即可。

2. C11 Annex K (Bounds-checking interfaces)

使用 C11 標準中引入的strncpy_s()等函數,比 strcpy()/strncpy() 等函數更安全。它要求指定源和目標的大小,并在復制過程中檢查這些大小,以防止溢出。如果發生錯誤(如無效參數或目標太?。瑂trncpy_s() 將設置 errno 并可以選擇使程序失敗。

較低版本的gcc不支持c11, 可以使用一些第三方實現,比如的openharmony的third_party_bounds_checking_function

3.  AddressSanitizer

詳見4.1

4. Valgrind memcheck

詳見4.2

三、內存泄漏:eBPF+火焰圖,高效直觀

1.Valgrind memcheck/AddressSanitizer/eBPF bcc-tools memleak比較

eBPF的最大的優點是“非侵入”,不需要重新編譯或重啟業務進程,對運行速度和內存用量的影響極小,可以忽略不計,可以線上使用。

2. eBPF bcc-tools memleak檢測原理

eBPF程序是事件驅動的,在內核或應用經過特定鉤子點(hook point)時運行。在memleak的源碼中可以看到注冊到了以下鉤子點

attach_probes("malloc")
attach_probes("calloc")
attach_probes("realloc")
attach_probes("mmap", can_fail=True)  # failed on jemalloc
attach_probes("posix_memalign")
attach_probes("valloc", can_fail=True)  # failed on Android, is deprecated in libc.so from bionic directory
attach_probes("memalign")
attach_probes("pvalloc", can_fail=True)  # failed on Android, is deprecated in libc.so from bionic directory
attach_probes("aligned_alloc", can_fail=True)  # added in C11
attach_probes("free", need_uretprobe=False)
attach_probes("munmap", can_fail=True, need_uretprobe=False)  # failed on jemalloc

3. 舉個栗子

先寫一段內存泄漏(不斷增長)的測試代碼

#include <iostream>
#include <chrono>
#include <thread>
#include <vector>
#include <string>

void LeakOnce(std::vector<std::string>& strs) {
    // Generate a random string
    std::string str;
    const std::string characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    for (int i = 0; i < 10; i++) {
        char randomChar = characters[rand() % characters.length()];
        str += randomChar;
    }
    strs.emplace_back(std::move(str));
}

void CallLeak(){
    std::vector<std::string> strs;
    while(true){
        LeakOnce(strs);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

int main() {
    CallLeak();
    return 0;
}

g++ ./leak_test.cc -o leak_test --std=c++11 -g

檢測結果如圖,符合預期~

memleak具體選項詳見-h,也可以參考官方例子。需要注意的是-O選項, attach to allocator functions in the specified object. 如果沒有使用glibc而是使用jemlloc或tcmalloc,需要使用-O指定二進制文件(靜態鏈接)或動態庫(動態鏈接)。

4.  改進memleak,支持火焰圖

實際的內存泄漏經常是小規模、長時間的,會混雜在大量正常的內存申請和釋放動作中,這時候memleak文本形式的輸出就不夠直觀了。想到cpu性能調優經常用到的火焰圖,如果memleak能生成直觀的火焰圖就好了。

火焰圖的格式并不復雜,格式如下:

[堆棧] [采樣值]
main;foo;bar 76

PR4766有一個繪制火焰圖的簡單實現,沒有合入主干很可惜??梢詤⒖妓?,來修改已安裝的bcc/tools/memleak。修改后執行:

/usr/share/bcc/tools/memleak2.py -p $(pgrep leak_test)  --report --report-file leak_test.stacks
flamegraph.pl --color=mem  --countname="bytes"< leak_test.stacks > leak_test.svg

在中大型項目中,火焰圖能夠很好地區分框架與業務模塊的內存操作,便于逐級排查,非常清晰。

四、其他內存問題:AddressSanitizer為主,Valgrind memcheck為輔

1. AddressSanitizer

編譯和鏈接時加上-fsanitize=address,完整選項見AddressSanitizerFlags,一些常用選項如下:

export :ASAN_OPTIONS="log_path=/my_path/asan:abort_on_error=1:disable_coredump=0:unmap_shadow_on_exit=1:debug=true:check_initialization_order=true:print_stats=true:strict_string_checks=true:dump_instruction_bytes=true"

AddressSanitizer會使程序運行慢約2倍,比Valgrind memcheck好太多,可以考慮使用線上節點排查問題。

2. Valgrind memcheck

運行速度慢10~50倍,消耗大量內存,可以通過關閉檢查項目來提高速度、減少內存使用。

五、多線程/協程的數據競爭(data race):ThreadSanitizer/Valgrind的helgrind和drd基本不可用,AddressSanitizer仍然可用

1. ThreadSanitizer

編譯和鏈接增加-fsanitize=thread,編譯通常遇到std::atomic_thread_fence報錯,官方解釋如下,好吧,std::atomic_thread_fence很常見,ThreadSanitizer基本不可用了。

-Wno-tsan Disable warnings about unsupported features in ThreadSanitizer. ThreadSanitizer does not support std::atomic_thread_fence and can report false positives.

除此之外,開啟ThreadSanitizer對運行速度和內存消耗也有較大影響:

The cost of race detection varies by program, but for a typical program, memory usage may increase by 5-10x and execution time by 2-20x.

2. Valgrind helgrind/drd

比起ThreadSanitizer,需要消耗更多內存。我做了個測試,一個使用內存2.5G的服務,使用Valgrind helgrind或drd啟動,32G內存都不夠、直接OOM,因此在規模大些的項目中基本不可用。

3. AddressSanitizer仍然可用

AddressSanitizer不針對data race,但能檢測內存異常。

下篇以排查某A服務內存問題的過程為例,演示上篇中工具的使用。其實,上篇的工具是下篇踩坑、填坑的經驗總結。

六、低成本解決歷史代碼崩潰問題

A 服務中有一大塊老舊的業務邏輯,稱之為模塊 B,其特點如下:

  • 代碼行數多, 2w+
  • 大量 C 風格字符串操作(如 strcpy 等),存在越界風險
  • 依賴大量老舊版本的第三方庫
  • 需求很少,處于維護狀態

問題出現:服務以前運行平穩,但從某天開始,線上節點隔三差五就會出現崩潰。查看 coredump 文件,發現崩潰在模塊B的代碼中, frame 0 中某些局部變量損壞。然而,重放崩潰前后一段時間內的請求并不能復現崩潰,應該是其他請求的棧緩沖區溢出,破壞了這條請求的棧。此類問題很難直接根據 coredump 文件定位。

排查過程:如 2.1 中所述,使用 -fstack-protector-strong 重新編譯并上線,結果斷斷續續地因為 __stack_chk_fail 出現崩潰,這就好辦了。按圖索驥,發現是某些請求觸發了歷史 bug,導致一些局部變量指針越界,針對性地添加邊界判斷就修復了,從而以較小的代價解決了復雜歷史代碼的崩潰問題。

后續措施:考慮到模塊 B 可能還有其他坑,一旦出現問題將導致 A 服務的節點崩潰,影響整體 SLA。因此將模塊 B 拆分成獨立的微服務 C。如果服務 A 調用服務 C 失敗,可以走降級鏈路,從而提高業務整體的可用性。

七、解決偶發崩潰問題

問題出現:A 服務頻繁上線,經常在一周內發布三四個版本。某段時間內,崩潰的概率顯著增加。查看 coredump 文件,發現經常崩潰在 STL 容器(如 std::map、std::unordered_map、std::vector 等)中 std::allocator 的析構相關函數,但backstrace不確定,有時在這個模塊中有時在那個模塊中。重放崩潰前后一段時間內的請求無法復現崩潰,推測又是內存踩踏問題。

第一次嘗試:逐一使用2.1 ~2.3的 GCC -fstack-protector /C11 Annex K/AddressSanitizer ,回放線上請求,結果都正常,這就尷尬了……

鑒于一時難以解決問題,首先采取措施確保線上穩定:

將容器的健康檢查方式從 TCP 改為 HTTP,這樣在 core dump 開始而不是結束后就能檢測出節點異常(core 文件約 20G,core dump 過程持續幾分鐘),盡早從北極星(服務注冊與發現平臺)上摘除,減少對線上的影響。這樣線上可以繼續開啟coredump,方便排查問題。

第二次嘗試:

通過監控逐漸發現一些規律:崩潰集中在進程啟動階段,日常運行時很少。因此懷疑與進程啟動時的狀態或特定請求有關。

下一步是復現問題。在崩潰概率最高的地域,新建一個旁路 workload(兩個節點),將北極星權重調為其他節點的 1/N,使用 API 定期重啟旁路 workload 的 pod。經過幾天,問題復現了!

backstrace與之前類似,找不出線索。那就上工具吧,能在線上使用的檢測工具也就只有 AddressSanitizer了,編譯一版部署到旁路 workload,繼續定期重啟,等待結果……

果然,斷斷續續出現了一些崩潰,但查看 coredump 文件的backstrace仍難以找到有效線索。有時崩潰在插件中,有時在 encode 過程中。咨詢相關插件的同學,他們也感到很奇怪,沒有思路。直到,直到,下面這個錯誤出現:

==181==ERROR: AddressSanitizer: attempting double-free on 0x61b000258480 in thread T14 (FiberWorker_02):
    #0 0x7f3a1f52a878 in operator delete(void*, unsigned long) ../../../../libsanitizer/asan/asan_new_delete.cpp:164
    #1 0x13d4f0c in std::__new_allocator<char>::deallocate(char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/new_allocator.h:158
    #2 0x13d4f0c in std::allocator<char>::deallocate(char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/allocator.h:200
    #3 0x13d4f0c in std::allocator_traits<std::allocator<char> >::deallocate(std::allocator<char>&, char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/alloc_traits.h:496
    #4 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_destroy(unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:300
    #5 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose() /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:294
    #6 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_mutate(unsigned long, unsigned long, char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.tcc:338
    #7 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_append(char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.tcc:420
    #8 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1430
    #9 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1396
    #10 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1338
    #11 0x1b91ac5 in construct_xx_query(thread_data*) xx/yy/zz/aa_util.cc:66
···
0x61b000258480 is located 0 bytes inside of 1539-byte region [0x61b000258480,0x61b000258a83)
freed by thread T13 (FiberWorker_01) here:
    #0 0x7f3a1f52a878 in operator delete(void*, unsigned long) ../../../../libsanitizer/asan/asan_new_delete.cpp:164
    #1 0x13d4f0c in std::__new_allocator<char>::deallocate(char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/new_allocator.h:158
    #2 0x13d4f0c in std::allocator<char>::deallocate(char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/allocator.h:200
    #3 0x13d4f0c in std::allocator_traits<std::allocator<char> >::deallocate(std::allocator<char>&, char*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/alloc_traits.h:496
    #4 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_destroy(unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:300
    #5 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose() /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:294
    #6 0x13d4f0c in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_mutate(unsigned long, unsigned long, char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.tcc:338
    #7 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_append(char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.tcc:420
    #8 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(char const*, unsigned long) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1430
    #9 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::append(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1396
    #10 0x1b91ac5 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&) /usr/lib64/gcc/x86_64-pc-linux-gnu/12.3.0/../../../../include/c++/12.3.0/bits/basic_string.h:1338
    #11 0x1b91ac5 in construct_xx_query(thread_data*) xx/yy/zz/aa_util.cc:66
··· 

construct_xx_query(thread_data*) xx/yy/zz/aa_util.cc:66的代碼是
thread_data->string_bb += judge_cc()

查看代碼上下文,終于找到了原因!在某類請求中使用協程并發調用后端服務,而 thread_data->string_bb(std::string 類型)變量是唯一的,多個協程同時修改 thread_data->string_bb,導致 double-free!由于同時寫入是小概率事件,所以崩潰是偶發的。原來是 data race 問題……

再查看提交歷史,發現多協程并發調用是在某個版本上線的,當時一切正常;上百個版本之后,調用流程中增加了這行問題代碼。冗長膨脹的流程函數中新增一行代碼很難引起注意,多人開發非常容易踩坑。

徹底解決問題需要從設計入手:重構流程,遵循單一職責,將修改集中到一處,便于檢查;傳參變成只讀引用,消除 data race。

測試通過,上線,不再崩潰!

總結

大部分問題,尤其是難以排查的問題,應該在設計階段就被解決掉,越往后代價越大。正所謂“善戰者無赫赫之功”。

責任編輯:趙寧寧 來源: 騰訊技術工程
相關推薦

2020-11-02 09:48:35

C++泄漏代碼

2010-01-21 17:30:12

C++復雜

2009-08-28 10:14:45

C#內存泄露

2018-10-15 10:52:24

C++內存程序員

2021-06-28 08:00:00

Python開發編程語言

2018-11-06 12:12:00

MySQL內存排查

2015-07-20 10:23:24

NET內存問題排查

2014-05-29 10:54:20

C++構造函數

2011-07-15 01:10:13

C++內存分配

2011-04-11 09:47:50

C++內存管理

2024-03-11 06:05:00

C++字符串

2024-01-25 16:19:27

2022-07-03 20:31:59

JVMJava虛擬機

2018-08-10 15:00:42

服務器內存排查

2010-08-18 09:52:25

Memcache

2021-07-30 20:59:21

MySQL內存.參數

2025-06-26 05:00:00

2011-06-21 11:16:24

cc++

2010-02-04 14:58:06

C++內存分配

2011-07-01 10:16:08

C++內存管理
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 国产91丝袜 | 久久精品亚洲一区二区三区浴池 | 国产精品18久久久久久白浆动漫 | 请别相信他免费喜剧电影在线观看 | 久免费视频 | 久久极品 | 性在线 | 91精品国产自产精品男人的天堂 | 国产九九九九 | 日韩精品一区二区三区 | 精品久久久久久国产 | 久久久无码精品亚洲日韩按摩 | 欧美福利精品 | 日韩91| 日本啊v在线 | 荷兰欧美一级毛片 | 亚洲日日夜夜 | 中文字幕视频网 | 欧美综合一区 | 91精品国产乱码久久久久久久久 | 日韩视频免费 | 国产精品毛片一区二区在线看 | 天天看夜夜 | 在线观看免费高清av | 精品国产乱码久久久久久图片 | 精品国产视频 | 国产成人一区二区三区 | 日韩高清在线 | 成年网站在线观看 | 日韩aⅴ片| 欧美一级免费看 | 巨大黑人极品videos精品 | avtt国产| 日韩三级在线观看 | 精品国产乱码久久久久久蜜柚 | 成人欧美一区二区三区黑人孕妇 | 国产九一精品 | 九九热在线观看视频 | 国产精品欧美一区二区三区 | 久久91精品久久久久久9鸭 | 午夜免费小视频 |