我实际上是在尝试理解Linux内核中的container_of()
宏,其中涉及到以下内容:
#ifndef offsetof
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#endif
#ifndef container_of
/**
* 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)); })
我发现表达式(type *)0)->member
令人讨厌。我不明白为什么该表达式在这些宏中起作用。我阅读了this文章,然后尝试提出一个程序来进一步理解它:
#include <stdio.h>
typedef struct {
int first;
int second;
int third;
}group;
int main(){
group a;
printf("Address of second is %p, group is %p\n", &a.second, &a);
size_t offset = &(((group*)0)->second);
printf("Offset of second is %zd\n", offset );
printf("Address of group is %p\n", (char*)&a.second - offset);
int val = ((group*)0)->second;
}
我确实知道表达式(group *)0)->member
不是void*
指针,而是group
类型的指针,但最终不是最终是一个NULL
地址提到?这条线工作正常
size_t offset = &(((group*)0)->second);
这将导致SIGSEGV
int val = ((group*)0)->second;
两种情况下的内存访问有何不同?
答案 0 :(得分:2)
区别在于,编译器已在编译时“知道”偏移量并且不需要计算偏移量,因此不需要内存访问,也不会发生段错误。这就是offsetof
将不与an opaque struct一起使用的原因。当您检查相应的x86_64
汇编代码时,这一点特别清楚。当我为以下C代码运行gcc -S
时:
#include <stdio.h>
typedef struct {
int first;
int second;
int third;
}group;
int main(){
group a;
size_t offset = (size_t) &(((group*)0)->second); # notice the cast to avoid a warning
return 0;
}
基本上只有两条指令与我的C程序的内容相对应:
movq $4, -24(%rbp) # move literal value 4 to *(rbp-24)
movl $0, %eax # move literal value 0 to eax (this is just a part of "return 0;" statement)
如果我现在要将C中的最后两行更改为:
size_t offset = (size_t) &(((group*)0)->third);
return 1;
汇编代码仅在这两个指令中有所不同。然后,他们将阅读:
movq $8, -24(%rbp)
movl $1, %eax
有4和8,因为在我的机器上int
等于4个字节。更重要的是,知道结构的成员是什么(这就是为什么不透明结构不起作用的原因-该信息被隐藏了。)由于编译器(或汇编器)从一开始就具有此信息,因此它可以并且它只是“硬编码”它。它不需要进行任何取消引用,因为它不需要这样做。
如果我现在将有问题的行添加到我的C代码中:
#include <stdio.h>
typedef struct {
int first;
int second;
int third;
}group;
int main(){
group a;
size_t offset = (size_t) &(((group*)0)->third);
int val = ((group*)0)->second;
return 0;
}
并组装它,我得到以下附加说明:
movl $0, %eax # move literal value 0 to eax
movl 4(%rax), %eax # dereference the value at *(rax + 4) and save it in eax
movl %eax, -28(%rbp) # move the value saved at eax to the *(rbp - 28)
第一行只将文字值0存储在rax
寄存器的下半部分(上半部分为zeroed anyway)中。当试图在位置rax + 4 = 4
上取消对内存的引用以尝试将获取的值存储到eax
寄存器时,下一条指令将触发段错误。实际上,在这里您可以再次看到编译器仅知道 struct group
成员second
的偏移量,即它只是如何偏移结构的位置(保存在{{1 }})的字面值4。恰巧这不是有效的内存,因此操作系统通过发送程序rax
来终止您的程序。
如评论中所述,在第一个示例中,您没有取消引用任何内容,而只是计算地址。在第二种情况下,您实际上要取消引用指向0的指针,这将导致段错误。您已将自己链接到the article中,就在这里:
现在结构偏移量已“标准化”,我们甚至不关心绿色构件的尺寸或结构的尺寸,因为很容易绝对偏移量与相对偏移量相同。这正是&(((TYPE *)0)-> MEMBER的作用。此代码将结构取消引用到内存的零偏移。
通常这不是一件聪明的事情,但是在这种情况下,不会执行或评估该代码。就像我上面用卷尺显示的一样,这只是一个技巧。 offsetof()宏将只返回成员相对于零的偏移量。这只是一个数字,您无法访问此内存。因此,做这个技巧,您唯一需要了解的就是结构的类型。
(我的重点。)