一个关于空指针的思考

最近在看代码时发现一个用于求结构体成员偏移量的方式

#define NBB_OFFSETOF(STRUCT, FIELD) (NBB_BUF_SIZE)((NBB_BYTE *)(&((STRUCT *)0)->FIELD) - (NBB_BYTE *)0)

奇怪的是对(STRUCT *)0)->FIELD的引用怎么不会出现错误呢?

于是写了如下代码进行简单的求证

#include <stdio.h>
#include <string.h>

#pragma pack(1)

typedef struct
{
char sex;
short score;
int age;
}student;

int main()
{
int x= (char *)&((student *)0)->age - (char *)0;
printf("x = %d\n",x);
return 0;
}

其中int x= (char *)&((student *)0)->age - (char *)0这一行代码用于求age在结构体中的偏移量(结果是3),对main函数反汇编后的结果如下:

08048424 <main>:
8048424: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048428: 83 e4 f0 and $0xfffffff0,%esp
804842b: ff 71 fc pushl -0x4(%ecx)
804842e: 55 push %ebp
804842f: 89 e5 mov %esp,%ebp
8048431: 51 push %ecx
8048432: 83 ec 24 sub $0x24,%esp #分配空间
8048435: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%ebp) #将0x3放入栈
804843c: 8b 45 f8 mov -0x8(%ebp),%eax 
804843f: 89 44 24 04 mov %eax,0x4(%esp) 
8048443: c7 04 24 20 85 04 08 movl $0x8048520,(%esp)
804844a: e8 05 ff ff ff call 8048354 <printf@plt>
804844f: b8 00 00 00 00 mov $0x0,%eax
8048454: 83 c4 24 add $0x24,%esp
8048457: 59 pop %ecx
8048458: 5d pop %ebp
8048459: 8d 61 fc lea -0x4(%ecx),%esp
804845c: c3 ret 


从上述可以看出,在为printf函数分配空间后直接计算出了结果($0x3),并将该值放入栈中,其中并没有对0地址进行任何访问

在对空指针错误发生的场景进行思考后,总结出了以下场景:
1:对空指针进行赋值,即写操作,如int *p =NULL;*p=6;
2:对空指针进行引用,即读操作,如int *p = NULL;int a = *p;

 

对场景1,写验证代码如下:

int main()
{
int *p =NULL;*p=6;
return 0;
}
反汇编后的结果为:
080483e4 <main>:
80483e4: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483e8: 83 e4 f0 and $0xfffffff0,%esp
80483eb: ff 71 fc pushl -0x4(%ecx)
80483ee: 55 push %ebp
80483ef: 89 e5 mov %esp,%ebp
80483f1: 51 push %ecx
80483f2: 83 ec 10 sub $0x10,%esp
80483f5: c7 45 f8 00 00 00 00 movl $0x0,-0x8(%ebp) #取0地址
80483fc: 8b 45 f8 mov -0x8(%ebp),%eax 
80483ff: c7 00 06 00 00 00 movl $0x6,(%eax) #将0x0地址内容设置为0x6,该处会段错误
8048405: b8 00 00 00 00 mov $0x0,%eax
804840a: 83 c4 10 add $0x10,%esp
804840d: 59 pop %ecx
804840e: 5d pop %ebp
804840f: 8d 61 fc lea -0x4(%ecx),%esp
8048412: c3 ret

对场景2,写验证代码如下:

int main()
{
int *p = NULL;int a = *p;
return 0;
}

反汇编后的结果为:

080483e4 <main>:
80483e4: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483e8: 83 e4 f0 and $0xfffffff0,%esp
80483eb: ff 71 fc pushl -0x4(%ecx)
80483ee: 55 push %ebp
80483ef: 89 e5 mov %esp,%ebp
80483f1: 51 push %ecx
80483f2: 83 ec 10 sub $0x10,%esp
80483f5: c7 45 f4 00 00 00 00 movl $0x0,-0xc(%ebp) #对p赋值0x0
80483fc: 8b 45 f4 mov -0xc(%ebp),%eax
80483ff: 8b 00 mov (%eax),%eax #对0地址取值 ,此处会导致段错误
8048401: 89 45 f8 mov %eax,-0x8(%ebp) #*p赋值给a
8048404: b8 00 00 00 00 mov $0x0,%eax
8048409: 83 c4 10 add $0x10,%esp
804840c: 59 pop %ecx
804840d: 5d pop %ebp
804840e: 8d 61 fc lea -0x4(%ecx),%esp
8048411: c3 ret

 

得出的总结如下:

导致空指针段错误的原因是对空指针地址进行了读或写操作(printf一个空指针其实也是对空指针进行了读操作,然后将内容写到显卡对应的内存)。

(NBB_BYTE *)(&((STRUCT *)0)->FIELD并没有对0地址进行读或写操作,该表达式中的0更应该看做是一个虚拟地址,代表了结构体的首地址,这样可以方便地计算出结构体成员的偏移量,因此 (NBB_BUF_SIZE)((NBB_BYTE *)(&((STRUCT *)0)->FIELD) - (NBB_BYTE *)0)可以简化为(NBB_BUF_SIZE)((NBB_BYTE *)(&((STRUCT *)0)->FIELD))

如有不正确的地方,欢迎探讨!

posted @ 2018-02-28 11:15  charlieroro  阅读(369)  评论(0编辑  收藏  举报