-
크로스 컴파일[작성중]프로젝트/운영체제 만들기 2023. 6. 3. 21:48
글의 참고
- https://wiki.osdev.org/GCC_Cross-Compiler
- https://wiki.osdev.org/Why_do_I_need_a_Cross_Compiler%3F
글의 전제
- 내가 글을 쓰다가 궁금한 점은 파란색 볼드체로 표현했다.
- 밑줄로 작성된 글은 좀 더 긴 설명이 필요해서 친 것이다.
글의 내용
- 크로스 컴파일 빌드
" 어디에 크로스 컴파일을 설치할 지를 정해야 한다. 시스템 디렉토리가 있는 곳에 설치하는 것은 최악이다. 대개는, `$HOME/opt/cross` 혹은 `usr/local/cross`에 설치를 많이 한다. $HOME/opt/cross는 나에게만 적용되는 것이고, 해당 컴퓨터가 서버라면 전역적으로 설치해야 하므로, `usr/local/cross`에 설치를 많이 한다. 추천하지는 않지만, `/usr`에 설치를 하는 방법도 있다. 이렇게 하면, 기본 시스템 컴파일러로 설정하는 것과 같아서 패스 설정 같은것이 필요없다. 그러나 이 방법은 상당히 위험하다. 설치되어있는 운영체제는 기본 컴파일러를 크로스 컴파일러로 바꾸는 작업이기 때문에, 시스템에 문제를 일으킬 여지가 많다.
" 빌드를 위해서 몇 가지 환경 변수를 설정하자.
export PREFIX="$HOME/opt/cross" export TARGET=i686-elf export PATH="$PREFIX/bin:$PATH"
" BINUTILS 부터 빌드하자. BINUTILS에는 어셈블러와 디스어셈블러, 여러 가지 다양한 바이너리 유틸리티들이 들어있다. 아래는 $TARGET 환경 변수에 설정된 아키텍처로 BINUTILS를 구성하여 빌드를 진행한다. 여기서 주의 사항이 있다. 소스가 존재하는 탑 레벨에서는 절대 `./configure`와 `make`를 실행하면 안된다. 이 에러가 발생한다. 그러므로, 아래처럼 소스 탑 레벨에서 임시 디렉토리(여기서는 `build-binutils`)를 하나 만들고, 그 디렉토리에서 `./configure`와 `make`를 실행한다.
cd $HOME/src ... 압축 푼다... cd binutils-x.y.z mkdir build-binutils cd build-binutils ../configure --target=$TARGET --prefix="$PREFIX" --with-sysroot --disable-nls --disable-werror make -j8 & make install
: 설치가 정상적으로 잘 되었다면, `$HOME/opt/cross/bin/` 관련 결과물들이 있게 된다. 환경 변수에 $PATH에 이미 설정이 되어 있으므로, `where i686-elf-as`로 어디에 해당 유틸리티가 설치되어 있는지 확인하자.
- GDB 빌드
: x86 운영체제를 만들기 위해서는 디버깅을 위해서는 GDB가 필요하다. 그러나, GDB는 명확한 한계가 있다. 각 모드가 전환된 이후에 코드를 정확히 인식하지 못한다. 그리고 x86 에서는 세그먼트 주소 지정 방식을 몰라서 세그먼트 레지스터 값을 설정해버리면 GDB가 이상해진다.
https://github.com/lordmilko/i686-elf-tools/releases
: 위의 링크에서 미리 컴파일된 `i686-elf-gdb`를 사용할 수 있다.
: 이제 GCC를 설치하자. GCC 설치도 당연히 소스 탑 레벨에서 구성과 빌드를 하면 안된다.
cd $HOME/src # The $PREFIX/bin dir _must_ be in the PATH. We did that above. which -- $TARGET-as || echo $TARGET-as is not in the PATH mkdir build-gcc cd build-gcc ../gcc-x.y.z/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers make all-gcc make all-target-libgcc make install-gcc make install-target-libgcc
: 참고로, 여기서 GCC만 빌드하는 것이 아니다. `libgcc`라는 것도 같이 빌드를 한다.
: GCC 빌드까지 완료되면 아래의 명령을 통해 자신의 크로스 컴파일러를 확인해보자. 나는 GCC 11.3.0 버전 소스를 컴파일했다.
$HOME/opt/cross/bin/$TARGET-gcc --version i686-elf-gcc (GCC) 11.3.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
: 이 컴파일러 패스는 현재 세션까지만 유효하다. 영구적으로 쓰고 싶다면 `~/.profile` 맨 끝에 아래의 명령을 추가하자.
export PATH="$HOME/opt/cross/bin:$PATH"
- 암묵적인 main 함수
: 이 링크 를 따라가면 라즈베리파이3B를 이용한 베어 메탈 내용을 볼 수 있다. 여기서 start.S 파일에 `bl main`을 제외하고, 그 어디에도 `main` 이라는 글자를 찾아볼 수가 없다. 왜 그럴까?
: 아래의 예시 코드를 보자.
; boot.asm bits 16 SECTION .text global _start _start: call main
: 위의 파일을 NASM으로 빌드하면 에러가 발생한다. 왜냐면, main 심볼이 없기 때문이다. main() 함수가 C 언어로 작성된 함수기 때문에, 어셈블리 파일에서 사용하려면 `extern main`을 사용해야 한다. 아래와 같이 수정하고, main.c 파일에 main() 함수가 있다고 전제하면 NASM은 에러를 발생시키지 않는다.
; boot.asm bits 16 extern main SECTION .text global _start _start: call main
: NASM이 아닌 다른 크로스 컴파일러들은 에러가 발생하지 않을 수 있다. 왜 그럴까? `ABI` 혹 `C 런타임` 혹 `부트 로더` 때문일 수 있다. ABI 마다 암묵적으로 `main` 심볼을 C언어 함수로 인식해서 `extern main`이 필요없는 경우가 있고, `extern main` 문장이 필요한 경우가 있다. i686-elf 툴체인은 명시적으로 `extern main`을 선언을 해줘야 한다. C 런타임을 사용하면 링커에서 기본적으로 C 런타임 cr0.o 파일이 먼저 불리게 되서 main() 를 호출해준다. 그러나, 이건 제외다. 왜냐면, 베어 메탈에서는 C 런타임을 사용하지 않기 때문이다. 그러면 부트 로더의 가능성도 있다. 라즈베리파이3B에 있는 부트 로더는 마치 BIOS 및 UEFI와 같다. 기본적인 초기화를 진행하고 우리에게 제어권을 넘긴다. 그러나 0번지로 점프하는걸로 알고 있는데, 이것도 아니것 같다. 결국 ARM의 ABI가 가장 의심스러운 부분이다.
- 크로스 컴파일 이름 포맷(Target Triplet)
: 대개 크로스 컴파일들은 아래와 같은 이름을 갖는다.
MACHINE-VENDOR-OPERATINGSYSTEM
" 그래서 내 컴퓨터 같은 경우, `gcc -dumpmachine`을 치면 `x86_64-linux-gnu`가 나온다. 참고로, `gcc -dumpmachine` 명령어는 현재 시스템에 기본적으로 등록된 컴파일러의 정보를 보여준다. 근데 뭔가 좀 이상하지 않나? 벤더에 리눅스가 들어가 있다. 사실 저 위에 벤더는 크게 의미가 없다고 한다. 대개는 생략도 한다.
" 자신만의 OS를 만들 때, 사용하는 크로스 컴파일러들은 다음과 같다.
1" x86 : i686-elf, x86_64-elf
2" ARM : arm-none-elf, aarch64-none-elf
3" RISC-V : riscv64-none-elf" 자신의 OS를 개발할 때는, 현재 자신이 사용하고 있는 크로스 컴파일러가 어떻게 만들어 졌는지를 알아야 한다. 예를 들어, 우분투를 사용하면 기본 컴파일러는 GCC다. 이 GCC는 리눅스용 컴파일러라서 특정 파일을 빌드하면 리눅스용 실행 파일을 만든다. 그런데, 우리가 만드는 프로그램은 리눅스용 실행 파일이 아니다. 우리가 만드는 건 말 그대로 아무것도 없는 `베어 메탈` 기반의 실행 파일이다. 어떠한 운영체제에서도 의존하지 않아야 한다. 단지 아키텍처에만 의존해야 한다. 그래서 우리는 각 아키텍처사(ARM, Intel, AMD 등)에서 제공하는 리눅스용 크로스 컴파일러가 아닌 베어 메탈 컴파일러로 OS를 개발해야 한다. `x86_64-linux-gnu` 같은 컴파일러는 인텔이 제공하는 64비트 리눅스용 컴파일러다. `none` 혹은 `elf`만 들어간 컴파일러가 대게는 베어 메탈 컴파일러다.
" 리눅스용과 베어 메탈용의 차이는 ABI가 달라지기 때문이다. 즉, 리눅스용 컴파일러로 파일을 빌드하면 실행 파일은 리눅스 ABI를 따른다. 베어 메탈용 컴파일러로 빌드하면, 해당 아키텍처의 ABI를 따르게 된다.
- 현재 사용중인 compiler 찾기
" 먼저 `file` 명령어를 사용할 수 있어야 한다. 그러면 대략적인 어떤 processor 에서 빌드가 된 건지를 알 수 가 있다. 그러면 이제 어떤 컴파일러를 사용하는지 알아봐야 한다. 이 때 여러 군데의 장소에서 컴파일러를 찾아봐야 한다.
1. /usr/bin
2. /usr/local
3. /opt/toolchain
4. /opt/crosstool- x86 크로스 컴파일
" 16비트 및 32비트는 `I686-elf`를 사용하다가, 64비트로 넘어가야 해서 `x86_64-elf` 로 바꾸게 됬다. 그런데, `x86_64-elf` 에서 `-m32` 옵션을 지원해서 32비트 빌드가 가능했다. x86 모든 모드에서 `x86_64-elf` 로 빌드가 가능한지는 테스트가 필요하다.
- i686 vs i386 vs x86_64
" 인텔에서 최초로 만든 x86기반의 32비트 CPU가 80386이다. 흔히, 이 모델을 i386이라고 부른다. 그리고 i686은 공식적인 이름이라고 부르기 어렵지만, 1997년에 등장한 P6 아키텍처를 의미한다. 즉, 펜티엄 2 이상의 모델을 의미한다. 정리하면, x86 기반의 32비트 CPU중에 그나마 좀 더 최신이 i686이다.
" x86_64는 32비트 및 64비트 호환 ISA다. 64비트의 사용은 x86_64에서만 가능하다. 실제 `x86_64`는 AMD가 만들어서 주로 `AMD64`라고 부른다.