前言
(1)今天在编写采用平台总线模型的LED点灯程序时候,发现每次装载驱动,都会报如下错误,最后一行报错说段错误。
(2)我一开始以为是因为自己的平台总线模型写错了,花了很长时间进行对比没有发现问题,最终使用printk定位到了问题所在。
问题代码部分
(1)我当时采用printk进行打印调试,最终定位问题在这一块。
(2)于是我就开始研究这三行代码。发现一个非常有意思的地方,这个地方的报错,有时候不会出现!他时好时坏,导致我定位问题花了很长时间。
(3)最后发现,我传入的gpios[i].name是一个字符指针,而别人的代码是传入的gpios[i].name是一个字符数组指针。
(4)最终我将结构体进行了如下更改,之后问题就解决了
/** 原来的结构体 **/
struct gpio_desc{
int gpio; //引脚编号
char *name; //名字
};
/** 更改后的结构体 **/
struct gpio_desc{
int gpio; //引脚编号
char name[128]; //名字
};
字符指针和字符数组指针的区别
传入字符指针,是否符合语法
(1)首先,我们先看看sprintf()的函数声明,我们可以看到,第一个参数是一个char*类型数据。
(2)那么,如果我们传入一个字符指针其实是符合语法规则的。
int sprintf(char *str, const char *format, ...);
为什么会报错
(1)符合语法,但是为啥会报错呢?这个就要讲到字符指针和字符数组指针的区别了。
(2)用最简单通俗的话来讲,字符数组指针其实就是字符指针的升级版本。为啥这么说呢?因为,字符数组指针和字符指针都是一个指针,只不过他们所指向的区域有所不同。
(3)字符数组指针
<1>他首先需要向系统申请一块未被使用的内存区域,比如我这里设置的是char name[128];那么就会向内存申请到一个连续的128字节未被使用的内存。
<2>申请到这一块内存之后,系统将会返回这一段内存的首地址。
(4)字符指针。当你申请了一个字符指针之后,他可能指向任何一块地方,哪一个地方可能是被申请的空间,可能是未被申请的空间。他所指向的区域是未知的,如果你没有主动给他赋值,那么他就是传说中的野指针。
(5)注释分割符上下两个代码等价
char name[128];
/*** 上下两个代码等价 ***/
char *name;
name = (char*)malloc(128*sizeof(char));
这个问题在所有芯片上都会进行报错吗?
MMU的作用
(1)在抛出这个问题之前,我们需要思考一个问题。成功编译,没有报错的代码,真的就没有问题吗?
(2)这个答案肯定是否定的,不然也不会总是出现一些玄学问题。这里一样,我们这个代码从语法的角度,其实是不存在问题的,编译器是可以顺利通过的。所以,当我对这一串代码进行编译的时候,没有出现任何问题。
(3)但是,为什么我在装载驱动的时候,我的PC端就出现了报错呢?这个就需要介绍到,操作系统中的内存管理单元MMU了。
(4)
<1>MMU是计算机硬件中的一个组件,负责虚拟地址到物理地址的转换,以及对内存的访问权限控制。
<2>当程序执行时,它会请求内存分配,例如使用malloc()函数在堆上分配内存,或者定义局部变量在栈上分配内存。MMU负责跟踪这些内存分配,并记录哪些内存块已经被分配给了进程。
<3>内存管理通常通过内存映射来实现。每个进程都有自己的虚拟地址空间,该空间被分为不同的区域,例如代码段、数据段、堆、栈等。当程序请求内存时,操作系统会在虚拟地址空间中为该程序分配合适的虚拟内存地址。随后,MMU会将这些虚拟地址映射到物理内存地址。
<4>当程序试图访问某个虚拟地址时,MMU会将该虚拟地址转换为相应的物理地址。在此过程中,MMU会检查该虚拟地址是否位于有效的分配内存范围内,并且访问权限是否正确。如果虚拟地址是无效的或者权限不允许,MMU会触发一个异常(例如,段错误或访问违规),操作系统就会介入并终止程序的执行。
<5>所以,操作系统可以通过MMU来知道一块空间是否被申请使用,以及它的访问权限。任何试图访问未申请的内存或访问权限受限的内存的行为都会导致操作系统介入,并防止程序继续执行,从而确保了系统的稳定性和安全性。
(5)看了上面关于MMU的介绍,我们知道了,MMU可以阻止访问未被申请的内存或者权限访问受限的内存行为。这个就很好的说明了,为什么,这个bug有时候出现,有时候又消失的原因了。
(6)我们都知道,将gpios[i].name定义成字符指针的时候,他本质上是一个野指针,所以他指向的空间是否有效,是未知的。如果他指向了一个没有被申请的内存。就会显示段错误。
(7)但是,如果gpios[i].name作为字符指针,作为野指针指向的是一个已经被申请的内存,那么就不会发生段错了。但是,这样MMU的作用也显得没有那么大。真的是这样的吗?
(8)MMU发现这个野指针指向的是一个已经被申请的内存,但是,他能够识别这个野指针是否具有访问权限。如果没有访问权限,虽然不会报段错误,也依然会有其他报错出现。
没有MMU会怎么样
(1)由此可见,MMU极大的保护了系统的安全性。但是,我们上面都说了,MMU是计算机硬件的一个组件,如果我们当前使用的芯片,没有MMU,只是一个普普通通的微控制器,如STM32F103这种,会怎么样呢?
(2)因为没有MMU,所以常规的MCU是无法从硬件的角度来实现检测内存情况的。
(3)但是,真的就没有救了吗?显然不是,Linux现在支持没有MMU的芯片。所以,肯定还是有办法处理的。
(4)常见的软件内存管理方法有:位图管理,链表管理,内存池。
(5)虽然存在软件的内存管理,但依然还是有缺陷的。因为没有硬件层面的地址转换和权限检查,这些方法可能会更容易受到错误、越界访问或资源争用等问题的影响。有MMU的系统能够更有效地隔离不同进程的内存空间,提供更好的安全性和稳定性。
(6)可能有人就会问了,为什么会更容易出现资源争用问题呢?依旧是拿我上面哪个报错的代码来举例子。
<1>首先,我们知道gpios[i].name定义成字符指针的时候,指向位置是未知的。那么就存在一个问题,假如gpios[i].name正好指向了一块被申请的,被其他变量占用的内存,会发生什么呢?
<2>这样我们如果对gpios[i].name进行数据调整,那么,对应的变量就会跟着改变。而按照正常逻辑来说,哪个变量应该是不会变的。那样就会引发未知的错误。
<3>这种野指针指向了一块其他变量存储区域的行为,就是资源争抢。
总结
(1)我们要清楚认识到,字符指针和字符数组指针的异同。如果字符指针没有被初始化,那么他就是一个危险的野指针。
(2)通过MMU机制,芯片能够进行很好的内存管理,来用于检测指针乱指向的问题。
(3)如果没有MMU机制,也可也通过位图管理,链表管理,内存池等方法进行内存管理,但是相比较有MMU而言,还是有所欠缺。