contributed by < afcidk
>
macro 在編譯時期會被編譯器展開成實際的程式碼,這樣做的好處是可以加速程式的速度。
在進行 function call 的時候,因為我們除了需要把參數 push 到特定的 register 或是 stack 上以外,還需要儲存當前 register 的值到 stack 上。function call 少時,這個影響還看不出來,但是一但數量變大,就會導致程式運作比用 macro 實作時慢。
實際執行一段簡單的程式碼,並用 gnuplot 顯示使用 function call 與 macro 的執行時間差異。
Linux 裡頭許多地方都會使用到 linked list 作為資料結構,因此各種 linked list 的基本操作也會被大量使用。如上段所述,這在 function call 和 macro 實作的效能差異會在量大時顯示出來,這也是為什麼 Linux 會採用 macro 來實作 linked list 的原因之一。
我認為使用 linked list 最大的好處是程式開發者不需要在編寫程式時就決定資料的長度。根據這個觀點,我找了下面三個例子
說到 process management,最常提到的應該就是 task_struct
這個結構了。
他定義在 sched.h,光是定義整個結構就需要 628 行!
因為整份程式碼太龐大,這邊我節錄在 task_struct
裡頭紀錄 parent/child 的部份解釋
在 real_parent
的前面有一個 __rcu
的巨集,全稱叫作 Read-copy-update,他可以允許動態分配位址的資料結構(e.g. linked-list)在讀取資料時不需要把整個結構用互斥鎖鎖住。
那麼 real_parent
和 parent
的差別在哪裡?
根據上方的註解,我們可以知道 real_parent
肯定是整個結構的父親,而 parent
則是 SIGCHLD
還有 wait4()
回傳結果的對象。
查看 man 7 signal
得到 SIGCHLD Child stopped or terminated
可以知道 SIGCHLD
是一個 child 暫停或是終止時會發出的訊號。
wait4()
是 BSD 風格的函式,和他相近的另一個規範在 POSIX 裡的函式是 waitpid()
,他們的用途是卡住程式,並等待 child 的狀態改變。
寫了寫現在還是不太明白,目前只知道大概跟 ptrace
有關係。
inode(index node) 是 Unix-like 系統裡面負責處理檔案系統的資料結構。
我們可以看到 fs.h 裡頭的 inode
結構也有 list_head
的蹤影
typeof 允許我們傳入一個變數,代表的會是該變數的型態。
舉例來說:
typeof
大多用在定義巨集上,因為在巨集裏面我們沒辦法知道參數的型態,在需要宣告相同型態的變數時,typeof
會是一個很好的幫手。
這邊以 max 的巨集為例子
至於為什麼我們需要那麼麻煩將 max 的巨集寫成這樣的形式呢?難道不可以寫成 #define max(a,b) (a>b?a:b)
就好嗎?
這樣的寫法會導致 Double evaluation 的問題,顧名思義就是會有某些東西被執行過兩次。
試想如下情況
實際執行後,我們會發現我們竟然呼叫了三次 doOneTime
函式,但是明明在 max
的地方我們只期待會呼叫兩次而已不是嗎?
這是因為在巨集展開後,原本的 max(f1(), f2())
會被改成這樣的形式
為了避免這個問題,我們必須在巨集中先用變數把可能傳入的函式回傳值儲存下來,之後判斷就不要再使用一開始傳入的函式,而是是用後來宣告的回傳值變數。
我們一步一步拆解內容。首先看到的是 __extension__
,他是一個修飾字,用來防止 gcc 編譯器產生警告。
什麼情況下編譯器會產生警告讓我們想要忽略呢?當我們在編譯時,編譯器可能會提醒我們,程式裏面有使用到非 ANSI C 標準的語句,我們寫的程式在現在的編譯器可能可以過,但是用其他的編譯器可能就不會過了。
在這邊的例子,出問題的地方應該是在開頭的 ({})
(braced-group within expression),實際編譯過後我們可以看到這樣的警告訊息
事實上這是 gcc 自己擴展的一個 extension,透過這種寫法可以讓我們的 macro 更安全。
如果不想要看見這個警告,就可以在最前面加上 __extension__
修飾字。
再來的話看到 __typeof__(((type *) 0)->member) * __pmember
這段
可以看到我們用 ((type*)0)->member
的型態再宣告一個新的指標 __pmember
。
(type*)0
代表的是一個被轉型成 type
型態的 struct,在這個地方我們不管他是不是一個合法的位址,所以可以直接寫 0 就好。再來我們讓他指向 member
,因此 __typeof__(((type*)0)->member)
代表的便是 member
的資料型態。
再往下看到 (type *) ((char *) __pmember - offsetof(type, member));
這段比較好解釋,其實就只是把 member 的位址扣除 member 在整個 struct 裡面的偏移後,得到整個 struct 的開頭位址而已。
以圖像的方式會長這樣
跟 poison 相關的定義可以查看 linux/poison.h
參考 Stack Overflow 上 Gil Hamilton 針對 what is the meaning of 0xdead000000000000? 的回答,list poisoning 的目的應該是為了程式開發者除錯方便而產生的一種寫法。
在使用 linked list 時,當我們需要把一塊元素從 list 中去除,通常需要做兩件事:
free(ptr)
NULL
但是 LIST_POISONING 的作法卻不是讓指標指向 NULL,他反而讓指標指向另一個違法的位址。
這格作法跟指向 NULL 相同的是,日後在存取到時都會造成程式錯誤。
不同的地方是我們可以大概得知程式到底會什麼為 crash。
在 poison.h
裡面的註解中提到
Architectures might want to move the poison pointer offset
into some well-recognized area such as 0xdead000000000000,
that is also not mappable by user-space exploits:
如果指標的前綴是 0xdead
(這只是其中一個例子,這個值我們可以自己設定,但是需要確定是在非法區段),代表是因為我們手動將空間釋放後才導致錯誤,反之則可能是因為沒有初始化等其他錯誤。