我们在看内核代码的时候,有一个关于C语言的一个小技巧,个人觉得可以介绍分享一下,做个记录。从而方便大家看代码的时候心里直接有个答案,无需脑子里面再转个弯。
我们知道内核头文件会定义结构体,在定义结构体的时候,默认会把重要的结构体的第一项作为子结构体。如下示例:
struct __drm_planes_state { struct drm_plane *ptr; struct drm_plane_state *state, *old_state, *new_state; };
这里我们有个C语言的知识点如下:
struct drm_plane *ptr 的地址是struct __drm_planes_state的地址
也就是说,如果我们一直在操作ptr指针,其实我可以随时和任意的操作struct __drm_planes_state指针,伪代码如下:
struct __drm_planes_state* stat = (struct __drm_planes_state*) &ptr; if(stat->state){ ...... }
这里必须要清楚的是,ptr指针一定要是__drm_planes_state的成员,不能是从其他地方构造和赋值的指针值地址,错误的例子如下:
struct drm_plane *p1 = ptr; stat = (struct __drm_planes_state*) &p1;
这里p1我们不能直接去做取&运算,因为它本身不是__drm_planes_state的成员, 它只是普普通通的一个栈区地址。
#include <stdio.h> #include <stdlib.h> struct lower { int a; }; struct upper { struct lower *k; int b; }; int main() { struct upper *u = malloc(sizeof(struct upper)); struct lower *l = u->k; u->b = 2; struct upper* t1 = (struct upper*)&(u->k); struct upper* t2 = (struct upper*)&l; printf("t1=%p t2=%p t1->b=%d \n", t1, t2, t1->b); }
这里我们构造了一个upper的结构体,设置成员b的值为2,然后我们提取了t1和t2,并打印了地址,得到输出如下:
# gcc test.c -o test && ./test t1=0x558beacb70 t2=0x7ffd087628 t1->b=2
发现没,t1大概在堆区地址范围上,t2在栈区范围上,我们通过t1能够直接找到b,其值为2。
这里我们知道了一个内核通用技巧,我们可以在内核代码中经常看到直接强制类型转换就拿到了父的结构体的指针,然后直接操作代码。非常方便大家理解内核的逻辑。
以后大家看内核代码的时候,这类操作就不需要停顿下来脑子去转弯了。
如果经常看代码,我们可以发现内核充斥着大量的container_of函数,这个函数的意思是:
输入一个成员变量实体,一个父结构体类型,一个结构体成员变量声明,输出结构体父指针
为什么会有这样的函数呢,我们可以根据上文我们可以很容易提出疑问
如果我想找到父结构体指针,那么我的成员必定是第一个成员,那多不方便啊。如果这个成员不是第一个,那有啥好办法能够找到父结构体指针呢? 关于此,我们有两个知识点需要准备:
那基于此,很容易得出这么一个想法
我知道结构体的成员地址,然后推算我之前有多少的偏移,直接拿自己的指针减去这个偏移不就是父指针的地址了么 所以,我们开始解析container_of的宏定义,如下:
#define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ ((type *)(__mptr - offsetof(type, member))); })
我们还需要看offsetof的定义,根据内核头文件,我们可以查到:
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
这里有一个技巧,那就是
运用0地址做强制类型转换,然后去取类型的成员,这样就能拿到成员在结构体的偏移量,然后将其强制类型转换成size_t类型用作指针的减法运算
#include <stdio.h> #include <stdlib.h> #include <stddef.h> #ifndef offsetof #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) #endif #define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ ((type *)(__mptr - offsetof(type, member))); }) struct lower { int a; }; struct upper { int c,d,e,f; struct lower *k; int b; }; int main() { struct upper *u = malloc(sizeof(struct upper)); // struct upper* t = ({ void *__mptr = (void *)(&u->k); ((struct upper *)(__mptr - ((size_t)&((struct upper *)0)->k))); }); struct upper* t = container_of(&u->k, struct upper, k); printf("t=%p up=%p k=%p\n", t, u, &u->k); }
这里我们传入u->k的地址,upper的结构体定义,k成员,我们就能拿到upper的指针t
此时我们运行如下:
# gcc test.c -o test && ./test t=0x559829eb70 up=0x559829eb70 k=0x559829eb80
可以发现t,up的地址完全相等,k刚刚差一个offset地址偏移,也就是4*4=16,就是int c,d,e,f;的占用空间
这里我们知道了内核非常普遍的container_of的实现,它能够直接获取成员的父结构体指针
至此,我们知道了内核操作结构体的小技巧,通过这个技巧,我们可以轻松的找到结构体指针以及成员。这里作为内核的入门知识,对了解内核的人而言至关重要。