1. 서론
리눅스를 빌드하는 명령어 과정을 중심으로 Makefile, Kconfig, cross-compile 원리에 대해 자세히 알아보고자 한다. Makefile과 관련된 명령어는 Makefile을 참고하며 된다.
1.1. 환경
아키택처 : x64
환경 : WSL
리눅스 버전 : v6.15
2. make defconfig
`make defconfig`는 make default configuration의 약자로 현재 아키택처의 기본 설정값을 통해 `.config` 파일을 생성하는 명령어이다. `.config`는 커널을 어떤 기능과 드라이버 등으로 구성할지를 결정하는 설정 파일이다. 리눅스 최초 빌드시 `make defconfig`없이 `make`를 한다면 아래와 같은 오류가 나게 된다.
2.1. 아키택처(필요시 공부)
만약 현재 x86 아키택처라면 `arch/x86/configs/x86_64_defconfig`를 참조하여 `.config`를 구성할 것이고, arm64 아키택처라면
`arch/arm64/configs/defconfig`를 참조하여 `.config`를 구성한다. 이는 아키택처별 기본 값일 뿐 특정 보드에 대한 설정할 수 있다. 보드 리스트는 `ls arch/<architecture>/configs/*_defconfig`로 확인할 수 있으며, 특정 보드로 설정 파일을 구성하고 싶으면 `make <config_file>`을 하면 된다.
2.1.1. 아키택처 구분
빌드하고자 하는 아키택처(현재 아키택처)를 어떻게 구분할까? 이는 `make defconfig`와 `make`시 굉장히 주요한 요점이다. Makefile에서는 빌드하고자 하는 아키택처를 `SRCARCH`변수로 관리하게 되며 생성되는 과정을 축약하자면 아래와 같다. (크로스 컴파일과 관련된 부분은 생략하겠다.)
include $(srctree)/scripts/subarch.include
ARCH ?= $(SUBARCH)
SRCARCH := $(ARCH)
ifeq ($(ARCH),i386)
SRCARCH := x86
endif
ifeq ($(ARCH),x86_64)
SRCARCH := x86
endif
`srctree`는 리눅스 소스 코드의 절대 경로를 나타내는 변수이며 `scripts/subarch.include`에서 `SUBARCH` 변수를 가져온다(정확히는 include 하였기 때문에 가져오는 것은 아님). `scripts/subarch.include` 코드는 아래와 같다.
SUBARCH := $(shell uname -m | sed -e s/i.86/x86/ -e s/x86_64/x86/ \
-e s/sun4u/sparc64/ \
-e /^arm64$$/!s/arm.*/arm/ -e s/sa110/arm/ \
-e s/s390x/s390/ \
-e s/ppc.*/powerpc/ -e s/mips.*/mips/ \
-e s/sh[234].*/sh/ -e s/aarch64.*/arm64/ \
-e s/riscv.*/riscv/ -e s/loongarch.*/loongarch/)
`uname -m`으로 쉘에서 현재 아키택처를 알아낸 후 표현식에 맞춰 아키택처를 카테고리화한다. 예를 들어 i.86은 x86으로, x86_64 역시 x86으로 바뀌어 값을 반환한다.
`ARCH ?= $(SUBARCH)`로 `ARCH`에 대입해 주어 아키택처명을 지정해 준다.(중복되어 보이는 과정은 크로스 컴파일과 관련되어 있기 때문이다.). `SRCARCH := $(ARCH)`로 `SRCARCH`에 아키택처명을 담고, 이후`ifeq`를 통해 아키택처별로 디렉터리를 재지정해준다.
2.1.2. 아키택처에 따른 defconfig 과정
빌드하고자 하는 아키택처가 어떻게 defualt defconfig를 가져오고 `.config`를 만드는 과정에 대해 알아보도록 하자. 아래 코드는 최상위 Makefile의 일부이다.
include $(srctree)/arch/$(SRCARCH)/Makefile
export KBUILD_DEFCONFIG KBUILD_KCONFIG CC_VERSION_TEXT RUSTC_VERSION_TEXT
위 코드를 통해 `KBUILD_DEFCONFIG`로 아키택처별로 default config 파일명을 알 수 있는데, x86 아키택처와 관련된 코드인 `arch/x86/Makefile`의 일부는 아래와 같다.
# select defconfig based on actual architecture
ifeq ($(ARCH),x86)
ifeq ($(shell uname -m | sed -e 's/i.86/i386/'),i386)
KBUILD_DEFCONFIG := i386_defconfig
else
KBUILD_DEFCONFIG := x86_64_defconfig
endif
else
KBUILD_DEFCONFIG := $(ARCH)_defconfig
endif
`KBUILD_DEFCONFIG`을 가져온 후, 최상위 Makefile에서는 config를 위한 코드는 아래와 같다.
config: outputmakefile scripts_basic FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@
%config: outputmakefile scripts_basic FORCE
$(Q)$(MAKE) $(build)=scripts/kconfig $@
config, 혹은 defconfig와 같이 defconf로 끝나는 타겟에 관한 명령어를 직접 실행하지 않고, `scripts/kconfig/Makefile`에서 실행한다. 참고로 두 코드 모두 같은 Makefile을 실행하고 있지만, Makefile 문법에 한계가 있어 나눠져있다고 한다. `scripts/kconfig/Makefile`의 일부는 아래와 같다.
defconfig: $(obj)/conf
@$(kecho) "*** Default configuration is based on '$(KBUILD_DEFCONFIG)'"
$(Q)$< $(silent) --defconfig=arch/$(SRCARCH)/configs/$(KBUILD_DEFCONFIG) $(Kconfig)
`make defconfig`를 하게 되면 아래와 사진과 같이 `scripts/kconfig/conf`가 만들어지게 되는데, 문구를 출력한 후 `conf` 실행파일에 d `--defconf` 옵션으로 default config파일의 경로가 들어가 `.config`파일이 만들어지게 된다. (c언어 해석 생략)
2.2. Kconfig
Kconfig는 커널의 어떤 기능을 켜고 끌지 혹은 모듈화 할지 정하며, Makefile은 이를 참조하여 해당 기능을 빌드할 것인지 말 것인지 혹은 모듈화 할 것인지 정한다. 이때 참고하는 파일은 `.config`이지만 `make defconfig`로 만든 `.config`는 최소한의 기본 설정 값으로 나머지 생략된 옵션은 Kconfig의 default값을 사용한다. 또한 3. 에서 다룰 `make menuconfig`로 사용자가 원하는 대로 `.config`를 구성할 수 있다.
`.config`를 출력하면 아래와 같은 문자열이 출력된다. 아래는 이해를 돕기 위한 예시이며 일부를 추출한 것이다.
CONFIG_RANDOMIZE_BASE=y
CONFIG_EFIVAR_FS=m
# CONFIG_TEST_DHRY is not set
공통적으로 `CONFIG_*`로 시작하며, y, m, (주석으로 된) is not set이 있다. `CONFIG_*에 들어가는 옵션은 Kconfig에 있으며, y는 빌드하겠다, is not set은 빌드하지 않겠다, m은 모듈(.ko)로 빌드하겠다는 뜻이다.
예시로 `CONFIG_RANDOMIZE_BASE=y`는 `arch/x86/Kconfig`에 있으며 내용은 아래와 같다.
config RANDOMIZE_BASE
bool "Randomize the address of the kernel image (KASLR)"
depends on RELOCATABLE
default y
help
In support of Kernel Address Space Layout Randomization (KASLR),
(중략)
If unsure, say Y.
`config RANDOMIZE_BASE`에 있는 `RANDOMIZE_BASE`는 CONF_*에 들어가게 되며, default값이 y로 이루어져 있기 때문에 `=y`로 설정된 것이다. `depeds on RELOCATABLE`은 RELOCATABLE이 반드시 y가 되어있어야 활성화시킬 수 있다는 뜻이다. 이외 설정은 3. 에서 다루도록 하겠다.
3. make menuconfig
`make menconfig`는 kconfig를 default값 외에도 사용자가 원하는 대로 사용할 수 있게 해주는 것이다. 명령어를 실행하면 창이 뜨지만 반드시 19라인, 90컬럼을 보장해야 한다.
아래 사진은 kaslr을 비활성화하는 menuconfig 화면이다.
-> Processor type and features
-> Build a relocatable kernel
-> Randomize the address of the kernel image (KASLR) (RANDOMIZE_BASE [=n])
최상단에 `-> processor type and features`와 같이 경로를 확인할 수 있다. 기호별 의미는 아래와 같다.
[]는 y/n으로 설정 가능하며, <>는 y/n/m으로 설정 가능하다.
[]/<>에 들어가는 *는 활성화, 공백은 비활성화, m은 모듈화이다.
-*-은 강제로 설정된다는 뜻이며 ---는 depends on 조건을 만족하지 않았기 때문에 비활성화된 것이다.
조작은 키보드로만 할 수 있으며 조작법과 자주 사용하는 단축키는 아래와 같다.
- 좌/우로 select, exit, save, load를 지정할 수 있으며 엔터로 실행할 수 있다.
- 위/아래로 설정등을 지정할 수 있으며, 스페이스바를 누르면 y, n, m과 같은 설정 여부를 정할 수 있다.
- `/`로 설정을 정규표현식으로 검색할 수 있다.
- `?`로 help와 같은 기능을 낼 수 있다.
- `esc`혹은 Exit로 뒤로 가거나 menuconfig를 나갈 수 있다.
그 결과 `.config`파일을 출력해 보았을 때 우측과 같이 `RANDOMIZE_BASE`가 비활성화가 된 것을 볼 수 있다.
반대로 `.config`파일을 수정한 결과 역시 `make menuconfig`를 통해 볼 수 있지만, 만약 `CONFIG_RANDOMIZE_BASE=m`과 같이 틀리게 수정하면 `make`명령어 실행 시 아래와 같이 다시 물어본다.
4. make
make는 Makefile을 참조하여 리눅스 커널을 빌드하게 한다. make는 linux 디렉터리에서 진행해 아하며 해당 Makefile을 시작으로 하위 디렉터리 Makefile을 참조하며 빌드하게 된다.
참고로 `find . -type f -name 'Makefile' | wc -l`를 사용하면 Makefile의 개수를 확인할 수 있으며 v6.15 기준으로 3075개가 있지만 상위 Makefile에서 하위 Makefile을 사용하지 않을 수 있기 때문에 실제로는 더 적은 수의 Makefile을 참조하게 된다.
4.1. 병렬 빌드
`make -j<core>`명령어로 리눅스 커널을 여러 코어에 걸쳐 빠르게 빌드할 수 있다. 인터넷 검색 시 흔히 나오는 명령어는 `make -j$(nproc)`이다. 여기서 `$(nproc)`는 전체 cpu코어를 의미하기 때문에 모든 코어를 사용하여 make를 하라는 명령어이다. 하지만 전체 코어를 사용하면 빌드 외 다른 업무를 수행하기 어려울 뿐만 아니라 코어가 2배 많이 사용한다고 정확히 2배만큼 빠른 것은 아니기 때문에 전체 코어의 60~70%만 사용하여 빌드하길 추천한다.
4.1.1. 병렬 빌드 화면 분석
왼쪽은 `make`로 빌드하였고, 오른쪽은 `make -j8`로 병렬 빌드하였다.
오른쪽 사진은 비교적 동일 디렉터리의 파일단위로 빌드되지만, 오른쪽은 무작위 해 보인다. 이는 병렬 빌드를 하게 되면 여러 코어가 각자 다른 파일을 담당하고 각자 완료되는 대로 결과를 출력하기 때문이다.
하지만 이로 인해 커널을 수정한 코드가 잘못되었을 때 아래와 같이 오류가 나오게 된다. 커널 빌드시 사용하는 `scripts/mod/modpost.h`를 임의 삭제 후 빌드해보았다.
여러 개의 코어에서 서로 다른 파일을 빌드하는데 `modpost.h`를 사용해아한다. 하지만 파일이 없으니 동시 다발적으로 똑같은 원인의 오류를 출력한다. 오류가 많을 경우 너무 많은 출력으로 인해 분석하기가 어렵다. 이때에는 병렬 빌드로 오류가 날 때까지 계속 빌드하고, 오류가 났을 때 `make`를 다시 실행하여 깔끔해진 출력으로 확인하는 것이 경험상 좋았다.
4.2. 설정파일
`make`를 실행하면 `.config`파일을 참조하여 여러 파일들을 만드는데 대표적으로 `include/config/auto.conf`와 `include/generated/autoconf.h`가 있다.
4.2.1. auto.conf
Makefile을 보면 `obj-$(CONFIG_NVMEM) += tee/`와 같이 조건이 충족될 때에만 해당 디렉터리 혹은 파일을 빌드한다. 이때 `include/config/auto.conf`를 참조하여 값을 넣어준다. 아래 `CONFIG_RISV`처럼 없을 수 있는데, 종속관계 등 빌드에 아무런 영향을 끼치지 않는 파일이라고 한다.
4.2.2 autoconf.h
`autoconf.h`는 빌드시 C언어 소스코드 전처리문과 관련된 파일이다. `.config`와 관련된 `define`이 매우 많이 정의되어 있다. 오른쪽 사진의 경우 매크로를 실제로 사용하는지 헤더가드를 위한 것인지는 불분명 하지만 대략적으로 c언어에서 사용함을 알 수 있다.
4.3. 아키택처
make 할 때 어떤 파일들을 빌드할까? 물론 Makefile을 보면 알 테지만 `/`에 있는 소스코드와 빌드하고자 하는 아키택처 안에 있는 디렉터리 내의 파일들을 빌드한다. 만약 내가 x86아키택처를 사용하면 `arch/x86/Makefile`로가 빌드하며, `arch/arm64/Makefile`등의 Makefile 등은 무시한다.
5. make clean
`make clean`은 `make` 명령어를 통해 생긴 결과물인 `.o`, `.ko`등 파일을 지우는 명령어이다. 만약 커널 소스코드를 수정하다 의존성 문제로 인해 모든 결과물을 지우는 게 나은 경우가 있다.
5.1. make mrproper
`make clean`을 하더라도 `.config`, `include/config/`, `include/generated/`와 같은 설정과 관련된 파일들은 지워지지 않는다. 만약 리눅스 소스코드를 완전 새것처럼 초기화하고 싶은 경우에 `make mrproper`를 하여 완전히 정리한다.