在現今的 x86 Linux 上,無論 32 位元還是 64 位元系統,我們都可以找到 vDSO 或 vsyscall 的蹤跡,然而,在網路上卻很難找到仔細討論的文章,有時候,即便找到了一些以前的文章,在對照現今的 Linux 核心原始碼時,總會覺得這樣或那樣的似是而非,因此,我決定追溯歷史,弄清楚這些差異源自哪裡。
根據 CPU 發展的過程,x86-64 實際上是由 x86-32 延伸產生的架構,正因如此的淵源,在其中之一發展的技術,多半可以應用到另一方身上,這也導致在 Linux 發展的過程中,有許多技術都是從其中一方移植、改進、然後再移植回去的,vDSO 的發展過程便是如此。
想要了解 vDSO 在 x86-32 或 x86-64 上發展的過程,只從其中一方了解是不夠的,必須同時查訪兩者的發展過程,並加以對照,才能徹底理解。
注意:
本文著重在 vDSO 或 vsyscall 的機制本身的變遷,包括它的實作方法,以及它提供的函式。
在當時的發展過程中,Linux 2.5.x 實際上屬於開發中版本,所以其中描述的改變,最後既有可能出現在 Linux 2.4.x,也有可能出現在 Linux 2.6.0。
x86-64 Linux:vsyscall 的出現
在 i386 版本的 Linux 2.4 上,所有的系統呼叫(system call,通常簡稱為 syscall)的調用方法都是相同的,然而標準的系統呼叫方式一定會牽涉到 CPU 模式(CPU mode)的轉換,需要耗費一些時間,對於大部分的系統呼叫來說,它們本身所需的時間都比這點時間要來的多許多,因此這是可以接受的;但是對某些系統呼叫來說,它們本身所需的時間極短,通常只是讀取某些特定的欄位資料,可是它們又常常會被呼叫到,此時,耗費的時間是否必要,就需重新思量了。
系統呼叫 gettimeofday
就是一個極佳的案例,它僅僅只是讀取核心中的時間資訊,而且對於許多程式來說,讀取系統時間都是必要而且要常常進行的行為。
早在 2000 年時的時候(這只是我所能找到最早資料),核心的開發者們便已經開始討論是否有加速的可能,在討論的過程中,提出了 vsyscall(virtual system call,虛擬系統呼叫)機制,然而,由於種種的顧慮,該機制一直沒有納入 32 位元 x86 的主流程式碼中。
直到 Linux 準備開始支援 x86-64 的時候(在 2002 年釋出的 Linux 2.5.5 找到最早的紀錄1),Andrea Arcangeli2 實現的 vsyscall 機制最終納入了 x86-64 核心之中。這個機制可以分成三個部分:資料存放處、修改該資料的程式碼、讀取該資料的 vsyscall 程式碼,資料存放處會映射到兩個虛擬記憶體位址,一個擁有寫入權限,供核心模式(kernel mode)下修改該資料的程式碼使用;另一個只有讀取權限,供用戶模式(user mode)下讀取該資料的 vsyscall 程式碼使用。
藉由這樣子的分割,當使用者想要調用系統呼叫時,只需調用替代的 vsyscall 程式碼即可,整個過程中就不會牽涉到 CPU 模式的轉換,從而達到減少消耗時間的目的。
而在 x86-64 中,vsyscall 的程式碼以及要被讀取的資料所映射的記憶體頁面,就稱為「vsyscall 頁面」,該頁面的映射起點是 0xffffffffff600000
,該位址同時也是 vgettimeofday
(gettimeofday
的 vsyscall 版本)的位址,而第二個 vsyscall 函式 vtime
則落在 0xffffffffff600000 + 1 KiB
(根據當時的設計方法,每個 vsyscall 函式相差 1 KiB)。
Linux 2.5.53:x86-32 引進 vsyscall
早期 32 位元的 x86 指令集(instruction set)並沒有專門用於系統呼叫的指令,作業系統通常安排用戶模式(user mode)以軟體中斷(software interrupt)的方式來實現系統呼叫的調用,Linux 使用的是 int 0x80
,直到 1997 年 Intel Pentium II(其微架構(microarchitecture)代號為「P6」)面世時,引進了專門用於系統呼叫的指令 sysenter
/sysexit
,使用該指令有著比指令 int 0x80
更快的速度。(使用這類更快速的系統呼叫指令,後來被統稱為快速系統呼叫(fast system call))
然而,由於 sysenter
的運作機制與 int 0x80
大不相同,為了讓作業系統可以保證對舊 CPU 以及舊程式的向下相容,當時 Linux 核心的開發者一直沒有把這套技術納入主流。一直到了 2002 年,在 Linux 核心開發者的 mailing list 上,出現了一篇名為〈Intel P6 vs P7 system call performance〉的文章,經過了大量的討論,判定由於當時 Pentium 4(微架構為「P7」)的設計方式,導致在 Pentium 4 使用 int 0x80
會比 Pentium III 要慢許多倍,於是設計新的方案就成為了勢在必行的選擇。
話雖如此,可是在設計新方案的時候,遇到了一個問題:
sysenter
是 Intel 引進的,AMD 的 64 位元 CPU 在相容模式(compatibility mode)3 下並不支援,它有自己引進的指令syscall
/sysret
。- 但反過來說,Intel 的 CPU 在 32 位元下也不支援
syscall
/sysret
。
正因如此,在編譯應用程式的時候,無論選定 sysenter
或 syscall
都會導致相容性的下降,而設計一套機制讓 C 函式庫(libc)在執行期決定又會降低效能4,最好的方法,是乾脆由核心來決定呼叫的方式。
於是,在 2002 年釋出的 Linux 2.5.53 裡5,最終引進了 vsyscall,在核心位址空間(kernel address space)6 的固定位址 0xffffe000
留出一個 vsyscall 頁面,並開放讀取以及執行權限給用戶模式,該頁面的內容,就是核心在開機時,根據 CPU 類型所決定好的一段程式碼,用來調用系統呼叫,而該程式碼在 vsyscall 頁面中的實際位置,會透過輔助向量(auxiliary vector)的 AT_SYSINFO
項目提供給應用程式,每當要調用系統呼叫的時候,程式只需要調用該段程式碼即可。GNU C 函式庫(GNU C library,俗稱 glibc)則從 2003 年釋出的 glibc 2.3.2 開始配合這套機制。
Linux 2.5.55:x86-32 的系統呼叫 sigreturn
與 rt_sigreturn
在 Linux 2.4 的時代,當核心要呼叫用戶空間(user space)所註冊的信號處理器(signal handler)時,必須在用戶模式的堆疊上加入信號資料框(signal frame),其中包含一個位址,指向一段調用系統呼叫 sigreturn
或 rt_sigreturn
的程式片段,以便在信號處理器返回時,自動調用 sigreturn
或 rt_sigreturn
來返回核心,那核心怎麼知道這個程式片段在哪裡呢?當時的做法是由核心將該程式片段塞進信號資料框,反正當時的堆疊本身是可執行的7。
而在有了 vsyscall 頁面後,開發者發覺這是一個更適合用來放置該程式片段的地方,於是在 2003 年釋出的 Linux 2.5.55 中,將用來調用 sigreturn
與 rt_sigreturn
的程式片段,放進了 x86-32 vsyscall 頁面。
Linux 2.5.69:x86-32 vsyscall DSO
一開始設計的 vsyscall 頁面,僅僅是包含二進位指令碼的記憶體區塊,然而,這樣的設計帶來了一種隱憂,對於 Linux 上的除錯器(debugger)而言,有時會利用程式出錯時產生的核心傾印(core dump,也譯做核心轉儲)檔案來重現出錯情況,可是,vsyscall 頁面只是一段程式碼,不利於傾印到檔案之中(最起碼少了符號表就會變得很麻煩)。
因此,在 2003 年釋出的 Linux 2.5.69 中,將 x86-32 vsyscall 頁面的內容,由一段程式碼,改變成了 ELF 的動態函式庫,稱為 vsyscall DSO(dynamic shared object)8,在編譯核心時,會針對各種情況,預先編譯出多個 DSO 檔並鑲嵌進核心中,到了系統啟動的時候,核心將判斷 CPU 類型,選擇適當的 DSO 並複製到 vsyscall 頁面,程式執行時,核心會透過輔助向量的 AT_SYSINFO_EHDR
項目將 DSO 的位址提供給應用程式。後來由於這樣的機制相當於在記憶體中載入了一個虛擬的 DSO,就有了更一般性的名稱,叫做 virtual DSO(vDSO)。
這個機制就一直延續到了正式版本的 Linux——2.6.0(2.5.x 實際上是開發中版本),成為了調用系統呼叫的正式方法9,而 glibc 則要 2.3.3 才支援上述的所有改變。
Linux 2.6.18:x86-32 vDSO 的位址隨機化
自從 2005 年 Linux 2.6.12 正式引入 ASLR(address space layout randomization)之後,正所謂「千里之堤,潰於蟻穴」,所有在程式中可以用固定位址來存取或執行的資料,都被認為是帶有資安風險的「蟻穴」,而能直接發起系統呼叫的 vDSO,更是巨大的威脅。
於是,在 2006 年釋出的 Linux 2.6.18 中,就將 vDSO 的位址移至用戶位址空間(user address space),並利用把動態函式庫載入位址隨機化的方法,對 vDSO 的位址進行隨機化10。
Linux 2.6.19:x86-64 上 vgetcpu
的引進
在大多數多核心處理器的系統上,每個 CPU 核心都具有專屬的快取資料,對於多執行緒(multi-thread)的程式來說,如果能把在相同核心上運行的執行緒的資料放在一起,就能有效降低快取失誤(cache miss)的次數,從而增進程式的效能。
為了達到這個目的,CPU 核心與 NUMA 節點(NUMA node)的編號對於用戶空間下的記憶體分配演算法而言,就是非常必要的資訊了,因此 2006 年釋出的 Linux 2.6.19 在 x86-32 與 x86-64 上新增了系統呼叫 getcpu
,而為了進一步地加速,同時也在 x86-64 上提供了 vsyscall 版本的函式 vgetcpu
11。
Linux 2.6.23:屬於 x86-64 的 vDSO
從 2007 年釋出的 Linux 2.6.21 開始,Linux 真正支援所謂的高解析度計時器(high-resolution timer)12,而既然號稱高解析度,那麼其本身的花費時間當然是越少越好。因此,在 x86-64 上為了加速關鍵的系統呼叫 clock_gettime
,開發者又將腦子動到了 vsyscall 頁面之上。
然而,與其他 vsyscall 函式不同的是,clock_gettime
是一個相當複雜的系統呼叫,而開發者所想加速的,只是其中幾個特殊的情況,其他情況仍需藉由標準系統呼叫來完成,這會導致 vsyscall 版本的 clock_gettime
不得不包含直接調用系統呼叫的指令。然而,vsyscall 頁面是以固定位址來暴露(expose)給一般程式的,對已經納入 ASLR 機制的 Linux 來說,「一個固定位址,還包含直接調用系統呼叫的指令」,當然是不可能接受的。
結果,x86-64 上的開發者決定借鏡 x86-32 上的 vDSO 機制,在 2007 年釋出的 Linux 2.6.23 中,正式在 x86-64 上引入 vDSO,其中包含了 gettimeofday
、clock_gettime
和 getcpu
三個函式13。
而這樣的改變,當然是需要 Glibc 配合的,因此緊隨其後發布的 glibc 2.7 也做了對應的改變,開始支援 x86-64 vDSO。
Linux 2.6.24:vdso_install
鑲嵌在核心中的 vDSO 為了盡可能的減小體積,使用了 objcopy -S
來剪除與正常運作無關的部分,與此同時,也將許多能協助除錯的資訊剪除了。從 2008 年釋出的 Linux2.6.24 開始,在 make
x86 核心時可以使用新的指令 vdso_install
,這會將編譯過程中原始的 vDSO 檔案放在安裝模組(module)的路徑裡面,這個檔案與核心的運作沒有關係,只是用來提供除錯器相關的除錯資訊而已。
Linux 3.0:x86-64 上 vDSO 的 time
,以及新的變數宣告方式
回顧 x86-64 上 vsyscall 的設計思想,其中一部份是讓核心暴露出一些特定的資料,以便用戶模式執行的 vsyscall 函式能夠存取,而在實作上,是以特製區段(section)的方式來儲存那些特定資料,之後才能從連結器腳本(linker script)對變數的參照動手腳,但是在一開始進行實作時,並沒有設計一個機制去定義這樣的一個變數,只是以同樣的技巧各自實作。
到了 Linux 2.6.23 引入 x86-64 vDSO 之後,情況就變得更複雜了,因為要為 vDSO 的存取做考量,導致如果想要讓核心增加暴露的資料,必須同時考慮到三個方面:核心本身的存取、vsyscall 頁面的存取、以及 vDSO 的存取,也因此,在引入 x86-64 vDSO 的時候,增加了巨集 VEXTERN()
,用來簡化一部分的複雜度,儘管如此,要新增一個這樣的變數,仍然需要同時修改多個核心的原始碼檔案14。
一直到了 2011 年釋出的 Linux 3.0,終於重新設計了實作的方式,移除了舊式巨集 VEXTERN()
,新增 DEFINE_VVAR()
和 DECLARE_VVAR()
等巨集,並藉此將「變數的實作方式」和「定義特定變數」兩件事切割開來15。實際上,這也為 Linux 3.1 的變革做好了準備。
除此之外,Linux 3.0 同時也將 x86-64 vsyscall 頁面的函式 vtime
移植到了 vDSO 上,讓 vDSO 完全支援所有 vsyscall 頁面上的函式16。
Linux 3.1:x86-64 vsyscall 頁面的倒數計時開始
從 Linux 2.6.23 引入 x86-64 vDSO 開始,Linux 核心的開發者就已經下定決心要捨棄舊有的 vsyscall 頁面機制了(不要忘記,固定位址會讓 ASLR 功虧一簣),幾年以後,使用 vsyscall 頁面的函式庫或應用程式已經大幅減少。開發者認為時機已至,該是著手清除 vsyscall 頁面的時刻了17。
在 2011 年釋出的 Linux 3.1 中,為 x86-64 新增了一個核心參數(kernel parameter)——vsyscall
,該參數的值有三種18:
none
:完全取消 vsyscall 頁面,如果有程式存取該頁面,就直接引發記憶體區段錯誤(segmentation fault),最終導致程式關閉。native
:保留 vsyscall 頁面,但其內容不再是特製的 vsyscall 函式,而是單純發起正常的系統呼叫。emulate
:vsyscall 頁面的內容與native
相同,但存取權限調整為唯讀且不可執行,當程式存取時,會觸發頁面錯誤(page fault),但核心會對觸發錯誤的位址進行篩選,如果是因為呼叫 vsyscall 頁面而導致,就在核心中模擬對應的 vsyscall 函式,然後正常返回。
在這三種選項中,即便是最接近原本 vsyscall 頁面機制的 native
選項,也只是正常調用系統呼叫罷了,因此,藉由這樣的改變,核心開始提醒還在使用 vsyscall 頁面的函式庫與程式:該是使用 x86-64 vDSO 的時候了。
順帶一提,在 Linux 3.1 中,核心參數 vsyscall
的預設值是 native
,到了 Linux 3.3 才調整成 emulate
19。
Linux 3.4:x32 vDSO
x86-64 允許系統管理更大量的記憶體,同時也讓行程(process)擁有更大的位址空間,在 x86-64 ABI(application biniry interface)之下,程式的指標也因此變成了 64 位元,但會因此浪費許多記憶體空間(指標本身也是要佔用記憶體的)。
從 2012 年釋出的 Linux 3.4 開始,x86-64 Linux 核心開始支援 x32 ABI,它限制用戶空間的大小,令指標大小變成 32 位元,由此獲得許多效能的增益。與此同時,也將 x86-64 vDSO 移植到了 x32。
Linux 3.15:x86-32 DSO 不再能映射到固定位址,以及新增的時間相關函式
在 Linux 2.6.18 將 x86-32 vDSO 移入用戶位址空間並隨機化的時候,為了保持相容性,增加了編譯選項 COMPAT_VDSO
,用來決定要不要把 vDSO 映射在舊的固定位址 0xffffe000
。為了這樣的可能性,於是 x86-32 vDSO 的程式碼在維護上變得麻煩許多。
多年以後,還會使用舊式機制的函式庫與程式已經幾乎都不存在了,核心的開發者決定徹底剔除映射到固定位址的行為,在 2014 年釋出的 Linux 3.15 中,調整了 COMPAT_VDSO
的意義,使得映射結果要嘛是落在用戶空間的 x86-32 vDSO,要嘛乾脆就不做映射了。
此外,x86-64 在時間相關的系統呼叫方面,早已從虛擬系統呼叫獲益了很久,在 Linux 3.15 中,終於也替 x86-32 以及 x86-64 相容模式的 vDSO 增加這方面的虛擬系統函式,包含了 clock_gettime
、gettimeofday
和 time
。
Linux 3.16:x86 上不假外物的 vDSO 前處理
為了讓鑲嵌進核心中的 DSO 檔能被正確使用,在 Linux 3.15 以前,核心在編譯時會利用 objcopy
、nm
等對 DSO 進行處理,再依賴特製的連結器腳本將處理後的 DSO 鑲進核心中,在核心裡,則還需要一些剖析 ELF 結構的程式碼,以便核心對 vDSO 做適當的調整(大致上是為了實現 alternative 機制20),這樣的作法有一些缺點:
在巧妙使用各種工具的過程裡,vDSO 的整個處理過程越來越複雜,牽扯到多份檔案、多個工具、多種特殊的程式碼伎倆。
vDSO 畢竟不是一般的 DSO 檔,與核心共享的資料是獨立於 DSO 之外的,在映射到用戶空間的時候,鑲進核心的 DSO 檔必然要與那些資料一同工作,可是,大部分的系統程式都是用在正常情況的,這導致某些映射情況理論上可行,但卻難以依賴系統程式來實現。
在 2014 年釋出的 Linux 3.16 裡,將 x86 上絕大多數對 DSO 檔的處理工作都交由核心建置階段中,一個自製的工具程式 vdso2c
來完成,它會輸出 .c
檔,不但將處理好的 DSO 檔鑲嵌在裡面,還會把核心所需要的各種額外資訊提前準備好。經由這樣的調整,不但將原本分階段處理的工作一次性完成,還加強了核心對於 vDSO 檔案的控制能力。
Linux 4.4:x86-32 快速系統呼叫調用的整合,以及 CFI 標記
在 x86-32 上,一開始引入 vsyscall 的目的就是為了調用系統呼叫的方法——int 0x80
、sysenter
以及 syscall
,Linux 4.3 以前,這些調用方法各自放在不同的 .S
檔裡面,這使得每次調整與快速系統呼叫有關的機制時,都要同時考量與三份檔案之間的配合。
於是在 2016 年釋出的 Linux 4.4 中,利用 alternative 機制,核心的開發者將這些方法整合在了一起21。
此外,為了讓二進位檔案能具有更佳的除錯資訊,Linux 上的編譯器會在編譯 .c
檔的過程中植入符合 DWARF 標準的除錯資訊,早期的核心為了讓 .S
檔也能支援這樣的除錯資訊,手動「鑲嵌」相關的資料在 .S
檔裡,這樣的方法當然不利於維護,也因此,Linux 4.4 同時在 x86 的 vDSO 方面引入了 GNU 組譯器(GNU Assembler,也叫 GAS)的 CFI(Call Frame Information)式標記,以更容易理解的方式安插除錯用資訊22。
結語
隨著時間流逝,vsyscall 頁面正逐漸退出歷史的舞台,但作為起源,它對現在的 vDSO 架構仍然有著無法抹滅的影響,從 vsyscall 頁面的出現到 Linux 4.4,本文整理了 vDSO 發展過程的一些事件,在研究相關程式碼的過程中,將這些大事件銘記在心,也許對於現在的 vDSO 為什麼會是「這個樣子」,就能更加心裡有數了。
本文以 StackEdit 撰寫。
附註:
- 關於這部分,我一直找不到準確的資料,這裡的說法是在搜尋 Linux 核心原始碼之後,發現 Linux 2.5 最早於 2.5.5 出現相關資料,Linux 2.4 則是從 2.4.20 開始出現相關資料,有鑑於在當時作為開發版本的 Linux 2.5 通常都走在穩定版的 Linux 2.4 之前,因而做此推論。
↩ - Andrea Arcangeli 曾參加 UKUUG 在 2001年舉辦的 Linux Developers’ Conference,其中有針對 vsyscall 的一場演講。
↩ - 相容模式是 x86-64 CPU 的一大特色,它讓作業系統得以在不修改應用程式的情況下,讓 32 位元的程式在 64 位元的作業系統上執行,所以 Linux 核心必須在設計 32 位元程式運作方式的時候,就預先設想「萬一在 64 位元相容模式執行會怎麼樣」。
↩ - 在看了〈Intel P6 vs P7 system call performance〉的內容後,我整理了一些以 libc 來實做的構思為何不被採用的理由,這些想法都是在程式一開始時取得 CPU 支不支援的資訊,然後再…
第一種,在要調用系統呼叫時,根據 CPU 資訊決定方式。然而,每次系統呼叫都要決定一次,效能上不可行。
第二種,在取得 CPU 資訊後,以低階方法修改用來調用系統呼叫的硬體指令。然而,這會導致記憶體的耗費(寫入時複製(copy-on-write)機制),而且當時有些注重安全性的程式會對函式庫做完整性(integrity)檢查,所以不可行。
第三種,準備多套函式庫,由動態連結器根據 CPU 資訊決定載入的函式庫。然而在那個年代,有許多程式是靜態連結的,它們將無法獲益於這種方法,因此實務上不可行。
↩ - 在更新日誌中,〈Add “sysenter” support on x86, and a “vsyscall” page〉以及〈Export the ‘vsyscall’ address to user space with the AT_SYSINFO〉是與 vsyscall 頁面最有關係的紀錄。
↩ - 在現代作業系統上,通常會對可用的記憶體位址做切割,一部份放置作業系統核心本身所需的程式碼與資料,禁止應用程式存取,稱為核心位址空間(kernel address space);另一部份放置開放給一般應用程式可存取的程式碼與資料,稱為用戶位址空間(user address space)。在 x86-32 Linux 上,預設的分界線是
0xc0000000
(也就是 3 GiB)。
↩ - 當時的做法其實有兩種,還有一種是在程式使用
sigaction
註冊信號處理器的時候,利用struct sigaction
中的sa_restorer
欄位傳遞給核心,然而這種做法影響了程式開發人員對信號處理的認知邏輯,程式開發人員必須對核心有著相當的了解才能正確使用,因此可以說是專供於 C 函式庫使用的方法。
至於本文裡所介紹的方法,以 Lnux 2.4.37 為例,在arch/i386/kernel/signal.c
的函式setup_frame
中,我們可以找到以下片段:
詳細的原理,可以參考由 Daniel P. Bovet 與 Marco Cesati 著作,O’Reilly 出版的〈Understanding the Linux Kernel, 2nd Edition〉。/* Set up to return from userspace. If provided, use a stub already in userspace. */ if (ka->sa.sa_flags & SA_RESTORER) { err |= __put_user(ka->sa.sa_restorer, &frame->pretcode); } else { err |= __put_user(frame->retcode, &frame->pretcode); /* This is popl %eax ; movl $,%eax ; int $0x80 */ err |= __put_user(0xb858, (short *)(frame->retcode+0)); err |= __put_user(__NR_sigreturn, (int *)(frame->retcode+2)); err |= __put_user(0x80cd, (short *)(frame->retcode+6)); }
↩ - 在提交資料中,〈i386 vsyscall DSO implementation〉是最有關係的紀錄。
↩ - 如果想探討一下,經過多次修改,到了 2.6 正式版時利用 vsyscall 頁面的系統呼叫方式究竟變成什麼樣了,〈Linux 2.6 对新型 CPU 快速系统调用的支持〉使用 Linux 2.6.0 對 x86-32 上的系統呼叫方式,做了完整的介紹。
↩ - 對於這部分的改變,可以參考當時〈vdso: randomize the i386 vDSO by moving it into a vma〉這份提交資料。
↩ - 這裡有一篇文章,是當時建議 Glibc 將
vgetcpu
所帶來的資訊納入考量的討論,其中對這部分的議題有更完整的說明。
↩ - 其實,早在 2006 年釋出的 Linux 2.6.16 就已經提出了高解析度計時器的架構,然而直到 Linux 2.6.21 才能完整提供所有功能,可以參考〈Clockevents and dyntick〉來了解前因後果。
↩ - 咦?x86-64 的 vsyscall 頁面不是還有
vtime
嗎?怎麼沒有一併實作在 vDSO 呢?這部分根據當時的原始碼來推敲,我自己做了一些推測:到了 Linux 2.6.21 的時候,系統呼叫time
的實作其實是間接以系統呼叫gettimeofday
完成的,為了保持行為的一致,在 2007 年釋出的 Linux 2.6.22 中,也將vtime
的行為改為間接調用vgettimeofday
。
對於 x86-64 的 vsyscall 頁面來說,程式碼是編譯在核心的固定位址的,因此使用上沒有問題;然而,對 vDSO 機制來說,程式碼是仿照正常動態函式庫編譯的(編譯時會有-fPIC
等旗標),所以如果呼叫了非static
的函式,就會需要做動態連結(dynamic linking)中所謂的的符號繫結(symbol binding),但實際上核心並沒有實現這部分的行為,所以除非核心開發者大手筆去實作這些東西,不然就只能放棄移植vtime
的想法了。
順帶一提,如果想了解動態連結跟函式呼叫之間是怎麼一回事,可以看看這篇文章。
↩ - 如果想了解當時要如何新增一個這樣的變數,可以參考 Linux Journal 的〈Creating a vDSO: the Colonel’s Other Chicken〉,它以 Linux 2.6.37 為例,講述如何新增 x86-64 vDSO 函式。
↩ - 對於 Linux 3.0 之後 x86-64 vDSO 的運作方式,可以參考 LWN.net 的〈Implementing virtual system calls〉,儘管時至今日,核心對 x86-64 vDSO 的設計又有所調整,但大致上是相通的。
↩ - 嗯?如果注意之前的附註,會發現前面不是剛剛解釋過 Linux 2.6.23 沒有移植
vtime
的原因嗎?其實,這是因為在 2008 年釋出的 Linux 2.6.24 中,調整了sys_time
的實作方式,於是系統呼叫time
又變成不依賴於gettimeofday
了。
後來,開發人員發現在交錯使用vtime
與系統呼叫sys_time
的時候,由於vtime
依賴於vgettimeofday
,所以會有機率使得呼叫的順序與回傳時間的排序不一樣,這會讓依賴時間戳記(timestamp)的檔案系統出現問題,於是 x86-64 上的vtime
在 2010 年釋出的 Linux 2.6.36 中受到調整,不再呼叫vgettimeofday
,也因此,阻礙vtime
移植到 vDSO 的因素就移除了。
↩ - Jonathan Corbet 撰寫的〈On vsyscalls and the vDSO〉對當時開發者的想法做了很好的說明。
↩ - 實際上,這個設計方式經過了幾次的修改,最終的定案是這筆提交,但其中部分的設計思維,要從這筆提交和這筆提交中才有說明。
↩ - 其實原本在 Linux 3.1 是打算使用
emulate
作為核心參數vsyscall
的預設值的,但由於當時在 UML(User-Mode Linux)中無法正確處理這樣的頁面錯誤,因此只好暫時使用native
,到了 Linux 3.3 問題解決,就立刻採用emulate
了。
↩ - 完整的名稱應該是 SMP 備選方案(SMP alternative),〈Linux 二進位檔中的特殊區段〉講述了它是怎麼依賴 ELF 區段(section)來實作的。
順帶一提,儘管核心本身很早就引入了這個機制,x86-64 vDSO 是在 Linux 3.1 才有實作;而 x86-32 到了 Linux 3.15 才有實作。
↩ - 這裡的修改與提交資料 1、提交資料 2、提交資料 3 有關。
↩ - 有趣的是,核心其實早在 Linux 2.6.18 的時候,為了支援基於 DWARF-2 的堆疊追蹤(stack trace)技術而引進了 CFI 式的標記(在提交資料中是一種「reliable stack trace」),然而後來這項技術因為種種原因(畢竟核心與一般二進位檔案差很多),早已從核心中移除,而這些遺留下來的 CFI 機制,由於缺乏妥善的管理方法,漸漸造成了維護上的不便,因此在 2015 年釋出的 Linux 4.2 中,就決定將 CFI 式的標記移除了。
然而,vDSO 檔案是提供給用戶空間使用的,甚至編譯時使用vdso_install
還能拿到原始的 DSO 檔案,在其中的除錯資訊並非為了核心分析堆疊使用,而是給一般除錯器用的,所以,它所加入的除錯資料當然要與一般二進位檔案相同——也就是 DWARF 標準。結果,為了在 Linux 4.4 替 vDSO 引入 CFI 式標記,核心只好又重新把一部份 CFI 式標記的相關定義加回核心之中。
↩
佩服作者的調查能力, 這篇文章正好解決了最近的一個疑問, 感謝!
回覆刪除