为什么goto危险?
goto本身不会引起不稳定。尽管大约有100,000 gotos,Linux内核仍然是稳定的模型。
goto本身不应引起安全漏洞。但是,在某些语言中,将其与try/ catch异常管理块混合使用可能会导致漏洞,如本CERT建议中所述。主流C ++编译器会标记并防止此类错误,但不幸的是,较早或更旧的编译器不会这样做。
goto导致无法读取和无法维护的代码。这也称为意大利面条代码,因为像意大利面条盘一样,当有太多的goto时,很难遵循控制流程。
即使您设法避免使用意大利面条式的代码,并且仅使用了几个goto,它们仍然会带来类似bug和资源泄漏的问题:
使用具有清晰嵌套的块和循环或开关的结构编程的代码很容易理解;它的控制流程非常可预测。因此,更容易确保不变量得到尊重。
通过goto声明,您可以打破那种直截了当的流程,并打破期望。例如,您可能不会注意到仍然必须释放资源。
许多goto不同地方的人都可以将您带到一个goto目标。因此,确定到达此位置时所处的状态并不明显。因此,做出错误/毫无根据的假设的风险很大。
附加信息和报价:
C提供了无限滥用的goto语句和要跳转到的标签。形式上goto从来没有必要,实际上,没有它,编写代码几乎总是容易的。(...)
尽管如此,我们将建议goto可能会找到位置的一些情况。最常见的用途是放弃某些深层嵌套结构中的处理,例如一次脱离两个循环。(...)
尽管我们对此并不拘谨,但似乎应该尽量少使用goto语句。
没有更多的Goto语句
Java没有goto语句。研究表明,goto被(误用)的原因不只是“因为它存在”而已。消除goto导致了语言的简化(...)对大约100,000行C代码的研究确定,大约90%的goto语句纯粹用于获得摆脱嵌套循环的效果。如上所述,多级中断并继续删除了对goto语句的大部分需求。
Bjarne Stroustrup在他的词汇表中以这些吸引人的术语定义了goto :
goto-臭名昭著的goto。在机器生成的C ++代码中主要有用。
什么时候可以使用goto?
像K&R一样,我也不是固执己见。我承认,在某些情况下,goto可能会减轻您的生活。
通常,在C语言中,goto允许多级循环退出,或错误处理要求到达适当的退出点,以释放/解锁到目前为止分配的所有资源(即,顺序进行多次分配意味着多个标签)。 本文量化了Linux内核中goto的不同用法。
我个人比较喜欢避免这种情况,并且在使用C语言的10年中,我最多使用10个goto。我更喜欢使用嵌套ifs,我认为它更易读。当这会导致嵌套太深时,我会选择将函数分解为较小的部分,或者在级联中使用布尔值指示器。如今的优化编译器足够聪明,可以生成几乎与相同的代码goto。
goto的使用很大程度上取决于语言:
在C ++中,正确使用RAII会使编译器自动销毁超出范围的对象,从而无论如何都将清除资源/锁,并且不再需要goto。
在Java中有没有需要跳转(请参阅Java的作者引用上面这个优秀的堆栈溢出的答案):垃圾收集,清理残局,break,continue,和try/ catch异常处理覆盖所有地方的情况goto可能是有用的,但在一个更安全和更好方式。Java的流行证明了可以用现代语言避免goto语句。
放大著名的SSL goto失败漏洞
重要免责声明:鉴于评论中的激烈讨论,我想澄清一下,我并不假装goto语句是导致此错误的唯一原因。我不假装没有goto就不会有bug。我只想表明goto可能与严重的错误有关。
我不知道goto编程历史中涉及多少个严重的错误:细节通常无法传达。但是,有一个著名的Apple SSL错误削弱了iOS的安全性。导致此错误的goto语句是错误的语句。
有人认为,错误的根本原因不是goto语句本身,而是错误的复制/粘贴,误导性的缩进,条件块周围缺少花括号或开发人员的工作习惯。我也无法确认其中任何一个:所有这些论点都是可能的假设和解释。没人真正知道。(同时,鉴于同一功能中的其他缩进不一致,合并的假设如注释中所建议的那样出错了,这似乎是一个很好的选择)。
唯一客观的事实是重复 goto导致过早退出该功能。查看代码,唯一可能导致相同结果的其他语句将是返回值。
该文件中的函数SSLEncodeSignedServerKeyExchange()存在错误:
if((err=ReadyHash(&SSLHashSHA1,&hashCtx))!=0)gotofail;if((err=...)!=0)gotofail;if((err=SSLHashSHA1.update(&hashCtx,&signedParams))!=0)gotofail;gotofail;// <====OUCH: INDENTATION MISLEADS: THIS IS UNCONDITIONDAL!!if(...)gotofail;...// Do some cryptographic operations herefail:...// Free resources to process error
确实,条件块周围的花括号可以防止该错误:
它可能导致编译时语法错误(并因此导致更正)或冗余的无害goto。顺便说一句,由于其可选的警告来检测不一致的缩进,GCC 6将能够发现这些错误。
但是首先,使用结构化的代码可以避免所有这些问题。因此,goto至少间接地是导致此错误的原因。至少有两种方法可以避免这种情况:
方法1:if子句或嵌套ifs
与其顺序测试大量条件是否出错,并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 时才执行其余代码(即,执行的函数没有错误)。一个安全且易读的替代方法是:
boolok=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或逻辑连接器错误。
如前所述:我不假装其他结构会避免任何错误。我只想说,他们本来可以使此错误更难以发生。