Linux From Scratch - Version 12.1-systemd
Important Preliminary Material
이전	위로 / 처음으로	다음
개요	기본적인 컴파일 과정

ii. 툴체인 기술 설명

이 섹션에서는 전반적인 빌드 방법의 근거와 기술적 세부 사항에 대해 설명합니다. 이 섹션의 모든 내용을 바로 이해하려고 하지 마세요. 이 정보의 대부분은 실제 빌드를 수행한 후에 더 명확해질 것입니다. 빌드 과정 중 언제든 다시 돌아와서 이 장을 다시 읽어보세요.

5장과 6장의 전반적인 목표는 호스트 시스템에서 격리된, 잘 작동하는 것으로 알려진 도구 세트가 포함된 임시 영역을 생성하는 것입니다. chroot 명령을 사용하면 3장에 남아있는 패키지의 컴파일이 해당 환경 내에서 격리되어 LFS 시스템을 깨끗하고 문제 없이 빌드할 수 있습니다. 이 빌드 과정은 신규 독자의 위험을 최소화하는 동시에 최고의 교육적 가치를 제공하도록 설계되었습니다.

이 빌드 과정은 크로스 컴파일을 기반으로 합니다. 크로스 컴파일은 일반적으로 빌드에 사용되는 컴퓨터와 다른 컴퓨터에서 컴파일러와 관련 툴체인을 빌드하는 데 사용됩니다. 새 시스템이 실행될 머신이 빌드에 사용된 머신과 동일하기 때문에 LFS에서는 반드시 필요한 것은 아닙니다. 그러나 교차 컴파일에는 한 가지 큰 장점이 있습니다. 교차 컴파일된 모든 것이 호스트 환경에서 독립되어 있다는 것입니다.

교차 컴파일 정보

참고 LFS 책은 크로스(또는 네이티브) 툴체인을 빌드하는 일반적인 튜토리얼이 아니며 포함하지도 않습니다. 자신이 하고 있는 일이 무엇인지 명확하게 이해하지 못한다면 이 책의 명령어를 LFS 빌드 이외의 다른 목적의 크로스 툴체인에 사용하지 마세요.

크로스 컴파일에는 몇 가지 개념이 포함되어 있습니다. 이 섹션은 처음 읽을 때는 생략할 수 있지만, 나중에 다시 읽어보면 과정을 더 잘 이해하는 데 도움이 될 것입니다.

먼저 이 장에서 용되는 몇 가지 용어를 정의해 보겠습니다.

Build
프로그램을 빌드하는 머신입니다. 이 머신을 “호스트”라고도 합니다.

Host
빌드된 프로그램이 실행될 컴퓨터/시스템입니다. 이 “호스트” 명칭의 사용은 다른 장의 그것과 동일하지 않다는 점에 유의하세요.

Target
컴파일러에만 사용됩니다. 컴파일러가 대상 코드를 생성하는 컴퓨터입니다. 빌드와 호스트 모두와 다를 수 있습니다.

예를 들어 다음 시나리오(“Canadian Cross”라고도 함)를 상상해 보겠습니다. 느린 머신에만 컴파일러가 있고, 이를 머신 A라고 하고 컴파일러를 ccA라고 합니다. 또한 빠른 머신(B)도 있지만 (B)용 컴파일러는 없으며, 세 번째 느린 머신(C)에서 실행되는 코드를 생성하려고 합니다. 머신 C용 컴파일러를 세 단계로 빌드하겠습니다.

Stage	Build	Host	Target	Action
1	A	A	B	머신 A에서 ccA를 이용해서 크로스 컴파일러 cc1 빌드
2	A	B	C	머신 A에서 cc1을 이용해서 크로스 컴파일러 cc2 빌드
3	B	C	C	머신 B에서 cc2을 이용해서 컴파일러 ccC 빌드

그런 다음 빠른 머신 B에서 cc2를 사용하여 머신 C에 필요한 모든 프로그램을 컴파일할 수 있습니다. B가 C용으로 제작된 프로그램을 실행할 수 없으며 머신 C 자체가 실행될 때까지 새로 빌드한 프로그램을 테스트할 방법이 없다는 점에 유의하세요. 예를 들어 ccC의 테스트 스위트를 실행하려면 네 번째 단계를 추가해야 할 수 있습니다.

Stage	Build	Host	Target	Action
4	C	C	C	머신 C에서 ccC을 이용해서 ccC 다시 빌드하고 테스트

위의 예에서 cc1과 cc2만 크로스 컴파일러, 즉 실행되는 컴퓨터와 다른 컴퓨터용 코드를 생성합니다. 다른 컴파일러인 ccA와 ccC는 실행되는 머신에 대한 코드를 생성합니다. 이러한 컴파일러를 네이티브 컴파일러라고 합니다.

LFS용 교차 컴파일 구현하기

참고 이 책에서 크로스 컴파일된 모든 패키지는 autoconf-base 빌드 시스템을 사용합니다. Autoconf-base 빌드 시스템은 시스템 트리플렛이라고 하는 cpu-vendor-kernel-os 형식의 시스템 유형을 허용합니다. 공급업체 필드는 관련이 없는 경우가 많으므로 자동 설정에서는 생략할 수 있습니다. 눈치 빠른 독자라면 왜 “삼중 항”이 네 가지 구성 요소 이름을 가리키는지 궁금할 것입니다. 커널 필드와 OS 필드는 하나의 “시스템” 필드로 시작되었습니다. 이러한 세 개의 필드 형식은 오늘날에도 일부 시스템(예: x86_64-unknown-freebsd)에서 여전히 유효합니다. 그러나 두 시스템이 동일한 커널을 공유하면서도 너무 달라서 동일한 삼중항을 사용하여 설명할 수 없습니다. 예를 들어 휴대폰에서 실행되는 Android는 동일한 유형의 CPU(ARM64)에서 실행되고 동일한 커널(Linux)을 사용하지만 ARM64 서버에서 실행되는 Ubuntu와는 완전히 다릅니다. 에뮬레이션 계층이 없으면 휴대폰에서 서버용 실행 파일을 실행할 수 없으며 그 반대의 경우도 마찬가지입니다. 따라서 이러한 시스템을 명확하게 지정하기 위해 “시스템” 필드를 커널 및 OS 필드로 구분했습니다. 이 예제에서 Android 시스템은 aarch64-unknown-linux-android로 지정되고 Ubuntu 시스템은 aarch64-unknown-linux-gnu로 지정됩니다. “삼중 항”이라는 단어는 어휘집에 계속 포함되어 있습니다. 시스템 트리플렛을 확인하는 간단한 방법은 많은 패키지의 소스와 함께 제공되는 config.guess 스크립트를 실행하는 것입니다. binutils 소스의 압축을 풀고 ./config.guess 스크립트를 실행한 다음 출력을 기록해 두세요. 예를 들어 32비트 인텔 프로세서의 경우 출력은 i686-pc-linux-gnu입니다. 64비트 시스템에서는 x86_64-pc-linux-gnu가 됩니다. 대부분의 리눅스 시스템에서는 더 간단한 gcc -dumpmachine 명령으로 비슷한 정보를 얻을 수 있습니다. 또한 동적 로더라고도 하는 플랫폼의 동적 링커의 이름을 알고 있어야 합니다(binutils의 일부인 표준 링커 ld와 혼동하지 마세요). 패키지 glibc에서 제공하는 동적 링커는 프로그램에 필요한 공유 라이브러리를 찾아서 로드하고 프로그램을 실행할 준비를 한 다음 실행합니다. 32비트 Intel 시스템의 동적 링커 이름은 ld-linux.so.2이며, 64비트 시스템에서는 ld-linux-x86-64.so.2입니다. 동적 링커의 이름을 확인하는 가장 확실한 방법은 다음과 같이 실행하여 호스트 시스템에서 임의의 바이너리를 검사하고 출력을 메모하는 것입니다: readelf -l <바이너리 파일명> | grep interpreter. 모든 플랫폼을 포괄하는 권위 있는 참조는 glibc 소스 트리의 루트에 있는 shlib-versions 파일에 있습니다.

LFS에서 교차 컴파일을 가장하기 위해 호스트 삼중 항의 이름을 약간 조정하여 LFS_TGT 변수의 “vendor” 필드를 “lfs”로 변경합니다. 또한 크로스 링커와 크로스 컴파일러를 빌드할 때 필요한 호스트 파일을 찾을 수 있는 위치를 알려주기 위해 –with-sysroot 옵션을 사용합니다. 이렇게 하면 6장에서 빌드한 다른 어떤 프로그램도 빌드 머신의 라이브러리에 링크할 수 없게 됩니다. 두 단계만 필수이며 테스트를 위해 한 단계가 더 필요합니다.

Stage	Build	Host	Target	Action
1	pc	pc	lfs	pc에서 cc-pc를 사용해서 크로스 컴파일러 cc1 빌드
2	pc	lfs	lfs	pc에서 cc-1를 사용해서 컴파일러 cc-lfs 빌드
3	lfs	lfs	lfs	lfs에서 cc-lfs를 사용해서 cc-lfs 다시 빌드하고 테스트

앞의 표에서 “pc“는 이미 설치된 배포판을 사용하는 컴퓨터에서 명령이 실행됨을 의미합니다. “lfs“는 명령이 chroot 환경에서 실행됨을 의미합니다.

이것이 아직 이야기의 끝이 아닙니다. C 언어는 단순한 컴파일러가 아니라 표준 라이브러리도 정의합니다. 이 책에서는 glibc라는 이름의 GNU C 라이브러리가 사용됩니다(대체 라이브러리인 “musl”도 있습니다). 이 라이브러리는 LFS 시스템용으로 컴파일해야 합니다. 즉, 크로스 컴파일러 cc1을 사용해야 합니다. 그러나 컴파일러 자체는 어셈블러 명령어 집합에서 사용할 수 없는 함수에 대한 복잡한 서브루틴을 제공하는 내부 라이브러리를 사용합니다. 이 내부 라이브러리의 이름은 libgcc이며, 이 라이브러리가 제대로 작동하려면 glibc 라이브러리에 연결해야 합니다. 또한 C++용 표준 라이브러리(libstdc++)도 glibc와 링크되어야 합니다. 이 “닭과 달걀 문제”에 대한 해결책은 먼저 스레드 및 예외 처리와 같은 일부 기능이 제한된 cc1 기반 libgcc를 빌드한 다음, 이 제한된 컴파일러를 사용하여 glibc를 빌드하고(glibc 자체는 저하되지 않음) libstdc++도 빌드하는 것입니다. 이 마지막 라이브러리는 libgcc의 일부 기능이 부족합니다.

앞 단락의 결론은 cc1은 성능 제한된 libgcc로 완전한 기능을 갖춘 libstdc++를 빌드할 수 없지만, 2단계에서 C/C++ 라이브러리를 빌드할 수 있는 유일한 컴파일러는 cc1이라는 것입니다. 2단계에서 빌드된 컴파일러인 cc-lfs를 바로 사용하지 않는 이유는 두 가지가 있습니다.

일반적으로 cc-lfs는 PC(호스트 시스템)에서 실행할 수 없습니다. PC용 트리플렛과 lfs용 트리플렛이 서로 호환되더라도 lfs용 실행 파일은 glibc-2.39에 의존해야 하며 호스트 배포판은 다른 libc 구현(예: musl) 또는 이전 릴리스(예: glibc-2.13)를 사용할 수 있습니다.

cc-lfs가 PC에서 실행될 수 있더라도 cc-lfs는 네이티브 컴파일러이기 때문에 PC에서 사용하면 PC 라이브러리에 연결될 위험이 있습니다.

따라서 gcc 2단계를 빌드할 때 빌드 시스템에 cc1로 libgcc 및 libstdc++를 재빌드하도록 지시하지만, libstdc++는 이전의 제한된 빌드 대신 새로 재빌드된 libgcc에 링크합니다. 이렇게 하면 재빌드된 libstdc++가 완전히 작동합니다.

8장(또는 “3단계”)에서는 LFS 시스템에 필요한 모든 패키지를 빌드합니다. 이전 장에서 패키지가 이미 LFS 시스템에 설치되어 있더라도 패키지를 다시 빌드합니다. 이러한 패키지를 다시 빌드하는 주된 이유는 패키지를 안정적으로 만들기 위해서입니다. 완성된 LFS 시스템에 LFS 패키지를 다시 설치하는 경우 다시 설치된 패키지의 내용은 8장에서 처음 설치했을 때와 동일한 패키지 내용이어야 합니다. 6장 또는 7장에 설치된 임시 패키지는 이 요구 사항을 충족할 수 없는데, 그 이유는 일부 패키지가 선택적 종속성 없이 빌드되고 자동 컴파일로 인해 6장에서 일부 기능 검사를 수행할 수 없기 때문에 임시 패키지에 선택적 기능이 부족하거나 최적이 아닌 코드 루틴이 사용되기 때문입니다. 또한 패키지를 다시 빌드하는 사소한 이유는 테스트 스위트를 실행하기 위해서입니다.

기타 절차적 세부 사항

크로스 컴파일러는 최종 시스템의 일부가 아니므로 별도의 $LFS/tools 디렉터리에 설치됩니다.

Binutils가 먼저 설치되는 이유는 gcc와 glibc의 configure 실행이 어셈블러와 링커에서 다양한 기능 테스트를 수행하여 어떤 소프트웨어 기능을 활성화 또는 비활성화할지 결정하기 때문입니다. 이것은 처음에 생각하는 것보다 더 중요합니다. gcc 또는 glibc를 잘못 구성하면 툴체인이 미묘하게 손상될 수 있으며, 이러한 손상으로 인한 영향은 전체 배포의 빌드가 거의 끝날 때까지 나타나지 않을 수 있습니다. 테스트 스위트 실패는 일반적으로 너무 많은 추가 작업을 수행하기 전에 이 오류를 강조 표시합니다.

Binutils는 어셈블러와 링커를 $LFS/tools/bin과 $LFS/tools/$LFS_TGT/bin의 두 위치에 설치합니다. 한 위치의 도구는 다른 위치에 하드 링크됩니다. 링커의 중요한 측면은 라이브러리 검색 순서입니다. ld에 –verbose 플래그를 전달하면 자세한 정보를 얻을 수 있습니다. 예를 들어, $LFS_TGT-ld –verbose | grep SEARCH는 현재 검색 경로와 그 순서를 보여줍니다. (이 예제는 lfs 사용자로 로그인한 상태에서만 표시된 대로 실행할 수 있습니다. 나중에 이 페이지로 돌아오면 $LFS_TGT-ld를 ld로 바꾸세요).

다음으로 설치되는 패키지는 gcc입니다. configure를 실행하는 동안 볼 수 있는 예는 다음과 같습니다.

이것은 위에서 언급한 이유 때문에 중요합니다. 또한 gcc의 configure 스크립트가 사용할 도구를 찾기 위해 PATH 디렉터리를 검색하지 않는다는 것을 보여줍니다. 그러나 실제 gcc 자체의 작동 중에는 반드시 동일한 검색 경로가 사용되는 것은 아닙니다. gcc가 사용할 표준 링커를 찾으려면 $LFS_TGT-gcc -print-prog-name=ld를 실행합니다. (나중에 다시 사용할 경우 $LFS_TGT- 접두사는 제거하세요.)

프로그램을 컴파일하는 동안 -v 명령줄 옵션을 전달하면 gcc에서 자세한 정보를 얻을 수 있습니다. 예를 들어 $LFS_TGT-gcc -v example.c(또는 나중에 다시 돌아오는 경우 $LFS_TGT- 없이)는 포함된 헤더에 대한 gcc의 검색 경로와 순서를 포함하여 전처리기, 컴파일 및 어셈블리 단계에 대한 자세한 정보를 표시합니다.

다음은 처리된 Linux API 헤더입니다. 이를 통해 표준 C 라이브러리(glibc)가 Linux 커널이 제공하는 기능과 인터페이스할 수 있습니다.

다음은 glibc입니다. glibc를 빌드할 때 가장 중요한 고려 사항은 컴파일러, 바이너리 도구 및 커널 헤더입니다. 컴파일러는 일반적으로 configure 스크립트에 전달된 –host 매개변수와 관련된 컴파일러(예: 우리의 경우 컴파일러는 $LFS_TGT-gcc)를 사용하므로 문제가 되지 않습니다. 바이너리 도구와 커널 헤더는 조금 더 복잡할 수 있습니다. 따라서 위험을 감수하지 않고 사용 가능한 구성 스위치를 사용하여 올바른 선택을 적용합니다. configure를 실행한 후 빌드 디렉터리에 있는 config.make 파일의 내용을 확인하여 모든 중요한 세부 정보를 확인하세요. 사용할 바이너리 도구를 제어하기 위해 CC=“$LFS_TGT-gcc”($LFS_TGT가 확장된 상태)를 사용하고 컴파일러의 포함 검색 경로를 제어하기 위해 -nostdinc 및 -isystem 플래그를 사용하는 것을 주목하세요. 이러한 항목은 빌드 기계 측면에서 매우 자급자족적이며 일반적으로 도구 체인 기본값에 의존하지 않는 glibc 패키지의 중요한 측면을 강조합니다.

위에서 언급했듯이 표준 C++ 라이브러리가 다음에 컴파일되고, 6장에서는 빌드 시 순환 종속성을 끊기 위해 교차 컴파일해야 하는 다른 프로그램이 이어서 컴파일됩니다. 이러한 모든 패키지의 설치 단계에서는 DESTDIR 변수를 사용하여 LFS 파일시스템에 강제로 설치합니다.

6장 마지막에 네이티브 LFS 컴파일러가 설치됩니다. 먼저 다른 프로그램과 동일한 DESTDIR 디렉터리에 binutils-pass2가 빌드된 다음, 중요하지 않은 일부 라이브러리를 생략한 두 번째 gcc 패스가 빌드됩니다. gcc의 구성 스크립트에 있는 이상한 로직으로 인해 호스트가 대상과 동일하지만 빌드 시스템과 다른 경우 CC_FOR_TARGET이 cc로 끝납니다. 그렇기 때문에 구성 옵션 중 하나로 CC_FOR_TARGET=$LFS_TGT-gcc가 명시적으로 선언됩니다.

7장에서 루트 환경으로 들어가면 툴체인의 올바른 작동에 필요한 프로그램의 임시 설치가 수행됩니다. 이 시점부터 핵심 툴체인은 독립적이고 자체 호스팅됩니다. 8장에서는 완전한 기능을 갖춘 시스템에 필요한 모든 패키지의 최종 버전을 빌드, 테스트 및 설치합니다.