일반적으로 임베디드 시스템 개발에서는 개발 PC에 Linux 가상 머신을 설정하고 크로스 컴파일 툴체인을 사용하여 실행 파일을 빌드합니다. 이렇게 생성된 실행 파일은 대상 ARM 보드로 복사되어 실행됩니다. 즉, 개발 PC에서 실행되는 GCC를 사용하여 ARM 보드에서 실행될 프로그램을 빌드하는 과정으로, 이를 크로스 컴파일이라고 합니다.
하지만 Automake로 제작된 소프트웨어 패키지의 경우, 크로스 컴파일은 매우 복잡할 수 있습니다. 대부분의 Linux 시스템 소프트웨어 패키지는 Automake를 사용합니다. Make, File, Mlocate와 같은 소규모 소프트웨어의 크로스 컴파일은 비교적 간단하지만, Apache, MySQL, PHP와 같은 대규모 소프트웨어의 경우 절차가 복잡해집니다. 보통은 먼저 개발 PC에 로컬 버전(x86_64 버전)을 빌드한 다음, 해당 로컬 버전의 도구를 사용하여 Makefile을 수정하고, 다시 크로스 컴파일러로 ARM 버전을 빌드하는 식으로 진행됩니다. 이 전체 과정은 번거롭고 오류가 발생하기 쉽습니다. 만약 이러한 소프트웨어를 보드에서 직접 로컬 컴파일할 수 있다면 훨씬 편리할 것입니다.
현재 ARM 크로스 컴파일 툴체인이 있다고 가정할 때, 이 크로스 컴파일러를 사용하여 GCC 소스 코드를 컴파일하여 ARM 보드에서 직접 실행되는 GCC (및 G++) 컴파일러를 만들 수 있을까요? 이 로컬 컴파일러를 사용하여 보드에서 직접 프로그램을 빌드하고 실행(네이티브 컴파일)하는 것이 가능할까요?
본 튜토리얼에서는 ARM 보드에서 직접 실행되는 GCC 및 G++ 컴파일러를 구축하는 방법을 안내합니다. 우리의 목표는 ARM 크로스 컴파일러를 사용하여 GCC 공식 웹사이트의 GCC 소스 코드를 컴파일하고 ARM 로컬 컴파일러를 얻는 것입니다. 이렇게 빌드된 GCC 컴파일러는 ARM 보드에서 실행되며, 이 GCC 컴파일러로 빌드된 실행 파일 역시 ARM 보드에서 실행됩니다.
GCC 공식 웹사이트 (GCC, the GNU Compiler Collection - GNU Project)의 GCC 소스 코드에는 다양한 플랫폼의 코드가 포함되어 있습니다. 개발 PC(x86_64)의 GCC 컴파일러로 이 소스 코드를 컴파일하면 개발 PC용 GCC 로컬 컴파일러가 생성됩니다. 마찬가지로 ARM GCC 컴파일러로 이 소스 코드를 컴파일하면 ARM 보드용 GCC 로컬 컴파일러가 생성됩니다.
본 튜토리얼에서 다루는 주요 소프트웨어 패키지는 다음과 같습니다:
| 소프트웨어 이름 | 보드 설치 경로 | 역할 | 필수 설치 여부 |
|---|---|---|---|
| gcc-10.3.0 + binutils-2.35.2 | /usr/local | C 프로그램 컴파일 (gcc), C++ 프로그램 컴파일 (g++), 어셈블 및 링크 (binutils) | 필수 |
| make-4.3 | /usr/local | make 명령 제공 | 필수 |
| gdb-10.2 | /usr/local | 프로그램 단계별 디버깅 | 선택 사항 |
| ncurses-6.2 | /usr | GDB TUI 모드 의존성 | GDB 설치 여부에 따라 결정 |
| file-5.39 | /usr/local | 파일 유형 및 실행 파일 플랫폼 확인 | 선택 사항 |
| mlocate-0.26 | /usr/local 및 /var | 파일 검색 (locate 명령) | 선택 사항 |
컴파일을 진행하기 전에, 개발 시스템(PC)에 C++ 컴파일러와 makeinfo 명령이 설치되어 있는지 확인해야 합니다. 예를 들어 Fedora 시스템에서는 다음 명령을 사용할 수 있습니다.
sudo dnf install g++ texinfo
makeinfo 명령이 없으면 GCC는 성공적으로 컴파일될 수 있지만, GDB 컴파일은 실패할 수 있습니다.
ARM 크로스 컴파일 툴체인 준비
대상 ARM CPU에 맞는 크로스 컴파일 툴체인을 준비합니다. 예를 들어 Arm Developer 웹사이트에서 AArch32 대상, 하드 플로트 (arm-none-linux-gnueabihf)를 위한 툴체인 (예: gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf.tar.xz)을 다운로드하여 개발 PC의 적절한 위치에 압축을 해제합니다. 본 튜토리얼에서는 /opt/arm-toolchain 경로를 사용한다고 가정합니다.
Busybox 루트 파일 시스템 설정
ARM 보드에는 최소한의 Linux 시스템(U-boot, Linux 커널, 장치 트리, Busybox 루트 파일 시스템)이 설정되어 있어야 합니다. 이 시스템은 GCC가 보드에서 실행될 수 있는 기본적인 환경을 제공합니다.
GCC 컴파일러가 보드에서 올바르게 작동하려면, 크로스 컴파일러 툴체인 내의 arm-none-linux-gnueabihf/libc 디렉토리의 모든 파일을 루트 파일 시스템에 복사해야 합니다. 특히 libc/usr/include 디렉토리에는 math.h, unistd.h 등 필수 C 언어 헤더 파일이 포함되어 있습니다. C 언어용 math.h와 C++ 언어용 math.h는 다르므로 유의해야 합니다.
크로스 컴파일러의 libc 디렉토리로 이동하여 모든 파일의 소유권을 root:root로 변경한 후, 이를 압축 파일로 만듭니다.
cd /opt/arm-toolchain/arm-none-linux-gnueabihf/libc
sudo chown -R root:root *
tar cvf libc_essential.tar *
생성된 libc_essential.tar 파일을 ARM 보드로 전송한 다음, 보드의 루트 디렉토리(/)에 압축을 해제합니다.
tar xvf libc_essential.tar -C /
arm-none-linux-gnueabihf/lib 디렉토리는 GCC 컴파일 과정에서 새로 생성되므로 따로 복사할 필요는 없습니다.
GCC 컴파일
현재 GCC의 최신 버전이 있을 수 있지만, 사용 중인 ARM 크로스 컴파일러 버전(예: 10.3)과의 호환성을 고려하여 GCC 소스 코드 버전도 10.3 (gcc-10.3.0.tar.xz)을 선택하는 것이 좋습니다. 최신 크로스 컴파일러가 구형 GCC 소스 코드를 컴파일하지 못하는 경우도 있기 때문입니다.
GCC 소스 코드를 다운로드하고 압축을 해제합니다.
wget ftp://ftp.fu-berlin.de/unix/languages/gcc/releases/gcc-10.3.0/gcc-10.3.0.tar.xz
tar xf gcc-10.3.0.tar.xz
GCC 컴파일에는 GMP, MPFR, MPC, ISL과 같은 필수 라이브러리들이 필요합니다. GCC 소스 디렉토리 내의 스크립트를 사용하여 이들을 다운로드할 수 있습니다.
cd gcc-10.3.0
contrib/download_prerequisites
# 출력 예시:
# ... GMP, MPFR, MPC, ISL 다운로드 메시지 ...
# All prerequisites downloaded successfully.
cd ..
GCC는 소스 코드 디렉토리 내에서 또는 그 하위 디렉토리에서 컴파일할 수 없습니다. 따라서 소스 디렉토리 외부에 새로운 빌드 디렉토리를 생성해야 합니다.
mkdir build_gcc_arm
cd build_gcc_arm
다음은 config_script.sh라는 스크립트로, GCC 빌드 설정을 위한 configure 명령과 기타 유틸리티 기능을 포함합니다.
#!/bin/sh
# 크로스 컴파일 툴체인 경로
CROSS_TOOLCHAIN_DIR="/opt/arm-toolchain/bin" # 실제 툴체인 경로로 변경
# PATH 환경 변수에 툴체인 경로 추가 (이미 존재하지 않는 경우)
if ! echo "$PATH" | grep -q "$CROSS_TOOLCHAIN_DIR"; then
echo "PATH에 크로스 컴파일러 툴체인 경로를 추가합니다."
export PATH="${CROSS_TOOLCHAIN_DIR}:${PATH}"
fi
case "$1" in
"config")
# GCC 소스 경로 설정
GCC_SOURCE_PATH="../gcc-10.3.0"
echo "GCC 설정을 시작합니다..."
"${GCC_SOURCE_PATH}/configure" \
--host=arm-none-linux-gnueabihf \
--target=arm-none-linux-gnueabihf \
--prefix=/usr/local \
--enable-shared \
--disable-libssp \
--disable-libmudflap \
--enable-checking=release \
--enable-languages=c,c++,fortran \
--with-arch=armv7-a \
--with-fpu=neon \
--with-float=hard \
--with-mode=thumb \
--disable-werror \
--with-pkgversion="Custom ARM GCC (KR)"
;;
"clean")
echo "빌드 디렉토리를 정리합니다..."
if [ -f Makefile ]; then
make distclean
fi
find . -maxdepth 1 -mindepth 1 ! -name "$(basename "$0")" -exec rm -rf {} +
;;
*)
echo "사용법: source $(basename "$0") [config|clean]"
exit 1
;;
esac
PATH 환경 변수 변경이 현재 터미널 세션에 적용되도록 하려면 스크립트를 source 명령으로 실행해야 합니다.
source config_script.sh config
configure 명령에서 --host와 --target 옵션을 모두 지정하는 것이 중요합니다. 다른 소프트웨어 컴파일 시에는 보통 --host만 지정해도 충분하지만, GCC 컴파일 시에는 두 옵션을 모두 지정해야 합니다.
설정이 성공하면, 이제 컴파일을 시작합니다. 이 과정은 수 시간이 소요될 수 있습니다.
make -j$(nproc) # 시스템 코어 수에 맞춰 병렬 컴파일 (선택 사항)
컴파일이 완료되면, 설치 파일을 생성합니다. 크로스 컴파일의 경우, 생성된 ARM 버전 실행 파일은 개발 PC에서 직접 실행할 수 없으므로, DESTDIR을 사용하여 설치 경로를 임시 디렉토리로 지정합니다.
make install DESTDIR=$(pwd)/_install_stage
_install_stage 디렉토리 내에 GCC 설치 파일들이 생성되었는지 확인합니다.
cd _install_stage
ls
# usr
cd usr/local/bin
file gcc
# 출력 예시: ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux), dynamically linked, ...
DESTDIR 옵션은 실제 설치될 루트 디렉토리를 임시로 지정하여, 크로스 컴파일된 바이너리가 개발 PC에 설치되는 것을 방지합니다. --prefix 옵션은 최종 대상 시스템(ARM 보드)에서의 설치 경로를 지정합니다. 이 경우 /usr/local에 설치됩니다.
현재 GCC 설치 파일만으로는 프로그램을 컴파일할 수 없습니다. as(어셈블러)와 ld(링커) 도구가 /usr/local/bin 디렉토리에 없기 때문입니다. GCC는 C 소스 코드를 어셈블리 코드로만 변환할 수 있으며, 실행 파일을 생성하려면 어셈블러와 링커가 필요합니다.
# 보드에서 간단한 C 코드 컴파일 시도 (as/ld 없음)
echo -e "int main(void) { return 0; }" > test_prog.c
gcc test_prog.c -o test_prog
# 출력 예시: gcc: fatal error: cannot execute 'as': execvp: No such file or directory
하지만 GCC가 C 코드를 어셈블리 코드로 변환하는 것은 가능합니다.
gcc -S test_prog.c
cat test_prog.s
# 출력 예시: ARM 플랫폼용 어셈블리 코드
이것은 우리가 올바르게 ARM 로컬 컴파일러를 만들고 있음을 증명합니다.
Binutils 컴파일
as와 ld 명령은 Binutils 패키지에 포함되어 있습니다. Binutils 소스 코드를 다운로드하고 압축을 해제합니다.
wget http://ftp.gnu.org/pub/gnu/binutils/binutils-2.35.2.tar.xz
tar xf binutils-2.35.2.tar.xz
다시 한번 소스 코드 외부의 빌드 디렉토리를 생성합니다.
mkdir build_binutils_arm
cd build_binutils_arm
config_script.sh를 수정하여 Binutils용 configure 명령을 사용합니다.
#!/bin/sh
# 크로스 컴파일 툴체인 경로
CROSS_TOOLCHAIN_DIR="/opt/arm-toolchain/bin" # 실제 툴체인 경로로 변경
# PATH 환경 변수에 툴체인 경로 추가 (이미 존재하지 않는 경우)
if ! echo "$PATH" | grep -q "$CROSS_TOOLCHAIN_DIR"; then
echo "PATH에 크로스 컴파일러 툴체인 경로를 추가합니다."
export PATH="${CROSS_TOOLCHAIN_DIR}:${PATH}"
fi
case "$1" in
"config")
# Binutils 소스 경로 설정
BINUTILS_SOURCE_PATH="../binutils-2.35.2"
echo "Binutils 설정을 시작합니다..."
"${BINUTILS_SOURCE_PATH}/configure" \
--host=arm-none-linux-gnueabihf \
--prefix=/usr/local \
--disable-werror
;;
"clean")
echo "빌드 디렉토리를 정리합니다..."
if [ -f Makefile ]; then
make distclean
fi
find . -maxdepth 1 -mindepth 1 ! -name "$(basename "$0")" -exec rm -rf {} +
;;
*)
echo "사용법: source $(basename "$0") [config|clean]"
exit 1
;;
esac
Binutils는 --target 옵션 없이 --host만 지정해도 됩니다.
source config_script.sh config
make -j$(nproc)
make install DESTDIR=$(pwd)/_install_stage
이제 GCC와 Binutils의 설치 파일을 합칩니다.
cp -r /path/to/build_gcc_arm/_install_stage/* ./_install_stage/
모든 설치 파일의 소유권을 root:root로 변경한 후, 압축 파일로 만듭니다.
cd _install_stage
sudo chown -R root:root usr
tar czf custom_gcc_binutils_10.3.0.tar.gz usr
생성된 custom_gcc_binutils_10.3.0.tar.gz 파일을 ARM 보드로 전송한 다음, 루트 디렉토리(/)에 압축을 해제합니다.
tar xvf custom_gcc_binutils_10.3.0.tar.gz -C /
이제 보드에서 gcc 명령을 실행하기 위해 PATH 환경 변수에 /usr/local/bin을 추가합니다.
export PATH=$PATH:/usr/local/bin
gcc -v
# 출력 예시: gcc version 10.3.0 (GCC)
/etc/profile 파일을 수정하여 PATH 환경 변수를 영구적으로 설정합니다.
# /etc/profile: system-wide .profile file for the Bourne shells
echo
echo -n "Processing /etc/profile... "
# no-op
# 기존 설정 유지 (사용자 환경에 따라 다름)
# awktext="\$1==\"$USER\"{print \$6}"
# export HOME=`awk -F: "$awktext" /etc/passwd`
# export PS1="[\\u@\\h \\w]\\$ "
export PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/bin
echo "Done"
echo
이제 보드에서 C 및 C++ 프로그램을 컴파일하고 실행해봅니다.
# C 프로그램 컴파일 및 실행
mkdir my_dev_area && cd my_dev_area
cat > thread_app.c << EOF
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <pthread.h>
static void* thread_worker(void* argument_ptr) {
const char* message = (const char*)argument_ptr;
printf("[스레드] 받은 메시지: %s\\n", message);
return (void*)"스레드 작업 완료";
}
int main(void) {
pthread_t my_thread_id;
void* thread_result_ptr;
double angle_rad = M_PI / 2.0;
printf("메인 프로그램 시작!\\n");
printf("sin(PI/2) 값: %lf\\n", sin(angle_rad));
if (pthread_create(&my_thread_id, NULL, thread_worker, (void*)"메인에서 스레드로 전달") != 0) {
perror("스레드 생성 실패");
return EXIT_FAILURE;
}
if (pthread_join(my_thread_id, &thread_result_ptr) != 0) {
perror("스레드 조인 실패");
return EXIT_FAILURE;
}
printf("[메인] 스레드로부터 받은 결과: %s\\n", (char*)thread_result_ptr);
return EXIT_SUCCESS;
}
EOF
gcc thread_app.c -o thread_app -lpthread -lm
./thread_app
# 예상 출력:
# 메인 프로그램 시작!
# sin(PI/2) 값: 1.000000
# [스레드] 받은 메시지: 메인에서 스레드로 전달
# [메인] 스레드로부터 받은 결과: 스레드 작업 완료
# C++ 프로그램 컴파일 및 실행
cat > cpp_message.cpp << EOF
#include <iostream>
#include <string>
#include <vector>
int main() {
std::string greeting = "안녕하세요";
std::string recipient = "세상";
std::string full_message = greeting + ", " + recipient + "!";
std::cout << "메시지: " << full_message << std::endl;
std::cout << "길이: " << full_message.length() << std::endl;
std::vector<int> numbers = {10, 20, 30};
std::cout << "첫 번째 숫자: " << numbers[0] << std::endl;
return 0;
}
EOF
g++ cpp_message.cpp -o cpp_message
./cpp_message
# 예상 출력:
# 메시지: 안녕하세요, 세상!
# 길이: 9
# 첫 번째 숫자: 10
Make 컴파일
이제 make 명령어를 보드에 이식합니다.
wget http://ftp.gnu.org/gnu/make/make-4.3.tar.gz
tar xf make-4.3.tar.gz
mkdir build_make_arm
cd build_make_arm
config_script.sh를 수정하여 Make용 configure 명령을 사용합니다.
#!/bin/sh
# 크로스 컴파일 툴체인 경로
CROSS_TOOLCHAIN_DIR="/opt/arm-toolchain/bin" # 실제 툴체인 경로로 변경
# PATH 환경 변수에 툴체인 경로 추가 (이미 존재하지 않는 경우)
if ! echo "$PATH" | grep -q "$CROSS_TOOLCHAIN_DIR"; then
echo "PATH에 크로스 컴파일러 툴체인 경로를 추가합니다."
export PATH="${CROSS_TOOLCHAIN_DIR}:${PATH}"
fi
case "$1" in
"config")
# Make 소스 경로 설정
MAKE_SOURCE_PATH="../make-4.3"
echo "Make 설정을 시작합니다..."
"${MAKE_SOURCE_PATH}/configure" \
--host=arm-none-linux-gnueabihf \
--prefix=/usr/local \
--disable-werror
;;
"clean")
echo "빌드 디렉토리를 정리합니다..."
if [ -f Makefile ]; then
make distclean
fi
find . -maxdepth 1 -mindepth 1 ! -name "$(basename "$0")" -exec rm -rf {} +
;;
*)
echo "사용법: source $(basename "$0") [config|clean]"
exit 1
;;
esac
source config_script.sh config
make -j$(nproc)
make install DESTDIR=$(pwd)/_install_stage
cd _install_stage
sudo chown -R root:root usr
tar czf custom_make_4.3.tar.gz usr
이 압축 파일을 보드에 전송하여 루트 디렉토리에 압축을 해제합니다. 이후 /etc/profile에 다음 환경 변수들을 추가하여 make 명령이 올바르게 작동하도록 합니다.
export CC=gcc
export CXX=g++
테스트용 Makefile을 준비합니다.
# 컴파일러 플래그
C_DEBUG_FLAGS=-g -Wall
# 모든 타겟 빌드
all: test_c_app test_cpp_app
# C 애플리케이션 빌드
test_c_app: simple_prog.c thread_app.c
$(CC) simple_prog.c -o simple_prog $(C_DEBUG_FLAGS)
$(CC) thread_app.c -o thread_app $(C_DEBUG_FLAGS) -lpthread -lm
# C++ 애플리케이션 빌드
test_cpp_app: cpp_message.cpp
$(CXX) cpp_message.cpp -o cpp_message $(C_DEBUG_FLAGS)
# 정리 작업
clean:
rm -f simple_prog thread_app cpp_message *.o
# 보드에서 make 테스트
make
./simple_prog
./thread_app
./cpp_message
make clean
make 사용 시 "warning: Clock skew detected." 경고가 발생하면, 시스템 시간이 잘못 설정된 것입니다. date 명령으로 시간을 올바르게 설정하거나 NTP를 사용하여 시간을 동기화해야 합니다.
GDB 컴파일
GDB 디버거를 보드에 설치하여 단계별 디버깅을 가능하게 합니다. GDB의 TUI(텍스트 사용자 인터페이스) 모드를 활성화하려면 Ncurses 라이브러리가 필요합니다.
Ncurses 라이브러리 컴파일
wget https://ftp.gnu.org/gnu/ncurses/ncurses-6.2.tar.gz # 최신 버전 확인
tar xf ncurses-6.2.tar.gz
mkdir build_ncurses_arm
cd build_ncurses_arm
config_script.sh를 수정하여 Ncurses용 configure 명령을 사용합니다.
#!/bin/sh
# 크로스 컴파일 툴체인 경로
CROSS_TOOLCHAIN_DIR="/opt/arm-toolchain/bin" # 실제 툴체인 경로로 변경
# PATH 환경 변수에 툴체인 경로 추가 (이미 존재하지 않는 경우)
if ! echo "$PATH" | grep -q "$CROSS_TOOLCHAIN_DIR"; then
echo "PATH에 크로스 컴파일러 툴체인 경로를 추가합니다."
export PATH="${CROSS_TOOLCHAIN_DIR}:${PATH}"
fi
case "$1" in
"config")
# Ncurses 소스 경로 설정
NCURSES_SOURCE_PATH="../ncurses-6.2"
echo "Ncurses 설정을 시작합니다..."
"${NCURSES_SOURCE_PATH}/configure" \
--host=arm-none-linux-gnueabihf \
--prefix=/usr \
--with-shared \
--disable-werror
;;
"clean")
echo "빌드 디렉토리를 정리합니다..."
if [ -f Makefile ]; then
make distclean
fi
find . -maxdepth 1 -mindepth 1 ! -name "$(basename "$0")" -exec rm -rf {} +
;;
*)
echo "사용법: source $(basename "$0") [config|clean]"
exit 1
;;
esac
--with-shared (또는 --enable-shared) 옵션은 동적 라이브러리(lib*.so)가 빌드되도록 하는 데 매우 중요합니다. 많은 Linux 소프트웨어가 Ncurses에 의존하며, 동적 라이브러리가 없으면 "dangerous relocation"과 같은 오류가 발생할 수 있습니다.
source config_script.sh config
make -j$(nproc)
make install 시 strip: Unable to recognise the format of the input file 오류가 발생할 수 있습니다. 이는 개발 PC의 strip 도구가 ARM 바이너리를 처리하지 못하기 때문입니다. 빌드 디렉토리 내의 progs/Makefile 파일에서 INSTALL_PROG 변수 값의 끝에 --strip-program=arm-none-linux-gnueabihf-strip를 추가하여 크로스 툴체인의 strip을 사용하도록 지시합니다.
# _install_stage/progs/Makefile (예시)
# ...
INSTALL_PROG = $(INSTALL) -s $(if $(subst $(STRIP_PROGRAM),,$(STRIP_PROGRAM)),--strip-program=$(STRIP_PROGRAM))
# ... 이 부분을 찾아
INSTALL_PROG = $(INSTALL) -s $(if $(subst $(STRIP_PROGRAM),,$(STRIP_PROGRAM)),--strip-program=$(STRIP_PROGRAM)) --strip-program=arm-none-linux-gnueabihf-strip
# ... 이렇게 수정합니다.
make install DESTDIR=$(pwd)/_install_stage
cd _install_stage
sudo chown -R root:root usr
tar czf custom_ncurses_6.2.tar.gz usr
GDB 자체 컴파일
GDB 컴파일 전에 texinfo 패키지(makeinfo 명령 포함)가 설치되어 있는지 확인합니다.
wget https://ftp.gnu.org/gnu/gdb/gdb-10.2.tar.xz # 최신 버전 확인
tar xf gdb-10.2.tar.xz
mkdir build_gdb_arm
cd build_gdb_arm
config_script.sh를 수정하여 GDB용 configure 명령을 사용합니다.
#!/bin/sh
# 크로스 컴파일 툴체인 경로
CROSS_TOOLCHAIN_DIR="/opt/arm-toolchain/bin" # 실제 툴체인 경로로 변경
# PATH 환경 변수에 툴체인 경로 추가 (이미 존재하지 않는 경우)
if ! echo "$PATH" | grep -q "$CROSS_TOOLCHAIN_DIR"; then
echo "PATH에 크로스 컴파일러 툴체인 경로를 추가합니다."
export PATH="${CROSS_TOOLCHAIN_DIR}:${PATH}"
fi
case "$1" in
"config")
# GDB 소스 경로 설정
GDB_SOURCE_PATH="../gdb-10.2"
echo "GDB 설정을 시작합니다..."
"${GDB_SOURCE_PATH}/configure" \
--host=arm-none-linux-gnueabihf \
--target=arm-none-linux-gnueabihf \
--prefix=/usr/local \
--enable-64-bit-bfd \
--enable-targets=arm-none-eabi,arm-none-linux-gnueabihf,armeb-none-eabi,armeb-none-linux-gnueabihf \
--enable-initfini-array \
--enable-plugins \
--enable-tui \
--disable-binutils \
--disable-sim \
--disable-as \
--disable-ld \
--disable-doc \
--disable-gdbtk \
--disable-nls \
--disable-werror \
--without-x \
--with-system-readline=no
;;
"clean")
echo "빌드 디렉토리를 정리합니다..."
if [ -f Makefile ]; then
make distclean
fi
find . -maxdepth 1 -mindepth 1 ! -name "$(basename "$0")" -exec rm -rf {} +
;;
*)
echo "사용법: source $(basename "$0") [config|clean]"
exit 1
;;
esac
source config_script.sh config
make -j$(nproc) \
CFLAGS="-I/path/to/build_ncurses_arm/_install_stage/usr/include" \
CPPFLAGS="-I/path/to/build_ncurses_arm/_install_stage/usr/include" \
LDFLAGS="-L/path/to/build_ncurses_arm/_install_stage/usr/lib"
make install DESTDIR=$(pwd)/_install_stage
cd _install_stage
sudo chown -R root:root usr
tar czf custom_gdb_10.2.tar.gz usr
Ncurses와 GDB 압축 파일을 보드에 전송하여 루트 디렉토리에 압축을 해제합니다.
tar xvf custom_ncurses_6.2.tar.gz -C /
tar xvf custom_gdb_10.2.tar.gz -C /
gdb 명령을 사용하여 cpp_message 프로그램을 디버깅합니다.
gdb cpp_message
# (gdb) break cpp_message.cpp:9 # 9번째 줄에 중단점 설정
# (gdb) start
# (gdb) next
# (gdb) print full_message
# (gdb) continue
# (gdb) quit
GDB의 TUI 모드를 사용하려면 gdb -tui를 실행합니다.
크로스 컴파일 시 x86 라이브러리 충돌 방지
크로스 컴파일 시 개발 PC의 x86(또는 x86_64) 라이브러리와 ARM 대상 라이브러리 간의 충돌을 방지하는 것이 중요합니다. -I, -L 컴파일러 옵션으로 ARM 라이브러리 경로의 우선순위를 높일 수 있습니다.
예를 들어, 사용자 정의 헤더 파일을 시스템 헤더 파일보다 우선적으로 포함하고 싶다면 다음과 같이 할 수 있습니다.
mkdir my_custom_headers
mkdir my_custom_headers/net
cat > my_custom_headers/net/network_api.h << EOF
#include_next <arpa/inet.h> // 시스템 arpa/inet.h 포함
struct custom_address_info {
char* host_name;
int port_number;
};
EOF
cat > network_test.c << EOF
#include "net/network_api.h"
#include <stdio.h>
#include <string.h>
int main(void) {
socklen_t addr_len = 16;
struct custom_address_info my_host;
my_host.host_name = (char*)"example.com";
my_host.port_number = 8080;
printf("호스트 이름: %s\\n", my_host.host_name);
printf("포트 번호: %d\\n", my_host.port_number);
printf("주소 길이 (시스템 타입): %u\\n", addr_len);
return 0;
}
EOF
-I. 옵션을 사용하여 현재 디렉토리를 헤더 검색 경로에 추가하면, 사용자 정의 net/network_api.h가 시스템 arpa/inet.h보다 먼저 검색됩니다. 사용자 정의 헤더 파일에 #include_next 를 추가하면, 시스템의 arpa/inet.h도 포함하여 socklen_t와 같은 시스템 정의 타입을 사용할 수 있습니다.
gcc network_test.c -o network_test -I.
./network_test
# 예상 출력:
# 호스트 이름: example.com
# 포트 번호: 8080
# 주소 길이 (시스템 타입): 16
file 및 locate 명령 컴파일
ARM 보드에는 file 및 locate와 같은 유용한 명령이 없을 수 있습니다.
file 명령
file 명령은 file 패키지에 있습니다.
wget ftp://ftp.astron.com/pub/file/file-5.39.tar.gz
tar xf file-5.39.tar.gz
mkdir build_file_arm
cd build_file_arm
# configure 스크립트: ../file-5.39/configure --host=arm-none-linux-gnueabihf --prefix=/usr/local
source config_script.sh config
make -j$(nproc)
make install DESTDIR=$(pwd)/_install_stage
cd _install_stage
sudo chown -R root:root usr
tar czf custom_file_5.39.tar.gz usr
보드에 전송하여 루트 디렉토리에 압축을 해제합니다.
tar xvf custom_file_5.39.tar.gz -C /
이제 file 명령을 사용할 수 있습니다.
file /bin/busybox
file ./thread_app
# 출력 예시: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, ...
locate 명령
locate 명령은 mlocate 패키지에 포함되어 있습니다.
wget https://releases.pagure.org/mlocate/mlocate-0.26.tar.xz
tar xf mlocate-0.26.tar.xz
mkdir build_mlocate_arm
cd build_mlocate_arm
# configure 스크립트: ../mlocate-0.26/configure --host=arm-none-linux-gnueabihf --prefix=/usr/local --localstatedir=/var
source config_script.sh config
make -j$(nproc)
make install DESTDIR=$(pwd)/_install_stage
cd _install_stage
sudo chown -R root:root usr var
tar czf custom_mlocate_0.26.tar.gz usr var
압축 파일을 보드에 전송하여 루트 디렉토리에 압축을 해제합니다.
tar xvf custom_mlocate_0.26.tar.gz -C /
locate 명령을 사용하기 전에 데이터베이스를 생성해야 합니다.
addgroup mlocate -g21 -S # mlocate 그룹 추가
updatedb # 데이터베이스 생성
locate cpp_message.cpp
# 예상 출력: /path/to/my_dev_area/cpp_message.cpp
네트워크 시간 동기화 (NTP)
시스템 시간 문제를 해결하기 위해 NTP(Network Time Protocol) 클라이언트를 설치하여 네트워크 시간을 동기화할 수 있습니다. ntpdate 명령을 제공하는 NTP 패키지를 빌드합니다.
NTP 공식 웹사이트에서 소스 코드(예: ntp-4.2.8p15.tar.gz)를 다운로드합니다.
이전 단계에서 로컬 컴파일한 NTP가 libcrypto.so.3와 같은 예기치 않은 호스트 라이브러리에 링크되는 문제가 발생할 수 있습니다. 이를 피하기 위해 NTP 패키지는 크로스 컴파일하는 것이 더 안전할 수 있습니다.
wget https://www.eecis.udel.edu/~ntp/ntp_spool/ntp4/ntp-4.2/ntp-4.2.8p15.tar.gz
tar xf ntp-4.2.8p15.tar.gz
mkdir build_ntp_arm
cd build_ntp_arm
# configure 스크립트: ../ntp-4.2.8p15/configure --host=arm-none-linux-gnueabihf --prefix=/usr/local --with-yielding-select=yes
source config_script.sh config
make -j$(nproc)
make install DESTDIR=$(pwd)/_install_stage
cd _install_stage
sudo chown -R root:root usr
tar czf custom_ntp_4.2.8p15.tar.gz usr
보드에 전송하여 루트 디렉토리에 압축을 해제하고 PATH에 /usr/local/sbin을 추가한 후 시간을 동기화합니다.
tar xvf custom_ntp_4.2.8p15.tar.gz -C /
export PATH=/sbin:/usr/sbin:/usr/local/sbin:/bin:/usr/bin:/usr/local/bin # sbin 경로 추가
ntpdate kr.pool.ntp.org # 한국 NTP 서버 사용
date
# 예상 출력: 업데이트된 시스템 시간
소프트웨어 제거 도구 생성
크로스 컴파일된 소프트웨어는 make uninstall로 제거할 수 없습니다. 따라서 설치된 파일을 추적하고 제거하는 도구가 필요합니다. 다음은 C++로 작성된 간단한 제거 도구입니다.
#include <iostream>
#include <string>
#include <vector>
#include <stack>
#include <stdexcept>
#include <cstdio> // For popen, pclose
#include <sys/stat.h> // For lstat, S_ISDIR
#include <unistd.h> // For unlink, rmdir
// 터미널 색상 정의
#define TERM_COLOR_RED "\033[31m"
#define TERM_COLOR_YELLOW "\033[33m"
#define TERM_COLOR_RESET "\033[0m"
// 아카이브에서 파일 목록을 가져오는 함수
std::vector<std::string> get_archive_file_list(const std::string& archive_path) {
std::vector<std::string> file_list;
// 'tar -tf' 명령어를 사용하여 아카이브 내부의 파일 목록을 얻습니다.
std::string command = "tar tf " + archive_path;
FILE* pipe = popen(command.c_str(), "r");
if (!pipe) {
throw std::runtime_error("tar 명령 실행 실패: " + archive_path);
}
char buffer[256];
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
std::string line = buffer;
// 개행 문자 제거
if (!line.empty() && line.back() == '\n') {
line.pop_back();
}
if (!line.empty()) {
file_list.push_back(line);
}
}
if (pclose(pipe) != 0) {
throw std::runtime_error("tar 명령 종료 실패 또는 오류 발생.");
}
return file_list;
}
// 특정 패키지를 언인스톨하는 함수
int perform_uninstall(const std::string& package_archive) {
std::cout << "패키지 제거 시작: " << package_archive << std::endl;
std::vector<std::string> files_to_remove;
std::stack<std::string> directories_to_remove;
try {
files_to_remove = get_archive_file_list(package_archive);
} catch (const std::runtime_error& e) {
std::cerr << TERM_COLOR_RED << "오류: " << e.what() << TERM_COLOR_RESET << std::endl;
return -1;
}
// 파일과 디렉토리를 분리하고, 디렉토리는 스택에 역순으로 저장
for (const std::string& item_name : files_to_remove) {
std::string full_path = item_name;
if (!full_path.empty() && full_path[0] != '/') {
full_path = "/" + full_path; // 루트 기준 경로로 변환
}
struct stat file_stat;
// lstat을 사용하여 심볼릭 링크 자체의 정보를 가져옵니다.
if (lstat(full_path.c_str(), &file_stat) == 0) {
if (S_ISDIR(file_stat.st_mode)) {
directories_to_remove.push(full_path);
} else {
// 파일 또는 심볼릭 링크 삭제
if (unlink(full_path.c_str()) == 0) {
std::cout << "삭제: " << full_path << std::endl;
} else {
std::cerr << TERM_COLOR_YELLOW << "삭제 실패: " << full_path << TERM_COLOR_RESET << std::endl;
}
}
} else {
std::cerr << TERM_COLOR_YELLOW << "파일 정보 가져오기 실패 (이미 삭제되었을 수도 있음): " << full_path << TERM_COLOR_RESET << std::endl;
}
}
// 스택에 저장된 디렉토리를 역순으로 (하위 디렉토리부터) 삭제
while (!directories_to_remove.empty()) {
std::string dir_path = directories_to_remove.top();
if (rmdir(dir_path.c_str()) == 0) {
std::cout << "디렉토리 삭제: " << dir_path << std::endl;
} else {
std::cerr << TERM_COLOR_YELLOW << "디렉토리 삭제 실패 (비어있지 않거나 이미 삭제됨): " << dir_path << TERM_COLOR_RESET << std::endl;
}
directories_to_remove.pop();
}
std::cout << "패키지 제거 완료: " << package_archive << std::endl;
return 0;
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cerr << "사용법: " << argv[0] << " <패키지_아카이브_파일> [패키지_아카이브_파일2 ...]" << std::endl;
return -1;
}
for (int i = 1; i < argc; ++i) {
if (perform_uninstall(argv[i]) != 0) {
return -1; // 하나라도 실패하면 종료
}
}
return 0;
}
Makefile을 사용하여 개발 PC에서 크로스 컴파일합니다.
CXX_CROSS_COMPILER := /opt/arm-toolchain/bin/arm-none-linux-gnueabihf-g++
all: uninstaller_tool
uninstaller_tool: uninstaller.cpp
$(CXX_CROSS_COMPILER) $(CXXFLAGS) $^ -o $@
clean:
rm -f uninstaller_tool
빌드된 uninstaller_tool을 보드에 전송하여 ./uninstaller_tool package_name.tar.gz 명령으로 패키지를 제거할 수 있습니다. rmdir 함수는 비어있는 디렉토리만 삭제하므로 안전하게 사용할 수 있습니다.
Bash 쉘 설치
Busybox의 기본 쉘은 배열을 지원하지 않는 등 기능에 제한이 있습니다. 더 풍부한 기능을 제공하는 GNU Bash 쉘을 설치하는 것이 좋습니다.
wget https://ftp.gnu.org/gnu/bash/bash-5.1.8.tar.gz # 최신 버전 확인
tar xf bash-5.1.8.tar.gz
cd bash-5.1.8
./configure --prefix=/usr/local # 보드에서 직접 컴파일
make -j$(nproc)
make install
설치된 Bash는 /usr/local/bin/bash에 있습니다. /bin 아래에 심볼릭 링크를 생성하고 시스템 설정을 변경합니다.
ln -s /usr/local/bin/bash /bin/bash
# /etc/inittab 및 /etc/passwd 파일에서 모든 /bin/sh를 /bin/bash로 변경
보드를 재부팅하면 Bash 쉘의 배열 기능 등을 사용할 수 있습니다. /etc/profile에서 PS1 환경 변수를 export PS1="[\\u@\\h \\W]\\$ "로 변경하여 짧은 경로 표시를 활성화할 수도 있습니다.
라이브러리 경로 설정
로컬 컴파일된 소프트웨어는 보통 /usr/local/lib에 라이브러리를 설치하지만, Linux 시스템은 기본적으로 /lib와 /usr/lib만 라이브러리 검색 경로로 인식합니다. 이로 인해 소프트웨어가 "error while loading shared libraries" 오류를 발생시키며 실행되지 않을 수 있습니다.
LD_LIBRARY_PATH 환경 변수를 수정할 수 있지만, 이는 sudo 명령으로 실행될 때 문제가 될 수 있습니다. 더 나은 방법은 /etc/ld.so.conf 파일을 생성하고 /usr/local/lib 경로를 추가한 다음, root 권한으로 ldconfig 명령을 실행하는 것입니다.
# /etc/ld.so.conf 파일 생성 및 내용 추가
echo "/usr/local/lib" > /etc/ld.so.conf
ldconfig # 라이브러리 캐시 업데이트
python # 이제 정상적으로 실행됨
ldconfig는 영구적으로 라이브러리 검색 경로를 설정하며, sudo 사용 시에도 유효합니다. 새로운 라이브러리 설치 후에도 ldconfig를 다시 실행하여 캐시를 업데이트해야 합니다.
join 명령 문제 해결
Busybox 기반의 루트 파일 시스템에는 join 명령이 포함되지 않아 일부 소프트웨어 컴파일 시 "join: command not found" 오류가 발생할 수 있습니다. join 명령은 coreutils 패키지에 포함되어 있습니다.
coreutils는 Busybox와 기능적으로 중복되는 부분이 많지만, 기본 설치 경로가 /usr/local이므로 Busybox와 충돌하지 않습니다.
wget https://ftp.gnu.org/gnu/coreutils/coreutils-8.32.tar.xz
tar xf coreutils-8.32.tar.xz
mkdir build_coreutils_arm
cd build_coreutils_arm
# configure 스크립트: ../coreutils-8.32/configure --host=arm-none-linux-gnueabihf --prefix=/usr/local
source config_script.sh config
make -j$(nproc)
make install DESTDIR=$(pwd)/_install_stage
cd _install_stage
sudo chown -R root:root usr
tar czf custom_coreutils_8.32.tar.gz usr
압축 파일을 보드에 전송하여 루트 디렉토리에 압축을 해제합니다. 이제 join 명령을 사용할 수 있습니다.
만약 ls나 expr와 같은 Busybox 명령 대신 coreutils 버전을 사용하고 싶다면, /usr/bin 아래의 심볼릭 링크를 수정하여 /usr/local/bin의 해당 명령을 가리키도록 할 수 있습니다.
rm /usr/bin/expr
ln -s /usr/local/bin/expr /usr/bin/expr
expr --help # 이제 coreutils 버전의 도움말이 출력됨
성공적인 로컬 컴파일 환경 구축 예시
위에서 설명한 로컬 컴파일 환경을 구축하면 ARM 보드에서 다양한 복잡한 소프트웨어를 직접 빌드하고 실행할 수 있습니다.
- Apache 서버: Apache HTTP 서버를 직접 로컬 컴파일하여 웹 서비스를 제공할 수 있습니다.
- MySQL 데이터베이스: MariaDB와 같은 MySQL 호환 데이터베이스를 로컬에서 빌드 및 설치할 수 있습니다.
- PHP 동적 웹 페이지: PHP 인터프리터를 컴파일하여 Apache 또는 Nginx와 연동하여 동적 웹 페이지를 서비스할 수 있습니다.
- Samba 파일 공유: Samba 서버를 빌드하여 Windows 시스템과 파일 공유를 설정할 수 있습니다.
Samba 서버 설정 예시:
/usr/local/samba/etc/smb.conf.default를smb.conf로 복사합니다.smb.conf파일의[global]섹션에서workgroup을 필요에 따라 수정합니다./usr/local/samba/bin/testparm으로 설정을 테스트합니다.nobody사용자 및 그룹을 추가합니다:addgroup -S nobody,adduser -G nobody -SDH nobody.- Samba에
root사용자를 추가합니다:/usr/local/samba/bin/smbpasswd -a root. - Samba 데몬을 시작합니다:
/usr/local/samba/sbin/nmbd -D,/usr/local/samba/sbin/smbd -D. ps | grep samba로 데몬이 실행 중인지 확인합니다.
PC에서 \\IP주소 또는 \\호스트이름으로 Samba 서버에 접속하여 공유 파일을 확인할 수 있습니다.
첫 번째 GCC 컴파일러의 기원 (부트스트랩)
GCC 소스 코드는 C 언어로 작성되었으므로, C 코드를 컴파일하려면 GCC 컴파일러가 필요합니다. 새로운 CPU 플랫폼에 GCC 컴파일러가 없을 때, 어떻게 해당 플랫폼용 GCC 로컬 컴파일러를 구축할 수 있을까요? 이는 닭이 먼저냐 달걀이 먼저냐와 같은 문제입니다.
해결책은 '부트스트랩' 과정에 있습니다.
- 먼저 새로운 플랫폼의 CPU 명령어 세트에 대한 지원을 포함하도록 GCC 소스 코드를 수정합니다.
- 개발 PC(x86 또는 x86_64)에서 PC용 GCC를 사용하여 이 수정된 GCC 소스 코드를 컴파일합니다. 이때
--host=PC_PLATFORM,--target=NEW_PLATFORM옵션을 지정하여 새로운 플랫폼용 크로스 컴파일러를 생성합니다. 이 크로스 컴파일러는 PC에서 실행되지만, 새로운 플랫폼에서 실행될 프로그램을 빌드할 수 있습니다. - 이 크로스 컴파일러를 다시 개발 PC에서 사용하여, 이전에 수정한 GCC 소스 코드를 다시 컴파일합니다. 이때는
--host=NEW_PLATFORM,--target=NEW_PLATFORM옵션을 지정합니다. 이 과정을 통해 새로운 플랫폼에서 직접 실행되는 GCC 로컬 컴파일러를 얻게 됩니다.
이처럼 먼저 크로스 컴파일러("닭")를 만든 다음, 그 크로스 컴파일러로 로컬 컴파일러("달걀")를 만드는 방식으로 새로운 플랫폼의 컴파일러 생태계를 구축합니다.
GCC 11.2 버전으로 업그레이드
이전 ARM 크로스 컴파일러(예: 10.3 버전)로는 GCC 11.0 또는 11.2 버전의 소스 코드를 직접 크로스 컴파일할 수 없는 경우가 있습니다 (예: 최신 언어 기능 '__is_nothrow_constructible', '__is_nothrow_assignable' 지원 부족).
하지만 이미 ARM 보드용 GCC 10.3 로컬 컴파일러가 있다면, 이 로컬 컴파일러를 사용하여 GCC 11.2 소스 코드를 보드에서 직접 네이티브 컴파일할 수 있습니다.
wget https://ftp.gnu.org/gnu/gcc/gcc-11.2.0/gcc-11.2.0.tar.gz
tar xf gcc-11.2.0.tar.gz
cd gcc-11.2.0
contrib/download_prerequisites
cd ..
mkdir build_gcc_11_native
cd build_gcc_11_native
이때 config_script.sh는 --host 및 --target 옵션 없이 네이티브 컴파일을 위한 configure 명령을 포함해야 합니다.
#!/bin/sh
# GCC 소스 경로 설정
GCC_SOURCE_PATH="../gcc-11.2.0"
echo "GCC 11.2 네이티브 설정을 시작합니다..."
"${GCC_SOURCE_PATH}/configure" \
--prefix=/usr/local \
--enable-shared \
--disable-libssp \
--disable-libmudflap \
--enable-checking=release \
--enable-languages=c,c++,fortran,c++ \
--with-arch=armv7-a \
--with-fpu=neon \
--with-float=hard \
--with-mode=thumb \
--disable-werror \
--with-pkgversion="Custom ARM GCC 11.2 (KR)"
./config_script.sh config
make -j$(nproc) # 약 27시간 소요
make install DESTDIR=$(pwd)/_install_stage_11
GCC 11.2는 Binutils 2.37 이상과 호환됩니다. binutils-2.37.tar.xz를 다운로드하여 10.3 로컬 컴파일러로 네이티브 컴파일합니다.
wget http://ftp.gnu.org/pub/gnu/binutils/binutils-2.37.tar.xz
tar xf binutils-2.37.tar.xz
mkdir build_binutils_2.37_native
cd build_binutils_2.37_native
# configure 스크립트: ../binutils-2.37/configure --prefix=/usr/local
./config_script.sh config # 이 스크립트는 네이티브 빌드용 config_script.sh여야 함
make -j$(nproc) # 약 22분 소요
make install DESTDIR=$(pwd)/_install_stage_binutils_2.37
새로운 GCC 11.2와 Binutils 2.37 설치 파일을 합치고 압축합니다.
# 합치기 및 압축 과정
# (예: 임시 디렉토리에서 GCC 11.2와 Binutils 2.37 스테이지 설치 디렉토리의 내용을 병합)
# (이후 sudo chown -R root:root 및 tar czf 실행)
tar czf custom_gcc_11.2.0_binutils_2.37.tar.gz merged_install_directory
이전 GCC 버전을 제거하고 새 11.2 버전으로 교체합니다.
./uninstaller_tool custom_gcc_binutils_10.3.0.tar.gz
tar xvf custom_gcc_11.2.0_binutils_2.37.tar.gz -C /
gcc -v
# 예상 출력: gcc version 11.2.0 (GCC)
C++20의 using enum과 같은 최신 기능을 사용하여 GCC 11.2가 제대로 작동하는지 확인합니다.
/* 컴파일 명령: g++ -std=gnu++20 enum_test.cpp -o enum_test */
#include <iostream>
enum class Color {
Red,
Green,
Blue
};
struct MyPalette {
using enum Color; // C++20 using enum 선언
};
int main() {
MyPalette palette;
std::cout << "Red: " << static_cast<int>(palette.Red) << std::endl;
std::cout << "Green: " << static_cast<int>(MyPalette::Green) << std::endl;
return 0;
}
Allwinner R16 개발 보드에서 GCC 컴파일러 실행
Allwinner R16 칩은 32비트 쿼드코어 ARMv7-A 아키텍처를 사용하며, 삼성 Exynos4412와 유사합니다. 이론적으로 Exynos4412 보드에서 빌드된 실행 파일은 R16 보드에서도 직접 실행될 수 있습니다. 그러나 실제로는 GLIBC 및 GLIBCXX 버전 불일치로 인해 호환성 문제가 발생할 수 있습니다.
이는 두 대의 x86 CPU를 가진 컴퓨터가 각각 Windows XP와 Windows 7을 실행하는 것과 같습니다. Windows 7용으로 빌드된 프로그램은 Windows XP에서 작동하지 않을 수 있습니다. 해결책은 대상 보드의 시스템 라이브러리(GLIBC, GLIBCXX) 버전에 맞춰 컴파일러 및 프로그램을 다시 빌드하는 것입니다.
Allwinner R16 보드용으로 GCC를 다시 크로스 컴파일하려면, 해당 보드에 맞는 크로스 컴파일 툴체인(예: gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz)과 호환되는 GCC 소스 코드 버전(예: 4.9.4)을 사용해야 합니다. 이때 개발 PC의 GCC 버전도 낮아야 특정 헤더 파일 충돌을 피할 수 있습니다 (예: Fedora 25 LiveCD 사용).
R16 보드의 루트 파일 시스템이 작거나 특정 압축 형식(예: tar.xz)을 지원하지 않을 경우, 개발 PC에서 압축 해제 후 재압축하여 보드로 전송해야 할 수 있습니다.
R16 보드에서 컴파일러를 실행하기 위해 다음 환경 변수와 심볼릭 링크를 설정합니다.
# 환경 변수 설정 (예시, 실제 경로는 다를 수 있음)
export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/mnt/UDISK/packages/_install/bin:/mnt/UDISK/packages/_install/sbin:/mnt/UDISK/packages/_install/usr/bin:/mnt/UDISK/packages/_install/usr/local/bin
export C_INCLUDE_PATH=/mnt/UDISK/packages/_install/usr/include
export LIBRARY_PATH=/mnt/UDISK/packages/_install/usr/lib
# 필수 라이브러리 심볼릭 링크 (실제 라이브러리 버전에 따라 다를 수 있음)
ln -s /mnt/UDISK/packages/_install/lib/libc.so.6 /lib/libc.so.6
ln -s /mnt/UDISK/packages/_install/usr/lib/libc_nonshared.a /usr/lib/libc_nonshared.a
이제 R16 보드에서 C 프로그램을 컴파일하고 실행할 수 있습니다.
# R16 보드에서 hello.c 컴파일 및 실행
mkdir test_r16_compilation && cd test_r16_compilation
cat > hello_r16.c << EOF
#include <stdio.h>
int main(void) {
printf("Hello from R16 board!\\n");
return 0;
}
EOF
gcc hello_r16.c -o hello_r16
./hello_r16
# 예상 출력: Hello from R16 board!
ARMv7-A 아키텍처를 공유하더라도, GLIBC/GLIBCXX와 같은 시스템 라이브러리의 버전 차이로 인해 바이너리 호환성이 깨질 수 있음을 보여주는 중요한 사례입니다.
C 및 어셈블리 언어 함께 컴파일
C 코드에서 ARM 어셈블리 함수를 호출하는 예시를 통해 GCC가 C와 어셈블리를 혼합하여 컴파일하는 능력을 보여줍니다.
#include <stdio.h>
#define BUFFER_SIZE 20 // 버퍼 크기
// 어셈블리 함수 선언
// 이 함수는 'target_buffer'에 값을 채우고, 채워진 요소의 개수를 반환합니다.
int fill_buffer_asm(unsigned int* target_buffer, int max_elements);
int main(void) {
unsigned int data_array[BUFFER_SIZE] = {0};
int filled_count;
printf("C 코드에서 어셈블리 함수 호출 시작.\\n");
// 어셈블리 함수 호출
filled_count = fill_buffer_asm(data_array, BUFFER_SIZE);
// 반환된 개수가 버퍼 크기를 초과하지 않도록 보정
if (filled_count > BUFFER_SIZE) {
filled_count = BUFFER_SIZE;
} else if (filled_count < 0) {
filled_count = 0;
}
printf("어셈블리 함수에서 채워진 요소 개수: %d\\n", filled_count);
for (int i = 0; i < filled_count; ++i) {
printf("data_array[%d] = 0x%08x\\n", i, data_array[i]);
}
return 0;
}
.global fill_buffer_asm
.syntax unified
.thumb
fill_buffer_asm:
@ r0: target_buffer (unsigned int*) - 첫 번째 인자
@ r1: max_elements (int) - 두 번째 인자 (사용되지 않음, 단순 예시)
@ 버퍼에 첫 번째 값 채우기: 0xAABBCCDD
ldr r2, =0xAABBCCDD @ r2에 리터럴 값 로드
str r2, [r0, #0] @ r0가 가리키는 주소(buffer[0])에 r2 값 저장
@ 버퍼에 두 번째 값 채우기: 0x11223344
ldr r2, =0x11223344 @ r2에 다른 리터럴 값 로드
str r2, [r0, #4] @ r0+4 주소(buffer[1])에 r2 값 저장 (4바이트 오프셋)
@ 버퍼에 세 번째 값 채우기: 정적 데이터 영역의 값 (0xDEADC0DE)
ldr r2, =static_value @ r2에 'static_value'의 주소 로드
ldr r3, [r2] @ r3에 'static_value' 주소의 실제 값 로드
str r3, [r0, #8] @ r0+8 주소(buffer[2])에 r3 값 저장
mov r0, #3 @ 함수 반환 값 설정: 채워진 요소 개수 (3)
bx lr @ 호출자로 복귀
static_value:
.word 0xDEADC0DE @ 4바이트 정적 데이터 정의
gcc main.c assembly_func.s -o mixed_app
./mixed_app
# 예상 출력:
# C 코드에서 어셈블리 함수 호출 시작.
# 어셈블리 함수에서 채워진 요소 개수: 3
# data_array[0] = 0xAABBCCDD
# data_array[1] = 0x11223344
# data_array[2] = 0xDEADC0DE
Positronic Alpha I.MX6ULL 개발 보드에서 GCC 컴파일러 실행
Positronic Alpha I.MX6ULL 개발 보드(CPU: MCIMX6Y2CVM08AB)는 Buildroot로 구축된 Linux 최소 시스템을 사용하며, make 4.1은 설치되어 있지만 GCC/G++ 컴파일러는 없습니다.
이전에 Exynos4412 보드용으로 컴파일된 GCC를 I.MX6ULL 보드에서 직접 실행하려고 하면, GLIBC_2.33 또는 GLIBCXX_3.4.26 버전이 없다는 오류가 발생합니다. 이는 두 보드의 시스템 라이브러리(GLIBC/GLIBCXX) 버전이 다르기 때문입니다. 동일한 ARMv7-A CPU라도 시스템 라이브러리 버전이 다르면 바이너리 호환성이 깨질 수 있습니다.
해결책은 I.MX6ULL 보드의 시스템 환경에 맞춰 GCC를 다시 크로스 컴파일하는 것입니다. Positronic에서 제공하는 크로스 컴파일 툴체인(예: gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz)과 호환되는 GCC 소스 버전(4.9.4)을 사용합니다.
컴파일을 위해 낮은 버전의 GCC를 사용하는 개발 PC(예: Fedora 25 LiveCD)를 준비합니다.
# 크로스 컴파일 툴체인 압축 해제
cd /opt/
tar xf gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz
# GCC 4.9.4 크로스 컴파일 (configure 옵션에 --host 및 --target이 arm-linux-gnueabihf로 변경됨)
wget ftp://ftp.lip6.fr/pub/gcc/releases/gcc-4.9.4/gcc-4.9.4.tar.bz2
tar xf gcc-4.9.4.tar.bz2
cd gcc-4.9.4 && contrib/download_prerequisites && cd ..
mkdir build_gcc_for_imx6ull
cd build_gcc_for_imx6ull
# config_script.sh에 다음 configure 명령 사용
# ../gcc-4.9.4/configure --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf --prefix=/usr/local --enable-shared --disable-libssp --disable-libmudflap --enable-checking=release --enable-languages=c,c++,fortran --with-arch=armv7-a --with-fpu=neon --with-float=hard --with-mode=thumb --disable-werror
source config_script.sh config
make -j$(nproc)
make install DESTDIR=$(pwd)/_install_stage
# Binutils (예: 2.38 버전) 크로스 컴파일
wget http://ftp.gnu.org/pub/gnu/binutils/binutils-2.38.tar.xz
tar xf binutils-2.38.tar.xz
mkdir build_binutils_for_imx6ull
cd build_binutils_for_imx6ull
# config_script.sh에 다음 configure 명령 사용
# ../binutils-2.38/configure --host=arm-linux-gnueabihf --prefix=/usr/local --disable-werror
source config_script.sh config
make -j$(nproc)
make install DESTDIR=$(pwd)/_install_stage
# GCC와 Binutils 설치 파일 병합 및 압축
# (이전과 동일한 방식으로 병합하고 압축하여 imx6ull_gcc_4.9.4_binary.tar.gz 생성)
# 크로스 컴파일러의 헤더 파일(stdio.h 등)을 보드에 설치
# 주의: 이 과정은 매우 오래 걸릴 수 있습니다.
tar -xvf /opt/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz \
gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/include
mv gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/include /usr
rm -rf gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf # 사용 후 삭제
새로 컴파일된 GCC 패키지(예: imx6ull_gcc_4.9.4_binary.tar.gz)를 I.MX6ULL 보드에 설치하고, PATH 및 C_INCLUDE_PATH, CPLUS_INCLUDE_PATH 환경 변수를 적절하게 설정해야 컴파일러가 정상적으로 작동합니다.
# /etc/profile에 다음 환경 변수 추가
export C_INCLUDE_PATH=/usr/include
export CPLUS_INCLUDE_PATH=/usr/local/include/c++/4.9.4:/usr/include
Java 관련 정보
Java는 C 언어와 달리, Oracle 공식 웹사이트 (Java Archive Downloads - Java SE 8)에서 미리 컴파일된 JDK(Java Development Kit) 버전을 제공하므로 소스 코드에서 직접 컴파일할 필요가 없습니다.
ARMv7-A CPU(예: Exynos4412)용 Java JDK 8u202 버전 파일명은 jdk-8u202-linux-arm32-vfp-hflt.tar.gz입니다. 이 파일을 다운로드하여 보드의 /opt 디렉토리(또는 다른 적절한 위치)에 압축을 해제한 다음, /opt/jdk1.8.0_202/bin 경로를 PATH 환경 변수에 추가하면 java 및 javac 명령을 사용할 수 있습니다.
Java 언어의 특징은 한 번 컴파일하면 어디서든 실행할 수 있다는 것입니다. 즉, 개발 PC에서 컴파일된 Java 패키지는 어떤 플랫폼에서든 직접 실행될 수 있습니다.
- 개발 PC에서
Hello.java소스 파일을 작성하고javac로Hello.class로 컴파일한 후, 이 컴파일된Hello.class파일을 보드로 복사하여 직접 실행할 수 있습니다. - Tomcat 서버와 같은 Java 기반 소프트웨어는 소스 컴파일 없이 컴파일된 바이너리 버전(예:
apache-tomcat-8.5.71.tar.gz)을 다운로드하여 압축 해제 후 바로 사용할 수 있습니다.
Tomcat 서버 설치 예시
환경 변수 PATH 및 JAVA_HOME을 /etc/profile에 추가합니다.
export PATH=$PATH:/opt/jdk1.8.0_202/bin
export JAVA_HOME=/opt/jdk1.8.0_202
공식 웹사이트에서 다운로드한 Tomcat 바이너리 압축 파일을 /opt에 압축 해제합니다.
tar xf apache-tomcat-8.5.71.tar.gz -C /opt
Tomcat 종료 시 오류를 방지하기 위해 /opt/jdk1.8.0_202/jre/lib/security/java.security 파일의 securerandom.source 설정을 수정합니다.
# 변경 전
securerandom.source=file:/dev/random
# 변경 후
securerandom.source=file:/dev/./urandom
Tomcat 서버를 시작하고 웹 브라우저로 접속합니다.
/opt/apache-tomcat-8.5.71/bin/startup.sh
# 웹 브라우저에서 http://보드_IP_주소:8080/ 접속
/opt/apache-tomcat-8.5.71/bin/shutdown.sh # 종료
웹 애플리케이션 파일은 webapps 디렉토리에 배치하여 서비스할 수 있습니다.