커널의 C 코드가 기술적으로 올바르지 않습니까?

커널의 C 코드가 기술적으로 올바르지 않습니까?

인터넷에서 다음과 같은 여러 스레드를 찾을 수 있습니다.

http://www.gossamer-threads.com/lists/linux/kernel/972619

사람들은 -O0을 사용하여 Linux를 빌드할 수 없다고 불평하고 이것이 지원되지 않는다는 말을 들었습니다. Linux는 자동으로 기능을 인라인하고, 데드 코드를 제거하고, 성공적인 빌드에 필요한 작업을 수행하기 위해 GCC 최적화에 의존합니다.

나는 적어도 일부 3.x 커널에 대해 이것을 직접 확인했습니다. 내가 시도한 것들은 -O0으로 컴파일된 경우 몇 초의 빌드 시간 후에 종료됩니다.

이는 일반적으로 허용되는 코딩 관행으로 간주됩니까? 컴파일러 최적화(예: 자동 인라인)는 신뢰할 수 있을 만큼 충분히 예측 가능합니까? 적어도 하나의 컴파일러만 다룰 때는? GCC의 향후 버전이 기본 최적화(예: -O2 또는 -Os)를 사용하여 현재 Linux 커널의 빌드를 중단할 가능성은 얼마나 됩니까?

좀 더 현학적인 요점: 3.x 커널은 최적화 없이는 컴파일할 수 없으므로 기술적으로 잘못된 C 코드로 간주되어야 합니까?

답변1

여러 가지 (그러나 관련된) 질문을 결합합니다. 그 중 일부는 실제로 주제(예: 코딩 표준)와 관련이 없으므로 무시하겠습니다.

커널이 "기술적으로 잘못된 C 코드"인지 여부부터 시작하겠습니다. 나는 대답이 커널이 차지하는 특별한 위치를 설명하기 때문에 여기에서 시작했는데, 이는 나머지를 이해하는 데 중요합니다.

커널의 C 코드가 기술적으로 올바르지 않습니까?

대답은 확실히 "잘못"입니다.

C 프로그램이 잘못된 것으로 간주될 수 있는 방법에는 여러 가지가 있습니다. 먼저 몇 가지 간단한 질문을 다루겠습니다.

  • C 구문을 따르지 않는(즉, 구문 오류가 있는) 프로그램은 올바르지 않습니다. 커널은 C 구문에 대한 다양한 GNU 확장을 사용합니다. C 표준에 관한 한 이는 구문 오류입니다. (물론 GCC에서는 그렇지 않습니다. -std=c99 -pedantic또는 이와 유사한 것으로 컴파일해 보십시오...)
  • 설계된 목적을 수행하지 않는 프로그램은 올바르지 않습니다. 커널은 거대한 프로그램이며, 변경 로그를 빠르게 확인해도 그것이 확실히 거대한 프로그램이 아니라는 것을 증명할 수 있습니다. 아니면 우리가 말하고 싶은 것처럼 버그가 있습니다.

C에서 최적화는 무엇을 의미합니까?

[참고: 이 섹션에는 실제 규칙에 대한 매우 부정확한 수정이 포함되어 있습니다. 자세한 내용은 표준을 참조하고 스택 오버플로를 검색하세요. ]

이제 더 많은 설명이 필요합니다. C 표준에서는 특정 코드가 특정 동작을 생성해야 한다고 규정합니다. 또한 구문적으로 유효한 특정 C 항목에는 "정의되지 않은 동작"이 있다고 말합니다. 한 가지(불행히도 일반적인!) 예는 배열 끝을 넘어서 액세스하는 것입니다(예: 버퍼 오버플로).

정의되지 않은 동작은 매우 강력합니다. 프로그램에 조금이라도 포함되어 있으면 C 표준은 더 이상 프로그램이 어떤 동작을 보이는지, 컴파일러가 이에 직면했을 때 어떤 출력을 생성하는지 신경 쓰지 않습니다.

그러나 프로그램에 정의된 동작만 포함되어 있더라도 C에서는 여전히 컴파일러에 많은 여유를 허용합니다. 간단한 예로서(참고: 내 예에서는 간결성을 위해 줄 등을 생략했습니다 #include):

void f() {
    int *i = malloc(sizeof(int));
    *i = 3;
    *i += 2;
    printf("%i\n", *i);
    free(i);
}

물론 5가 인쇄되어야 하고 그 뒤에 개행 문자가 와야 합니다. 이것이 C 표준이 요구하는 것입니다.

이 프로그램을 컴파일하고 출력을 디스어셈블한 경우 메모리를 얻기 위해 malloc이 호출되고, 반환된 포인터가 어딘가(아마도 레지스터)에 저장되고, 값 3이 해당 메모리에 저장된 다음, 그 메모리에 2가 추가될 것으로 예상할 수 있습니다. 메모리(아마도 로드, 추가 및 저장도 필요함)를 수행한 다음 메모리를 스택에 복사하고 "%i\n"도트 문자열을 스택에 넣은 다음 printf함수를 호출합니다. 꽤 많은 일이 있습니다. 하지만 대신 다음과 같이 쓴 내용이 표시될 수도 있습니다.

/* Note that isn't hypothetical; gcc 4.9 at -O1 or higher does this. */
void f() { printf("%i\n", 5) }

문제는 다음과 같습니다. C 표준에서는 이를 허용합니다. C 표준은 다음 사항에만 관심을 갖습니다.결과, 구현 방식보다는.

이것이 C 언어 최적화의 전부입니다. 컴파일러는 C 표준에서 요구하는 결과를 얻기 위해 더 똑똑한 방법(플래그에 따라 일반적으로 더 작거나 더 빠름)을 제시합니다. GCC -ffast-math옵션과 같은 몇 가지 예외가 있지만 그렇지 않은 경우 최적화 수준은 기술적으로 올바른 프로그램(즉, 정의된 동작만 포함하는 프로그램)의 동작을 변경하지 않습니다.

정의된 동작만 사용하여 커널을 작성할 수 있습니까?

계속해서 샘플 프로그램을 확인해 보겠습니다. 컴파일러가 변환하는 버전이 아니라 우리가 작성하는 버전입니다. 우리가 가장 먼저 하는 일은 malloc메모리를 얻기 위해 호출하는 것입니다. C 표준은 무엇을 해야 할지 알려주지 malloc만, 그것이 어떻게 수행되는지는 알려주지 않습니다.

malloc속도보다는 명확성을 목표로 하는 구현을 살펴보면 큰 메모리 덩어리를 얻기 위해 일부 시스템 호출(예: mmapwith )을 수행한다는 것을 알 수 있습니다. MAP_ANONYMOUS블록의 어느 부분이 사용되고 어느 부분이 사용 가능한지 알려주는 일부 데이터 구조를 내부적으로 유지합니다. 최소한 요청한 크기만큼 큰 여유 블록을 찾아 요청한 양만큼 잘라낸 후 해당 블록에 대한 포인터를 반환합니다. 또한 완전히 C로 작성되었으며 정의된 동작만 포함합니다. 스레드로부터 안전한 경우 일부 pthread 호출이 포함될 수 있습니다.

이제 마지막으로 무슨 일이 일어나고 있는지 살펴보면 mmap온갖 흥미로운 것들이 보입니다. 먼저 시스템에 매핑에 사용할 수 있는 RAM 및/또는 스왑 공간이 충분한지 확인하기 위해 몇 가지 검사를 수행합니다. 다음으로 블록을 넣을 여유 주소 공간을 찾습니다. 그런 다음 페이지 테이블이라는 데이터 구조를 편집하고 프로세스에서 일련의 인라인 어셈블리 호출을 수행할 수 있습니다. 실제로 물리적 메모리의 일부 여유 페이지(예: 실제 DRAM 모듈의 실제 비트)를 찾을 수도 있습니다. 이 프로세스는 다른 메모리를 강제로 교체해야 할 수도 있습니다. 요청된 전체 블록에 대해 이 작업을 수행하지 않으면 해당 메모리에 처음 액세스할 때 발생하도록 설정합니다. 대부분의 작업은 인라인 어셈블리, 다양한 매직 주소 작성 등을 통해 수행됩니다. 또한 특히 교체가 필요한 경우 코어의 많은 부분을 사용한다는 점도 참고하세요.

인라인 어셈블리, 매직 주소 작성 등은 C 사양 외부에 있습니다. 이는 놀라운 일이 아닙니다. C는 1970년대 초에 C가 발명되었을 때 거의 상상할 수 없었던 많은 아키텍처를 포함하여 다양한 시스템 아키텍처에서 실행될 수 있습니다. 기계별 코드를 숨기는 것은 커널(및 어느 정도 C 라이브러리)의 핵심 부분입니다.

물론, 예제 프로그램으로 돌아가보면 분명 printf유사하다는 것을 알 수 있을 것입니다. 표준 C에서 모든 서식 지정 등을 수행하는 방법은 매우 명확하지만 실제로는 모니터에 표시됩니다. 아니면 다른 프로그램으로 파이프합니까? 이번에도 커널(그리고 아마도 X11이나 Wayland)은 많은 마법을 수행합니다.

커널이 수행하는 다른 작업을 생각해보면 많은 부분이 C 외부에 있습니다. 예를 들어, 커널은 디스크(C는 디스크, PCIe 버스 또는 SATA에 대해 알지 못함)의 데이터를 물리적 메모리(C는 malloc만 알고 DIMM, MMU 등은 알 수 없음)로 읽어 실행 가능하게 만듭니다(C는 아무것도 모릅니다). 알려진 프로세서 실행 비트에 대해) 함수로 호출합니다(C 외부뿐만 아니라 매우 허용되지 않습니다).

커널과 컴파일러 사이의 관계

이전 내용을 기억한다면, 프로그램에 정의되지 않은 동작이 포함되어 있으면 C 표준에 관한 한 모든 것이 잘못되었습니다. 그러나 커널에는 정의되지 않은 동작이 포함되어야 합니다. 따라서 커널과 컴파일러 사이에는 적어도 커널 개발자가 C 표준을 위반하더라도 커널이 작동할 것이라는 확신을 줄 수 있을 만큼 충분한 관계가 있어야 합니다. 적어도 Linux의 경우에는 커널이 GCC의 내부 작동에 대해 어느 정도 알고 있어야 합니다.

깨질 확률은 얼마나 되나요?

향후 GCC 버전에서는 커널이 손상될 수 있습니다. 이런 일이 이전에도 여러 번 일어났기 때문에 나는 자신 있게 말할 수 있습니다. 물론 GCC의 엄격한 별칭 최적화와 같은 것들은 커널 외에도 많은 것들을 깨뜨립니다.

또한 Linux 커널이 의존하는 인라인은 자동 인라인이 아니라 커널 개발자가 수동으로 지정한다는 점에 유의하세요. -O0을 사용하여 커널을 컴파일하고 몇 가지 사소한 문제를 해결한 후에는 대부분 작동한다고 보고하는 사람들이 많이 있습니다. (그 중 하나는 당신이 링크한 스레드에도 있습니다). 대부분 커널 개발자는 -O0부작용으로 최적화를 요구하고 컴파일할 이유가 없다고 생각하여 몇 가지 트릭이 작동하도록 하고 아무도 테스트에 사용하지 않으므로 -O0지원되지 않습니다.

예를 들어 다음 항목 이상으로 컴파일하고 링크 -O1하지만 다음 항목으로는 링크하지 않습니다 -O0.

void f();

int main() {
    int x = 0, *y;
    y = &x;

    if (*y)
        f();
    return 0;
}

f()최적화를 통해 gcc는 절대 호출되지 않고 무시된다는 것을 확신할 수 있습니다 . 최적화가 없으면 gcc는 호출을 보존하고 링커는 실패합니다 f(). 커널 개발자는 커널 코드를 더 쉽게 읽고 쓸 수 있도록 유사한 동작에 의존합니다.

답변2

~에서젠투 GCC 최적화 위키

섹션 2.3: -O 플래그

-O 다음은 -O 변수입니다. 이는 전반적인 최적화 수준을 제어합니다. 이로 인해 코드를 컴파일하는 데 더 많은 시간이 걸리고 특히 최적화 수준을 높이면 더 많은 메모리를 사용할 수 있습니다.

-O 설정에는 -O0, -O1, -O2, -O3, -Os, -Og 및 -Ofast의 7가지가 있습니다. /etc/portage/make.conf에서는 그 중 하나만 사용해야 합니다.

-O0 외에도 각 -O 설정은 여러 추가 플래그를 활성화하므로 GCC 매뉴얼의 최적화 옵션에 대한 장을 읽고 각 -O 수준에서 활성화되는 플래그와 해당 기능에 대한 설명을 확인하십시오.

각 최적화 수준을 확인해 보겠습니다.

-O0: 이 수준(즉, "O" 뒤에 0이 오는 문자)은 최적화를 완전히 끄고 CFLAGS 또는 CXXFLAGS에 -O 수준이 지정되지 않은 경우 기본값입니다. 이렇게 하면 컴파일 시간이 줄어들고 디버깅 정보가 향상될 수 있지만 일부 응용 프로그램은 최적화를 활성화하지 않으면 제대로 작동하지 않습니다. 이 옵션은 디버깅 목적을 제외하고는 권장되지 않습니다.
-O1: 가장 기본적인 최적화 수준입니다. 컴파일러는 컴파일 시간을 너무 많이 소비하지 않고 더 빠르고 작은 코드를 생성하려고 노력합니다. 매우 기본적이지만 항상 작업을 완료해야 합니다.
-O2: -O1보다 한 단계 더 발전한 것입니다. 특별한 요구 사항이 없는 한 이는 권장되는 최적화 수준입니다. -O1에 의해 활성화된 플래그 외에도 -O2는 더 많은 플래그를 활성화합니다. -O2를 사용하면 컴파일러는 크기에 영향을 주거나 컴파일 시간을 너무 많이 차지하지 않고 코드 성능을 향상시키려고 합니다.
-O3: 가능한 가장 높은 최적화 수준입니다. 컴파일 시간과 메모리 사용량 측면에서 비용이 많이 드는 최적화가 가능합니다. -O3으로 컴파일해도 성능 향상이 보장되지 않으며 실제로 더 큰 바이너리와 증가된 메모리 사용량으로 인해 많은 경우 시스템 속도가 느려질 수 있습니다. -O3은 여러 패키지를 깨뜨릴 수도 있습니다. 따라서 -O3 사용은 권장되지 않습니다.
-Os: 이 옵션은 코드 크기를 최적화합니다. 생성된 코드의 크기를 늘리지 않는 모든 -O2 옵션을 활성화합니다. 디스크 저장 공간이 극히 제한되어 있거나 CPU 캐시가 작은 컴퓨터에 유용합니다.
-Og: GCC 4.8에서는 새로운 일반 최적화 수준 -Og가 도입되었습니다. 합리적인 수준의 런타임 성능을 제공하는 동시에 빠른 컴파일과 우수한 디버깅 환경에 대한 요구 사항을 충족합니다. 전반적인 개발 경험은 기본 최적화 수준 -O0보다 좋아야 합니다. -Og는 -g를 의미하지 않으며 디버깅을 방해할 수 있는 최적화를 비활성화할 뿐입니다.
-Ofast: -O3와 -ffast-math, -fno-protect-parens 및 -fstack-arrays로 구성된 GCC 4.7의 새로운 기능입니다. 이 옵션은 엄격한 표준 준수를 위반하므로 권장되지 않습니다. 앞서 언급했듯이 -O2가 권장되는 최적화 수준입니다. 패키지 컴파일이 실패하고 -O2를 사용하지 않는 경우 이 옵션을 사용하여 다시 빌드해 보세요. 대체 방법으로 CFLAGS 및 CXXFLAGS를 -O1 또는 -O0 -g2 -ggdb(오류 보고 및 가능한 문제 확인용)와 같은 낮은 최적화 수준으로 설정해 보십시오.

최적화가 아닌 -O0에 대해 구체적으로 질문하셨습니다. 위 내용을 읽어보면 O0은 디버깅 전용이라는 것을 알 수 있습니다. menuconfig를 사용해 본 적이 있다면 커널 디버깅을 활성화하거나 비활성화하는 옵션이 있다는 것을 알게 될 것입니다. 활성화되면 이 옵션은 O0이 정보를 제공하는 것과 거의 동일한 방식으로 디버깅 정보를 출력합니다. 또한 전체 시스템이 단 하나의 최적화 설정으로 구축되거나 컴파일된다는 점, 즉 O0에서 커널을 컴파일하고 O2에서 시스템의 나머지 부분을 컴파일할 수 없다는 점을 간과했을 수도 있다고 생각합니다.


GCC 버전 간의 이전 버전과의 호환성과 관련하여 GCC는 한 버전에서 -O 플래그를 활성화하는 것이 새 버전에서 -O 설정과 동일하기 때문에 항상 버전 간 호환성을 유지합니다. GCC4.7 및 -Ofast 옵션에 대한 위의 참고 사항을 참조하세요. 해당 옵션은 4.7 이상에서만 사용할 수 있지만 4.7의 -O2 = 모든 버전의 -O2입니다.

관련 정보