-
크로스 컴파일[작성중]프로젝트/운영체제 만들기 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가 가장 의심스러운 부분이다.
- 컴파일 옵션
" -f & -m [참고1]
-f : machine independent
-m : machine dependent" -iprefx
The -iprefix option in GCC is used to specify a prefix that will be applied to the paths of files searched for during the compilation process, particularly when looking for include files, libraries, or other resources. It’s typically paired with options like -I, -L, or -B, which define directories for headers, libraries, or compiler binaries, respectively.Here’s how it works:
- When you use -iprefix <prefix>, it prepends the specified <prefix> to any subsequent paths provided by options like -I (for include directories), -L (for library directories), or -B (for compiler executable locations).
- It’s a way to simplify or standardize path specifications, especially in cross-compilation or when working with a toolchain installed in a non-standard location.
Example
gcc -iprefix /usr/local/ -Iinclude -Llib main.c
- -iprefix /usr/local/ sets the base path.
- -Iinclude becomes /usr/local/include.
- -Llib becomes /usr/local/lib.
Key Points
- It doesn’t replace the default system paths (e.g., /usr/include) unless explicitly overridden by other options.
- It’s particularly useful in build systems or when porting code across different environments where file locations might vary.
- If no subsequent path-related options (-I, -L, etc.) follow, -iprefix has no practical effect.
This option is less commonly used in everyday compilation compared to -I or -L alone, but it shines in scenarios involving toolchains with relocated or custom directory structures. Let me know if you’d like a deeper dive or an example tailored to a specific use case!" -isysroot
The -isysroot option in GCC specifies the system root directory (or "sysroot") to be used during compilation. The sysroot is a directory that acts as the logical root of the file system for locating system headers, libraries, and other resources needed by the compiler. This is particularly useful in cross-compilation, where the target system’s file structure differs from the host system’s.How It Works
- When you provide -isysroot <directory>, GCC prepends <directory> to the paths it uses to search for system include files (e.g., those included with <stdio.h>) and libraries.
- It effectively replaces the default system root (typically /) with the specified directory for these lookups.
- This doesn’t affect user-specified include paths (via -I) or library paths (via -L) unless combined with other options.
Common Use Case
Cross-compiling for a different platform (e.g., compiling for an embedded system or a different OS):
- You might have a sysroot like /path/to/arm-sysroot/ containing usr/include, usr/lib, etc., tailored for the target architecture.
- Using -isysroot /path/to/arm-sysroot tells GCC to look there instead of the host’s /usr/include or /usr/lib.
Example
gcc -isysroot /path/to/arm-sysroot -I./myheaders main.c
- System headers (e.g., <stdlib.h>) are searched in /path/to/arm-sysroot/usr/include.
- User headers (e.g., "myheader.h") are searched in ./myheaders (relative to the current directory).
- Libraries are looked up in /path/to/arm-sysroot/usr/lib.
Interaction with Other Options
- -isysroot pairs well with --sysroot, which sets the sysroot for both the compiler and linker, while -isysroot is more specific to include paths.
- It overrides the default system paths but can be fine-tuned with -nostdinc (to disable standard include paths entirely) or -I for additional custom paths.
Why It’s Useful
- Ensures the compiler uses the correct set of system files for the target platform, avoiding mismatches (e.g., using the host’s glibc when targeting a different libc).
- Simplifies workflows in environments like macOS (with Xcode’s SDKs) or Linux-based cross-toolchains.
On macOS, for instance, you might see -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk when building with a specific SDK. In embedded development, it’s a staple for pointing GCC to a target’s filesystem.
Let me know if you want a practical example or clarification on a specific setup!
- 크로스 컴파일 이름 포맷(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`라고 부른다.