当前位置: 首页 > 科技观察

Linux内核中Container_Of宏详解

时间:2023-03-13 08:04:13 科技观察

我们在分析Linux内核链表时,注意到内核在求解结构偏移量时巧妙地使用了container_of宏定义。今天我们就来详细分析一下内核是如何解决结构体成员变量的地址的。1.结构体在内存中是如何存储的2.container_of宏3.typeof4.(((type*)0)->member)5.consttypeof(((type*)0)->member)*__mptr=(指针);6.offsetof(type,member))7.(type*)((char*)__mptr-offsetof(type,member))8.例1.结构如何存储在内存中intmain(){Studentstu;stu.id=123456;strcpy(stu.name,"feizhufeifei");stu.math=90;stu.PE=80;printf("学生:%p\r\n",&stu);printf("学生ID:%p\r\n",&stu.ID);printf("stu.name:%p\r\n",&stu.name);printf("stu.math:%p\r\n",&stu.math);return0;}打印结果如下://结构体的地址Student:0xffffcbb0//结构体第一个成员的地址stu.ID:0xffffcbb0//偏移地址+0stu.name:0xffffcbb4//偏移地址+4stu.math:0xffffcbd4//偏移地址+24??我们可以看到结构的地址与结构的第一个成员的地址相同。这也是之前HowtoportandusethegenerallinkedlistoftheLinuxkernel(withcompletecodeimplementation)中提到的结构体中我们将structlist_head放在首位的原因。不太明白的看这两个例子:structA{inta;字符b;诠释c;烧焦d;};a的偏移量为0,b的偏移量为4,c的偏移量为8(大于4+1到4的最小整数倍),d的偏移量为12。A的对齐为4且大小为16。结构B{一个;字符b;字符c;长d;};a的偏移量为0,b的偏移量为4,c的偏移量为5,d的偏移量为8。B的对齐方式为8,大小为16。我们可以看到结构体中的成员变量实际上是作为偏移地址存放在内存中的。也就是说,结构体A的地址+成员变量的偏移地址=结构体成员变量的起始地址。因此,我们也可以根据结构体变量的起始地址和成员变量的偏移地址推导出结构体A的地址。2.container_of宏#defineoffsetof(TYPE,MEMBER)((size_t)&((TYPE*??)0)->MEMBER)#definecontainer_of(ptr,type,member)({\consttypeof(((type*)0)->member)*__mptr=(ptr);\(type*)((char*)__mptr-offsetof(type,member));})??先看下三个参数,ptr是指向成员变量的指针,而type指的是结构体的类型,member是成员变量的名字。??container_of宏的作用是通过一个成员变量在结构体中的地址、变量名、结构体类型,找到结构体变量的地址。这里用到的是利用编译器技术的trick,即先获取结构体成员在结构体中的偏移量,然后根据成员变量的地址反向获取主结构体变量的地址。下面对每一部分进行详细分析。3.typeof首先看typeof,它用来返回一个变量的类型。这是GCC编译器的扩展功能,也就是说typeof是编译器相关的。它既不是C语言规范所要求的,也不是标准的一部分。typeofintmain(){inta=5;//这里定义一个和a同类型的变量btypeof(a)b=6;printf("%d,%d\r\n",a,b);//56return0;}4。(((type*)0)->member)((TYPE*)0)将0转换为type类型的结构体指针,换句话说就是让编译器认为这个结构体从程序段开始,起始位置为0.如果是从0地址开始,我们得到的成员变量的地址直接等于成员变量的偏移地址。(((type*)0)->member)指结构中的MEMBER成员。typedefstructstudent{intid;charname[30];intmath;}Student;intmain(){//这里强制将结构体转换为0地址,然后打印name的地址。printf("%d\r\n",&((Student*)0)->name);//4return0;}5.consttypeof(((type*)0)->member)*__mptr=(ptr);这段代码的意思是利用typeof()获取结构体中成员成员属性的类型,然后定义一个该类型的临时指针变量__mptr,并将ptr指向的成员地址赋值给__mptr;为什么不直接使用ptr有什么意义呢?我想可能是为了避免损坏ptr和prt指向的内容。6、offsetof(type,member))((size_t)&((TYPE*??)0)->MEMBER)size_t定义在标准C库中,在32位架构中一般定义为:typedefunsignedintsize_t;而在64位架构中定义为:typedefunsignedlongsize_t;从定义可以看出,size_t是一个非负数,所以通常用size_t来进行计数(因为计数不需要负数面积):for(size_ti=0;i<300;i++)为了使该程序非常可移植,因此内核使用size_t而不是int和unsigned。((size_t)&((TYPE*??)0)->MEMBER)结合前面的解释,我们可以知道这句话的意思是找到MEMBER相对于0地址的一个偏移值。7.(type*)((char*)__mptr-offsetof(type,member))表示将__mptr转换为char*类型。因为offsetof得到的偏移量是以字节为单位的。两者相减得到结构的起始位置,然后转换为type类型。8.例子#defineoffsetof(TYPE,MEMBER)((size_t)&((TYPE*??)0)->MEMBER)#definecontainer_of(ptr,type,member)({\consttypeof(((type*)0)->member)*__mptr=(ptr);\(type*)((char*)__mptr-offsetof(type,member));})typedefstructstudent{intid;charname[30];intmath;}Student;intmain(){Studentstu;Student*sptr=NULL;stu.id=123456;strcpy(stu.name,"zhongyi");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);return0;}运行结果如下:sptr=0xffffcb90sptr=0xffffcb90sptr=0xffffcbb4宏展开intmain(){Studentstu;Student*sptr=NULL;stu.id=123456;strcpy(stu.name,"zhongyi");stu.math=90;//扩展替换sptr=({constunsignedchar*__mptr=(&stu.id);(Student*)((char*)__mptr-((size_t)&((Student*)0)->id));});printf("sptr=%p\n",sptr);//扩展替换sptr=({constunsignedchar*__mptr=(&stu.name);(Student*)((char*)__mptr-((size_t)&((Student*)0)->name));});printf("sptr=%p\n",sptr);//扩展替换sptr=({constunsignedint*__mptr=(&stu.math);(Student*)((char*)__mptr-((size_t)&((Student*)0)->math));});printf("sptr=%p\n",sptr);return0;}本文转载自微信公众号《嵌入式与Linux那些事》。公众号。