链表在windows内核开发中是最最最常见的数据结构了。 主要分为单向链表和双向链表。 单向链表的链表节点只有一个链表节点指针。 双向则是两个。 分别是指向前链表节点和后链表节点。
双向链表指向了前后两个节点。所以链表在插入移除上面的操作比单向链表更为方便。
typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;
在内核中链表就是一个结构体定义的数据结构。 而链表的使用就是将其放到 其它结构中。并且通过某种方法来进行链接起来。
首先我们先定义一个链表,作为Node节点。
然后我们定义一个我们自己的结构
如下:
typedef struct _MY_TEST_STRUCT { ULONG m_ulDataA; ULONG m_ulDataB; ULONG m_ulDataC; ULONG m_ulDataD; }MY_TEST_STRUCT,*PMY_TEST_STRUCT;
结构定义好了我们如何使用链表哪? 我们可以在结构中定义一个Node节点。
如下:
typedef struct _MY_TEST_STRUCT { ULONG m_ulDataA; ULONG m_ulDataB; LIST_ENTRY m_listentry; ULONG m_ulDataC; ULONG m_ulDataD; }MY_TEST_STRUCT,*PMY_TEST_STRUCT;
我们定义了一个链表节点 名字为 m_listentry 但是其实我们可以将这个成员定义在任何地方。
我们看下在内存的布局吧。
而如果我们多定义几个结构,并且让他们互相链接起来。那么在内存中表现形式就如下:
我们可以看到 节点A中的 Fink指向节点B中的Fink位置 好好品味我这句话 它并不是指向节点B的首地址, 也就是并不是指向m_ulDataA位置
而Bink则是指向前节点位置的Fink位置。
这里操作系统设计的很巧妙。 通过在任意结构中定义 Node节点 那么这个结构就是一个双向链表的节点。
那么可能有人问了。如果给我一个节点A。 那么我要遍历双向链表的时候要怎么遍历。 因为节点位置不固定,且Flink指向的位置并不是结构体的开头。
其实这个问题很简单。 我们算一下结构体的偏移。
假设 m_listentry节点在任意结构体位置处定义,那么我们只需要计算出 m_listentry结构距离 结构体首地址的偏移即可。
如下:
typedef struct _MY_TEST_STRUCT { ULONG m_ulDataA; //设在内存中的地址为0位置 ULONG m_ulDataB; //+4 LIST_ENTRY m_listentry; //+8 ULONG m_ulDataC; ULONG m_ulDataD; }MY_TEST_STRUCT,*PMY_TEST_STRUCT;
通过上面结构体可以看到 +8位置是m_listentry地址。 而我们通过遍历链表所得出的地址也是+8的位置。 那么我们只需要把的出来的地址-8即可得到结构体首地址
例如下:
PMY_TEST_STRUCT BNode = xxx.m_listentry -8
而如果计算出-8这个偏移。 其实我们可以利用指针原理 来获取偏移量。
如下:
(&(MY_TEST_STRUCT*)0).m_listentry
这里利用了个小技巧,设0地址为结构体首地址。 并且强转为我们自己的结构体。然后再去访问成员变量m_listentry。 但是有人说了0地址直接访问变量肯定是错的。直接会蓝屏。因为0地址根本就不是我们自己的结构。 所以这里还有个设计的小技巧。 我们取的是0的地址。 这样一来我们并没有访问变量了。
所以上面的公式变成了
PMY_TEST_STRUCT BNode = xxx.m_listentry -((&(MY_TEST_STRUCT*)0).m_listentry)
是不是挺恶心的。不过别怕,操作系统给我们定义了一个宏。如下:
#define CONTAINING_RECORD(address, type, field) ((type *)( \ (PCHAR)(address) - \ (ULONG_PTR)(&((type *)0)->field)))
这里的宏就是我们上面所说的意思。
看下如何使用吧。
伪代码:
MY_TEST_STRUCT testA; MY_TEST_STRUCT testB; PMY_TEST_STRUCT testB = CONTAINING_RECORD(&testB.m_listentry, MY_TEST_STRUCT, m_listentry);
参数一就是我们双向链表得出的地址。 比如 节点A的双向链表位置是+8位置。 那么参数1就是+8地址。
参数二就是结构体类型
参数三就是结构体成员。
看如下图应该更能明白。
链表的头节点是不携带任何内容的,只是表示链表的头部,对链表的所有操作都是从头部开始的。
如果链表只有一个头节点,那么这个链表就是一个空的链表。 头节点的Flink Blink都是指向自身额