Ftrace function graph簡介

引言

由於android開發的需要與systrace的普及,現在大家在進行性能與功耗分析時候,經常會用到systrace跟pefetto. 而systrace就是基於內核的event tracing來實現的。以如下的一段pefetto爲例。可以看到tid=1845的線程,在被喚醒到CPU5上之後,在runnable狀態上維持了503us纔開始運行,一共運行了498us.

image

image

通過查找systrace裏面的原始events信息,具體如下

image

我們可以從systrace找到相應的trace events的信息

image

可以看到上面systrace顯示的runnable狀態,其實是通過解析sched_waking及sched_switch事件來獲取到的(237.160859 - 237.160356 = 0.000503),而running時間是通過2次sched_switch事件解析出來的(237.161357 - 237.160859 = 0.000498)

一、ftrace function graph是什麼

除了上面提到的trace events之外,tracer提供了很多其餘的功能(如下的config宏開關),本文主要介紹function graph的實現。

CONFIG_FUNCTION_TRACER=y

CONFIG_FUNCTION_GRAPH_TRACER=y

CONFIG_CONTEXT_SWITCH_TRACER=y

CONFIG_NOP_TRACER=y

#CONFIG_SHADOW_CALL_STACK is not set

通過打開上面的一些宏定義,並且關閉CONFIG_SHADOW_CALL_STACK(具體爲什麼要關閉這個宏,後面再講)。我們可以看到如下的一些tracer。

/sys/kernel/tracing # cat available_tracers

blk function_graph preemptirqsoff preemptoff irqsoff function nop

通過echo xxxx > current_tracer可以動態切換tracer

/sys/kernel/tracing # cat current_tracer

nop

/sys/kernel/tracing # echo function_graph > current_tracer

/sys/kernel/tracing # cat current_tracer

function_graph

最終看到的trace信息如下圖。我們可看到進程的內核函數的調用關係,並且可以看到每一個函數的執行時間(又一個性能調試神器)。

image

二、打開function graph時做了什麼

那麼具體內核是如何實現這個功能的呢?

linux在打開ftrace的相關編譯宏之後,在編譯的時候加入gcc的-pg編譯選項。在函數中加入_mcount函數。以cpu_up函數爲例,通過反彙編內核的kernel/cpu.c文件,可以看到如下彙編代碼。

image

可以看到在做完一系列壓棧準備之後,直接跳轉到了_mcount函數。這個函數定義在arch/arm64/kernel/entry-ftrace.S文件裏面。最終函數調用到了ftrace_caller函數。

image

在詳細進入這段彙編代碼的解釋之前,我們先看一下在設置current_tracer的時候具體發生了什麼。通過寫current_tracer節點來切換tracer的話,調到了內核的tracing_set_trace_write函數,如果是使用function_graph的話,最終調用了函數ftrace_enable_ftrace_graph_caller

image

這個函數比較重要:

  1. 獲取ftrace_graph_call這個函數的地址,放到pc這個變量裏面
    2.通過aarch64_insn_gen_branch_imm 函數,產生一條到ftrace_graph_caller的跳轉指令。

  2. 最終通過ftrace_modify_code來修改ftrace_graph_call原來所在位置的代碼(步驟2中產生的跳轉指令,這樣可以直接跳轉到ftrace_graph_caller這個函數)

image

三、function graph的功能實現

下面我們看一下_mcount函數, 第一個是mcount_enter宏。
image

這裏面要不得不提到ARM64平臺的ABI(Application Binary Interface)

image

簡而言之,X0~X7寄存器用來進行函數的傳參,X29作爲FP(frame pointer,幀指針,用來指向一段函數的棧頂,注意不是整個程序的棧頂),X30作爲LR(link register)。

所以其實mcount_enter函數就是將FP及LR寄存器的值保持在棧裏面。同時將當前的棧指針SP作爲新函數的FP(frame pointer)。

ENTRY(ftrace_caller)

mcount_enter

mcount_get_pc0 x0 // function's pc

mcount_get_lr x1 // function's lr

.global ftrace_call

ftrace_call: // tracer(pc, lr);

nop // This will be replaced with "bl xxx"

// where xxx can be any kind of tracer.

#ifdef CONFIG_FUNCTION_GRAPH_TRACER

.global ftrace_graph_call

ftrace_graph_call: // ftrace_graph_caller();

nop // If enabled, this will be replaced

// "b ftrace_graph_caller"

#endif

mcount_exit

ENDPROC(ftrace_caller)

image

由於我們在使能function graph的時候在ftrace_enable_ftrace_graph_caller裏面把ftrace_graph_call地址所在的nop指令改成了b ftrace_graph_caller(注意這裏面是無返回的跳轉,沒有保存lr)

ENTRY(ftrace_graph_caller)

mcount_get_lr_addr x0 // pointer to function's saved lr

mcount_get_pc x1 // function's pc

mcount_get_parent_fp x2 // parent's fp

bl prepare_ftrace_return // prepare_ftrace_return(&lr, pc, fp)

mcount_exit

ENDPROC(ftrace_graph_caller)

我們前面說到了X0~X7是默認用來進行參數傳遞的。在跳轉到prepare_ftrace_return之前,先準備一下傳入參數。這裏面的prepare_ftrace_return函數是C語言的,我們看一下這個函數的3個輸入參數。

void prepare_ftrace_return(unsigned long *parent, unsigned long self_addr,unsigned long frame_pointer)

image

prepare_ftrace_return函數裏面,除了function_graph_enter

之外,最重要的就是*parent = return_hooker.

這個代碼非常重要!parent指針具體指向哪裏?

我們再次回到ftrace_graph_caller函數裏面準備parent參數的地方。

mcount_get_lr_addr x0 // pointer to function's saved lr

image

看起來有點難懂,我們再次回到mcount_enter函數。

image

將X29(FP)與X30(LR)寄存器的內容壓棧,然後當前的棧地址設置爲當前函數的FP。

image

由於棧是遞減的,所以這張圖的上面是棧的高地址,下面是低地址部分。隨着函數調用,棧從下往上遞減。再回到代碼

image

X29即FP,爲當前函數的棧頂。由於棧地址是遞減的,所以[X29]裏面保持的內容,就是下圖中的箭頭指向的FP,即函數的棧頂。

image

而[X29] + 8 ,就是綠框所在的地址(注意是地址,是一個指針)。

*parent = return_hooker就是將函數在棧裏面保存的LR值給改成了return_hooker。

會產生什麼結果呢?

image

依然以上面的cpu_up的彙編代碼爲例,首先通過壓棧將LR、FP寄存器的內容保存在棧裏。在函數結束時,通過ldr x29, x30, [SP], #32將棧裏面的LR及FP的內容恢復到寄存器裏面。然後最終直接ret指令。這樣在函數調用中就實現了“從哪兒來,回哪兒去”。

但是執行了*parent = return_hooker這條代碼之後,棧內的LR的內容就被改變了。

函數會返回執行return_to_handler函數。

這一段依然是彙編代碼。

image

其中save_return_regs將X0~X7的值保持在棧裏面,restore_return_regs

用於將內容重新restore到寄存器裏面。

image

爲什麼要這麼做呢?因爲這時候函數主體已經執行完了,應該返回父函數繼續往下跑。但是因爲開啓了function graph,這時候並沒有直接返回父函數繼續執行,而是在執行return_to_handler函數。這時候X0X7裏面保持了一些返回值(函數主體的執行結果,需要返回給調用的地方進行返回值的判斷),而且X0X7(見Aarch64 ABI)本身又是用來進行參數傳遞的,會用來給return_to_handler的一些子函數ftrace_return_to_handler進行傳參。所以爲了防止這些返回值被破壞,就臨時保持在棧裏面。

在prepare_ftrace_return裏面,除了替換了函數的LR之外,還將原來的LR的值進行了保存。調用ftrace_push_return_trace函數將old的LR值(即原始的LR返回地址)保存在current->ret_stack[index].ret = ret; 裏面(可以看到function graph之後,task_struct結構體裏面增加了不少字段)。最終通過調用ftrace_pop_return_trace將LR的值恢復。這樣回到了正常的父函數裏面繼續往下執行了。

四、小結

本文介紹了ftrace的function graph tracer,通過在函數的調用開始及調用結束分別調用了prepare_ftrace_return及ftrace_return_to_handler來進行LR的修改與恢復。這樣可以統計到每一個函數的調用關係與具體執行時間(在開始與結束時分別記錄了時間)。該功能可以幫助讀者在性能調試的時候識別到性能瓶頸,以便於後期的進一步性能優化調優。

由於在執行過程中要動態的修改棧內容,所以需要關閉CONFIG_SHADOW_CALL_STACK;在比較舊的內核版本上是需要關閉CONFIG_STRICT_MEMORY_RWX和KERNEL_TEXT_RDONLY, 因爲代碼段是隻讀的,不允許動態修改。在linux內核的熱補丁中也用到類似的技術。

當然有些函數用notrace進行修飾,如u64 notrace trace_clock(void)。具體原因留給讀者思考。通過ftrace function graph的整個代碼的學習,我們可以再次梳理一下在arm64架構上函數之間的調用是如何實現的、aarch64上一些ABI的規範要求的參數傳遞方式與結果返回方式。

本文基於kernel-4.19的代碼進行解讀分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章