Linux 内核中 offset_of 和 container_of 宏的实现

Linux 内核中 offset_of 和 container_of 宏的实现

offset_of

via: https://elixir.bootlin.com/linux/latest/source/tools/include/linux/kernel.h#L23

获取成员在结构体中的偏移量

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

在以前我很奇怪,为什么用 0 指针
后来在某个群里面和群主讨论了一下,发现这个设计很巧妙

其实就是把地址 0当成是结构体的起始地址,这样,获取它的成员的地址,就相当于获取成员相在结构体里的偏移

举个例子,有这样一个结构体

struct demo {
  int i_var;
  int i2_var;
  int i3_var;
};

((struct demo *) 0) 它是这样的

        +--------------+
0x0     |              |
0x1     |    i_var     |
0x2     |              |
0x3     |              |
        +--------------+
0x4     |              |
0x5     |    i2_var    |
0x6     |              |
0x7     |              |
        +--------------+
0x8     |              |
0x9     |    i3_var    |
0xa     |              |
0xb     |              |
        +--------------+

我想获取 i3_var 的偏移量,只要 &(((struct demo *) 0) -> i3_var)
得到的是 0x8

可以看到这里是取 i3_var 地址,为什么是取地址?
因为这个结构体的起始地址是 0i3_var 的地址肯定是:结构体的起始地址加上偏移量,因为起始地址是 0 所以取 i3_var 的地址就相当于是获得它在结构体里面的偏移量

container_of

via: https://elixir.bootlin.com/linux/latest/source/tools/include/linux/kernel.h#L26

通过成员获取结构体地址

参数

@ptr:指向成员的指针

@type:成员位于的结构体

@member:成员在结构体里面的名称

/**
 * 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) ({			
	const typeof(((type *)0)->member) * __mptr = (ptr);	
	(type *)((char *)__mptr - offsetof(type, member)); })

开始分析之前需要补一下 {()} 表达式

这个表达式就是返回最后一条语句的 运算结果 记住必须是 运算表达式 或者 是一个 ,不能是赋值语句(不知道该怎么表达,自己多写几个 demo 试试就知道了)

demo

#include <stdio.h>

int main() {
    int v = ({
            int v2 = 1;
            int v3 = 2;
            v2 + v3;
    });
    printf("%d
", v);
    return 0;
}

上面的程序运行结果会是 3

#include <stdio.h>

int main() {
    int v = ({
            int v2 = 1;
            int v3 = 2;
            0;
    });
    printf("%d
", v);
    return 0;
}

上面的程序运行结果会是 0

错误演示

#include <stdio.h>

int main() {
    int v = ({
            int v2 = 1;
            int v3 = 2;
            int v4 = v2 + v3;
    });
    printf("%d
", v);
    return 0;
}

gcc 编译的话会直接报错

main.c: 在函数‘main’中:
main.c:4:13: 错误:void 值未如预期地被忽略
    4 |     int v = ({
      |             ^

回归正题一句一句分析

const typeof(((type *)0)->member) * __mptr = (ptr);

获取 member 的类型,然后声明一个 对应 member 类型的常量指针(在常量指针中,指针指向的内容是不可改变的,指针看起来好像指向了一个常量) __mptr__mptr = ptr,这样保证我们的操作不会误操作修改了 ptr导致程序出错,反正就是 __mptr 是指向已知成员的地址的,并且是不可修改的

typeofcGNU 扩展函数,它会返回参数对应的类型

int int_var = 0;

typeof(int_var) 会得到 int 类型

(type *)((char *)__mptr - offsetof(type, member));

先把 __mptr 转成 char * 然后在减去 member 在结构体里的偏移量就能得到结构体的真实地址

为什么要先把 __mptr 转成 char *

因为对指针进行加减运算的时候并不是说 addr + 1 得到的地址就是 addr 加上 1,实际上加的是 1 * (sizeof(typeof(*addr))) (自己细品),转成 char * 后就相当于 sizeof(typeof(char)),其实就是 1,加 n 变成:(n * 1)

现在一个结构体 demo

struct demo {
    int i_var;
    int i2_var;
    int i3_var;
};
offset                         address

           +--------------+
   0x0     |              |	0x7ffd7a0a8c0c
   0x1     |    i_var     |
   0x2     |              |
   0x3     |              |
           +--------------+
   0x4     |              |	0x7ffd7a0a810
   0x5     |    i2_var    |
   0x6     |              |
   0x7     |              |
           +--------------+
   0x8     |              |	0x7ffd7a0a814
   0x9     |    i3_var    |
   0xa     |              |
   0xb     |              |
           +--------------+

我要通过 i3_var 的地址去找对应的 demo 结构体实例的地址

假设 i3_var 的地址是 0x7ffd7a0a8d4 偏移量是 0x8,只要用地址减去偏移量就能获得其对应的 demo 结构体实例的地址

其实这两个宏相关的文章有很多人写过,我看过很多,写的都比我的好,我只是想以自己的理解去写一篇

可以看看这一篇:https://radek.io/2012/11/10/magical-container_of-macro/

原文地址:https://www.cnblogs.com/crybaby/p/13274408.html