16. Strange Problems

It's not even worth asking the rhetorical question, Have you ever had a baffling bug that you just couldn't track down? Of course you have; everyone has. C has a number of splendid “gotcha!s” lurking in wait for the unwary; this chapter discusses a few of them. (In fact, any language powerful enough to be popular probably has its share of surprises like these.)



Q 16.1
왜 아래 loop는 항상 한 번만 실행될까요?
  for (i = start; i < end; i++);
  {
    printf("%d\n", i);
  }
Answer
for를 쓴 문장의 마지막 뒤에 실수로 붙인 세미콜론 때문에, for가 반복할 문장(statement)은 “null statement”가 됩니다. 중괄호로 둘러싼 블럭은, 얼핏 보기에는 for가 반복할 코드로 보이지만, 사실은 for와 무관한, 그냥 다음 statement일 뿐입니다.

덧붙여 질문 [*]2.18도 참고하시기 바랍니다.

Note
위 코드를 다음과 같이 띄어쓰면 쉽게 이해할 수 있습니다:
  for (i = start; i < end; i++)
    ;

  {
    printf("%d\n", i);
  }



Q 16.1b
문법 오류(syntax error)가 엄청나게 많이 발생했는데 도저히 그 이유를 모르겠습니다.

Answer
먼저 헤더 파일과 소스 파일 전체에서 #if / #ifdef / #ifndef / #else / #endif의 쌍이 맞지 않는지 검사해보기 바랍니다. (덧붙여 질문 2.18, 10.9, 11.29도 참고하시기 바랍니다.)



Q 16.1c
왜 제 프로시져를 호출하는 부분이 동작하지 않을까요? 아무리 봐도 컴파일러가 프로시져 호출 부분을 건너 뛰어 버리는 것 같습니다.

Answer
혹시 다음과 같이 코드를 작성하지 않았나요?
  myprocedure;
C 언어는 함수(function)만을 지원합니다. 그리고 함수에 전달되는 인자가 하나도 없더라도 괄호로 둘러싼 `argument list'를 써 주어야 합니다. 따라서 다음과 같이 써야 합니다:

  myprocedure();



Q 16.2
파일의 첫 부분에서 이상한 syntax error가 납니다. 그런데 제가 보기에는 아무런 문제가 없습니다.
Answer
질문 [*]10.9를 보기 바랍니다.



Q 16.3
동작하기도 전에 프로그램이 깨져버립니다. (debugger로 한 스텝씩 따라하면 main()에 들어가기도 전에 죽어 버립니다.)
Answer
아마도 하나 이상의 (1KB 이상의) 매우 큰 local 배열을 썼을 것입니다. 대부분의 시스템은 고정된 크기의 스택을 가지고 있고, 동적으로 스택을 할당하는 (e.g. Unix) 시스템이더라도 스택이 갑자기 단 한번에 엄청나게 커지려고 하면 혼란을 겪게 됩니다. 매우 큰 배열이 필요하다면 정적(static)으로 할당하는게 좋습니다 (함수를 호출할 때마다 재귀 호출과 같은 이유로, 새로운 배열이 필요하다면 malloc()을 써서 할당하는 게 좋습니다. 덧붙여 질문 [*]1.31도 참고하시기 바랍니다.).

또, 프로그램을 잘못 link했을 확률도 있습니다 (각각 다른 컴파일러 옵션을 써서 만든 object module을 합쳤거나, 잘못된 dynamic library를 link했을 경우). 또는 run-time dynamic library linking이 어떤 이유로 실패했거나, main을 잘못 선언했을 수도 있습니다.

(덧붙여 질문 [*]11.12b, [*]16.4, [*]16.5, [*]18.4도 참고하시기 바랍니다.)

Note
물론, 위 답변에서 쓴 1KB의 크기는 시스템에 따라 매우 달라질 수 있습니다. 이 글이 써진 시점이 조금 오래되었다는 것을 알기 바랍니다. 그러나, 어떤 시스템이든 상관없이, 그 시스템에서 상대적으로 큰 크기의 스택을 쓰려 했을 경우, 같은 현상이 일어날 수 있다는 것을 기억하기 바랍니다.



Q 16.4
제 프로그램은 정상적으로 동작하는데, 끝나기 직전에 박살납니다. 즉 main()의 마지막 문장을 실행한 다음에 깨지는 거죠. 왜 그럴까요?
Answer
적어도 다음 세 가지 사항을 검사해보기 바랍니다:
  1. main 바로 위에 선언한 선언에서 세미콜론(`;')이 빠져 있으면, main 함수의 리턴 타입이 잘못되어 structure를 리턴하도록 선언되었을 가능성이 있습니다. 이 경우 run-time startup 코드와 충돌하여, 이런 현상이 일어날 수 있습니다. 질문 [*]2.18, [*]10.9 참고
  2. setbuf, setvbuf에 전달한 버퍼가 main에서 선언한 local (그리고 automatic) 변수라면, main이 끝나고 stdio 라이브러리가 적절한 cleanup 코드를 수행할 때, 이 버퍼가 존재하지 않으므로 이러한 문제가 발생할 수 있습니다.
  3. atexit로 등록한 함수가 에러가 나서 이런 문제가 생길 수도 있습니다. 아마도 main 또는 다른 함수에 속해 있는 (local), 그래서 더 이상 존재하지 않는 데이터에 접근하려 했을 수 있습니다.

(두 번째와 세 번째 문제는 질문 [*]7.5와 밀접한 관계가 있습니다; 덧붙여 질문 [*]11.16도 참고하시기 바랍니다.)

References
[CT&P] § 5.3 pp. 72-3
Note
또다른 경우, main에서 선언한 local이며 automatic인 변수를 (대개는 배열) 다룰 때, 이 변수의 범위를 벗어난 곳에 접근했을 (특히 write) 때, 이러한 현상이 나타납니다. 물론 범위를 벗어난 곳에 접근하려 하는 것은 그 자체가 “undefined behavior”를 낳습니다.



Q 16.5
이 프로그램은 어떤 컴퓨터에서는 완벽하게 돌아가지만, 다른 컴퓨터에서 돌리면 이상한 결과를 만들어 냅니다. 더욱 이상한 것은, 가끔 debugging 목적으로 코드의 중간 중간에 출력하는 문장을 넣으면 때때로 동작하기도 합니다. 왜 그럴까요?

Answer
이런 증상을 만들어 내는 이유는 많습니다. 아래에 일반적으로 문제를 일으키는 경우가 있으니, 참고하시기 바랍니다:

올바른 함수 prototype을 사용한다면 이런 종류의 많은 문제를 미리 잡아낼 수 있습니다; lint를 쓰는 것도 좋은 방법입니다. 덧붙여 질문 [*]16.3, [*]16.4, [*]18.4도 참고하시기 바랍니다.

Note
위에서 printf에 특히, long int를 잘못 전달했을 때, 그러한 문제가 발생할 수 있다고 쓴 것은, (대부분의 16-bit 시스템처럼) intlong int가 실제 크기가 다른 시스템을 예상하고 말한 것입니다. (대부분의 32-bit 시스템처럼) 만약 intlong int의 크기가 같은 경우라면 이러한 문제가 자주 발생하지는 않습니다만, 어차피 잘못된 코드이기 때문에 고쳐야 할 부분입니다. C99는 long long int를 지원하므로, long intlong long int를 잘못 썼을 때 이런 문제가 발생할 수 있는 확률이 높다고 할 수 있습니다.



Q 16.6
왜 이 코드가 동작하지 않을까요?:
  char *p = "hello, world!";
  p[0] = 'H';

Answer
문자열 상수(string constant)도 상수입니다. 즉, 컴파일러는 문자열 상수를 쓰기가 금지된 곳에 배치시킬 수 있으므로, 이것을 변경하려는 시도는 위험합니다. 쓸(write) 수 있는 문자열이 필요하다면 쓸 수 있는 메모리를 배열을 선언하거나 malloc으로 할당해 주어야 합니다. 다음과 같이 해 보기 바랍니다:
  char a[] = "hello, world!";
같은 이유에서, 다음과 같이, 전통적으로 쓰는, UNIX mktemp 쓰임새는 잘못된 것입니다:
  char *tmpfile = mktemp("/tmp/tmpXXXXXX");
올바른 방법은 다음과 같습니다:
  char tmpfile[] = "/tmp/tmpXXXXXX";
  mktemp(tmpfile);

덧붙여 질문 [*]1.32도 참고하시기 바랍니다.

References
[ANSI] § 3.1.4
[C89] § 6.1.4
[H&S] § 2.7.4 pp. 31-2



Q 16.7
External structure를 푸는 코드를 받았습니다만, 실행하면 항상 “unaligned access”라는 에러가 납니다. 이게 무슨 뜻인가요? 실제 코드는 다음과 같습니다:
  struct mystruct {
    char c;
    long int i32;
    int i16;
  };

  char buf[7], *p;
  fread(buf, 7, 1, fp);
  p = buf;
  s.c = *p++;
  s.i32 = *(long int *)p;
  p += 4;
  s.i16 = *(int *)p;
Answer
The problem is that you're playing too fast and loose with your pointers. 어떤 시스템들은 데이터 값들이 정확히 align되어 있어야만 그 값을 access할 수 있습니다. 예를 들어, 2-byte short int는 2의 배수 형태를 띄는 주소값에서만 읽을 수 있으며, 4-byte long int는 4의 배수 형태를 띄어야만 access가 가능합니다. (질문 [*]2.12 참고). (아무 곳이나 가리킬 수 있는) char * 타입의 포인터를 강제로 int *long int * 포인터로 바꿔 쓰면, 프로세서에게 multitype 값을 제대로 align되지 않은 곳에 access하게 만들게 됩니다.

주어진 역할을 좀 더 올바르게 수행할 수 있는 방법은 다음과 같습니다:

  unsigned char *p = buf;

  s.c = *p++;

  s.i32 = (long *)p++ << 24;
  s.i32 |= (long *)p++ << 16;
  s.i32 |= (unsigned)(*p++ << 8);
  s.i32 |= *p++;

  s.i16 = *p++ << 8;
  s.i16 |= *p++;
This code also gives you control over byte order. (This example, though, assumes that a char is 8 bits and the long int and int being unpacked from the “external structure” are 32 and 16 bits, respectively.) (비슷한 코드를 보여주는) 질문 [*]12.42를 보기 바랍니다.

덧붙여 질문 [*]4.5도 참고하시기 바랍니다.

References
[ANSI] § 3.3.3.2, § 3.3.4
[C89] § 6.3.3.2, § 6.3.4
[H&S] § 6.1.3 pp. 164-5



Q 16.8
“Segmentation violation”이나 “Bus error”가 뭘까요? “core dump”는 또 뭘까요?

Answer
일반적으로 이런 메시지의 (또 memory-access violation 또는 protection fault와 비슷한 모든 메시지) 뜻은, 여러분의 프로그램이 access할 수 없는 메모리 공간을 (잘못된 포인터 사용으로) access하려 했다는 것을 의미합니다. 이런 메시지를 발생시킬 수 있는 몇가지 예는: UNIX에서, 이런 모든 행위는 “core dump”가 납니다; 즉, core16.2라는 파일이 현재 디렉토리에 만들어 지며, 박살난 프로세스를 디버깅할 수 있도록 그 프로세스의 메모리 이미지가 저장되어 있습니다.

“bus error”와 “segmentation violation”의 차이는 중요하지 않습니다; 각각 다른 상황에서 각각 다른 버전의 UNIX는 이러한 시그널들을 만들어 냅니다. 간단히 말해, segmentation violation은, 아예 존재하지도 않는 메모리에 access하려 했다는 것이고, bus error는 메모리를 잘못된 방법으로 access하려 (대개 잘못된 align을 가지는 포인터, 질문 [*]16.7 참고) 했다는 것을 뜻합니다.

덧붙여 질문 [*]16.3, [*]16.4도 참고하시기 바랍니다.

Seong-Kook Shin
2018-05-28