最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

golang uretprobe的崩潰原因與模擬實現(xiàn)

2023-07-25 23:49 作者:清澄秋爽  | 我要投稿

https://mp.weixin.qq.com/s/-LPlETem33rbL6zKomS-mQ

前言

在eCapture[1]最初支持golang的https明文捕獲時,是不支持request\response完整的匹配的。這點不同于C語言編寫的程序,是因為golang的uretprobe類型鉤子有個較為致命的bug,會導(dǎo)致被掛載進程崩潰,這問題在BCC社區(qū)也有討論過:Go crash with uretprobe #1320[2], 火焰圖作者brendangregg也提到,在他的一篇博客[3]里,用戶評論如下:

Another problem I ran into: the uretprobe seems to place the return probes by modifying the stack, which is in conflict with how Go manages stack (stacks in Go can grow/shrink at anytime, it does so by copying entire stack to a new larger area, adjusting the pointers in the stack to point to new area etc). So if we are doing a uretprobe, and stack happens to grow (or shrink) at that time, it can lead Go runtime panics. Please see here for an example panic message:go.stp#L32-L58[4]

也就是說

uretprobe似乎通過修改堆棧來放置返回探針,這與Go管理堆棧的方式?jīng)_突(Go中的堆??梢栽谌魏螘r候增長/縮小,它通過將整個堆棧復(fù)制到一個新的較大區(qū)域,調(diào)整堆棧中的指針以指向新區(qū)域等方式實現(xiàn))。因此,如果我們正在進行uretprobe操作,并且堆棧在此期間發(fā)生增長(或縮?。?,它可能導(dǎo)致Go運行時發(fā)生錯誤。請參閱此處的示例錯誤消息:go.stp#L32-L58[5]

親自驗證

是的,筆者在為eCapture增加go tls的明文捕獲時,也是attach到Go 函數(shù)的uretprobe上,結(jié)果自然是,被掛載的進程崩潰了。經(jīng)過漫長的debug、查資料,終于有點眉目。這其實跟Golang的runtime、寄存器等實現(xiàn)機制有關(guān),我寫了一個DEMO,驗證一番。

這個DEMO是我在5月初寫的,期間一直想寫篇簡單的文章給大家介紹一下,奈何太忙了,接著這次出差的機會,周末整理一下,分享給大家。時間相隔太久,可能很多細節(jié)都忘記了,筆者水平有限,如有錯誤,歡迎指出。

golang uretprobe沖突

話不多說,Go程序崩潰的核心原因為Go的棧在runtime管理時,被插入了異常的內(nèi)存地址。Go中常見的堆棧變化為協(xié)程goroutine的創(chuàng)建與銷毀。棧內(nèi) 被插入異常內(nèi)存地址是因為eBPF的實現(xiàn)機制是向函數(shù)的返回地址前,插入了斷點指令(i386和x86_64[6]是INT3)。兩個條件的疊加,就出現(xiàn)了這個錯誤。

那么重現(xiàn)起來也比較簡單,寫一個協(xié)程goroutine數(shù)量不停變化的程序,并使用eBPF uretprobe掛載上去即可。

案例演示

被HOOK的測試代碼

被掛載的函數(shù)是CountCC,他的返回值應(yīng)該是101,這段代碼被Go編譯后,CountCC在符號表里名字是main.CountCC,這個就是eBPF掛載的函數(shù)名。要注意,在代碼里務(wù)必使用go:noline語法來讓Go編譯器不要對這段代碼進行內(nèi)聯(lián)inline,否則編譯后的可執(zhí)行文件中,符號表內(nèi)就找不到main.CountCC函數(shù)了。

執(zhí)行掛載動作的代碼

內(nèi)核空間代碼:

其中SEC的參數(shù)uretprobe/countcc在編譯為ebpf字節(jié)碼后,會被用戶空間程序讀取,關(guān)聯(lián)到uretprobe_countcc這個符號上。

用戶空間代碼:

執(zhí)行掛載動作的代碼,也很好實現(xiàn),使用筆者的golang eBPF管理SDK ebpfmanager[7],只需要幾行代碼,以下為用戶空間程序:

掛載類型uretprobe/countCC,被Go的eBPF類庫解析為uretprobe類型程序。掛載的eBPF執(zhí)行函數(shù)為uretprobe_countcc,掛載目標符號為main.CountCC

執(zhí)行重現(xiàn)

編譯后,觀測程序是main,被觀測程序是demo。

  1. 啟動觀測程序bin/main

  2. 啟動被觀測程序bin/demo

崩潰棧信息

可以看到被觀測程序立刻崩潰,崩潰的信息如下:

其中致命的錯誤信息是fatal error: unknown caller pc,是的,重現(xiàn)了。

Go程序uretprobe掛載解決方案

沖突點

正如前文所說,這是golang 協(xié)程收縮容,導(dǎo)致stack變動, int3指令執(zhí)行后,添加到stack中,破壞原來的棧,執(zhí)行報錯。如何解決這個問題呢,在之前的issue里,有人提了一個用uprobe模擬uretprobe的思路。

圖片

給定一個Golang二進制文件,解析ELF符號表并獲取我們想要跟蹤的符號的地址。如果需要,在該地址附加一個uprobes。

不要將uretprobe附加到符號地址,而是從該地址開始讀取ELF文本部分,并解碼匯編指令,直到達到符號的結(jié)束。在掃描過程中,在每個返回過程的指令(例如對于x86-64,RETN指令,操作碼為0xC2和0xC3)處放置一個uprobes。對于我感興趣的符號,通常只有很少的RET指令,大約在1到5個范圍內(nèi),這是合理的。

當在上述點安裝的任一uprobes觸發(fā)時,實際上就像我們執(zhí)行了一個uretprobe一樣,除了我們沒有干擾堆棧,因此當Go運行時移動堆棧時,解決方案足夠穩(wěn)健以避免崩潰(至少看起來是這樣)。而且,由于uprobes恰好放置在RET指令之前,棧指針已經(jīng)方便地放置在幀的開頭,因此我們可以輕松訪問輸入?yún)?shù)和返回值,因為它們在Go中都存儲在棧上。

評論者還提到,這種方法具有一些輕微的性能優(yōu)勢,因為我們避免了uretprobe的開銷。但缺點是我們現(xiàn)在必須在用戶空間中解碼ELF文件的匯編指令,所以相比標準的替代方案要麻煩得多,而且,無法使用BCC之類工具,只能自己實現(xiàn)eBPF程序。

Go函數(shù)的RET偏移地址

這可難不到我,筆者一直不太用BCC,更喜歡自己寫eBPF程序。實現(xiàn)起來也很簡單,只需要按照DWARF Debugging Standard[8]規(guī)范,讀取Golang的ELF文件,查找符號表內(nèi)對應(yīng)main.CountCC函數(shù)對應(yīng)符號的匯編指令,并按照X86格式解析,循環(huán)判斷是否為RET,并記錄當前指令在整個函數(shù)符號的偏移地址即可。

內(nèi)核空間程序

因為是用uprobe來模擬uretprobe,eBPF內(nèi)核代碼肯定要調(diào)整的了,為了要驗證能否拿到返回值,這里也增加了返回值的獲取。

可以看到,這里新增一個函數(shù)uprobe_countcc,將用于用戶空間的eBPF執(zhí)行函數(shù)。

用戶空間程序調(diào)整

經(jīng)過ELF文件分析,將RET指令的偏移地址保存到offsets中,在用戶空間掛載到函數(shù)的偏移位置上:

可以看到Section改成了uprobe/countcc, 并掛載到內(nèi)核函數(shù)uprobe_countcc上。以及新增 ?UprobeOffset字段,并設(shè)定offset,這樣就實現(xiàn)自動的uprobe偏移量掛載。(PS:你就說,筆者的 ebpfmanager[9]方便不方便吧)

模擬驗證

按照之前的步驟,先啟動觀測程序,打開內(nèi)核調(diào)試的日志,再啟動被觀測程序:

  1. 啟動觀測程序,bin/main -e,這里多了-e參數(shù),來使用模擬模式。

  2. 打開內(nèi)核調(diào)試日志,方便觀察是否能拿到main.CountCC函數(shù)的返回值,命令為cat /sys/kernel/debug/tracing/trace_pipe。

  3. 啟動被觀測程序,bin/demo

觀測程序

觀測程序啟動后,可以看到終端日志中,搜索到兩處RET指令,并分別進行uprobe`掛載。


圖片

main.CountCC函數(shù)內(nèi),RET匯編指令的偏移地址分別為0x7A、0xE3 ,且都掛載成功,執(zhí)行的內(nèi)核函數(shù)為uprobe_countcc

被觀察程序

如你所見,被觀測程序沒有崩潰,可以正常運行,并輸出結(jié)果。


圖片

觀察結(jié)果

筆者的DEMO里沒有將內(nèi)核調(diào)試結(jié)果傳輸?shù)接脩艨臻g,直接打印了。

可以看到,demo-18960 (程序名+PID)運行結(jié)果后,出現(xiàn)了我們打印的日志。并且,捕獲的結(jié)果是101,符合預(yù)期。

圖片

總結(jié)

eBPF掛載uretprobe崩潰的問題,只在Golang程序上發(fā)生,這跟Golang的協(xié)程縮容、擴容機制有關(guān),受到CPU中斷指令插入影響,破壞原有調(diào)用棧,導(dǎo)致問題發(fā)生。其他編譯型語言上,不會有這個問題。假如有的語言也跟Golang一樣,使用stack來做運動時管理,哪也會遇到這個問題。

關(guān)于 Golang的這個問題,在其社區(qū)里也有關(guān)于runtime: fatal error: unknown caller pc when uprobes are attached #27077[10]的討論,Go語言開發(fā)者**aclements[11]**認為,這不是Go的問題,近期也不會考慮修復(fù),希望uretprobe的管理層面,自動做返回地址棧的修復(fù)。

感謝提供的參考資料,@sillyousu。這些資料確認了我的猜測,很不幸地,我們實際上無法有效地解決uretprobes損壞堆棧的問題。

既然我們無能為力,而且這并不是一個Go的錯誤,我決定關(guān)閉這個問題。如果將來uretprobes能夠提供足夠的信息來恢復(fù)用戶空間中被破壞的返回地址,我們可以重新考慮這個問題,并可能找到解決方法。

所以,這個問題,大家還是自己使用模擬的方法來解決Golang程序的函數(shù)返回值觀測需求吧。eCapture也是自己寫了PR支持了Go TLS的明文捕獲:support gotls request and response #357[12]。本次DEMO的測試代碼在GitHub倉庫:cfc4n/go_uretprobe_demo[13] ,祝大家玩得開心。

圖片

寫于2023年6月11日,周末,雷陣雨,北京望京。

參考資料

[1]

eCapture: https://ecapture.cc

[2]

Go crash with uretprobe #1320: https://github.com/iovisor/bcc/issues/1320

[3]

博客: https://www.brendangregg.com/blog/2017-01-31/golang-bcc-bpf-function-tracing.html

[4]

go.stp#L32-L58: https://github.com/surki/misc/blob/master/go.stp#L32-L58

[5]

go.stp#L32-L58: https://github.com/surki/misc/blob/master/go.stp#L32-L58

[6]

x86_64: https://c9x.me/x86/html/file_module_x86_id_280.html

[7]

ebpfmanager: https://github.com/gojue/ebpfmanager

[8]

DWARF Debugging Standard: https://dwarfstd.org/dwarf5std.html

[9]

ebpfmanager: https://github.com/gojue/ebpfmanager

[10]

runtime: fatal error: unknown caller pc when uprobes are attached #27077: https://github.com/golang/go/issues/27077

[11]

aclements: https://github.com/aclements

[12]

support gotls request and response #357: https://github.com/gojue/ecapture/pull/357

[13]

cfc4n/go_uretprobe_demo: https://github.com/cfc4n/go_uretprobe_demo




golang uretprobe的崩潰原因與模擬實現(xiàn)的評論 (共 條)

分享到微博請遵守國家法律
罗平县| 无为县| 大同市| 赫章县| 深泽县| 梅河口市| 武强县| 永嘉县| 卢龙县| 安国市| 沧州市| 平谷区| 松桃| 临海市| 防城港市| 炉霍县| 玛纳斯县| 高阳县| 贵港市| 商南县| 灵武市| 新和县| 万宁市| 六盘水市| 刚察县| 同心县| 高阳县| 龙游县| 文昌市| 察隅县| 八宿县| 宜宾县| 大方县| 昆明市| 乃东县| 沾益县| 布拖县| 荥经县| 茂名市| 芦溪县| 阿鲁科尔沁旗|