从一个小问题说开去 - 兼论编程责任

 

最近在写一个更新平台固件的程序,我们采用的方法基本上是在固件运行阶段安装一个SMI Handler,这个Handler在进入Windows/DOS/Linux等OS后依然存在。这个SMI把系统后台的一个runtime driver(主要访问SPI的)的几个读/写FLASH的接口给expose出来。这样当OS下的程序需要更新固件的时候,它们可以通过触发软件SMI,调用这些接口。这样就实现了更新FLASH。之所以这么设计,主要是考虑到最大限度的复用代码,因为现在BIOS都把FLASH本身存放Setup设置的场所,所以BIOS本身就可以写FLASH。事实上每一次开机,FLASH里面都会被更新几个值。这样一来,如果一个更新FLASH的代码再次实现一边读/写的代码,就有点重复了,且不同的平台读写的方法有可能因为芯片组的不一样而不太一样,为了避免要不断升级更新程序,这种实现方式是最好的。它第一可以保证仅有本公司的固件可以被更新(因为只有我们自己的固件才会内建这些服务);其次可以最大限度的保证刷新工具的通用性。

 

不过我今天不打算继续谈及这个技术问题,也许将来我会专门写一篇文章来论述固件更新的一些详细细节。我今天想谈谈我在做这样一件事情的时候,遇到的一些让我很有些感想的事情。

 

我们的刷新工具在BIOS下工作的很好,于是我决定在DOS下测试。结果一运行就挂了,而且是彻底的锁死机器。看起来像是SMI处理程序飞了。我百思不得其解,SMI都是在SMM内运行的,这一段内存早就在传递给操作系统的内存表内标记为Reserved,任何操作系统都不会使用,而且SMI在实现的时候就保证了其在runtime阶段不会调用任何已经不存在的boot time的服务。可我们的程序为什么还会飞了呢?

 

幸好我手头现在有Arium,于是我顺手跟了跟代码,结果令我大吃一惊。我们的那个SPI的驱动有类似的这样的代码:

 

EFI_STATUS

WaitForOperationDone (

VOID

) {


UINTN StallCount;

for (StallCount = 1000; StallCount > 0; StallCount --) {

if (*RegisterAddress & BIT_8) {

// Previous operation is finished. Let's continue

return EFI_SUCCESS;

}

// Let's wait for a while 1ms

gBS->Stall (1000);

}


return EFI_DEVICE_ERROR;

}

 

上文当然不是实际的代码,不过已经足够说明问题了,这段代码的作用是等待某一个硬件操作完成,方法是检测某个memory map的寄存器的第八位是否为1,如果是1则说明这一操作已经完成。否则继续等待。为了防止由于硬件本身的损坏而无限等待造成将机器锁死的问题,作者设置了一个最大的等待时间1ms * 1000 = 1s,即如果在1秒内还没有完成那就说明硬件可能出现了某些问题。

 

这段代码初看起来比较OK,设置处理可能的硬件损坏,但是有一个致命的缺点。这段代码是SPI驱动的一部分,这个驱动本身被设计成runtime驱动,所谓的runtime是指在操作系统起来之后,仍然继续驻留内存,为操作系统和用户提供某些服务的程序。这类程序很特殊,要求很高的鲁棒性。而这段代码的作者显然忽略了这一点,他直接调用了一个系统服务来实现延时:

 

gBS->Stall (1000);

 

Stall()是一个所谓的“启动时服务”(Boot Services)。换言之,这个服务只在固件启动操作系统之前有效。所以此类服务绝对不能出现在runtime的代码内。这就导致了SPI驱动只能在BIOS启动阶段正常的工作。如果有人在runtime阶段调用这个驱动提供的服务(比如我),呵呵,那它只能把系统彻底锁死。更糟糕的是由于我们是在SMI内调用这个服务,而SMI对操作系统是完全透明的,所以整个机器都将被彻底的崩溃,相比之下,操作系统内的某个进程崩溃所造成的损失还小一些,起码他不会使用户的其他进程受到损失。由此可见这个bug是如何的重大。

 

我很自然找到了当时写这个代码的工程师,结果我得到的回答令我更加震惊,程度丝毫不亚于我看到这个bug的时候,他说:反正又没人调!

 

看来我们在培养工程师时候忽略了对编程责任的培养,我们很多工程师想的只是如何快速的完成任务,以及骗过QA的眼睛。就拿上述程序来说,如果不调用gBS->Stall,那么在runtime下作精确延迟的确非常苦难,可能涉及到对CPU指令的测量,也许正是这些,促使了我们的程序员开始采用一些看起来能够明显减轻工作量的”聪明“方法。诚然,目前由于操作系统还不能调用EFI的服务,但是未来的操作系统一定是会调用的。如果用户那个时候安装了一个这样的操作系统,那么结果呢?我不敢想象,也许随之而来的是大规模的召回,几十亿的损失。而这一切,仅仅是一个简单的Stall。

 

由此想到了软件史上另一个有名的bug -- 由于少写了一个break,使得一个switch case没有中断,而是继续执行了下面的那个case,结果导致了北美地区某个电话网络大面积瘫痪。损失不可计数。这些问题本来都是可以避免的。但是由于程序员的忽视,甚至不负责的行为,导致了不可预料的结果。

 

CMMI要求在软件开发环节增加代码评审流程,看来是十分必要的。现在很多公司为了节约开发成本,都随意省略这个环节,结果一些问题代码就这样的进入了市场,给我们的生活留下了无尽的隐患。

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值