为什么goto危险?
goto
本身不会引起不稳定。尽管大约有100,000 goto
s,Linux内核仍然是稳定的模型。
goto
本身不应引起安全漏洞。但是,在某些语言中,将其与try
/ catch
异常管理块混合使用可能会导致漏洞,如本CERT建议中所述。主流C ++编译器会标记并防止此类错误,但不幸的是,较早或更旧的编译器不会这样做。
goto
导致无法读取和无法维护的代码。这也称为意大利面条代码,因为像意大利面条盘一样,当有太多的goto时,很难遵循控制流程。
即使您设法避免使用意大利面条式的代码,并且仅使用了几个goto,它们仍然会带来类似bug和资源泄漏的问题:
- 使用具有清晰嵌套的块和循环或开关的结构编程的代码很容易理解;它的控制流程非常可预测。因此,更容易确保不变量得到尊重。
- 通过
goto
声明,您可以打破那种直截了当的流程,并打破期望。例如,您可能不会注意到仍然必须释放资源。
- 许多
goto
不同地方的人都可以将您带到一个goto目标。因此,确定到达此位置时所处的状态并不明显。因此,做出错误/毫无根据的假设的风险很大。
附加信息和报价:
C提供了无限滥用的goto
语句和要跳转到的标签。形式上goto
从来没有必要,实际上,没有它,编写代码几乎总是容易的。(...)
尽管如此,我们将建议goto可能会找到位置的一些情况。最常见的用途是放弃某些深层嵌套结构中的处理,例如一次脱离两个循环。(...)
尽管我们对此并不拘谨,但似乎应该尽量少使用goto语句。
什么时候可以使用goto?
像K&R一样,我也不是固执己见。我承认,在某些情况下,goto可能会减轻您的生活。
通常,在C语言中,goto允许多级循环退出,或错误处理要求到达适当的退出点,以释放/解锁到目前为止分配的所有资源(即,顺序进行多次分配意味着多个标签)。 本文量化了Linux内核中goto的不同用法。
我个人比较喜欢避免这种情况,并且在使用C语言的10年中,我最多使用10个goto。我更喜欢使用嵌套if
s,我认为它更易读。当这会导致嵌套太深时,我会选择将函数分解为较小的部分,或者在级联中使用布尔值指示器。如今的优化编译器足够聪明,可以生成几乎与相同的代码goto
。
goto的使用很大程度上取决于语言:
放大著名的SSL goto失败漏洞
重要免责声明:鉴于评论中的激烈讨论,我想澄清一下,我并不假装goto语句是导致此错误的唯一原因。我不假装没有goto就不会有bug。我只想表明goto可能与严重的错误有关。
我不知道goto
编程历史中涉及多少个严重的错误:细节通常无法传达。但是,有一个著名的Apple SSL错误削弱了iOS的安全性。导致此错误的goto
语句是错误的语句。
有人认为,错误的根本原因不是goto语句本身,而是错误的复制/粘贴,误导性的缩进,条件块周围缺少花括号或开发人员的工作习惯。我也无法确认其中任何一个:所有这些论点都是可能的假设和解释。没人真正知道。(同时,鉴于同一功能中的其他缩进不一致,合并的假设如注释中所建议的那样出错了,这似乎是一个很好的选择)。
唯一客观的事实是重复 goto
导致过早退出该功能。查看代码,唯一可能导致相同结果的其他语句将是返回值。
该文件中的函数SSLEncodeSignedServerKeyExchange()
存在错误:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) != 0)
goto fail;
if ((err =...) !=0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail; // <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!
if (...)
goto fail;
... // Do some cryptographic operations here
fail:
... // Free resources to process error
确实,条件块周围的花括号可以防止该错误:
它可能导致编译时语法错误(并因此导致更正)或冗余的无害goto。顺便说一句,由于其可选的警告来检测不一致的缩进,GCC 6将能够发现这些错误。
但是首先,使用结构化的代码可以避免所有这些问题。因此,goto至少间接地是导致此错误的原因。至少有两种方法可以避免这种情况:
方法1:if子句或嵌套if
s
与其顺序测试大量条件是否出错,并fail
在出现问题的情况下每次发送给标签,if
不如选择在-statement中执行加密操作,该声明仅在没有错误的前提条件时才这样做:
if ((err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0 &&
(err = ...) == 0 ) &&
(err = ReadyHash(&SSLHashSHA1, &hashCtx)) == 0) &&
...
(err = ...) == 0 ) )
{
... // Do some cryptographic operations here
}
... // Free resources
方法2:使用错误累加器
这种方法基于这样一个事实,即几乎所有的语句都调用某个函数来设置err
错误代码,并仅在该代码err
为0 时才执行其余代码(即,执行的函数没有错误)。一个安全且易读的替代方法是:
bool ok = true;
ok = ok && (err = ReadyHash(&SSLHashSHA1, &hashCtx))) == 0;
ok = ok && (err = NextFunction(...)) == 0;
...
ok = ok && (err = ...) == 0;
... // Free resources
在这里,没有一个转到:没有跳转到故障出口点的风险。而且在视觉上,很容易发现线条未对准或遗忘了ok &&
。
这种结构更紧凑。这是基于以下事实:在C中,&&
仅当逻辑和()的第一部分为true时才对其进行评估。实际上,由优化编译器生成的汇编器几乎等效于使用gotos的原始代码:优化器能够很好地检测条件链并生成代码,该代码在第一个非null返回值时会跳到最后(在线证明)。
您甚至可以设想在功能结束时进行一致性检查,以在测试阶段识别出ok标志和错误代码之间的不匹配。
assert( (ok==false && err!=0) || (ok==true && err==0) );
在调试阶段很容易发现错误,如==0
无意中替换为!=0
或逻辑连接器错误。
如前所述:我不假装其他结构会避免任何错误。我只想说,他们本来可以使此错误更难以发生。