这是你了解的空指针吗?

老师从小就教育我们,C语言编程一定要注意指针的有效性,一定要注意指针的有效性,一定要注意指针的有效性。所以当定义一个指针时,我们通常将它初始化为空指针:

int *p = NULL;

这样即使后续有代码不小心对该指针做解引用访问,系统也能侦测到这个错误,程序能及时被终止。如果没有初始化为空指针,而是一个随机值,也就是所谓的野指针,那程序就有可能“成功”地修改掉其他地址的内存,产生一个非预期的结果。程序误改其他内存导致的错误和系统崩溃,通常更难分析定位,因为问题现场已经离错误源头有了一定的距离。

那么系统是如何能侦测到空指针的呢?不妨从Linux内核里面看一看蛛丝马迹。如下是在Linux内核中做的几个实际测试,测试环境:内核版本5.10,体系结构ARM64,编译工具链 aarch-linux-gnu-*,运行环境qemu。

测试代码1:

static int gpio_keys_probe(struct platform_device *pdev){...        char *test_pointer;        test_pointer = NULL;        *test_pointer = 0xAA;...

运行结果1:

看看这幅图的第一行,内核发生了异常,提示在地址0处发生了空指针解引用的操作。这个很符合我们的预期,别着急,再看看第2个例子。

测试代码2:

static int gpio_keys_probe(struct platform_device *pdev){...        char *test_pointer;        test_pointer = 0x10;        *test_pointer = 0xAA;...

运行结果2:

有没有发现,内核还是提示了NULL pointer的异常,但是发生的地址是0x10。所以看起来,空指针不仅仅是指向地址0的指针。别着急,再看看第3个例子。

测试代码3:

static int gpio_keys_probe(struct platform_device *pdev){...        char *test_pointer;        test_pointer = 4095;        *test_pointer = 0xAA;...

测试结果3:

看到没有,4095(0xfff)这个地址,内核还是会报NULL pointer异常。看起来空指针包括了某个范围内的一段地址?那这个范围是多大呢?别着急,继续看看第4个例子。

测试代码4:

static int gpio_keys_probe(struct platform_device *pdev){...        char *test_pointer;        test_pointer = 4096;        *test_pointer = 0xAA;...

测试结果4:

情况终于发生了变化,这次内核也报异常了,但是不再是NULL pointer的异常,而是paging request异常,无法通过页表找到4096(0x1000)这个地址。到这里,我们大概能推测出空指针的定义范围是0~4095。是时候查阅源码了。

内核异常时的信息由如下函数打印,可以看到具体的错误提示和地址是通过参数msg和addr传入的:

static void die_kernel_fault(const char *msg, unsigned long addr,           unsigned int esr, struct pt_regs *regs){  bust_spinlocks(1);  pr_alert("Unable to handle kernel %s at virtual address %016lx\n", msg,     addr);  mem_abort_decode(esr);  show_pte(addr);  die("Oops", regs, esr);  bust_spinlocks(0);  make_task_dead(SIGKILL);}

https://elixir.bootlin.com/linux/v5.10.179/source/arch/arm64/mm/fault.c#L283

该函数会被__do_kernel_fault函数调用,__do_kernel_fault函数中有如下关键代码:

  } else if (addr < PAGE_SIZE) {    msg = "NULL pointer dereference";  } else {    msg = "paging request";  }

可以看到,当addr小于PAGE_SIZE时,错误信息是"NULL pointer dereference",否则是"paging request"。

__do_kernel_fault函数又会被do_page_fault函数调用。do_page_fault函数是缺页异常的核心处理函数,先进行正常的缺页处理,如果处理失败,那么就会进入错误处理流程。错误处理会针对user mode和kernel mode做不同的处理。如果是user mode,则向用户进程发送段错误(SIGSEGV)的信号。如果是kernel mode,则调用__do_kernel_fault函数实现如前文所示的内核报错。

static int __kprobes do_page_fault(unsigned long addr, unsigned int esr,           struct pt_regs *regs){...  /*   * If we are in kernel mode at this point, we have no context to   * handle this fault with.   */  if (!user_mode(regs))    goto no_context;...no_context:  __do_kernel_fault(addr, esr, regs);  return 0;}

可以看出,空指针异常和其他地址访问异常,没有本质区别,都是访问到了用户进程或者内核的无效地址区域,只不过空指针异常是在0地址附近的一段特定区域,这个区域的大小,在不同的操作系统、不同的处理器体系结构上可能是不同的。

--- END ---

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值