마르틴 우에커(Martin Uecker)는 초기화되지 않은 변수를 사용하는 C 코드에서 Clang이 경고 없이 코드를 빈 main 함수로 최적화하는 사례를 소개한다. GCC는 최적화가 켜져 있으면 경고를 내지만, C 표준에서 초기화되지 않은 변수 읽기가 언제나 정의되지 않은 동작은 아니라는 점도 짚는다.

지난해 다룬 다른 사례 두 건처럼 일부 C 컴파일러는 정의되지 않은 동작(UB)을 더 잘 진단하게 됐다. 하지만 항상 그런 것은 아니다. 다음 예제는 버그 보고서를 바탕으로 수정한 것이다.

예제 1:

#if __clang__
[[clang::noinline]]
#else
[[gnu::noinline]]
#endif
int foo(int i, int x)
{
 return i ? x + 1 : 0;
}

int main()
{
 int x;

 for (int i = 0; i < 10; i++)
 x = foo(i, x);

 return x;
}

최적화를 켜면 GCC는 -Wall에 포함된 -Wuninitialized로 초기화되지 않은 변수 x의 사용을 진단한다. 반면 Clang은 코드를 적극적으로 최적화해 빈 main 함수를 만들면서도 경고를 내지 않는다. -Wconditional-uninitialized 옵션을 사용하면 Clang도 경고할 수 있지만, 오탐이 많아 이 옵션은 -Wall이나 -Wextra에 포함되지 않는다.

우에커는 Clang의 최적화가 적극적인 반면 경고는 코드에 대한 피상적인 분석을 수행하는 컴파일러 프런트엔드에서만 나온다는 점이 이런 불일치를 낳는다고 설명한다. noinline을 제거하면 GCC도 더는 문제를 진단하지 않지만, 코드를 잘못 컴파일하지는 않는다. 이후 컴파일 단계에서는 원본 코드와의 연결이 일부 사라져 경고를 내기가 쉽지 않고, 이 경우에도 오탐과 누락이 모두 생길 수 있다는 설명이다.

C 표준에서 초기화되지 않은 변수 읽기는 변수를 가리키는 주소를 취하지 않은 경우에 정의되지 않은 동작이 될 수 있다. 따라서 초기화되지 않은 변수를 읽는 행위가 C에서 언제나 정의되지 않은 동작이라는 통념은 정확하지 않다. 우에커는 정의되지 않은 동작이 되는 경우가 두 가지라고 설명한다. 첫째는 주소를 취하지 않은 자동 변수의 경우다. 이 규정은 최적화를 위해서가 아니라, 초기화되지 않은 레지스터를 읽을 때 트랩이 발생할 수 있는 Itanium 프로세서에서 문제를 감지하기 위한 안전 기능으로 추가됐다. 둘째는 형식에 유효한 값이 아닌 표현이 있고, 초기화되지 않은 객체의 불확정 표현이 그 표현에 해당하는 경우다. 컴파일러는 프로그램이 표현 바이트를 직접 관찰하지 않는 한 이 경우를 가정할 수도 있다.

우에커는 int에 유효하지 않은 값 표현이 없는 플랫폼에서는 다음 코드가 ISO C 기준으로 정의되지 않은 동작이 아니지만, Clang이 여전히 잘못 컴파일한다고 주장한다. 그는 이 문제를 버그로 보고했다.

예제 2:

#if __clang__
[[clang::noinline]]
#else
[[gnu::noinline]]
#endif
int foo(int i, int x)
{
 return i ? x + 1 : 0;
}

int main()
{
 int x;

 (void)&x;

 for (int i = 0; i < 10; i++)
 x = foo(i, x);

 return x;
}

우에커가 시험한 예제에서는 변수의 주소를 실제로 전달하면 어떤 컴파일러도 코드를 잘못 컴파일하지 않았다. 그는 이 방식이 기존 C 코드 상당수도 깨뜨릴 수 있다고 본다.

예제 3:

int foo(int i, int *p)
{
 int x = *p;
 return i ? x + 1 : 0;
}

int main()
{
 int x;

 for (int i = 0; i < 10; i++)
 x = foo(i, &x);

 return x;
}

우에커는 C 컴파일러의 경고와 분석 도구가 과거보다 나아졌지만 GCC와 Clang 모두 해결되지 않은 문제가 남아 있다고 결론짓는다. 특히 적극적인 최적화에 비해 경고가 충분히 강하지 않은 점이 문제이며, 프런트엔드에서만 경고를 내는 Clang에서 이런 불일치가 더 두드러진다고 지적한다. 그는 컴파일러가 최적화를 이미 제한하는 C 표준을 무시하거나 매우 창의적으로 해석하는 경우도 우려한다.

우에커는 자신의 견해로는 ISO C를 준수하지 않는 Clang으로 C를 작성할 때 -ftrivial-auto-var-init=zero와 -fno-strict-aliasing을 함께 사용할 것을 권한다. 그는 다음 글에서 앨리어싱 관련 표준 준수 문제를 다룰 계획이라고 밝혔다. GCC가 대체로 더 안전하고 표준을 잘 준수한다고 보지만, GCC에도 비슷한 문제가 있다고 덧붙였다.

부록: 원문 예시

text
: In function 'main':
:17:13: warning: 'x' is used uninitialized [-Wuninitialized]
 17 | x = foo(i, x);
 | ^~~~~~~~~
:14:9: note: 'x' was declared here
 14 | int x;
 | ^