macro 是由 gcc toolchain 中的 preprocesser 提供的一個語言特性,會在編譯時期將代碼中的 macro 展開,而不是使用 function call,就如同 inline function 一樣。function call 相較於 macro 多出了兩個階段
分別是用於建立與摧毀 function 存放區域變數所需的 stack frame,過程中會對 stack 進行讀寫(push, pop),造成記憶體讀寫的時間開銷。所以若是沒有需要維護 function 中的區域變數時,或許可以改採用 macro 的形式來寫作,而 linux kernel 中的 linked list 操作就是如此 (在後方問題中會詳細說明)。
和一般撰寫程式時直接利用該 struct 的指標操作不同,linux kernel 中的 linked list 實作多是這種單一作用的元件,配合 container_of
macro 讓 linked list 變成是一個能夠被模組化的特性,也就不必為每個 struct 寫不同的 linked list 操作函式,我認為這是相當值得借鑒的。
在 task_struct
中利用 list_head
來建立 parent, children 的樹狀關係
先給個用法:
看起來很複雜,總之先一步步拆解
__extension__
翻閱 gcc 文件中的 Alternative Keywords 提到
You can prevent such warnings within one expression by writing
__extension__
before the expression.__extension__
has no effect aside from this.
看來是為了 surpress 可能的警告訊息,不過有趣的是翻閱目前的 linux kernel v5.0-rc8 版本中,所有的 container_of
定義都沒有用到這個 gcc 擴展。
__typeof__
一樣在 gcc 文件中的 Alternative Keywords 可以得知__
是為了相容性考量而加的,作用和 keyword typeof
相同,在 Typeof 章節中可以得知 typeof 是用來獲取變數的型別。
((type *) 0)->member
照理來說 NULL pointer dereference 應該會出錯,不過在這裡的寫法行得通的原因我認為是使用了 typeof
的緣故,Typeof 章節中提到參數只有在特殊情況下(type 在編譯時期無法確定)才會被 evaluate。
The operand of typeof is evaluated for its side effects if and only if it is an expression of variably modified type or the name of such a type.
offsetof
給出 macro 定義
這裡的 &((TYPE *)0)->MEMBER
並不是求值後取地址,struct 中的成員變數地址是相對於 struct 的初始地址的,並且編譯時期就可以確定也就沒必要在運行時求值了,所以在此得到的是 0 + offset of MEMBER in struct
{}
第一次看到時覺得很奇怪,這 macro 到底要怎麼 return(廣義上的) 結果阿?
gcc docs 的 Statement-Exprs 章節中提到,這被稱為 compound statement 且回傳值是最後一個 expression 的結果
The last thing in the compound statement should be an expression followed by a semicolon; the value of this subexpression serves as the value of the entire construct. (If you use some other kind of statement last within the braces, the construct has type void, and thus effectively no value.)
首先宣告一個和 member 型別相同的指標 __pmember = ptr,將他扣除 member 在 type 中的 offset,回傳上述的值。
總覺得中間那行有些多此一舉,畢竟一開始就有 ptr
了,最後操作前也會被轉型成 char *
結果寫完才發現 list.h
裡面有這個版本,一個想法是支援 typeof
extension 的話就可以在編譯時檢查型別,進而避免一些錯誤。
這是資安上的考量,list_del()
的上方的註解敘述中提到這是為了防止 node 被移除後 next/prev pointer 仍被拿來使用的情況,以資安術語來說就是防止 UAF (Use-After-Free),也降低 memory address leak 的可能。
LIST_POISONING can be enabled during build-time to provoke an invalid memory access when the memory behind the next/prev pointer is used after a list_del.
This only works on systems which prohibit access to the predefined memory addresses.
在 Google 開發的 javascript 引擎 V8 中也可以看到類似的做法。
提到 linked list 跟資訊安全,當然就要拿出 glibc 來參考XD
glibc 是以 C 語言撰寫的通用函式庫,平常寫 C 程式在最上面 #include <stdio.h>
就是引入了 glibc,其中 malloc.c
當中多是用於管理動態配置記憶體的相關函式。
這裡稍微簡介一下 malloc.c 中的動態記憶體管理方式:
malloc.c
實作中 chunk 是 malloc/free 操作的基本單元,除了 user 要求的 size 以外,還會多配置一些空間用於儲存 metadata 如大小, 配置的方式…等
free 時會將不同大小的 chunk 放入各自的 linked list (也就是 bins) 中由 glibc 負責維護,下一次 malloc 時若是有大小相同的 chunk 就可以直接將其 return 給 user。
為了防止許多在操作 linked list 時可能出現的安全漏洞,例如上述的 UAF 情形下,其用來移除 chunk 所使用的 function unlink_chunk()
就加上不少檢查,例如以下就是檢查要被從 list 中刪除的節點 p
,它的 prev->next, next->prev 是否都等於自己
當然也會有方式繞過這些檢查,有興趣的人可以自行 Google 關鍵字 heap exploit
XD
採用環狀 linked list 的優勢在於…我不知道 (繼續思考)
雙向 linked list 可以讓多個相同結構的 linked list 中的任一節點移除操作只需要藉由一個指標即可完成,時間複雜度 ,且不需要知道需要移除的節點在哪一個 linked list 上。
/lib/list_sort.c
中的 list_sort