Linux 内核中常用的两个宏定义

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

#ifndef container_of
#define container_of(ptr, type, member) ({                  \
        const typeof(((type*)0)->member)* __mptr = (ptr);   \
        (type*)((char*)__mptr - offsetof(type, member));})
#endif

见招拆招 - 第一式:编译器做了什么?

offsetof 用于计算 TYPE 结构体中 MEMBER 成员的偏移位置
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#endif
  • 编译器清楚的知道结构体成员的偏移位置
  • 通过结构体变量首地址与偏移量定位成员变量

编程实验: offsetof 原理剖析

#include <stdio.h>

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

struct ST
{
    int i;  // 0
    int j;  // 4
    char c; // 8
};

void func(struct ST *pst)
{
    int *pi = &(pst->i);    // (unsigned int)pst + 0
    int *pj = &(pst->j);    // (unsigned int)pst + 4
    char *pc = &(pst->c);   // (unsigned int)pst + 8

    printf("pst = %p\n", pst);
    printf("pi  = %p\n", pi);
    printf("pj  = %p\n", pj);
    printf("pc  = %p\n", pc);
}

int main()
{
    struct ST s = {0};

    func(&s);

    printf("---------\n");

    func(NULL);

    printf("---------\n");

    printf("offset i : %d\n", offsetof(struct ST, i));
    printf("offset j : %d\n", offsetof(struct ST, j));
    printf("offset c : %d\n", offsetof(struct ST, c));

    return 0;
}

结论

计算成员变量与其结构体变量首地址的偏移量

见招拆招 - 第二式:({})

  • ({}) 是 GNU 编译器的语法扩展
  • ({}) 与逗号表达时类似,结果为最后一个语句的值
#include <stdio.h>

void method_1()
{
    int a = 0;
    int b = 0;

    int r = (
                a = 1,
                b = 2,
                a + b
            );

    printf("r = %d\n", r);
}

void method_2()
{
    int r = ({
                 int a = 1;
                 int b = 2;
                 a + b;
             });

    printf("r = %d\n", r);
}

int main()
{
    method_1();
    method_2();

    return 0;
}

输出:

r = 3
r = 3

见招拆招- 第三式: typeof 是一个关键字吗?

  • typeof 是 GUN C 编译器的特有关键字
  • typeof 只在编译期生效,用于得到变量的类型
#include <stdio.h>

int main()
{
    int i = 100;
    typeof(i) j = i;
    const typeof(j) *p = &j;

    printf("sizeof(j) = %d\n", sizeof(j));
    printf("j = %d\n", j);
    printf("*p = %d\n", *p);

    return 0;
}

输出:

sizeof(j) = 4
j = 100
*p = 100

见招拆招 - 第四式:最后的原理

image.png

数学关系:

pc = p + offset

==>

size_t offset = offsetof(struct ST, c);
struct ST *p = (struct ST*)((char*)pc - offset);

编程实验:container_of 原理剖析

#include <stdio.h>

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

#ifndef container_of
#define container_of(ptr, type, member) ({                  \
        const typeof(((type*)0)->member)* __mptr = (ptr);   \
        (type*)((char*)__mptr - offsetof(type, member));})
#endif

struct ST
{
    int i;
    int j;
    char c;
};

int main()
{
    struct ST s = {0};
    char *pc = &s.c;

    struct ST *pst = container_of(pc, struct ST, c);

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}

输出:

&s = 0061FEB8
pst = 0061FEB8

结论

根据成员变量地址推导结构体变量首地址


思考:

const typeof(((type*)0)->member)* __mptr = (ptr);
下面这一段代码测试证明没有这一行也可以得到正确的结果,那它到底有什么作用呢?

#include <stdio.h>

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

#ifndef container_of
#define container_of(ptr, type, member) ({                  \
        const typeof(((type*)0)->member)* __mptr = (ptr);   \
        (type*)((char*)__mptr - offsetof(type, member));})
#endif

#ifndef container_of_new
#define container_of_new(ptr, type, member) ((type*)((char*)ptr - offsetof(type, member)))
#endif

struct ST
{
    int i;
    int j;
    char c;
};

int main()
{
    struct ST s = {0};
    char *pc = &s.c;

    struct ST *pst = container_of_new(pc, struct ST, c);  // 注意!!!

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}

输出:

&s = 0061FEBC
pst = 0061FEBC
答疑 1:为了能够提供微弱但很重要的类型安全检查

因为 continer_of 只能用宏来实现,但宏是由预处理器处理,仅进行简单的文本替换,不会进行任何的类型检查。这就有可能导致在编写代码时,由于疏忽传递了错误的类型指针而编译器不发出任何警告。

测试:

int main()
{
    struct ST s = {0};

    int e = 0;
    int *pe = &e;

    struct ST *pst = container_of_new(pe, struct ST, c);

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}

输出:【编译无错误,无警告,但运行结果错误】

&s = 0061FEBC
pst = 0061FEB0
int main()
{
    struct ST s = {0};

    int e = 0;
    int *pe = &e;

    struct ST *pst = container_of(pe, struct ST, c);

    printf("&s = %p\n", &s);
    printf("pst = %p\n", pst);

    return 0;
}

编译输出:

warning: initialization from incompatible pointer type [-Wincompatible-pointer-types]
         const typeof(((type*)0)->member)* __mptr = (ptr);   \
                                                    ^
答疑 2: 为什么使用 ({})

当定义指针变量时,一行代码再也无法完成 continer_of 的整体功能(逗号表达式中不可以定义变量),于是使用了 GNU 的扩展语法({})分多行定义局部变量。

答疑 3: 此行代码是怎样获取成员变量的类型的呢?

typeof(((type*)0)->member)

小结

  • 编译器清楚的知道结构体成员变量的偏移位置
  • ({}) 与逗号表达式类型,结果为最后一个语句的值
  • typeof 只在编译期生效,用于得到变量的类型
  • container_of 使用 ({}) 进行类型安全检查

以上内容整理于狄泰软件学院系列课程,请大家保护原创!


TianSong
734 声望138 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧