Linux 內(nèi)核熱補(bǔ)丁可以修復(fù)正在運(yùn)行的 linux 內(nèi)核,是一種維持線上穩(wěn)定性不可缺少的措施,現(xiàn)在比較常見(jiàn)的比如 kpatch 和 livepatch。內(nèi)核熱補(bǔ)丁可以修復(fù)內(nèi)核中正在運(yùn)行的函數(shù),用已修復(fù)的函數(shù)替換掉內(nèi)核中存在問(wèn)題的函數(shù)從而達(dá)到修復(fù)目的。
函數(shù)替換的思想比較簡(jiǎn)單,就是在執(zhí)行舊函數(shù)時(shí)繞開(kāi)它的執(zhí)行邏輯而跳轉(zhuǎn)到新的函數(shù)中,有一種比較簡(jiǎn)單粗暴的方式,就是將原函數(shù)的第一條指令修改為“ jump 目標(biāo)函數(shù)”指令,即直接跳轉(zhuǎn)到新的函數(shù)以達(dá)到替換目的。
那么,問(wèn)題來(lái)了,這么做靠譜嗎?直接將原函數(shù)的第一條指令修改為 jump 指令,會(huì)破壞掉原函數(shù)和它的調(diào)用者之間的寄存器上下文關(guān)系,存在安全隱患!本文會(huì)針對(duì)該問(wèn)題進(jìn)行探索和驗(yàn)證。
安全性沖擊:?jiǎn)栴}呈現(xiàn)
對(duì)于函數(shù)調(diào)用,假設(shè)存在這樣兩個(gè)函數(shù) funA 和 funB,其中 funA 調(diào)用 funB 函數(shù),這里稱 funA 為 caller(調(diào)用者),funB 為 callee(被調(diào)用者),funA 和 funB 都使用了相同的寄存器 R,如下所示:
圖1 funA 和 funB 都使用了寄存器 R,funA 再次使用 R 時(shí)已經(jīng)被 funB 修改
因此,當(dāng) funA 再次使用到 R 的數(shù)據(jù)已經(jīng)是錯(cuò)誤的數(shù)據(jù)了。如果 funA 在調(diào)用 funB 前保存寄存器 R 中的數(shù)據(jù),funB 返回后再將數(shù)據(jù)恢復(fù)到 R 中,或者 funB 先保存 R 中原有的數(shù)據(jù),然后在返回前恢復(fù),就可以解決這類問(wèn)題。
唯一的調(diào)用約定
那寄存器該由 caller 還是 callee 來(lái)保存?這就需要遵循函數(shù)的調(diào)用約定(call convention),不同的 ABI 和不同的平臺(tái),函數(shù)的調(diào)用約定是不一樣的,對(duì)于 Linux 來(lái)說(shuō),它遵循的是 System V ABI 的 call convention,x86_64 平臺(tái)下函數(shù)調(diào)用約定有且只有一種,調(diào)用者 caller 和被調(diào)用者 callee 需要對(duì)相應(yīng)的寄存器進(jìn)行保存和恢復(fù)操作:
Caller-save registers : RDI, RSI, RDX, RCX, R8, R9, RAX, R10, R11
Callee-save registers : RBX, RBP, R12, R13, R14, R15
調(diào)用約定,gcc 它遵守了嗎?
設(shè)問(wèn):當(dāng)函數(shù)實(shí)現(xiàn)很簡(jiǎn)單,只用到了少量寄存器,那沒(méi)使用到的還需要保存嗎?
答案:it depends。根據(jù)編譯選項(xiàng)決定。
眾所周知,GCC 編譯器有 -O0、-O1、-O2 和 -Ox 等編譯優(yōu)化選項(xiàng),優(yōu)化范圍和深度隨 x 增大而增大(-O0是不優(yōu)化,其中隱含的意思是,它會(huì)嚴(yán)格遵循 ABI 中的調(diào)用約定,對(duì)所有使用的寄存器進(jìn)行保存和恢復(fù))。
Linux 內(nèi)核選用的都是 -O2 優(yōu)化。GCC 會(huì)選擇性的不遵守調(diào)用約定,也就是設(shè)問(wèn)里提到的,不需要保存沒(méi)使用到的寄存器。當(dāng)【運(yùn)行時(shí)替換】撞見(jiàn)【調(diào)用約定】
GCC 之所以可以做這個(gè)優(yōu)化,是因?yàn)?GCC 高屋建瓴,了解程序的執(zhí)行流。當(dāng)它知道 callee,caller 的寄存器分配情況,就會(huì)大膽且安全地做各種優(yōu)化。
但是,運(yùn)行時(shí)替換破壞了這個(gè)假設(shè),GCC 所掌握的 callee 信息,極有可能是錯(cuò)誤的。那么這些優(yōu)化可能會(huì)引發(fā)嚴(yán)重問(wèn)題。這里以一個(gè)具體的實(shí)例進(jìn)行詳細(xì)說(shuō)明,這是一個(gè)用戶態(tài)的例子( x86_64 平臺(tái)):
//test.c 文件//編譯命令:gcc test.c -o test -O2 (kernel 采用的是 O2 優(yōu)化選項(xiàng))//執(zhí)行過(guò)程:。/test//輸入參數(shù):4
#include 《sys/mman.h》#include 《string.h》#include 《stdio.h》#include 《math.h》
#define noinline __attribute__ ((noinline)) //禁止內(nèi)聯(lián)
static noinline int c(int x){ return x * x * x;}
static noinline int b(int x){ return x;}
static noinline int newb(int x){ return c(x * 2) * x;}
static noinline int a(int x){ int volatile tmp = b(x); // tmp = 8 ** 3 * 4 return x + tmp; // return 4(not 8) + tmp}
int main(void){ int x; scanf(“%d”, &x);
if (mprotect((void*)(((unsigned long)&b) & (~0xFFFF)), 15, PROT_WRITE | PROT_EXEC | PROT_READ)) { perror(“mprotect”); return 1; }
/* 利用 jump 指令將函數(shù) b 替換為 newb 函數(shù) */ ((char*)b)[0] = 0xe9; *(long*)((unsigned long)b + 1) = (unsigned long)&newb - (unsigned long)&b - 5; printf(“%d”, a(x)); return 0;}
程序解釋:該程序是對(duì)輸入的數(shù)字進(jìn)行計(jì)算,運(yùn)行時(shí)利用 jump 指令將程序中的函數(shù) b 替換為 newb 函數(shù),即,將 y = x + x 計(jì)算過(guò)程替換為 y = x + (2x) ^ 3 * x;
程序編譯:gcc test.c -o test -O2,這里我們采用的是與編譯內(nèi)核相同的優(yōu)化選項(xiàng) -O2;
程序執(zhí)行:。/test,輸入?yún)?shù):4,輸出結(jié)果:2056;
程序錯(cuò)誤:2056 是錯(cuò)誤的結(jié)果,應(yīng)該是 2052,而且直接調(diào)用 newb 函數(shù)編譯執(zhí)行的結(jié)果是 2052。
該例子說(shuō)明,直接使用 jump 指令替換函數(shù)在 -O2 的編譯優(yōu)化下,會(huì)出現(xiàn)問(wèn)題,安全性受到了質(zhì)疑和沖擊!??!
安全性沖擊:分析問(wèn)題
上述例子中,我們將函數(shù) b 用 jump 指令替換為 newb 函數(shù),在 -O2 的編譯優(yōu)化下出現(xiàn)了計(jì)算錯(cuò)誤的結(jié)果,因此,我們需要對(duì)函數(shù)的調(diào)用執(zhí)行過(guò)程進(jìn)行仔細(xì)分析,挖掘問(wèn)題所在。首先,我們先來(lái)查看一下該程序的反匯編(指令:objdump -d test),并重點(diǎn)關(guān)注 a、b 和 newb 函數(shù):
圖2 -O2 編譯優(yōu)化的反匯編結(jié)果
匯編解釋:main:-》 將參數(shù) 4 存放到 edi 寄存器中-》 調(diào)用 a 函數(shù):-》 調(diào)用 b 函數(shù),直接跳轉(zhuǎn)到 newb 函數(shù): -》 將 edi 寄存器中的值存放到 edx 寄存器 -》 edi 寄存器與自身相加后結(jié)果放入 edi -》 調(diào)用 c 函數(shù): -》 將 edi 寄存器中的值存到 eax 寄存器 -》 edi 乘以 eax 后結(jié)果放入 eax -》 edi 乘以 eax 后結(jié)果放入 eax -》 返回到 newb 函數(shù) -》 將 edx 與 eax 相乘后結(jié)果放入 eax-》 返回到 a 函數(shù)-》 將 edi 與 eax 相加后結(jié)果放入 eax-》 返回 main 函數(shù)
(注意:b 函數(shù)中沒(méi)有對(duì) edi 寄存器進(jìn)行寫(xiě)操作,而且它的代碼段被修改為 jump 指令跳轉(zhuǎn)到 newb 函數(shù))
數(shù)據(jù)出錯(cuò)的原因在于,在函數(shù) newb 中,使用到了 a 函數(shù)中使用的 edi 寄存器,edi 寄存器中的值在 newb 函數(shù)中被修改為 8,當(dāng) newb 函數(shù)返回后,edi 的值仍然是 8,a 函數(shù)繼續(xù)使用了該值,因此,計(jì)算過(guò)程變?yōu)椋?^3 * 4 + 8 = 2056,而正確的計(jì)算結(jié)果應(yīng)該是 8^3 * 4 + 4 = 2052。
接下來(lái)不進(jìn)行編譯優(yōu)化(-O0),其輸出結(jié)果是正確的 2052,反匯編如下所示:
圖3 不進(jìn)行編譯優(yōu)化的反匯編
從反匯編中可以看到,函數(shù) a 在調(diào)用 b 函數(shù)前,將 edi 寄存器的值存在了棧上,調(diào)用之后,將棧上的數(shù)據(jù)再取出,最后進(jìn)行相加。這就說(shuō)明,-O2 優(yōu)化選項(xiàng)將 edi 寄存器的保存和恢復(fù)操作優(yōu)化掉了,而在調(diào)用約定中,edi 寄存器本就該屬于 caller 進(jìn)行保存/恢復(fù)的。至于為什么編譯器會(huì)進(jìn)行優(yōu)化,我們此刻的猜想是:
a 函數(shù)本來(lái)調(diào)用的是 b 函數(shù),而且編譯器知道 b 函數(shù)中沒(méi)有使用到 edi 寄存器,因此調(diào)用者 a 函數(shù)沒(méi)有對(duì)該寄存器進(jìn)行保存和恢復(fù)操作。但是編譯器不知道的是,在程序運(yùn)行時(shí),b 函數(shù)的代碼段被動(dòng)態(tài)修改,利用 jump 指令替換為 newb 函數(shù),而在 newb 函數(shù)中對(duì) edi 寄存器進(jìn)行了數(shù)據(jù)讀寫(xiě)操作,于是出現(xiàn)了錯(cuò)誤。
這是一個(gè)典型的沒(méi)有保存 caller-save 寄存器導(dǎo)致數(shù)據(jù)出錯(cuò)的場(chǎng)景。而編譯內(nèi)核采用的也是 -O2 選項(xiàng)。如果將該場(chǎng)景應(yīng)用到內(nèi)核函數(shù)熱替換是否會(huì)出現(xiàn)這類問(wèn)題呢?于是,我們帶著問(wèn)題繼續(xù)探索。
安全性沖擊:探索問(wèn)題
不再觀察到 bug
我們構(gòu)造了一個(gè)內(nèi)核函數(shù)熱替換的實(shí)例,將上面的用戶態(tài)的例子移植到我們構(gòu)造的場(chǎng)景中,通過(guò)內(nèi)核模塊修改原函數(shù)的代碼段,用 jump 指令直接替換原來(lái)的 b 函數(shù)。然而加載模塊后,結(jié)果是正確的 2052,經(jīng)過(guò)反匯編我們發(fā)現(xiàn),內(nèi)核中 a 函數(shù)對(duì) edi 寄存器進(jìn)行了保存操作:
圖4 內(nèi)核中 a 函數(shù)的反匯編
內(nèi)核和模塊編譯時(shí)采用的是 -O2 優(yōu)化選項(xiàng),而此處 a 函數(shù)并沒(méi)有被優(yōu)化,仍然保存了 edi 寄存器。
此時(shí)我們預(yù)測(cè):對(duì)于內(nèi)核函數(shù)的熱替換來(lái)說(shuō),使用 jump 做函數(shù)替換是安全的。
神奇的 -pg 選項(xiàng)
我們猜想是否是內(nèi)核編譯時(shí)使用其它的編譯選項(xiàng)導(dǎo)致問(wèn)題不能復(fù)現(xiàn)。果不其然,經(jīng)過(guò)探索我們發(fā)現(xiàn)內(nèi)核編譯使用的 -pg 選項(xiàng)導(dǎo)致問(wèn)題不再?gòu)?fù)現(xiàn)。
通過(guò)翻閱 GCC 手冊(cè)得知,-pg 選項(xiàng)是為了支持 GNU 的 gprop 性能分析工具所引入的,它能在函數(shù)中增加一條 call mount 指令,去做一些分析工作。
在內(nèi)核中,如果開(kāi)啟了 CONFIG_FUNCTION_TRACER,則會(huì)使能 -pg 選項(xiàng)。
圖5 開(kāi)啟 CONFIG_FUNCTION_TRACER 使能 -pg 選項(xiàng)
FUNCTION_TRACE 即我們常說(shuō)的 ftrace 功能,ftrace 大大提升了內(nèi)核的運(yùn)行時(shí)調(diào)試能力。ftrace 功能除了 -pg 選項(xiàng),還要求打開(kāi) -mfentry 選項(xiàng),后者的作用是將函數(shù)對(duì) mcount 的調(diào)用放到函數(shù)的第一條指令處,然后通過(guò) scripts/recordmcount.pl 腳本將該條 call 指令修改為 nop 指令。但 -mfentry 與本文主題沒(méi)有關(guān)聯(lián),不再細(xì)說(shuō)。
為了驗(yàn)證這個(gè)結(jié)論,我們回到上一節(jié)的用戶態(tài)例子,并且增加了 -pg 編譯選項(xiàng):“gcc test.c -o test -O2 -pg”,此時(shí)運(yùn)行結(jié)果果然正確了。查看其反匯編:
圖6 增加 -pg 選項(xiàng)后的匯編
可以看到,每個(gè)函數(shù)都有 call mcount 指令,而且 a 函數(shù)中將 edi 寄存器保存到 ebx 中,在 newb 函數(shù)中又保存 ebx 寄存器。為什么在增加了 call mount 指令后,會(huì)做寄存器的保存操作?我們猜想,會(huì)不會(huì)是因?yàn)椋捎?call mount 操作相當(dāng)于調(diào)用了一個(gè)未知的函數(shù)( mcount 沒(méi)有定義在同一個(gè)文件中),因此,GCC 認(rèn)為這樣未知的操作可能會(huì)污染了寄存器的數(shù)據(jù),所以它才進(jìn)行了保存現(xiàn)場(chǎng)的操作。
于是我們?nèi)サ袅?-pg 選項(xiàng),手動(dòng)增加了 call mount 的行為進(jìn)行驗(yàn)證:在另一個(gè)源文件 mcount.c 中增加一個(gè)函數(shù) void mcount() { asm(“nop ”); },在 test.c 文件中增加對(duì) mcount 函數(shù)的聲明,a 函數(shù)中增加對(duì)該函數(shù)的調(diào)用:
extern void mcount(); //聲明 mcount 函數(shù)
static noinline int a(int x){ int volatile tmp = b(x); // tmp = 8 ** 3 * 4 mcount(); return x + tmp; // return 4(not 8) + tmp}
經(jīng)過(guò)編譯:gcc test.c mcount.c -O2 后運(yùn)行,發(fā)現(xiàn)計(jì)算結(jié)果正確,而且反匯編中 a 函數(shù)保存了寄存器:
圖7 調(diào)用 mcount 函數(shù)后的匯編
繼續(xù)驗(yàn)證猜想,將 mcount 函數(shù)放在 test.c 文件中,計(jì)算結(jié)果錯(cuò)誤,而且,反匯編中沒(méi)有保存寄存器,于是我們得到了這樣的猜想結(jié)論:
GCC 在編譯某個(gè)源文件時(shí),如果文件內(nèi)的某個(gè)函數(shù)(比如場(chǎng)景中的函數(shù) a)調(diào)用了其它文件中的一個(gè)未知函數(shù)(比如場(chǎng)景中的 mcount 函數(shù)),則 GCC 會(huì)在該函數(shù)中保存寄存器;
開(kāi)啟 -pg 選項(xiàng),增加了對(duì) mcount 的調(diào)用,因此會(huì)在函數(shù)中增加對(duì)寄存器現(xiàn)場(chǎng)的保存操作,對(duì) -O2 選項(xiàng)的函數(shù)調(diào)用優(yōu)化起到了屏蔽作用。
神秘的 -fipa-ra 選項(xiàng):真正的幕后主使
經(jīng)過(guò)我們的探索和資料的查閱,發(fā)現(xiàn)了這個(gè) -fipa-ra 選項(xiàng),可以說(shuō)它是優(yōu)化的幕后主使。GCC 手冊(cè)中給出 -fipa-ra 選項(xiàng)的解釋是:
Use caller save registers for allocation if those registers are not used by any called function. In that case it is not necessary to save and restore them around calls. This is only possible if called functions are part of same compilation unit as current function and they are compiled before it. Enabled at levels -O2, -O3, -Os, however the option is disabled if generated code will be instrumented for profiling (-p, or -pg) or if callee’s register usage cannot be known exactly (this happens on targets that do not expose prologues and epilogues in RTL)。
這里主要是說(shuō),如果開(kāi)啟這個(gè)選項(xiàng),那么,callee 中如果沒(méi)有使用到 caller 使用的寄存器,就沒(méi)有必要保存這些寄存器,前提是,callee 與 caller 在同一個(gè)編譯單元中而且 callee 函數(shù)比 caller 先被編譯,這樣才可能出現(xiàn)前面的優(yōu)化。如果開(kāi)啟了 -O2 及以上的編譯優(yōu)化選項(xiàng),則會(huì)使能 -fipa-ra 選項(xiàng),然而,如果開(kāi)啟了 -p 或者 -pg 這些選項(xiàng),或者,無(wú)法明確 callee 所使用的寄存器,-fipa-ra 選項(xiàng)會(huì)被禁用。
這段話,其實(shí)已經(jīng)能 cover 掉我們前面大部分猜想的測(cè)試驗(yàn)證:
-O2 選項(xiàng)自動(dòng)使能 -fipa-ra 進(jìn)行優(yōu)化:在我們的場(chǎng)景中,函數(shù) a 使用的 edi 寄存器,在函數(shù) b 中沒(méi)有使用到,因此函數(shù) a 被優(yōu)化,沒(méi)有保存 edi 寄存器,但是在 newb 函數(shù)中,使用到了 edi 寄存器,且數(shù)據(jù)被修改,將 newb 函數(shù)替換函數(shù) b,則計(jì)算結(jié)果出錯(cuò);
在 -O2 中使用 -pg 選項(xiàng)會(huì)禁用 -fipa-ra:編譯時(shí)使用 -pg 選項(xiàng),計(jì)算結(jié)果是正確的,而且函數(shù) a 保存了 edi 寄存器,說(shuō)明沒(méi)有對(duì)函數(shù) a 進(jìn)行優(yōu)化;
不在同一編譯單元不會(huì)被優(yōu)化:去掉 -pg 選項(xiàng),在函數(shù) a 中手動(dòng)調(diào)用 mcount 函數(shù),將這個(gè)函數(shù)放在 test.c(與函數(shù) a 為同一編譯單元)與放在另一個(gè)文件 mcount.c(不同編譯單元)中的計(jì)算結(jié)果不同:同一編譯單元中計(jì)算結(jié)果是錯(cuò)誤的,而且函數(shù) a 沒(méi)有保存寄存器現(xiàn)場(chǎng);不在同一編譯單元中,計(jì)算結(jié)果是正確的,函數(shù) a(caller) 保存了寄存器現(xiàn)場(chǎng),因?yàn)榫幾g器無(wú)法明確函數(shù) b(callee)所使用的寄存器。
notrace:它是二度沖擊嗎?
用過(guò) ftrace 或者內(nèi)核開(kāi)發(fā)者應(yīng)該對(duì) notrace 屬性不陌生,內(nèi)核中有一些被 notrace 修飾的函數(shù)。notrace 其實(shí)就是給函數(shù)增加 no_instrument_function 屬性。例如,在 X86 的定義:
#define notrace __attribute__((no_instrument_function))
字面上來(lái)看,notrace 和 -pg 的含義可以說(shuō)完全對(duì)立,-pg 讓 jump 變得安全,是否又會(huì)在 notrace 上栽一個(gè)跟斗呢?幸運(yùn)的是,我們接下來(lái)將看到,notrace 僅僅是禁止了 instrument function,而沒(méi)有破壞安全性。
gcc 手冊(cè)中的 -pg 選項(xiàng)給出這樣的解釋:
Generate extra code to write profile information suitable for the analysis program prof (for -p) or gprof (for -pg)。 You must use this option when compiling the source files you want data about, and you must also use it when linking. You can use the function attribute no_instrument_function to suppress profiling of individual functions when compiling with these options.
這里主要是說(shuō),加上 notrace 屬性的函數(shù),不會(huì)產(chǎn)生調(diào)用 mcount 的行為,那么,是否意味著不再保護(hù)寄存器現(xiàn)場(chǎng),換句話說(shuō),notrace 的出現(xiàn)是否會(huì)繞過(guò)“-pg 選項(xiàng)對(duì) -fipa-ra 優(yōu)化的屏蔽”?于是我們又增加 notrace 屬性進(jìn)行驗(yàn)證:在 a 函數(shù)中增加 notrace 的屬性,因?yàn)?a 函數(shù)是 caller,編譯時(shí)開(kāi)啟 -pg 選項(xiàng),然后檢查計(jì)算結(jié)果及反匯編,最后發(fā)現(xiàn),計(jì)算結(jié)果正確,而且匯編代碼中保存了寄存器現(xiàn)場(chǎng)。
我們又對(duì)所有的函數(shù)追加了 notrace 屬性,計(jì)算結(jié)果正確且寄存器現(xiàn)場(chǎng)被保護(hù)。但是這些簡(jiǎn)單的驗(yàn)證不足以證明,于是我們通過(guò)閱讀 GCC 源碼發(fā)現(xiàn)
通過(guò)源碼閱讀,可以確定的是,當(dāng)使用了 -pg 選項(xiàng)后,會(huì)禁用 -fipa-rq 優(yōu)化選項(xiàng),GCC 檢查每一個(gè)函數(shù)的時(shí)候都會(huì)檢查該選項(xiàng),如果為 false,則不會(huì)對(duì)該函數(shù)進(jìn)行優(yōu)化。
由于 flag_ipa_ra 是一個(gè)全局選項(xiàng),并不是函數(shù)粒度的,notrace 也無(wú)能為力。因此,這里可以排除對(duì) notrace 的顧慮。
安全性保障:得出結(jié)論
經(jīng)過(guò)上述的探索分析以及官方資料的查閱,我們可以得出結(jié)論:
內(nèi)核函數(shù)的熱替換,利用 jump 指令直接跳轉(zhuǎn)到新函數(shù)的方式是安全的;
論據(jù):
Linux 遵循的 System V ABI 中的 call conversion 在 x86-64 下有且只有一種;
GCC -fipa-ra 選項(xiàng)會(huì)對(duì) call conversion 進(jìn)行優(yōu)化,-O2 選項(xiàng)會(huì)自動(dòng)使能該選項(xiàng),但是 -pg 選項(xiàng)會(huì)禁用 -fipa-ra 優(yōu)化選項(xiàng);
notrace 屬性無(wú)法繞過(guò)“ -pg 禁用 -fipa-ra”。
ARM64 下的探索驗(yàn)證
通過(guò)翻閱手冊(cè)得知,ARMv8 ABI 中對(duì)過(guò)程調(diào)用時(shí)通用寄存器的使用準(zhǔn)則如下
Argument registers (X0-X7)
These are used to pass parameters to a function and to return a result. They can be used as scratch registers or as caller-saved register variables that can hold intermediate values within a function, between calls to other functions. The fact that 8 registers are available for passing parameters reduces the need to spill parameters to the stack when compared with AArch32.
Caller-saved temporary registers (X9-X15)
If the caller requires the values in any of these registers to be preserved across a call to another function, the caller must save the affected registers in its own stack frame. They can be modified by the called subroutine without the need to save and restore them before returning to the caller.
Callee-saved registers (X19-X29)
These registers are saved in the callee frame. They can be modified by the called subroutine as long as they are saved and restored before returning.
Registers with a special purpose (X8, X16-X18, X29, X30)
X8 is the indirect result register. This is used to pass the address location of an indirect result, for example, where a function returns a large structure.
X16 and X17 are IP0 and IP1, intra-procedure-call temporary registers. These can be used by call veneers and similar code, or as temporary registers for intermediate values between subroutine calls. They are corruptible by a function. Veneers are small pieces of code which are automatically inserted by the linker, for example when the branch target is out of range of the branch instruction.
X18 is the platform register and is reserved for the use of platform ABIs. This is an additional temporary register on platforms that don‘t assign a special meaning to it.
X29 is the frame pointer register (FP)。
X30 is the link register (LR)。
Figure 9.1 shows the 64-bit X registers. For more information on registers, see 。 For information on floating-point parameters, see Floating-point parameters.
可見(jiàn),ARMv8 ABI 中對(duì)函數(shù)調(diào)用時(shí)的寄存器使用有了明確的規(guī)定。
我們對(duì)于前面 x86-64 下的探索驗(yàn)證過(guò)程在 arm64 平臺(tái)下重新做了測(cè)試,相同的代碼和相同的測(cè)試過(guò)程,得出的結(jié)論和 x86-64 下的結(jié)論是一致的,即,在 arm64 下,直接利用 jump 指令實(shí)現(xiàn)函數(shù)替換同樣是安全的。
其它場(chǎng)景的討論
其它語(yǔ)言不能保證其安全性
對(duì)于 C 語(yǔ)言而言,在不同的架構(gòu)和系統(tǒng)下都有固定的 ABI 和 calling conventions,但是其它的語(yǔ)言不能保證,比如 rust 語(yǔ)言,rust 自身并沒(méi)有固定的 ABI,比如社區(qū)對(duì) rust 定義 ABI 的討論,而且 rustc 編譯器的優(yōu)化和 gcc 可能會(huì)有不同,因此可能也會(huì)出現(xiàn)上述 caller/callee-save 寄存器的問(wèn)題。
kpatch 的真面目
kpatch 利用的是 ftrace 進(jìn)行函數(shù)替換的,它的原理如下所示:
ftrace 的主要作用是用來(lái)做 trace 的,會(huì)在函數(shù)頭部或者尾部 hook 一個(gè)函數(shù)進(jìn)行一些額外的處理,這些函數(shù)在運(yùn)行過(guò)程中可能會(huì)污染被 trace 的函數(shù)的寄存器上下文,因此 ftrace 定義了一個(gè) trampoline 進(jìn)行寄存器的保存和恢復(fù)操作(圖11 中的紅框),這樣從 hook 函數(shù)回來(lái)后,寄存器現(xiàn)場(chǎng)仍然是原來(lái)的模樣。
kpatch 用 ftrace 進(jìn)行函數(shù)替換,hook 的函數(shù)是 kpatch 中的函數(shù),該函數(shù)的作用是修改 regs 中的 ip 字段的值,也就是將新函數(shù)的地址給到了 ip 字段,等 trampoline 恢復(fù)寄存器現(xiàn)場(chǎng)后,就直接跳轉(zhuǎn)到新的函數(shù)函數(shù)去執(zhí)行了。所以,對(duì)于 kpatch 而言,ftrace 的保存和恢復(fù)現(xiàn)場(chǎng)操作保護(hù)的是 kpatch 中修改 ip 字段函數(shù)的過(guò)程,而不是它要替換的新函數(shù)。
如果修復(fù)的是一個(gè)熱函數(shù),那么 ftrace 的 trampoline 會(huì)對(duì)性能產(chǎn)生一定的影響。所以,若考慮到性能的場(chǎng)景,那么使用 jump 指令直接替換函數(shù)可以很大的減少額外的性能開(kāi)銷。
責(zé)任編輯:haq
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1372瀏覽量
40276 -
Linux
+關(guān)注
關(guān)注
87文章
11292瀏覽量
209323 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4327瀏覽量
62569
原文標(biāo)題:內(nèi)核熱補(bǔ)丁,真的安全么?
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論