Linux 核心預備知識:淺析 offsetof 巨集以及新手的所思所想

wing_rg發表於2021-08-18

最近一頭扎進了 Linux 核心的學習中,對於我這樣一個沒什麼 C 語言基礎的新生代 Java 農民工來說實在太痛苦了。Linux 核心的學習,需要的基礎知識太多太多了:C 語言、組合語言、資料結構與演算法、作業系統原理、計算機組成原理、計算機體系結構。在囫圇吞棗補完一些計算機基礎知識後,還是在一開始就被一個小小的 offsetof 巨集搞暈了。

offsetof 巨集

先來看看offsetof巨集是什麼,這是定義在 <linux/stddef.h>中的一個巨集,用來計算一個 struct 結構體中某個成員相對於結構體首地址的偏移量。這是一個很有用的巨集,因為 Linux 核心的資料結構大量用了嵌入式的結構體(什麼是嵌入式結構體,可以參考 <linux/list.h> 的巧妙設計,這個以後再講)。

// offsetof 巨集的定義
#define offsetof(TYPE, MEMBER)   ((size_t) &((TYPE *)0)->MEMBER)

當看到這個東西完全傻眼了,size_t 是啥東東,((TYPE *)0) 又是啥東東,這個 0 又是什麼鬼?特別看到後面是訪問一個成員,我去,這不是 Java Farmer 眼中的 NPE 嗎?因為這個巨集展開後沒見任何一個結構體的例項。-_-! 於是上網搜尋一番。

size_t 基本知道了就是代表一個整數型別,只是為了程式的可移植、效率等原因定義成這樣,具體解釋可以看《為什麼size_t重要?》這篇文章。

至於 &((TYPE *)0)->MEMBER) 這段程式碼,簡單來說就是取 TYPE 型別的結構體里名字為 MEMBER 的成員的地址,是相對 0 的地址(0 就是 TYPE 結構體的首地址)。C 語言裡指標就是個無符號整數,所有 0 也可以轉成一個 TYPE 型別的指標,那麼不寫 0 行嗎?答案是肯定的,但算偏移量需要後面再減去首地址值,例如((size_t) (&((TYPE *)1000)->MEMBER)-1000),這樣也行,但是,這就有點多此一舉了。

另外,很重要的一點:這樣算偏移地址僅僅是從邏輯計算上來寫計算的表示式,實際上程式執行時是不會發生任何計算,而是編譯器直接就能取到這個地址偏移量,因而也不會有任何的訪存操作。下面從一個例子可以證明:

1、先寫個 C 測試程式

#include <stdio.h>

// 定義一個取偏移量的巨集
#define offsetof(TYPE, MEMBER)   ((size_t) &((TYPE *)0)->MEMBER)

// 定義一個結構體
struct my_struct {
    int a,b,c;
    struct my_struct *next; // 後面我要算這個成員的偏移量
};

// main 函式很簡單就是輸出偏移量的值
void main() {
    printf("offsetof next=%d\n", offsetof(struct my_struct, next));
}

編譯,執行,最後輸出的結果是:offsetof next=16,為什麼是16?next 前面有三個 int 型別的成員,各佔 4 位元組,那 next 應該是從 12 開始,其實這要看編譯的是 64 位還是 32 位,因為筆者的機器是 64 位的 Redhat,而 gcc 編譯選項沒加 -m32,所以編譯出來的程式自然是 64 位的了,因此 next 指標是 8 個位元組,要 8 位元組對齊的話,自然不能從 12 開始,要從 16 開始。整個結構體的長度是 24 位元組(即 sizeof(struct my_struct) = 24)。

2、第二部再將上面的 C 程式碼編譯成彙編看看,指令是怎麼執行的

/**
 *  以 . 開頭的行我們不用管它,都是些編譯器生成的東西,只看彙編指令即可
**/
	.file	"mymain.c"
	.section	.rodata
.LC0:
	.string	"offsetof next=%d\n"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$16, %esi        /* printf 的第二個引數,看,這裡沒有任何計算,編譯時就知道偏移量是 16,直接存到 esi 暫存器作為 printf 函式的實參 */
	movl	$.LC0, %edi      /* printf 的第一個引數,就是上面的字串常量 */
	movl	$0, %eax
	call	printf           /* 呼叫 printf 函式,要說明的是,在 x86-64 結構體系中,有 6 個暫存器是可以用於傳參的(這裡用了 esi 和 edi),多於 6 的其餘就壓棧,也就是上面 rsp 所指的棧頂 */
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
	.section	.note.GNU-stack,"",@progbits

好,到這裡,從上面彙編指令可以看到,offsetof 巨集展開後就是一個 16 這個值,編譯器直接就優化算好了,所有彙編指令僅僅是為了呼叫 printf 函式所做的壓棧保護現場,傳參,彈棧恢復現場這些指令。當然,上面說的是在 x86 結構體系下的指令結構。

小結

Linux 是一個非常龐大的系統,幾乎涵蓋了所有計算機基礎知識。學習 Linux 核心是非常艱鉅的,不但需要非常牢固的計算機基礎,還需要想象力,大局觀。學習了一個月,總結幾點經驗:

1、基礎知識要時時溫習,“溫故知新”。每次看都會有不同的感悟和理解。像這篇學習筆記就是對基礎知識的溫故,從彙編指令角度看編譯器對巨集展開做的工作。永遠不要相信網上一些什麼視訊教程說的不需要什麼基礎,學習 Linux 核心需要的基礎知識太多了,而且這些視訊也不必要浪費時間看,浪費錢買,都是一些二手知識;也永遠不要相信什麼“一文讀懂xxx”這類的文章,同樣是一些二手知識,是不是發現看這些文章很容易就忘了?掌握知識從來沒有捷徑。

基礎、基礎,基礎才是最重要的,計算機技術發展了這麼多年,以及近些年來火起來的什麼大資料,AI,其實都不是什麼新東西,本質還是那些計算機基礎知識原理和數學

  基礎知識脫節,沒可能入門 Linux 核心,不要說入門,入窗戶都不可能。所以想學 Linux 核心,從基礎知識開始,無論基礎有多差,只要肯下功夫,不成問題,這些基礎知識包括:

  • 計算機組成原理:站在抽象的層次理解計算機的工作原理,CPU 如何取指執行(這個可以說是現代計算機工作的本質),記憶體如何工作,快取記憶體如何工作,中斷的原理,外設如何協同並行工作等等;

  • C 語言:這個不用說了,肯定最重要的,C 語言玩得溜,可以省大量時間;

  • 資料結構及演算法:Linux 裡可以說是各種資料結構和演算法的大雜燴,你能想到的裡面都有,同樣這個玩得溜,可以省大量時間;

  • 組合語言(計算機體系結構):彙編其實很簡單,沒什麼好學的,這是要與某一個結構體系緊密結合(基本都 x86 最熟吧),不用強記(記也記不住),只要混個臉熟就好,需要用的時候查手冊即可,主要是結構體系的原理,快取記憶體、快取一致,流水線原理;

  • 作業系統原理:理論指導實踐,有了理論,才容易形成藍圖。而學習 Linux 核心只是實踐。

2、大局觀,抓主線,雖然 Linux 核心程式碼將近 800MB,其實大部分不怎麼需要看。網上很多教程,其實都不怎麼好,要麼泛泛而談,要麼講些過時的(很多將0.11版的核心,個人覺得沒啥價值,純屬浪費時間),要麼一下子就從某一結構體系講起,初學者很容易被繞暈,還有些直接就從怎麼自己寫一個作業系統開始,我們要學的是 Linux 核心,一開始講這些個人覺得沒學會走路就學飛;不可否認,講這些教程的人也許很牛,但個人認為不是一個好老師。所以:

  • 我們學 Linux 的目的是什麼,不同的人有不同的需求,像 Java 過來的新生代農民工,應該著重學習 Linux 核心的設計哲學,例如 kernel 是如何能像我們 Java 物件導向一樣,與各種結構體系(arch)完美適配的,設計的哲學,這些都是網上那些視訊沒講的。再進一步就是細緻到程式管理、記憶體管理、磁碟這些怎麼管理,學會這些,那些老喜歡被問的什麼 kafka 原理啊、零拷貝啊這些簡直就是小菜。作為 Javaer,工作的環境就是 Linux 核心,因此,Linux 太重要了,能學多深就學多深。

  • 要多想象,根據上面的基礎知識,想象,愛因斯坦也說過,想象力比知識跟重要。所以,我們在學習 Linux 核心時要多想象,猜測,帶著問題去學,驗證;

  • Linux 是一個巨複雜的系統,Javaer 更應該學習的是如何應對複雜系統的方法;

  • 上面三點個人才覺得是一個工程師最有價值的地方,這些工程師才是工匠。

3、多動手,搭建環境學習原始碼,多編寫程式碼驗證,特別是從 Java 轉過來的。“紙上得來終覺淺,絕知此事要躬行”。

4、由於筆者也是剛剛才開始學 Linux 核心不久,水平有限,有不正確的地方多多交流,不勝感激。

相關文章