Linux 內核調試工具ftrace 之(_mcount的實現原理)
ftrace
是 Linux 內核中的一種跟蹤工具,主要用于性能分析、調試和內核代碼的執行跟蹤。它通過在內核代碼的關鍵點插入探針(probe)來記錄函數調用和執行信息。這對于開發者排查問題、優化性能或者理解內核行為非常有用。
linux中主要支持兩種ftrace的實現方式:
_mcount
機制,(主要在內核為5.10前版本)- 雙
NOP
指令動態插樁機制(主要在內核為5.10及以后版本),見文章《ftrace之雙nop機制實現原理》
下面將分別深入介紹兩種機制的實現原理:
一、_mcount
機制的實現
* Gcc with -pg will put the following code in the beginning of each function:* mov x0, x30* bl _mcount* [function's body ...]* "bl _mcount" may be replaced to "bl ftrace_caller" or NOP if dynamic* ftrace is enabled.
- gcc編譯內核時加上
-pg
選項將會在每個支持被插樁的函數前面插入mov x0, x30
與bl _mcount
指令。 - 如果開啟了動態插樁,那
bl _mcount
會被bl ftrace_caller
或NOP
指令替換,當需要對該函數進行追蹤時,將重新插入bl _mcount
,取消追蹤時會重新替換為bl ftrace_caller
或NOP
指令。這樣會降低ftrace對性能的損耗。
_mcount
入口的分析
- 下面是實際的編譯的驅動函數匯編代碼:
_mcount
被插樁在函數的b74
地址處(同樣mov x0, x30
也被插樁)。
0000000000000b58 <pcie_adc_ioctl>:b58: a9bd7bfd stp x29, x30, [sp, #-48]!b5c: 910003fd mov x29, spb60: a90153f3 stp x19, x20, [sp, #16]b64: d50320ff xpaclrib68: 2a0103f4 mov w20, w1b6c: aa1e03e0 mov x0, x30b70: aa0203f3 mov x19, x2b74: 94000000 bl 0 <_mcount>b78: 90000000 adrp x0, 0 <__stack_chk_guard>b7c: f9400001 ldr x1, [x0]b80: f90017e1 str x1, [sp, #40]
- 插樁的兩條指令并不是插入在函數的最前面第一、二地址處,而是在該函數將該函數的棧分配好以及保存好現場后再進行插樁。
- 下述的三點是編譯器默認的規定(
x0-x8
andx18-x30
are live (x18
holds the Shadow Call Stack pointer), andx9-x17
are safe to clobber.)即:- 將父函數的
FP
、父函數的返回地址lr
入棧(即x29
與x30
)。stp x29, x30, [sp, #-48]!
保護FP
、lr
以及函數棧的分配
x18~x28
中后續函數體要用到的寄存器進行入棧保存,如果用不到則不用入棧保存stp x19, x20, [sp, #16]
- 如果
x0~x7
中為函數傳參則也需要將對應的寄存器進行保存(一般保存到x18~x26
寄存器中),參數的傳遞一般是前8個參數由x0~x7
寄存器,后面的參數都有棧進行傳遞。所以在被調用函數中如果要用到調用者傳入的寄存器中的參數就需要保存。mov w20, w1
mov x19, x2
- 由于在該函數中并沒有用到第一個參數,所以編譯器就進行優化了,沒有進行x0寄存器值保存。
- 將父函數的
- 在上面的現場保存后函數棧的分布如下圖:
- 然后跳轉到
_mcount
.macro mcount_enterstp x29, x30, [sp, #-16]!mov x29, sp
.endm
SYM_FUNC_START(_mcount)mcount_enterldr_l x2, ftrace_trace_functionadr x0, ftrace_stubcmp x0, x2 // if (ftrace_trace_functionb.eq skip_ftrace_call // != ftrace_stub) {mcount_get_pc x0 // function's pcmcount_get_lr x1 // function's lr (= parent's pc)blr x2 // (*ftrace_trace_function)(pc, lr);skip_ftrace_call: // }
#ifdef CONFIG_FUNCTION_GRAPH_TRACERldr_l x2, ftrace_graph_returncmp x0, x2 // if ((ftrace_graph_returnb.ne ftrace_graph_caller // != ftrace_stub)ldr_l x2, ftrace_graph_entry // || (ftrace_graph_entryadr_l x0, ftrace_graph_entry_stub // != ftrace_graph_entry_stub))cmp x0, x2b.ne ftrace_graph_caller // ftrace_graph_caller();
#endif /* CONFIG_FUNCTION_GRAPH_TRACER */mcount_exit
SYM_FUNC_END(_mcount)
-
進去也是對
x29, x30
(FP 和 LR)進行保存(FP為棧基指針) -
這時候的棧分布如下圖:
- 對
mcount_get_pc x0
指令取到追蹤函數B的地址的分析:mcount_get_pc x0
->ldr x0, [x29, #8]
可以看出是FP_M + 8的地址處的值給x0,即LR_B給到x0,剛好LR_B就是B中bl _mcount
指令下一條指令地址。
- 對
mcount_get_lr x1
指令取到調用者函數的地址的分析:mcount_get_lr x1
->ldr x1, [x29]
以及ldr x1, [x1, #8]
,可以看出第一條指令ldr x1, [x29]
從FP_M的地址處取到內容FP_B存到x1中,然后第二條指令ldr x1, [x1, #8]
從x1 + 8(= FP_B + 8)地址處取到內容LR_A給到x1,這樣就取到了A的LR地址,即調用者函數的返回地址。
- 經過上面的分析可以看到對于調用者A以及被追蹤者B函數的內容以及返回地址都可以拿到并保存。
- 接下來就是進入對應的追蹤器執行。
- 保存必要的信息,比如LR_A、LR_B、FP_A、FP_B等,并做其他ftrace的信息處理,然后將BL到LR_B中繼續執行完B函數(進入B函數時LR寄存器的地址為實際trace回調函數中的地址)。
- 當B函數執行完后,返回到trace回調函數,在trace函數中做該被追蹤函數B的記錄結尾,然后將直接返回到函數A繼續執行了。
- 對于超過8個參數的參數讀取也不受限制,直接通過父函數的FP指針訪問(并沒有破壞該函數的棧)。
至此bl _mcount
機制的實現原理已經解釋完,其他的就是對ftrace具體回調函數中的一些工作,這里就不再說明(主要是記錄函數調用運行的一些信息,并放入到ring buf中,開放應用層接口供應用層查看)。大致跳轉流程圖如下:
具體的ftrace操作
見文章《ftrace-內核調試工具》