Linux From Scratch - Version 12.4

중요한 배경 지식

이전
개요

다음
이후 컴파일 진행 방법

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

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

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

교차 컴파일 정보

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

GCC pass 2를 진행하면 크로스 툴체인이 끊어지는 것으로 알려져 있습니다. 우리는 이를 버그로 여기지 않습니다. 왜냐하면 GCC Pass 2는 책에서 마지막으로 크로스 컴파일되는 패키지이기 때문이면, 앞으로 GCC Pass 2 이후에 어떤 패키지를 크로스 컴파일해야 할 때까지는 “수정”하지 않을 것이기 때문입니다.

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

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

  • 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 형식의 시스템 유형을 허용합니다. 공급업체 필드(vendor)는 관련이 없는 경우가 많으므로 autoconf에서는 생략할 수 있습니다.

눈치 빠른 독자라면 왜 트리플렛이 네 가지 구성 요소 이름을 가리키는지 궁금할 것입니다. 커널 필드와 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 wiki에 있습니다.

크로스 컴파일에는 두가지 핵심 사항이 있습니다.

  • “호스트”에서 실행되어야 할 기계어를 생성하고 처리할 때는 반드시 크로스 툴체인을 사용해야 합니다. “빌드”의 네이티브 툴체인은 여전히 “빌드”에서 실행되어야 할 기계어를 생성하기 위해 호출될 수 있습니다. 예를 들어, 빌드 시스템은 네이티브 시스템으로 생성기를 빌드 한 뒤, 생성기로 C 소스 파일을 생성하고 마지막으로 크로스 툴체인으로 C소스 파일을 컴파일하여 생성된 코드가 “호스트”에서 실행될 수 있도록 합니다.

    Autoconf 기반 빌드 시스템에서는 –host 스위치를 사용해서 “host”의 트리플렛을 지정해서 이 사항을 보장합니다. 이 스위치를 통해 빌드 시스템은 <호스트 트리플렛>이 붙은 툴체인 컴포넌트를 사용해서 “호스트”의 기계어를 생성하고 처리하게 됩니다. 예를 들어 컴파일러는 <호스트 트리플렛>-gcc, readelf<호스트 트리플렛>-readelf이 됩니다.
  • 빌드 시스템은 “호스트”에서 실행되어야 하는 기계어를 실행하려고 시도해서는 안됩니다. 예로 유틸리티를 네이티브로 빌드할 때는 –help 스위치로 유틸리티를 실행하여 메뉴얼 페이지 출력할 수 있지만, 일반적으로 크로스 컴파일 시 유틸리티가 “빌드”에서 실행되지 않을 수 있기 때문에 불가능합니다. 에뮬레이터 없이 X86 CPU 에서 ARM64 기계어를 실행하는 것을 명백하게 불가능합니다.

    Autoconf 기반 빌드 시스템에서는 이 사항이 “크로스 컴파일 모드”에서 충족되며, 빌드를 진행하는 동안 “호스트”를 위해 기계어 코드를 실행해야 하는 선택적 기능을 비활성화 합니다. “호스트 트리플렛”이 명시적으로 지정되지 않으면 “크로스 컴파일 모드”는 configure 스크립트가 “호스트” 기계어로 컴파일된 더미 프로그램을 실행하지 못하거나, –build 스위치를 통해서 명시적으로 지정되어 “호스트” 트리플렛과 다를때만 활성화 됩니다.

LFS 임시 시스템의 패키지를 교차 컴파일하기 위해 시스템 트리플렛의 이름을 약간 조정하여 LFS_TGT 변수의 'vendor' 필드를 'lfs'로 바꾸고, LFS_TGT –host를 통해 '호스트' 트리플렛으로 지정하여 크로스 툴체인이 LFS 임시 시스템의 일부로 동작하는 기계어 생성 및 처리에 사용됩니다. 또한 “교차 컴파일 모드”를 활성화해야 합니다: “호스트” 기계어, 즉 LFS 임시 시스템의 기계어가 현재 CPU에서 실행할 수 있지만, “빌드”(호스트 배포판)에 없는 라이브러리를 참조하거나, 존재하지 않거나 라이브러리에 다르게 정의된 코드나 데이터가 있을 수 있습니다. LFS 임시 시스템의 패키지를 교차 컴파일할 때, 더미 프로그램의 문제를 configure 스크립트만으로 감지할 수 없습니다. 더미는 호스트 배포판 libc가 제공하는 몇 가지 컴포넌트만 libc에서 사용하므로(호스트 배포판이 Musl 같은 다른 libc 구현을 사용하는 경우는 제외), 정말 유용한 프로그램처럼 실패하지 않을 것입니다. 따라서 “크로스 컴파일 모드”를 활성화하려면 “빌드” 트리플렛을 명시적으로 지정해야 합니다. 우리가 사용하는 값은 기본값, 즉 config.guess 출력의 원래 시스템 트리플렛일 뿐이지만, “교차 컴파일 모드”는 앞서 언급한 명시적 명세에 따라 달라집니다.

우리는 크로스링커와 크로스 컴파일러를 만들 때 -with-sysroot 옵션을 사용해 “호스트”에 필요한 파일을 어디서 찾을 수 있는지 알려줍니다. 이로 인해 6장에서 만들어진 프로그램들은 '빌드' 내 라이브러리에 거의 연결할 수 없게 됩니다. “거의”라는 단어가 사용되는 이유는 컴파일러의 “호환성” 래퍼이자 Autoconf 기반 빌드 시스템의 링커인 libtool이 너무 영리하게 링커가 “빌드”의 라이브러리를 찾도록 옵션을 잘못 전달할 수 있기 때문입니다. 이런 문제를 막기 위해 libtool 아카이브(.la) 파일을 삭제하고 Binutils 코드가 포함된 오래된 libtool 복사본을 수정해야 합니다.

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++를 구축할 수 없지만, cc1만이 2단계에서 C/C++ 라이브러리를 빌드할 수 있는 유일한 컴파일러입니다. 앞서 말했듯이, cc-lfs는 pc(호스트 배포판)에서 실행할 수 없습니다. 왜냐하면 빌드(호스트 배포판)에 없는 라이브러리, 코드 또는 데이터가 필요할 수 있기 때문입니다. 그래서 gcc 2단계를 빌드할 때, 라이브러리 검색 경로를 오버라이드하여 Libstdc++를 새로 재구성된 libgcc와 연결하게 합니다. 이로 인해 재구성된 libstdc++가 완전히 기능할 수 있게 되었습니다.

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

크로스 컴파일러는 최종 시스템의 일부가 아니므로 별도의 $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-ldld 바꾸세요).

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

checking what assembler to use... /mnt/lfs/tools/i686-lfs-linux-gnu/bin/as
checking what linker to use... /mnt/lfs/tools/i686-lfs-linux-gnu/bin/ld

이것은 위에서 언급한 이유 때문에 중요합니다. 또한 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입니다. 이것이 처음으로 크로스 컴파일하는 패키지입니다. 빌드 시스템에 $LFS_TGT- 붙은 도구들을 사용하도록 –host=$LFS_TGT 옵션을 사용하고, –build=$(.. /scripts/config.guess) 옵션을 통해 앞서 이야기한 대로 “크로스 컴파일 모드”를 활성화할 수 있습니다. DESTDIR 변수는 설치 위치를 LFS 파일 시스템으로 설정하는 데 사용됩니다.

위에서 언급했듯이 표준 C++ 라이브러리가 다음에 컴파일되고, 6장에서는 빌드 시 순환 의존성을 끊기 위해 크로스 컴파일해야 하는 다른 프로그램이 이어서 컴파일됩니다. 이런 패키지들의 진행 과정은 glibc와 비슷합니다.

6장 끝나면 네이티브 LFS 컴파일러가 설치됩니다. 먼저 다른 프로그램과 동일한 DESTDIR 디렉터리에 binutils-pass2가 빌드된 다음, 중요하지 않은 일부 라이브러리를 생략한 gcc-pass2가 빌드됩니다.

7장에서 chroot 환경에 진입하면 툴체인의 작동에 필요한 프로그램의 임시 설치가 이어집니다. 이 시점부터 핵심 툴체인은 자급자족적이고 자체 호스팅 형태가 됩니다. 8장에서는 완전한 기능을 갖춘 시스템에 필요한 모든 패키지의 최종 버전이 빌드, 테스트, 설치 과정을 거칩니다.


이전
개요

다음
이후 컴파일 진행 방법

  • linuxfromscratch/12.4/linux_from_scratch/041-toolchain_technical_notes.txt
  • 마지막으로 수정됨: 2026/01/07 00:12
  • 저자 baecy