最近在看代码时发现一个用于求结构体成员偏移量的方式
#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 ",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))
如有不正确的地方,欢迎探讨!