왜 고토가 위험한가요?
goto
자체적으로 불안정성을 유발하지 않습니다. 약 10 만 goto
초 에도 Linux 커널 은 여전히 안정성 모델입니다.
goto
그 자체만으로는 보안 취약점이 발생하지 않아야합니다. 그러나 일부 언어에서는 예외 관리 블록 과 함께 try
/ catch
예외 관리 블록을 혼합하면 이 CERT 권장 사항에 설명 된대로 취약점이 발생할 수 있습니다 . 주류 C ++ 컴파일러는 이러한 오류를 표시하고 방지하지만 불행히도 이전 또는 더 이국적인 컴파일러는 그렇지 않습니다.
goto
읽을 수없고 유지할 수없는 코드를 발생시킵니다. 이것은 스파게티 코드 라고도 합니다 . 왜냐하면 스파게티 플레이트 에서처럼 너무 많은 고토가있을 때 제어 흐름을 따르는 것이 매우 어렵 기 때문입니다.
스파게티 코드를 피하고 고토를 몇 개만 사용하더라도 버그와 리소스 누출을 여전히 촉진합니다.
- 명확한 중첩 블록과 루프 또는 스위치와 함께 구조 프로그래밍을 사용하는 코드는 따르기 쉽습니다. 제어 흐름은 매우 예측 가능합니다. 따라서 불변량을 존중하는 것이 더 쉽습니다.
- A의
goto
문, 당신은 간단 흐름을 중단하고 기대를 휴식. 예를 들어, 여전히 자원을 비워야한다는 것을 알지 못할 수 있습니다.
goto
다른 장소의 많은 사람들 이 단일 goto 대상으로 보낼 수 있습니다. 따라서이 장소에 도달했을 때의 상태를 확실하게 알 수 없습니다. 그러므로 틀리거나 근거가없는 가정을 할 위험은 상당히 크다.
추가 정보 및 인용문 :
C는 무한히 어설픈 goto
문장과 레이블을 제공합니다 . 공식적으로 goto
는 결코 필요하지 않으며 실제로는 코드없이 코드를 작성하는 것이 거의 항상 쉽습니다. (...)
그럼에도 불구하고 우리는 고토의이 장소를 찾을 수있는 몇 가지 상황을 제안합니다. 가장 일반적인 용도는 한 번에 두 개의 루프를 끊는 것과 같이 일부 중첩 된 구조에서 처리를 포기하는 것입니다. (...)
우리는 그 문제에 대해 독단적이지는 않지만, goto 선언문을 조금만 사용해야 하는 것 같습니다 .
언제 갈 수 있나요?
K & R처럼 저는 고 토스에 대해 독단적이지 않습니다. 나는 goto가 인생을 편하게 할 수있는 상황이 있음을 인정한다.
일반적으로 C에서 goto는 다중 레벨 루프 종료 또는 지금까지 할당 된 모든 자원을 해제 / 해제하는 적절한 종료점에 도달해야하는 오류 처리를 허용합니다 (예 : 순서대로 다중 할당은 다중 레이블을 의미 함). 이 기사 는 Linux 커널에서 goto의 다른 용도를 정량화합니다.
개인적으로 나는 그것을 피하고 10 년 동안 최대 10 개의 고토를 사용했습니다. 나는 if
더 읽기 쉽다고 생각하는 중첩 된을 사용하는 것을 선호합니다 . 이것이 너무 깊은 중첩으로 이어질 때 함수를 작은 부분으로 분해하거나 부울 표시기를 계단식으로 사용하도록 선택했습니다. 오늘날의 최적화 컴파일러는와 동일한 코드와 거의 동일한 코드를 생성 할만큼 영리 goto
합니다.
goto의 사용은 언어에 따라 크게 다릅니다.
C ++에서 RAII를 올바르게 사용 하면 컴파일러가 범위를 벗어난 객체를 자동으로 파괴하므로 리소스 / 잠금이 정리되어 더 이상 갈 필요가 없습니다.
자바에서는 더 고토의 필요성 (자바의 위 저자의 인용이 볼 수 없다 우수한 스택 오버플로 대답 ) : 엉망이, 청소 가비지 컬렉터 break
, continue
및 try
/ catch
예외 처리는 모든 케이스 커버 goto
도움이 될 수있는, 그러나으로는 더 안전하고 더 나은 방법. Java의 인기는 현대 언어로 goto 문을 피할 수 있음을 증명합니다.
유명한 SSL goto fail 취약점 확대
중요 면책 조항 : 의견의 격렬한 논의를 고려할 때 goto 문 이이 버그의 유일한 원인이라고 주장하지 않는다는 것을 분명히하고 싶습니다. 나는 goto가 없으면 버그가 없다고 가정하지 않습니다. 고토가 심각한 버그에 관여 할 수 있음을 보여주고 싶습니다.
나는 goto
프로그래밍의 역사에서 몇 가지 심각한 버그가 관련되어 있는지 잘 모른다 . 그러나 iOS의 보안을 약화시킨 유명한 Apple SSL 버그 가있었습니다. 이 버그를 일으킨 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
실제로 조건부 블록을 둘러싼 중괄호는 버그를 방지 할 수있었습니다.
컴파일시 구문 오류 (따라서 수정) 또는 중복 무해한 이동으로 이어질 수 있습니다. 그건 그렇고, GCC 6은 일관성없는 들여 쓰기를 감지하는 옵션 경고 덕분에 이러한 오류를 발견 할 수 있습니다.
그러나 처음에는 더 구조화 된 코드로 이러한 모든 문제를 피할 수있었습니다. 따라서 goto는 최소한이 버그의 원인입니다. 그것을 피할 수있는 최소한 두 가지 방법이 있습니다.
접근법 1 : if 절 또는 중첩 if
s
오류에 대한 많은 조건을 순차적으로 테스트하는 대신 fail
문제가 발생한 경우 레이블로 보낼 때마다 if
잘못된 전제 조건이없는 경우에만 암호화 작업을 수행하도록 선택할 수 있습니다 .
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
여기에는 단일 goto가 없습니다. 실패 종료 지점으로 빠르게 이동할 위험이 없습니다. 그리고 시각적으로 잘못 정렬 된 선이나 잊혀진 부분을 쉽게 발견 할 수 있습니다 ok &&
.
이 구조는 더 간결합니다. C에서 논리의 두 번째 부분과 ( &&
)은 첫 번째 부분이 참인 경우에만 평가 된다는 사실에 근거 합니다. 실제로 최적화 컴파일러에 의해 생성 된 어셈블러는 gotos가 포함 된 원래 코드와 거의 동일합니다. 옵티마이 저는 조건 체인을 매우 잘 감지하고 코드를 생성합니다. 처음에는 null이 아닌 반환 값이 끝으로 이동합니다 ( online proof ).
테스트 단계에서 ok 플래그와 오류 코드 사이의 불일치를 식별 할 수있는 기능 종료시 일관성 검사를 예상 할 수도 있습니다.
assert( (ok==false && err!=0) || (ok==true && err==0) );
실수 ==0
로 !=0
또는 논리적 커넥터 오류 로 대체 된 실수 는 디버깅 단계에서 쉽게 발견됩니다.
말했듯이 : 나는 대체 구문이 버그를 피했다고 가정하지 않습니다. 나는 그들이 버그를 더 어렵게 만들 수 있다고 말하고 싶습니다.