LV020-获取结构体地址
怎么获取结构体的地址?直接一个 & 不就行了吗?确实可以,不过这里我们来学习一个内核函数 container_of() ,严格来说它其实是一个宏。
一、container_of()
1. container_of 的作用
container_of 宏 在 linux 内核代码里面使用次数非常非常多,我们来看一下它的定义:
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })简单说,container_of 的作用的通过结构体成员变量地址获取这个结构体的地址。使用场景是什么?内核函数调用常常给函数 传入的是结构体成员地址,然后在函数里面又 想使用这个结构体里面的其他成员变量,所以就引发了这样的问题。
可以使用 grep -rn container_of ./|wc -l 统计下 kernel/drivers/input/目录下的 container_of 出现的次数,将会有上百条打印信息出现。
2. 简化版
我们接下来吧上面的 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) );})3. 如何使用?
3.1 调用方式
container_of(ptr, type, member) 中:
- ptr: 表示结构体中 member 的地址
- type: 表示结构体类型
- member: 表示结构体中的成员, 结构体里面一定要有这个成员,不能瞎搞啊
- 返回结构体的首地址

我们直接先看一个实例(container_of 定义在内核头文件(如 <linux/kernel.h>)中。用户态程序无法直接包含内核头文件,因为它们依赖内核特有的构建环境、配置宏以及内核内部定义的数据类型,这里我们只能自己实现一个,或者写内核模块):
#include <stdio.h>
#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) );})
// 2. 定义一个结构体
struct Student {
int id;
char name[8];
int age; // 我们将通过这个成员的地址来找回结构体
};
int main(int argc, char *argv[]) {
// 3. 定义并初始化一个结构体变量
struct Student stu = {
.id = 1001,
.name = "sumu",
.age = 25
};
// 4. 假设我们只有一个指向 age 成员的指针
int *p_age = &stu.age;
printf("&stu=%p, &p_age=%p, p_age=%p\n", &stu, &p_age, p_age);
printf("&stu.id=%p,&stu.name=%p,&stu.age=%p\n", &stu.id,&stu.name,&stu.age);
printf("\n");
// 5. 使用 container_of 宏,通过 p_age 找回结构体指针
struct Student *p_stu = container_of(p_age, struct Student, age);
printf("p_stu=%p, &p_stu=%p\n\n", p_stu, &p_stu);
// 6. 验证:通过计算出的指针访问其他成员
if (p_stu == &stu) {
printf("p_stu->id=%d, p_stu->name=%s, p_stu->age=%d\n", p_stu->id, p_stu->name, p_stu->age);
printf("&p_stu->id=%p, &p_stu->name=%p, &p_stu->age=%p\n", &p_stu->id, p_stu->name, &p_stu->age);
}
return 0;
}我们会得到以下打印信息:
sumu@virtual-machine:~/hk/alpha$ gcc main.c -Wall
sumu@virtual-machine:~/hk/alpha$ ./a.out
&stu=0x7ffe9cbc9ac0, &p_age=0x7ffe9cbc9aa8, p_age=0x7ffe9cbc9acc
&stu.id=0x7ffe9cbc9ac0,&stu.name=0x7ffe9cbc9ac4,&stu.age=0x7ffe9cbc9acc
p_stu=0x7ffe9cbc9ac0, &p_stu=0x7ffe9cbc9ab0
p_stu->id=1001, p_stu->name=sumu, p_stu->age=25
&p_stu->id=0x7ffe9cbc9ac0, &p_stu->name=0x7ffe9cbc9ac4, &p_stu->age=0x7ffe9cbc9acc3.2 内存分析

二、原理解析
我们来分析这个自己写的版本:
#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) );})1. ({}) 表达式
先说这个表达式,这个表达式是 GNU C 扩展(GCC 特有),C 语言其实并不支持这种语法,它叫做 语句表达式(Statement Expression)。它返回最后一个表达式的值。比如 x=({a;b;c;d;}),最终 x 的值应该是 d。
#include <stdio.h>
int main(int argc, char *argv[]) {
int result = ({ // 注意:这里没有分号
int a = 10;
int b = 20;
a + b; // 最后一条语句的值作为返回值
});
printf("result = %d\n", result); // 输出: result = 30
return 0;
}这里编译可能会有警告,我们忽略直接运行,可以得到 a 为 14。
2. typeof 获取变量类型
首先看下 typeof,是用于返回一个变量的类型,这是 GCC 编译器的一个扩展功能,也就是说 typeof 是编译器相关的。既不是 C 语言规范的所要求,也不是某个标准的一部分。具体可以看:Typeof (Using the GNU Compiler Collection (GCC))
#include <stdio.h>
int main(int argc, char *argv[]) {
int a = 6;
typeof(a) b = 9;
printf("%d %d\n",a,b);
}将会输出以下内容:
6 93. (((type *)0)->member)
3.1 指针与内存
内存就是内存,它存储的就是数据,其实并没有 int、char 这些类型之分,只是我们在进行解释的时候指定了这一段是什么类型,然后用对应的指针去取。指针其实就是地址而已,它怎么知道一次性要取多少数据?这个问题可以看 01-编程语言/01-C 语言/16-指针/LV005-指针简介.md - 6. 怎么知道取多少字节数据?
就比如:
内存中的数据(同一块内存):
地址: 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
数据: 48 65 6E 67 00 ?? ?? ??
不同类型的指针看到不同的"解释":
char *p → 读 1 字节 → 'H' (ASCII 48)
int *p → 读 4 字节 → 0x006E6548 (小端序)
double *p → 读 8 字节 → 5.3156e-31 (乱解释)3.2 有什么效果?
我们再来看 ((type *)0)->member 这部分,我们传入的 type 为结构体的类型,在这里就是 struct Student,所以这里其实是 ((struct Student *)0)->member。
尺子我们应该都用过,比如我想用尺子量一本书本的长度,我们第一时间就需要找到尺子的 0 刻度的位置,然后用这个 0 刻度的位置去对准书本的边,然后再贴合对齐,在书本的另一边查看尺子刻度就可以知道书本的长度了。
现在我们需要量一个结构体的长度,我们也可以用尺子来量,我们只要找到这个 0 刻度的位置就可以了。同理,即使我们不知道 0 刻度位置,我们 首尾刻度相减 一样可以计算出结构体的长度。
但是在 C 语言里什么是尺子呢?sizeof 吗,不幸的是,这个并不能满足我们的需要,所以才有了 (struct Student *),这个当作尺子真的再好不过了。我们写个示例看一下这个 ((struct Student *)0)->member 到底是啥,前面我们传入的 member 为 age,这里就用 age 替换 member。
#include <stdio.h>
struct Student {
int id;
char name[8];
int age;
};
int main(int argc, char *argv[]) {
printf("%p, %d\n", &((struct Student *)0)->age, &((struct Student *)0)->age);
return 0;
}这里可能会有一个警告,但是不要按照警告去修改,直接运行即可,然后会得到下面的打印信息:
0xc, 12其实不只是对于 0,用其他数字一样是有效的,比如下面的代码,编译器关心的是类型,而不在乎这个数字。
printf("%p, %d\n", &((struct Student *)4)->age, &((struct Student *)4)->age); // 将会得到: 0x10, 16我们来分析一下,((TYPE *)0) 将 0 转换为 type 类型的结构体指针,换句话说就是让编译器认为这个 结构体是开始于程序段起始位置 0,开始于 0 地址的话,我们 得到的成员变量的地址就直接等于成员变量的偏移地址 了。需要注意这个类型只是 告诉编译器如何解释这块内存,并不会真正访问它,所以不会崩溃(我们打印空指针的值是不会出现崩溃的,访问这个非法地址才会)。

我们看个例子帮助理解,假设有这些数字:
// 假设有这些 "数字"
int num = 100;
// 加上不同 "类型",编译器就用不同方式解释它:
int *p1 = (int *)100; // 100 被当作 int 类型变量的地址
struct Student *p2 = (struct Student *)100; // 100 被当作 struct Student 的地址
char *p3 = (char *)100; // 100 被当作 char 类型变量的地址再来看 ((struct Student *)0)->age,
0= 一个数字(地址)(struct Student *)= 告诉编译器 "用 struct Student 的结构来解释这个地址"->age= 访问 age 成员
编译器内部做的是:
age 在 struct Student 中的偏移量 = (struct Student 起始地址) + 偏移
= 0 + 偏移
= 偏移量(一个常数)它根本不关心地址 0 处是否真的有数据,只关心结构体的 内存布局。
3.3 帮助理解
为什么可以这样认为?指针,其实本质就是一个地址,这个地址就是一个地址数字,0 也好,0x7ffe9cbc9ac0 也好,都是一个数值,所以 0 当然可以是一个指针。可以这样来理解,我们定义了一个指针 p,这个 p 的值是 0,它被当作一个指向 struct Student 的指针来使用。

我们可以再看一个实例:
#include <stdio.h>
struct Student {
int id;
char name[8];
int age;
};
int main(int argc, char *argv[])
{
struct Student* p = (struct Student*)0;
printf("p=%p, sizeof(int)=%ld\n", p, sizeof(int));
printf("&p->id=%p\n", &p->id);
printf("&p->name=%p\n", p->name);
printf("&p->age=%p\n", &p->age);
return 1;
}可以看到如下打印信息:
sumu@virtual-machine:~/hk/alpha$ gcc main.c -Wall
sumu@virtual-machine:~/hk/alpha$ ./a.out
a=(nil), sizeof(int)=4
&a->id=(nil)
&a->name=0x4
&a->age=0xc更加精确一点的说法就是,其实并没有真正 定义 一个叫 p 的变量,而是 临时 把 0 当作指针用。所以其实下面两种写法其实可以理解为等价:
struct Student *p = 0;
p->age;
// 实际写法(没有定义变量,直接用)
((struct Student *)0)->age;4. const typeof( ((type *)0)->member ) *__mptr = (ptr)
这句代码意思是用 typeof() 获取结构体里 member 成员属性的类型,然后定义一个该类型的临时指针变量 __mptr,并将 ptr 所指向的 member 的地址赋给 __mptr;
Tips:为什么不直接使用
ptr而要多此一举呢?我想可能是为了避免对ptr及ptr指向的内容造成破坏。
根据前面传入的内容,这里 type 是 struct Student,member 是 age,ptr 是 p_age,所以就是:
struct Student stu = {
.id = 1001,
.name = "sumu",
.age = 25
};
int *p_age = &stu.age;
const typeof( ((struct Student *)0)->age ) *__mptr = (&stu.age);
// typeof 是取类型,所以这里其实相当于
const int *__mptr = &stu.age;这一步的 __mptr 存放的就是当前结构体实际在内存中对应成员的地址。
5. offsetof(type, member))
offsetof 这个宏是:
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)size_t 是标准 C 库中定义的,在 32 位架构 和 64 位架构 中普遍定义是不一样的:
typedef unsigned int size_t; // 32 位架构
typedef unsigned long size_t;// 64 位架构可以从定义中看到,size_t 是一个非负数,所以 size_t 通常用来计数(因为计数不需要负数区):
for(size_t i = 0; i < 300; i++){
//...
}为了使程序有很好的移植性,因此内核使用 size_t,而不是 int,unsigned。对于 ((size_t) &((TYPE*)0)->MEMBER) 结合之前的解释,我们可以知道这句话的意思就是求出 MEMBER 相对于 0 地址的一个偏移值。
在上面的示例中,这里 type 是 struct Student,member 是 age,所以就是:
offsetof(struct Student, age)
// 展开为
((size_t)&((struct Student *)0)->age)根据前面的实例,这里我们得到的值为 0xc(12), 也就是 age 成员在结构体中的偏移量。

6. (type * )((char * )__mptr - offsetof(type, member))
我们一部分一部分来看,offsetof(type, member)) 这部分上一小节已经解释过了,我们看一下 (char * )__mptr,它是把 __mptr 强制转换为 char * 类型指针。因为指针运算都是以指针类型为单位进行加减的,而后面的 offsetof 得到的偏移量是以字节为单位 的,所以这里要强转一下,然后两者相减就得到了结构体的起始位置。

7. 小结
上面已经分析完了,来做个梳理,其实从头到尾,主要要理解的就是 ((type *)0)->member 这个语句,这句话中的 (type *)0 其实就相当于把 0 这个地址为起始点的一段内存解释为 type 类型,这里就是下面这个结构体:
struct Student {
int id;
char name[8];
int age;
};这里就相当于我们定义了一个 struct Student 类型指针,指向了 0 这个地址的内存,这个时候我们就可以通过 (type *)0 来结构体成员的地址,由于地址是从 0 开始,当我们取地址 &((type *)0)->member 的时候其实就相当于得到了这个 member 在整个结构体中的偏移位置,当我们知道这个成员的实际地址的时候,两者相减,就得到结构体的起始地址了。
#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) );})
// ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ==== ====
struct Student {
int id;
char name[8];
int age;
}*pt;
//用这个来举例
container_of(&pt->age, struct Student, age)
const typeof( ((struct Student *)0)->age ) *__mptr = (&pt->age);
const int *__mptr = (int *)(&pt->age);// 第一句解析完,实际上就是获取 age 的地址, 这个是已知的,传递进来的。
(type *)( (char *)__mptr - offsetof(type,member) );
// 这个展开就是
(struct Student *)( (char *)__mptr - ((unsigned int)&((struct Student*)0)->age));
// 这句的意思,把 age 的地址减去 age 对结构体的偏移地址长度,那就是结构体的地址位置了。参考资料:
Linux 内核宏 Container_Of 的详细解释_Linux_脚本之家
【Linux API 揭秘】container_of 函数详解 - 董哥聊技术 - 博客园
Typeof (Using the GNU Compiler Collection (GCC))