시작하기 전에
이 튜토리얼에서는 대상 시스템에 리눅스를 어떻게 설치할지 보인다. 미리 만들어진 리눅스 배포판을 쓰는 게 아니라 처음부터 독자들 스스로 만들어내는 것이다. 당연히 대상에 따라 상세 과정은 다르지만 일반적인 주제는 똑같이 적용된다.
이 튜토리얼을 끝내고 나면(적절한 대상 시스템을 갖고 있다면) 셸 프롬프트가 깜빡거리는 제대로 동작하는 리눅스 시스템이다.
튜토리얼은 크로스 컴파일에 관한 이슈 토론으로 시작한다. 그러고 나서 리눅스 시스템의 컴포넌트가 무엇인지 그리고 이것들을 어떻게 한데 넣을 것인지 논의한다. 빌드와 설치 그리고 대상 시스템의 설정에 대해서도 다룬다.
여기서 논의할 구체적인 대상 시스템인 Technologic Systems의 TS-7800은 기본적으로 스스로 부트할 수 있는 기능을 갖고 있다. 다른 시스템은 다른 메커니즘을 갖고 있을 것이다. 그리고 이 튜토리얼에서는 이외 모든 가능한 부트 로더에 대해 그다지 자세하게 다루지는 않을 것이다.
대상으로 삼을 임베디드 시스템에 관심을 갖고 있는 개발자들이든, 꼭 그렇지 않더라도 그저 리눅스 시스템이 어떻게 생겼는지 들여다보고 싶은 개발자들이든 이 튜토리얼에서 많은 걸 얻을 것이다.
사용할 호스트 환경은 우분투(Ubuntu)이지만 다른 시스템도 동작한다. 기본적으로 유닉스(UNIX®)나 리눅스 시스템 관리를 할 수 있다고 가정한다. 이 튜토리얼에서는 호스트 시스템에 루트(root) 접근을 갖고 있다고 가정한다.
이 튜토리얼에서는 여러분이 사용할 셸이 본(Bourne)셸 종류라고 가정한다. C셸을 사용한다면 아마도 프롬프트가 다르게 나타날 텐데 환경 변수 설정할 때 다른 명령어를 사용해야 할 필요가 있을 것이다.
크로스 컴파일링을 위해 필자는 2008년 5월에 발표된 crosstool-ng 버전 1.1.0을 사용했다. 배포 사이트(참고자료 참조)에서 다운로드할 수 있을 것이다. 설치와 설정에 대해서는 상세 내용을 참조하기 바란다.
타깃과 아키텍처
필자가 선택한 타깃은 Technologic Systems의 TS-7800(더 자세한 내용은 참고자료 참조)이다. 이 제품은 소형 임베디드 ARM 시스템으로 내장형 그리고 착탈 가능한 플래시 저장장치뿐 아니라 SATA 컨트롤러가 장착되어 있다. 이 튜토리얼에서는 미리 만들어진 바이너리에 의존하지 않고 부트하도록 시스템을 설정할 것이다.
필자는 ARM 아키텍처를 선택했는데 주어진 바이너리가 호스트인지 타깃인지 체크하기 좀 더 쉽도록, 그리고 언제 일어날지 모르는 호스트 오염 상황을 관찰하기 쉽게 할 수 있기 때문이었다. 또한 대략 총 5W의 전력을 쓰면서 완벽하리만치 고요하게 동작하는 머신이라는 것도 훌륭하다.
크로스 컴파일
크로스 컴파일에서는 다른 머신에서 동작하는 코드를 개발하기 위해 시스템에서 어떤 다른 컴파일러를 사용한다. 크로스 컴파일은 평범한 유닉스 사용자들에겐 상대적인 드문 경우다. 기본적으로 평범한 시스템에서는 시스템에서 설치된 컴파일러를 기본적으로 사용하기 때문이다. 그렇지만 크로스 컴파일은 임베디드 시스템을 타킷으로 할 때는 꽤 일반적이다(상대적으로). 심지어 호스트와 타킷이 같은 아키텍처일 때라도 컴파일러 간에는 분리가 필수적이다. 라이브러리도 다른 버전일 수도 있고, 다른 컴파일러 옵션으로 만들어진 라이브러리일 수도 있기 때문에 호스트 컴파일러를 써서 컴파일된 것들이 구동에 실패할 수도 있거나 타깃 시스템에서 예측과는 다르게 동작할 수도 있다.
이론적으로 크로스 컴파일러를 스스로 빌드하는 것도 가능하지만 상당히 비현실적이다. 필요한 일련의 부트스트랩 단계들은 어려울 수 있으며 엄청난 시간이 드는 작업이다. 그리고 상당히 최소화한 컴파일러를 만드는 작업이 종종 요구되는데 이는 부분적으로는 라이브러리를 설정하고 빌드하는 데 사용되고, 그러고 나서 또 컴파일러를 재빌드하는 데 사용되는 것들을 또 재빌드하고... 이런 식이다. 다양한 아키텍처의 조합이 가능한 수많은 상용 크로스 컴파일러 소스를 이용할 수 있을 뿐 아니라 자유롭게 쓸 수 있는 크로스 컴파일러 툴킷도 몇 가지 나와 있다.
Dan Kegel의 crosstool(자세한 내용은 참고자료 참조)에는 여러 시스템의 툴체인을 자동으로 빌드할 수 있도록 하는 다양한 전문 기술과 몇 가지 특수화된 패치를 모아두었다. crosstool은 그간 업데이트가 되지 않고 있다가 이 작업 동안 crosstool-ng 프로젝트가 새롭게 이뤄지고 있다. 이 튜토리얼에서는 2008년 5월에 출시된 crosstool-ng 버전 1.1.0을 사용했다. 배포 사이트(참고자료 참조)에서 다운로드하기 바란다.
crosstool-ng는 configure
스크립트를 갖고 있다. 설정하려면 --prefix
에 위치를 명시하고 스크립트를 실행하면 끝난다. 예를 들면 이렇다.
$ ./configure --prefix=$HOME/7800/ctng
설정이 끝나면 make
를 사용해 빌드하고 make install
한다. 빌드 과정에서 crosstool-ng 빌드 스크립트에 명기된 디렉터리 하에 7800 작업 디렉터리 내에 ctng
디렉터리를 생성한다. 경로에 ctng/bin
하위 디렉터리를 추가한다.
$ PATH=$PATH:$HOME/7800/ctng/bin
crosstool-ng는 리눅스 커널이 사용하는 것과 비슷하게 .config
파일을 사용한다. crosstool-ng를 사용하기 위해 타깃과 일치하는 설정 파일을 생성할 필요가 있다. crosstool-ng 빌드를 위해 작업 디렉터리를 만들자.
$ mkdir toolchain-build
$ cd toolchain-build
이제 기본 설정을 복사한다. 하나씩 crosstool-ng를 설정하는 것도 가능하지만 여기서는 우연히도 예제 설정 파일 하나가 타깃에 딱 들어맞았다고 하자.
$ cp ../ctng/lib/ct-ng-1.1.0/samples/arm-unknown-linux-uclibc/* .
마지막으로 crosstool.config
파일의 이름을 바꾼다.
$ mv crosstool.config .config
이 작업은 TS-7800에서 사용되는 모델인 armv5te 프로세서를 타깃으로 하는 설정 파일을 복사하는 작업이다. uClibc로 빌드하는데 uClibc는 임베디드 시스템에 맞춰진 libc의 변종이다. 그런데 설정 파일이 수정해줘야 할 게 하나 있다.
crosstool-ng 빌드에 대한 기본 타깃 디렉터리는 $HOME/x-tools/$TARGET
이다. 예를 들어 이 빌드에서라면 x-tools/arm-unknown-linux-uclibc
가 된다는 뜻이다. 타깃이 많다면 유용하긴 하지만 딱 하나 뿐이라면 그렇게 유용하지는 않다. .config
를 열어 CT_PREFIX_DIR
을 ${HOME}/7800/toolchain
으로 수정하자.
툴체인을 빌드하기 위해 build
인수를 주고 ct-ng
스크립트를 구동한다. 성능을 개선하려면, 특히 다중 코어 시스템이라면 build.#
처럼 작업 개수를 명시하고 싶을 수도 있다. 예를 들어 다음 명령은 네 개의 작업으로 빌드한다.
$ ct-ng build.4
호스트 시스템의 사정에 따라 빌드 작업은 꽤 오래 걸릴지도 모른다. 완료되면 툴체인은 $HOME/7800/toolchain
에 설치된다. 디렉터리와 디렉터리에 들어가 있는 파일들은 읽기 전용으로 되어 있을 것이다. 지우거나 옮기고 싶다면 chmod u+w
를 쓰기 바란다. ct-ng
스크립트에는 이외에도 help
같은 다른 인수를 줄 수 있다. ct-ng
는 표준 make
유틸리티용 스크립트이며, 결과적으로 --help
를 주면 나오는 출력은 그저 표준 make
의 도움말이다. crosstool-ng의 도움말을 보려면 ct-ng help
를 사용하기 바란다.
전에 이런 트릭을 본 적이 없다면 간단 명료하게 설명하겠다. 현대 유닉스 시스템은 실행 파일을 해석할 때 첫 번째 줄이 #!
로 시작하면 스크립트라고 인식한다. 구체적으로 말해 줄의 나머지에 이름 붙은 프로그램의 스크립트라고 본다. 예를 들어 많은 셸 스크립트들은 #!/bin/sh
로 시작한다. 파일의 이름은 프로그램에 전달된다. 동작할 스크립트의 첫 번째 인수로 취급되기에 이 정도면 충분하다. 반면 make
는 자동으로 뭘 할 수가 없기 때문에 여러분이 내용을 -f
플래그를 사용해 파일로 건네줘 동작할 수 있게끔 해줘야 한다. ct-ng
의 첫 번째 줄을 보면 #!/usr/bin/make -rf
다. -r
플래그는 make의 내장된 기본 규칙을 수행하지 않으며, -r
플래그는 make
에게 이후에 나올 이름이 Makefile
이라고 붙은 파일이 아닌 대신 사용할 파일 이름임을 알려준다. 결과적으로 셸 문법 대신에 make
문법을 사용하는 실행 스크립트다.
처음 시작하는 사람들이라면 컴파일러를 담고 있는 디렉터리에 경로를 추가하자.
$ PATH=~/7800/toolchain/bin:$PATH
경로에 넣었다면 이제 프로그램을 컴파일할 수 있다.
$ arm-unknown-linux-uclibc-gcc -o hello hello.c $ file hello
hello: ELF 32-bit LSB executable, ARM, version 1 (SYSV), for
GNU/Linux 2.4.17, dynamically linked (uses shared libs), not stripped
바이너리를 링크하기 위해 툴체인에 의해 사용되는 라이브러리는 toolchain
디렉터리 밑의 arm-unknown-linux-uclibc/sys-root
에 저장되어 있다. 이 디렉터리는 궁극적으로 루트 파일 시스템의 토대를 형성하고 있으며 커널이 만들어지고 나면 파일 시스템에 대한 내용을 다룰 때 다룰 주제다.
벤더가 제공하는 커널 배포 트리는 이미 크로스 컴파일용으로 설정되어 있다. 아주 간단한 경우(이번 경우), 리눅스 커널 크로스 컴파일을 위해 여러분이 해야 할 거라곤 최상위 레벨 Makefile에서 CROSS_COMPILE
변수를 설정하는 것뿐이다. 이 접두어는 빌드 동안 사용되는 다양한 프로그램(gcc, as, ld)의 이름에 적용된다. 예를 들어 CROSS_COMPILE
을 arm-
으로 설정하면 컴파일 시 경로에서 arm-gcc
라는 프로그램을 찾으려고 시도할 것이다. 그렇다면 우리의 경우라면 arm-unknown-linux-uclibc
이다. 경로 설정에 의존하고 싶지 않다면 다음 예처럼 전체 경로를 명시할 수도 있다.
CROSS_COMPILE ?= $(HOME)/7800/toolchain/bin/arm-unknown-linux-uclibc-
커널 빌드
Technologics의 리눅스 소스와 TS-7800 설정 파일을 다운로드하여 적당한 위치에서 압축을 푼다.
커널 설정에 대해 완벽한 논의를 한다는 건 이 튜토리얼의 범위를 넘어서는 일이다. 이번 경우에서는 ts7800_defconfig
타깃이 필자에겐 기본적으로 쓸 만한 설정이었는데, CONFIG_DMA_ENGINE
설정하는 걸로 끝날 정도였다.
make menuconfig
를 써서 커널을 조정하는 게 보통은 최상인데 make menuconfig에서는 커널 설정 시 어느 정도의 그래픽 인터페이스(역자 주: curses 기반 텍스트 기반 그래픽 인터페이스)를 제공한다. 이 인터페이스를 이용하면 커서 이동 시 화살표 키를 써서 네비게이션하며 화면 하단에서 옵션을 선택하는 데 탭 키를, 옵션을 선택 시 스페이스 키나 엔터 키를 사용한다. 예를 들어 변경하지 않고 종료하려면 화면 하단의 가 반전될 때까지 탭 키를 누른 후 엔터 키를 누른다. 다시 make menuconfig
를 구동하여 편집기를 다시 연다.
TS-7800은 일반적으로 조용하게 부트하는데 기본 커널 설정에서 화면을 고요하게 하도록 널(null) 콘솔 장치를 명시해 놓았기 때문이다. 이를 바꾸려면 "부트 옵션(Boot options)"을 화살표를 써서 살펴보고 엔터 키를 누른다. 세 번째 줄에 기본 커널 옵션이 보이는데 여기서 램디스크(ramdisk), 시작 스크립트, 콘솔을 선택한다. 화살표키를 이용하여 이 줄로 내려가서 엔터 키를 누르고 console none
에서 console ttyS0, 105200으로 변경한다. 그리고 탭 키를 눌러 커서를 패널 하단의 로 이동하고 엔터 키를 누른다. 이제 탭 키를 눌러 를 선택한 후 엔터 키를 누른 다음 주 메뉴로 돌아온다.
가능한 한 빠르게 부팅한다는 목표를 달성하기 위해 콘솔 장치는 별로 유용하지 못하다. 사실 데이터 전송률이 높은 시스템에서도 커널 메시지 전송에는 시스템이 부트하는 시간에 상당 부분을 차지할 수 있다. 그렇긴 해도 디버깅 용도나 이것저것 해보는 차원에서는 콘솔을 쓰고 싶을 것이다.
"Device drivers"로 내려가서 엔터 키를 누른다. 여기 표시되는 내용은 표시되는 것보다 길기 때문에 "DMA Engines"에 대한 옵션까지 가려면 저 끝까지 스크롤해 내려가야 한다. 화살표 키를 이용해 간 다음 엔터 키를 누른다. 예/아니오 두 가지 선택을 할 수 있는 대괄호([])가 이 페이지의 상단에 있다. 필자가 시작했을 때에는 기본값으로 "Support for DMA engines"는 켜져 있지 않았다. 화살표 키를 이용하여 간 다음 스페이스 키를 눌러 상태를 변경했다. 이제 탭 키와 엔터 키를 써서 각 화면에서 프로그램 상위 단계로 빠져나오기 위해 를 선택한 다음, 프로그램 종료를 위해 한 번 더 를 한다. 새로운 커널 설정을 저장하냐고 물어온다면 로 탭을 하고 엔터 키를 누른다.
make
를 타이핑한다. 그렇다. 정말 간단하다. 이렇게 하면 커널을 빌드하고 마찬가지로 모듈도 빌드한다. 다시 말하지만 멀티 코어 CPU를 이용하면 사용자들은 여러 개의 작업(job)을 돌릴 수 있다. make -j 5
를 해보라. 이 프로젝트의 목적 상 필자는 커널 모듈은 무시하고 넘어갈 것이고 필요한 기능은 몽땅 다 커널에 컴파일해 넣는 걸로 할 것이다. 필요한 드라이버들을 일찌감치 커널로 넣어버리는 부트스트랩 램디스크 기법은 좀 과도한 작업처럼 보일 것이고, 루트 파일 시스템을 빌드하는 것도 충분히 복잡한 일이다. 물론 이러한 것들은 커널이 어떻게 부팅하는지에 대한 의문을 불러올 것이며 다음 장의 주제이기도 하다.