新年第一帖,總得拿出點乾貨才行,雖然這篇水分仍是有點大,你們能夠曬乾了溫水沖服。這段時間一直在整理內核學習的基礎知識點,期間又碰到了container_of()這個宏,固然還包括一個叫作offsetof()的傢伙。在這兩個宏定義裏都出現將「零」地址強轉成目標結構體類型,而後再訪問其成員屬性的情形。若是有童鞋看過我以前的博文《
Segmentation fault究竟是何方妖孽》的話,估計此時內心會犯嘀咕:不是說0地址不能夠訪問麼,那container_of()和offsetof()宏定義裏用0時怎麼沒報錯呢?到底該TM如何理解「零」地址?結構體被編譯時有沒有什麼貓膩呢?程序究竟是如何訪問結構體裏的每一個成員屬性的?本篇,咱們就來聊聊這幾個問題。
先從內核宏定義
container_of()入手:
-
- #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
-
- /**
- * container_of - cast a member of a structure out to the containing structure
- * @ptr: the pointer to the member.
- * @type: the type of the container struct this is embedded in.
- * @member: the name of the member within the struct.
- *
- */
- #define container_of(ptr, type, member) ({ \
- const typeof( ((type *)0)->member ) *__mptr = (ptr); \
- (type *)( (char *)__mptr - offsetof(type,member) );})
這個宏定義咱們已經不止一次遇到過,相信你們對其做用和用法已經瞭解了(啥玩意?不瞭解,那就猛擊這裏)。
今天咱們主要探究的是container_of()的實現原理相關層面的技術細節。要說清container_of()仍是得先過了offsetof()這關才行:
- #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
關於這行代碼你要是到網上去搜,百分之99%的答案都是:將零地址強制轉換成目標結構體類型TYPE,而後訪問其成員屬性MEMBER,就獲得了該成員在其宿主結構體裏的偏移量(按字節計算)。固然,人家這個回答也無可厚非,也不能說人家錯,可爲何0地址能被這樣用呢?編譯器就不報錯?OK,先讓咱們看一個簡單的例子:
- #include <stdio.h>
-
- #pragma pack(1)
- typedef struct student{
- unsigned char sex;
- unsigned int age;
- unsigned char name[32];
- }Student;
-
- int main(int argc,char** argv)
- {
- Student stu;
- printf("size_of_stu = %d\n",sizeof(stu));
- printf("add_of_stu = %p\n",&stu);
- printf("add_of_sex = %p\n",&stu.sex);
- printf("add_of_age = %p\n",&stu.age);
- printf("add_of_name = %p\n",&stu.name);
- return 0;
- }
其中第三行代碼是取消編譯默認的結構體對齊優化,這樣一來Student結構體所佔內存空間大小爲37字節。運行結果以下:
咱們能夠看到,Student結構體對象stu裏的三個成員屬性的地址,按照咱們的預期進行排列的(-_-|| 這TM不廢話麼,難道還倒着排不成)。此時咱們知道stu對象的地址是個隨機值,每次運行的時候都會變,可是不管怎麼變stu.sex的地址永遠和stu的地址是一致:
咱們來反彙編一下可執行程序test:
若是你對AT&T的彙編語言不是很熟悉,建議先看一下個人另一篇博文《深刻理解C語言的函數調用過程 》。上面的反彙編代碼已經和C源代碼關聯起來了,注意看第20行反彙編代碼「lea 0x1b(%esp),%edx」,用lea指令將esp向高地址偏移27字節的地址,也就是棧空間上stu的地址裝載到edx寄存器裏,lea指令的全稱是load effective address,因此該指令是將要操做的地址裝載到目標寄存器裏。另外,咱們看到,在打印stu.age地址時,第26行也裝載的是 0x1b(%esp)地址;打印stu.age時,注意第3二、33行代碼,由於棧是向高地址增加的,因此age的地址比stu.sex的地址值要大,這裏在編譯階段編譯器就已經完成了地址偏移的計算過程;一樣地,stu.name的地址,觀察第3九、40行代碼,是在0x1b(%esp)的基礎上,增長了stu.sex和stu.age的偏移,即5個字節後找到了stu.name的地址。
也就是說,編譯器在編譯階段就已經知道結構體裏每一個成員屬性的相對偏移量,咱們源代碼裏的全部對結構體成員的訪問,最終都會被編譯器轉化成對其相對地址的訪問,代碼在運行時根本沒有變量名、成員屬性一說,有的也只有地址。OK,那就簡單了,咱們再看一下下面的程序:
- #include <stdio.h>
-
- #pragma pack(1)
- typedef struct student{
- unsigned char sex;
- unsigned int age;
- unsigned char name[32];
- }Student;
-
- int main(int argc,char** argv)
- {
- Student *stu = (Student*)0;
-
- printf("size_of_stu = %d\n",sizeof(*stu));
- printf("add_of_stu = 0x%08x\n",stu);
- printf("add_of_sex = 0x%08x\n",&stu->sex);
- printf("add_of_age = 0x%08x\n",&stu->age);
- printf("add_of_name = 0x%08x\n",&stu->name);
- return 0;
- }
運行結果:
反彙編:
第8行「movl $0x0,0x1c(%esp)」 爲指針stu賦值,爲了打印stu指針所指向的地址值,第1八、19行準備將0x1c(%esp)的值壓棧,爲調用printf()作準備;準備打印stu->sex時,參見第2三、25兩行所作的事情,與第1八、19行相同;當準備打印stu->age時,參見第2九、30行,eax裏已經保存了stu所指向的地址0,是從棧上0x1c(%esp)裏取來的,而後lea指令將eax所指向地址向「後」偏1字節的地址值裝載到edx裏,和上面第一個實例代碼同樣。由於eax的值是0,因此0x1(%eax)的值確定就是1,即此時在stu=NULL的前提下,找到了stu->age的地址。到這裏,咱們的問題也就差很少明朗了:
第一:對於任何一個變量,任什麼時候候咱們均可以訪問該變量的地址,可是卻不必定能訪問該地址裏的值,由於在保護模式下對地址裏的值的訪問是受限的;
第二,結構體在編譯期間就已經肯定了每一個成員的大小,進而明確了每一個成員相對於結構體頭部的偏移的地址,源代碼裏全部對結構體成員的訪問,在編譯期間都已經靜態地轉化成了對相對地址的訪問。
換句話說,源代碼裏你能夠寫相似於int *ptr = 0x12345;這樣的語句代碼,對ptr執行加、減,甚至強制類型轉換都沒有任何問題,可是若是你想訪問ptr地址裏的內容,那麼很不幸,你可能會收到一個「Segmentation Fault」的錯誤提示,由於你訪問了非法的內存地址。
最後,讓咱們回到開篇的那個問題:
- #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
相信你們如今對offsetof()定義裏那個奇怪的0應該再也不會感到奇怪了吧。其實container_of()裏還有一個名叫typeof的東東,是用於取一個變量的類型,這是GCC編譯器的一個擴展功能,也就是說typeof是編譯器相關的。既不是C語言規範的所要求,也不是某個神馬標準的一部分,僅僅是GCC編譯器的一個擴展特性而已,Windows下的VC編譯器就不帶這個技能。讓咱們繼續刨一刨container_of()的代碼:
- #define container_of(ptr, type, member) ({ \
- const typeof( ((type *)0)->member ) *__mptr = (ptr); \
- (type *)( (char *)__mptr - offsetof(type,member) );})
第二句代碼意思是用typeof()獲取結構體裏member成員屬性的類型,而後定義一個該類型的臨時指針變量__mptr,並將ptr所指向的member的地址賦給__mptr;第三句代碼意思就更簡單了,__mptr減去它自身在結構體type裏的偏移量就找到告終構體的入口地址,最後將該地址強轉成目標結構體的地址類型就OK了。若是咱們將使用了container_of()的代碼進行宏展開後,看得會比較清楚一點:
- #include <stdio.h>
-
- #pragma pack(1)
- typedef struct student{
- unsigned char sex;
- unsigned int age;
- unsigned char name[32];
- }Student;
-
- #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
- #define container_of(ptr, type, member) ({ \
- const typeof( ((type *)0)->member ) *__mptr = (ptr); \
- (type *)( (char *)__mptr - offsetof(type,member) );})
-
- int main(int argc,char** argv)
- {
- Student stu;
- Student *sptr = NULL;
- sptr = container_of(&stu.name,Student,name);
- printf("sptr=%p\n",sptr);
- sptr = container_of(&stu.age,Student,age);
- printf("sptr=%p\n",sptr);
- return 0;
- }
運行結果:
宏展開後的代碼以下:
- int main(int argc,char** argv)
- {
- Student stu;
- Student *sptr = ((void *)0);
- sptr = ({ const typeof(((Student *)0)->name ) *__mptr = (&stu.name); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->name) );});
- printf("sptr=%p\n",sptr);
- sptr = ({ const typeof(((Student *)0)->age ) *__mptr = (&stu.age); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->age) );});
- printf("sptr=%p\n",sptr);
- return 0;
- }
GCC在接下來的編譯過程當中會將typeof()進行替換處理,咱們能夠認爲此時上述的代碼和下面的代碼是等價的:
- int main(int argc,char** argv)
- {
- Student stu;
- Student *sptr = ((void *)0);
- sptr = ({ const unsigned char *__mptr = (&stu.name); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->name) );});
- printf("sptr=%p\n",sptr);
- sptr = ({ const unsigned int *__mptr = (&stu.age); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->age) );});
- printf("sptr=%p\n",sptr);
- return 0;
- }
最後向偉大的程序猿、攻城獅們致敬!! 向「自由、開源」精神致敬!!