轉自:http://blog.csdn.net/hgf1011/article/details/4635888
CONTAINING_RECORD IN EFI
EFI BIOS幾乎全部用C完成,它幾乎將C語言的各種技巧發揮到了極致。C的精髓泰半是指針,另外宏也是非常值得稱道的。程序員對於宏的評價可謂褒貶不一,有人說它是萬惡之源,有人則贊其爲一把利刃。我個人覺得運用之妙,存乎一心,宏不是萬能的,但是有一些場合使用宏確實可以大大的提高程序的可讀性,有些跨平臺的特性離開了宏還真是不行。_CR是EFI之中經常會被用的宏,我們來看看它的廬山真面目吧:
//
// CONTAINING_RECORD - returns a pointer to the structure
// from one of it's elements.
//
#define _CR(Record, TYPE, Field) ((TYPE *) ((CHAR8 *) (Record) - (CHAR8 *) &(((TYPE *) 0)->Field)))
找出TYPE類型結構體成員Field所在結構體的地址,Record是Field的地址。
這個宏的作用是根據一個結構體成員變量的的地址獲得該結構體基地址。舉例如下:
struct _Test
{
CHAR8 t0;
UINT16 t1;
UINT8 t2;
UINT32 t3;
};
我們在某一個地方獲得了t2的地址t2Ptr,這時如果要得到t2所在結構體的基地址就可以這樣做:struct _Test* bPtr = _CR(t2Ptr,struct _Test,t2);預處理展開之後就是這副樣子 struct _Test* bPtr = ((struct _Test*)((CHAR8*)(t2Ptr)-(CHAR8*)&((struct _Test *)0)->t2),其實我覺得關鍵的部分在這句“(TYPE *) 0)->Field”,它其實就是獲得該成員在結構體中的偏移位置(offset),該成員變量的地址減去它在結構體中的偏移也就獲得了基地址了。
一圖勝千言,如上圖所示,假設t2的地址是0x1005,那麼bPtr應該是多少呢?只要用t2-t2的offset(0x03)即可,也就是0x1002。是不是很簡單?
ANSI C標準允許值爲0的常量被強制轉換成任何一種類型的指針,並且轉換結果是一個NULL指針,因此((type *)0)的結果就是一個類型爲type *的NULL指針。如果利用這NULL指針來訪問type的成員當然是非法的,但&( ((type *)0)->field )的意圖僅僅是計算field字段的地址。聰明的編譯器根本就不生成訪問type的代碼,而僅僅是根據type的內存佈局和結構體實例首址在編譯期計算這個(常量)地址,這樣就完全避免了通過NULL指針訪問內存的問題。又因爲首址爲0,所以這個地址的值就是字段相對於結構體基址的偏移。以上方法避免了實例化一個type對象,並且求值在編譯期進行,沒有運行期負擔。
大概是英雄所見略同吧,這種形式的宏在Linux kernel 和Windows Kernel中也都有存在,只不過名字叫的不一樣罷了。先看看Linux下長的什麼樣子吧:
/**
* 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) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
再來看看它在Windows Kernel中的小模樣:
#define CONTAININT_RECORD(address, type, field) /
((type*)((PCHAR)(address) - (PCHAR)(&((type*)0)->field)))
他們的作用都是一樣的,是不是有點天下文章一大抄的感覺啊,呵呵…