上一节回绝造轮子!如何移植并应用 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 // 偏移地址 +0
stu.name:0xffffcbb4// 偏移地址 +4
stu.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=0xffffcb90
sptr=0xffffcb90
sptr=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 学习材料。