上一节回绝造轮子!如何移植并应用Linux内核的通用链表(附残缺代码实现)咱们在剖析Linux内核链表的时候留神到内核在求解构造体偏移的时候奇妙的应用了container_of宏定义,明天咱们来具体分析下内核到底是如何求解构造体成员变量的地址的。
@[TOC]
构造体在内存中是如何存储的
int main(){ Student stu; stu.id = 123456; strcpy(stu.name,"feizhufeifei"); stu.math = 90; stu.PE = 80; printf("Student:%p\r\n",&stu); printf("stu.ID:%p\r\n",&stu.ID); printf("stu.name:%p\r\n",&stu.name); printf("stu.math:%p\r\n",&stu.math); return 0;}
打印后果如下:
//构造体的地址Student:0xffffcbb0//构造体第一个成员的地址stu.ID:0xffffcbb0 //偏移地址 +0stu.name:0xffffcbb4//偏移地址 +4stu.math:0xffffcbd4//偏移地址 +24
咱们能够看到,构造体的地址和构造体第一个成员的地址是雷同的。这也就是咱们之前在回绝造轮子!如何移植并应用Linux内核的通用链表(附残缺代码实现)中提到的为什么在构造体中要把 struct list_head
放在首位。
不太了解的再看下这两个例子
struct A { int a; char b; int c; char d; };a 偏移为 0 , b 偏移为 4 , c 偏移为 8 (大于 4 + 1 的 4 的最小整数倍), d 偏移为 12 。 A 对齐为 4 ,大小为 16 。
struct B { int a; char b; char c; long d; };a 偏移为 0 , b 偏移为 4 , c 偏移为 5 , d 偏移为 8 。 B 对齐为 8 , 大小为 16 。
咱们能够看到,构造体中成员变量在内存中存储的其实是偏移地址。也就是说构造体A的地址+成员变量的偏移地址 = 构造体成员变量的起始地址。因而,咱们也能够依据构造体变量的起始地址和成员变量的偏移地址来反推出构造体A的地址。
container_of宏
#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)); })
首先看下三个参数, ptr是成员变量的指针, type是指构造体的类型, member是成员变量的名字。
container_of宏的作用是通过构造体内某个成员变量的地址和该变量名,以及构造体类型。找到该构造体变量的地址。这里应用的是一个利用编译器技术的小技巧,即先求得构造成员在构造中的偏移量,而后依据成员变量的地址反过来得出主构造变量的地址。上面具体分析下各个局部:
typeof
首先看下typeof,是用于返回一个变量的类型,这是GCC编译器的一个扩大性能,也就是说typeof是编译器相干的。既不是C语言标准的所要求,也不是某个规范的一部分。
typeof
int main(){ int a = 5; //这里定义一个和a类型雷同的变量b typeof(a) b = 6; printf("%d,%d\r\n",a,b);//5 6 return 0;}
(((type *)0)->member)
((TYPE *)0)
将0转换为type类型的构造体指针,换句话说就是让编译器认为这个构造体是开始于程序段起始地位0,开始于0地址的话,咱们失去的成员变量的地址就间接等于成员变量的偏移地址了。
(((type *)0)->member)
援用构造体中MEMBER成员。
typedef struct student{ int id; char name[30]; int math;}Student;int main(){ //这里时把构造体强制转换成0地址,而后打印name的地址。 printf("%d\r\n",&((Student *)0)->name);//4 return 0;}
const typeof(((type )0)->member)__mptr = (ptr);
这句代码意思是用typeof()获取构造体里member成员属性的类型,而后定义一个该类型的长期指针变量__mptr,并将ptr所指向的member的地址赋给__mptr;
为什么不间接应用 ptr 而要多此一举呢? 我想可能是为了防止对 ptr 及prt 指向的内容造成毁坏。
offsetof(type, member))
((size_t) &((TYPE*)0)->MEMBER)
size_t
是规范C库中定义的,在32位架构中被广泛定义为:
typedef unsigned int size_t;
而在64位架构中被定义为:
typedef unsigned long size_t;
能够从定义中看到,size_t是一个非正数,所以size_t通常用来计数(因为计数不须要正数区):
for(size_t i=0;i<300;i++)
为了使程序有很好的移植性,因而内核应用size_t和,而不是int,unsigned。((size_t) &((TYPE*)0)->MEMBER)
联合之前的解释,咱们能够晓得这句话的意思就是求出MEMBER
绝对于0地址的一个偏移值。
(type )((char )__mptr - offsetof(type, member))
这句话的意思就是,把 __mptr 转换成 char * 类型, 因为 offsetof 失去的偏移量是以字节为单位。 两者相减失去构造体的起始地位, 再强制转换成 type 类型。
举例
#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) );}) typedef struct student{ int id; char name[30]; int math;}Student;int main(){ Student stu; Student *sptr = NULL; stu.id = 123456; strcpy(stu.name,"feizhufeifei"); stu.math = 90; sptr = container_of(&stu.id,Student,id); printf("sptr=%p\n",sptr); sptr = container_of(&stu.name,Student,name); printf("sptr=%p\n",sptr); sptr = container_of(&stu.math,Student,id); printf("sptr=%p\n",sptr); return 0; }
运行后果如下:
sptr=0xffffcb90sptr=0xffffcb90sptr=0xffffcbb4
宏开展可能会看的更分明一些
int main(){ Student stu; Student *sptr = NULL; stu.id = 123456; strcpy(stu.name,"feizhufeifei"); stu.math = 90; //开展替换 sptr = ({ const unsigned char *__mptr = (&stu.id); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->id) );}); printf("sptr=%p\n",sptr); //开展替换 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.math); (Student *)( (char *)__mptr - ((size_t) &((Student *)0)->math) );}); printf("sptr=%p\n",sptr); return 0; }
养成习惯,先赞后看!如果感觉写的不错,欢送关注,点赞,珍藏,转发,谢谢!
以上代码均为测试后的代码。如有谬误和不妥的中央,欢送指出。
如遇到排版错乱的问题,能够通过以下链接拜访我的CSDN。
CSDN:CSDN搜寻“嵌入式与Linux那些事”
欢送欢送关注我的公众号:嵌入式与Linux那些事,支付秋招口试面试大礼包(华为小米等大厂面经,嵌入式知识点总结,口试题目,简历模版等)和2000G学习材料。