指针取消引用的不同行为

时间:2019-11-01 15:05:06

标签: c pointers

我实际上是在尝试理解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;

两种情况下的内存访问有何不同?

1 个答案:

答案 0 :(得分:2)

区别在于,编译器已在编译时“知道”偏移量并且不需要计算偏移量,因此不需要内存访问,也不会发生段错误。这就是offsetofan 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()宏将只返回成员相对于零的偏移量。这只是一个数字,您无法访问此内存。因此,做这个技巧,您唯一需要了解的就是结构的类型。

(我的重点。)