韦德娱乐,韦德1946,韦德娱乐官网

新闻中心

EEPW首页 > 牛人业话 > 数组越界真可怕,莫名就闯到了别人家

数组越界真可怕,莫名就闯到了别人家

作者:天雷君时间:2018-11-30来源:韦德娱乐官网

“悄悄是别离的笙箫,沉默是今晚的康桥。悄悄的我走了,正如我悄悄的来;我挥一挥衣袖,不带走一片云彩。”情圣徐志摩的这首《再别康桥》脍炙人口,当年传遍大江南北,撩动了无数少女的芳心。民国才女林徽因经受住了徐志摩诸多情诗的狂轰滥炸,与梁家大公子思成君喜结连理,一对金童玉女完美结合,双双投身建筑事业,为中国古建筑的保护和传承留下了不可磨灭的功勋。

本文引用地址:/article/201811/395096.htm

1543555630791693.jpg

  相较之下,头顶民国四大美女之一光环的陆小曼就没有那般矜持了,贪恋美色的志摩君在一股股不能自抑的荷尔蒙的冲击下,将发妻、老父和家庭伦理置于脑后,将社会舆论、闲言碎语视若浮云,对国色天香的陆小曼狂轰乱炸,在那甜到心里、酥到骨里的情诗的几番引逗之下,嫁做人妇的陆小曼出了轨,越了界,离了婚,和志摩君各取所需、“相亲相爱”地结合在了一起。只是,“王子和公主并没有永远幸福地生活在一起”,为了满足陆小曼纸醉金迷、骄奢淫逸的生活,徐志摩拖着肾亏的身体四处走穴讲课,一代才子竟最终死于空难,客死他乡。

  不知道,这是不是“青年导师”俞敏洪先生口中所谓的“女子堕落导致家庭堕落”的经典案例呢?

  看来,“越界”真的没有好下场,破坏了两个家庭不说,最终自己也搞到家破人亡。天圆地方、大道以常,不只是爱情、婚姻的“越界”会遭到天谴,搞嵌入式写代码的,遇到“越界”也会被搞得栖栖遑遑。

  1

  我一直觉得,程序员就是一个多疑到有些神经兮兮的群体,每每出了bug,便怀疑起天,怀疑起地,怀疑起赖以生存的空气。这不,天雷君又开始作了,对着一个bug,竟然无端地怀疑起1+1不等于2起来了。

1543555662632882.jpg

  照例,洒家先不惜笔墨,把这个bug放在实际应用背景和程序中简单描述一番。

  笔者这款产品带有遥控功能,接收到遥控器发送的遥控报文后,从中提取出相应的命令数据,根据命令执行相关操作。程序的第一步就是接收遥控报文中的数据场中的数据位流,把这些数据位按照每八位组合成一个字节的方式,提取出字节形式的数据。

  通过巧妙地设计遥控报文的格式,程序可以检测出“报文数据场”的第一个数据位,然后依序将每八个数据位存储到一个字节形式的成员中,统计到固定的数据位长度后,“报文数据场”检测结束,之后便是对数据进行解密、解析的事情了。

  显然,这里的数据位长度是8的倍数,在这个遥控报文中,数据位的个数是288,数据长度为288/8=36个字节,字节形式的数据定义为Rx_rawbit[36]。在程序里,数据位的长度以一个16位的变量Rf_bit_count表示,接收到一位数据位,将Rf_bit_count加一,Rf_bit_coun加到288,就说明接收完了所有的数据位。

  上述Rx_rawbit数组成员为Rx_rawbit[0]- Rx_rawbit[35],数组下标从0开始,数据位的一般表示方法为0-7位,显然,第一个数据位存放在第0个字节的第0位,第二个数据位存放在第0个字节的第1位,。。。,第九个数据位存放在第1个字节的第0位,。。。,

  以此类推,第n+1个数据位存放在第(n>>3)个字节的第(n & 0x07)位。

  2

  正经八百的解释到此基本结束,洒家不由得想起了《末代皇帝》里的经典画面,三岁的溥仪被接入皇宫,举行登基大典,小宣统被乌压压的场面烦躁地不行,眼看着就要大哭起来,他的父亲-当时的摄政王不停地安慰他,‘快完了,快完了!’我想告诉各位看官的是,我的描述还差一点点,‘快完了!’

  还记得那个“把大象放进冰箱分为几步?”的经典笑话吗?做为一名优秀的“唠嗑钟点工”,丹丹大妈给出的答案是两步,第一步,打开冰箱,第二步,把大象放到冰箱里。虽说是典型的脑筋急转弯,但是蕴含着深刻的智慧。

1543555699728751.jpg

  洒家借花献佛,也把这里存放数据位的操作分为两步。

  第一步,打开冰箱,即计算出该数据位的字节位置和位位置,如前所述,第n+1个数据位的字节位置为(n>>3),位位置为(n & 0x07),这是通过下面这个函数实现的,

  void SetByteBitIdx(void)

  {

  uint16_t idx;

  idx = Rf_bit_count;

  Rf_rawbyte_idx = idx >> 3;

  Rf_rawbit_idx  = idx & 0x07;

  Rf_bit_count++;

  }

  第二步,把数据位0或1这个‘大象’放到前面打开的‘冰箱’里,这是通过下面这个函数实现的。

  void StoreRfBit (bool data)

  {

  SetByteBitIdx();

  if(1 == data){

  Rx_rawbit[Rf_rawbyte_idx] |= 1 << Rf_rawbit_idx;

  }else{

  Rx_rawbit[Rf_rawbyte_idx] &= ~(1 << Rf_rawbit_idx);

  }

  }

  乍看下来,设计方案构思缜密,程序代码实现巧妙,设计和实现都堪称天衣无缝。按下遥控器,射频信号在空间汩汩流动,遥控接收板有序地把数据整齐码好,踏着相同的节拍,大家一起嗨起来。

  3

  但是,理想是美好的,现实永远是骨感的。

1543555728229384.jpg

  小心翼翼地把代码写好,反复检查了几遍,水平有限,着实检查不出来任何毛病,那就是骡子是马,拉出来溜溜吧。结果,一通测试下来,兴奋劲还没过去,一头冷水便照头泼了下来。按了遥控键,十次中倒有个两三次不好使,“永不消逝的电波”就好像泥牛入海,转眼间便走散了消息,不见了踪迹。

  被风撕碎的一片片白云在辽阔高远的天空中肆意飘荡着,火红的太阳炙烤着滚烫的大地。洒家背靠窗台,一面感受着顽强得透过隔温玻璃的阳光洒在脊背上的暖意,一边在空调房中体味着心中的阵阵寒意。电波到底去哪儿了?

  洒家搜索的目光在电脑屏幕上不断游离,心中条分缕析,早把各行代码都当成了嫌疑,最后,目光定格在判断接收到一帧完整的报文的语句那里:

  if(Rf_bit_count >= RF_RAWBIT_LEN){

  SetRfFrameComplete();

  }

  基本上,钥匙每短按一次,就会发送三帧报文,既然遥控不好使,大抵可以确认中间存在漏报文的情形,为了验证这种猜测,洒家加了一条测试语句:

  if(Rf_bit_count >= RF_RAWBIT_LEN){

  Rf_frame_times++;

  SetRfFrameComplete();

  }

  显然,短按一次,Rf_frame_times应该为3,短按n次,Rf_frame_times应该为(n*3)。

  马不停蹄地测试下来,果不其然,短按过十次后,Rf_frame_times的值不到30。难道是报文解析程序出了问题?

  4

  笔者曾经在《天灵灵地灵灵,遥控为何会失灵》一文中讲述过报文解析程序的一部分原理和设计,

  “射频位到数据位采用了曼彻斯特编码形式,以射频位01表示数字位1,以射频位10表示数字位0,BCM采用上升沿触发中断的方式,根据相邻两个上升沿之间的时间间隔来赋值射频位。BCM根据遥控报文的格式提取出“数据场”中的射频位位流,然后进行曼彻斯特解码,计算出数据位位流,进而提取出字节形式的数据。。。。相邻两个上升沿的间隔取值只可能为2T、3T、4T,T为射频位位宽。”

  前文所述“把数据位0或1这个‘大象’放到前面打开的‘冰箱’里”,指的就是赋值射频位的过程,

  如果是2T,执行StoreRfBit(0); StoreRfBit(1);

  如果是3T,执行StoreRfBit(1); StoreRfBit(0); StoreRfBit(1); 或者StoreRfBit(0); StoreRfBit(1);

  StoreRfBit(0);

  如果是4T,执行StoreRfBit(1); StoreRfBit(0); StoreRfBit(0); StoreRfBit(1);

  遥控报文的格式非常规整,能够很容易地找到数据场的第一位,然后次序收完数据场的最后一位,本不该出现接收报文失败的问题。既然无法通过分析代码找出bug,那就只好祭出“调试大法”了。继续添加测试语句:

  if(Rf_bit_count >= RF_RAWBIT_LEN){

  Rf_frame_times++;

  StoreRfCompleteIdx();

  SetRfFrameComplete();

  }

  洒家这次没有吝惜RAM,开了个足以存储好几条报文射频位位宽数据的轮转型大数组,在判断接收到一帧完整的报文语句那里存储完整报文最后一个数据位在轮转型大数组中的下标位置,设置断点,运行到断点位置后,便可以从这个数组下标往前搜索,看看之前几个报文的射频位位宽数据是否有什么异常。

  聪明的读者肯定已经抢先一步意识到了,射频位位宽没有出现任何异常。报文头很规则,数据场很整齐,报文尾部也很利落。

  总之,所有的射频位数据安安静静地等待在时间的无涯荒野里,没有早一步,也没有晚一步,我遇上了,却没有轻轻地说一句:哦,原来你也在这里呵!

  洒家揉了揉因为看数据看得有些发胀的眼睛,缓缓走到窗台前,眯缝起眼睛,打量了一下远处笼罩在一片蜃气之中的青山。每每遇到难解的问题,洒家总要到窗台阳光的沐浴中,眺望一下远处的山,让脑袋放空,然后闭上眼睛,静下心来,等着灵感不请自来。

1543555761907915.gif

  中央空调沙沙的换气声音、同事的窃窃私语声不时入耳,我在心里不断回忆着有限的职业生涯里遇到的一个个bug,同时盘算着当前程序可能的缺陷。报文格式的分段解析肯定没问题,射频位位宽的判断也没问题,射频位的赋值也没问题,想来都没有问题,这个bug隐藏地够深的。

  5

  还得调试!行文至此,我不禁怀疑起了自己的智商,为什么基本上所有难题的解决都是靠调试解决的?没有一次是灵光电闪,看代码直捣黄龙,找出bug的?

  被这个难题耗损了大半豪气的我,老老实实地开始调试起来。为了定位bug,笔者特意修改了程序结构,之前的程序是边接收边解析,实时性固然好,但是接收了几个数据便解析,不利于调试,所以改为收到两三条报文后,再集中进行解析。

  洒家看代码找bug的本事没有,设计调试方案找bug的能力还是有的。修改程序之后,调试下来,问题就慢慢浮上水面了。程序解析出报文头部,进入数据场之后,在赋值射频位的过程中,结果出现了Rf_bit_count从288累加到33的情形!!!

timg (1).gif

  奇怪了,288累加一下应该是289,怎么就变成33了?莫非1+1不等于2了不成?!!此处肯定有蹊跷,洒家打眼一瞧,马上看出了一点端倪,289和33正好差了一个256,就好像是把向高字节的进位吃掉了一般。问题开始变得有趣了!

  Rf_bit_count是个16位的数据,好像突然变成了8位数据,显然,它的高位字节发生了不为人知的变化!写过多年代码的洒家,立马想到了是和Rf_bit_count临近空间的数据搞的鬼,回到定义位置一看,

  uint8_t  Rx_buffer[RF_DATA_LEN];

  uint8_t  Rx_rawbit[RF_RAWDATA_LEN];

  uint16_t Rf_bit_count;

  一切都了然了,Rf_bit_count挨着Rx_rawbit这个数组,肯定是Rx_rawbit这个数组搞的事,这个数组越了界,就会改变Rf_bit_count的数据,下面就简单了,看看是不是这回事!

  我飞速地在纸上算了一下,当Rf_bit_count=288时,右移三位为36,Rf_rawbyte_idx =36时,Rx_rawbit数组正好越界,由于Rf_bit_count的位置正好在Rx_rawbit之后,而且所使用的处理器是大端模式,大端方式将高位存放在低地址,小端方式将低位存放在低地址。

1543555836409141.jpg

所以,Rf_bit_count的高字节正好挨着Rx_rawbit数组,肯定是对Rx_rawbit[36]赋值为0了,导致Rf_bit_count本来是1的高字节变成了0,于是289就变成了33,这样一来,判断接收到一条完整报文的语句里的if语句里的条件肯定是false了,于是,好好的一帧数据就这样被漏掉了。

  if(Rf_bit_count >= RF_RAWBIT_LEN){

  SetRfFrameComplete();

  }

  当然,细心的读者可能会比较奇怪,只需要接收288个射频位,为什么Rf_bit_count还会累加到289呢?

  这是因为,最后一个数据位可能是1也可能是0,最后统计到的射频位宽可能是2T、3T或者4T,它们执行的Rf_bit_count是不一样的,有时正好统计到288,那自然万事大吉,可以接收到报文,可是有时就会超过288,如前所述,这时它的高字节就有可能会被Rx_rawbit[36]吃掉,这就接收不到报文了。

  后记

  笔者学过一段时间的Java,Java对数组进行了一定的安全处理,在运行期间会自动判断数组下标是否越界,当时看的洒家羡慕得不得了。在计算机的世界里,C语言饱经沧桑,年头太老了,这种优异的特性显然指望不上,我等嵌入式工程师只能擦亮慧眼,始终保持警惕,要知道:数组越界真可怕,莫名就闯到了别人家。



关键词: 数组

评论

技术专区

关闭