8 분 소요

1.3: Kernel build system

리눅스 커널 구조를 살펴본 후 어떻게 빌드하고 실행하는 지를 살펴보는 것은 시간을 보낼 가치가 있습니다. 리눅스는 또한 make 유틸리티를 사용하여 커널을 빌드합니다, 비록 리눅스 Makefile은 좀 더 복잡지만. Makefile을 살펴보기 전에 “kbuild”라고 불리는 리눅스 빌드 시스템에 대한 중요한 개념을 배워 봅시다.

A few essential build concepts

  • 빌드 과정은 kbuild 변수들을 사용하여 커스터마이징(customizing)하실 수 있습니다. 이러한 변수들은 Kconfig 파일안에 정의되 있습니다. 여기서 우리만의 변수와 기본 변수들을 정의할 수 있습니다. 변수들은 문자열, 논리값(boolean)과 정수(integer)를 포함한 다른 타입들을 가질 수 있습니다. Kconfig 파일에서는 변수들 사이에 의존성 또한 설정하실 수 있습니다.(예를 들면 만약 x변수가 선택되면 y변수는 내포적으로 선택되는 것을 말할 수 있습니다). 한 예시로 arm64 Kconfig file을 살펴보실 수 있습니다. 이 파일은 arm64아키텍쳐에 특정된 모든 변수들을 정의합니다. Kconfig 기능은 표준 make 의 일부분은 아니고 리눅스 Makefile에서 수행됩니다. Kconfig에서 정의된 변수들은 커널 소스코드와 포함된 Makefile에 또한 노출됩니다. 변수 값들은 커널 설정 단계에서 설정될 수 있습니다(예를 들어 만약 make menuconfig 를 입력한다면 커널 변수들의 값을 커스터마이징할 수 있고 값을 .config 에 변수를 저장할 수 있게 해줍니다. make help 커맨드를 사용하면 커널을 설정할 수 있는 모든 가능한 옵션들을 보실 수 있습니다)

  • 리눅스는 재귀적인 빌드를 사용합니다. 이것은 리눅스 커널의 각각의 서브 폴더가 자기 자신의 Makefile, Kconfig를 정의할 수 있다는 것을 의미합니다. 대부분의 내장된 Makefile들은 매우 간단하고 단순히 어떤 오브젝트 파일이 컴파일 되어야 하는지 정의합니다. 보통 이러한 정의는 아래와 같은 포맷을 가집니다.

    obj-$(SOME_CONFIG_VARIABLE) += some_file.o
    

    이러한 정의는 some_file.c이 컴파일될 것이고 만약 SOME_CONFIG_VARIABLE이 설정되어야만 커널에 링크될 것임을 의미합니다. 만약 무조건적으로 파일을 컴파일하고 링크하고 싶다면 아래와 같이 이전 정의를 바꿔야합니다.

    obj-y += some_file.o
    

    내장된 Makefile의 예시는 여기서 찾으실 수 있습니다.

  • 다음으로 나아가기 전에 여러분들이 기본 make 규칙의 구조를 이해하고 make 용어에 익숙해지셔야 합니다. 기본 규칙 구조는 아래 다이어그램과 같습니다

    targets : prerequisites
            recipe
            …
    
    • targets는 띄어쓰기로 분리된 파일 이름들입니다. 타겟들은 해당 규칙이 실행된 후 생성됩니다. 보통 규칙 당 단 한 개의 타겟만 있습니다.
    • prerequisitesmake가 타겟을 업데이트할 필요가 있는지 보기 위해 추적하는 파일들입니다.
    • receipe는 배쉬 스크립트입니다. Make는 어떤 prerequisites가 수정될 때 receipe를 호출합니다. receipes은 타겟을 생성하는 역할을 합니다.
    • 타겟과 prerequisites 와일드카드 (%)를 포함할 수 있습니다. 와일드카드가 사용될 때는 receipe는 각각 매치된 prerequisites에 의해 실행됩니다. 이러한 경우 $<$@ 변수를 receipe안에 prerequisite와 타겟을 참조하기 위해 사용할 수 있습니다. 우리는 이미 RPi OS makefile 에 적용했습니다. make 규칙에 대한 추가적인 정보를 위해서는 공식 문서를 참참조해주세요.
  • make는 어떤 prerequisites이 변경되는지 감지하고 다시 빌드가 되어야하는 타겟만을 업데이트하는 것에 매우 좋습니다. 그러나 만약 receipe이 동적으로 업데이트 된다면 make은 이러한 변화를 감지하지 못합니다. 이러한 경우는 어떻게 발생하는 걸까요? 매우 쉽습니다. 좋은 예시로는 설정 변수를 수정하여 receipe에 추가적인 옵션을 추가하게 되는 경우입니다. 기본적으로 이러한 경우 prerequisites가 변하지 않고 오로지 receipe만 업데이트 되었기 때문에 make는 이전에 생성된 오브젝트 파일을 재 컴파일 하지 않습니다. 이러한 점을 극복하기 위해서 리눅스는 if_changed 함수를 도입했습니다. 어떻게 작동하는 지 살펴보기 위해 아래 예시를 생각해봅시다.

  • cmd_compile = gcc $(flags) -o $@ $<
      
    %.o: %.c FORCE
        $(call if_changed,compile)
    

    여기서는 compile을 매개변수로 if_changed 함수를 호출하여 각 .c파일을 위한 .o파일을 빌드합니다. if_changedcmd_compile 변수를 찾아봅니다(cmd_접두어를 첫 번째 매개변수에 추가하게 됩니다) 그리고 이 변수 또는 prerequisites가 이전 실행 이후 변경되었는 지 확인합니다. 만약 변경되었다면 cmd_compile 커맨드는 실행되고 오브젝트 파일은 재생성됩니다. 위의 샘플 규칙은 2개의 prerequisites: 소스파일 .cFORCE를 가집니다. FORCEmake 커맨드가 호출될 때 마다 해당 receipe가 호출되도록 강제하는 특별한 prerequisite입니다. 이 것 없이는 receipe는 .c파일이 변경되었을 경우에만 호출될 것입니다. FORCE 타겟에 대해서 여기서 더 읽어보실 수 있습니다.

Building the kernel

리눅스 빌드 시스템에 대한 중요한 컨셉을 배우는 지금 make 커맨드를 입력한 후 정확히 어떤 일이 일어나는 지 알아봅시다. 이러한 과정은 매우 복잡하고 대부분 넘어가게 될 많은 디테일을 포함합니다. 이번에 목표는 2개의 질문에 답을 할 수 있는 것입니다.

  1. 정확히 어떻게 소스파일들이 오브젝트 파일로 컴파일되는가?
  2. 어떻게 오브젝트 파일들이 os 이미지로 링크 되는가?

2번 째 질문을 우선 살펴봅시다.

  • make help 커맨드의 출력을 보시면 커널을 빌드하는 역할을 하는 기본 타겟은 vmlinux라고 불립니다.
  • vmlinux 타겟 정의는 여기서 찾으실 수 있고 아래와 유사합니다.
cmd_link-vmlinux =                                                 \
    $(CONFIG_SHELL) $< $(LD) $(LDFLAGS) $(LDFLAGS_vmlinux) ;    \
    $(if $(ARCH_POSTLINK), $(MAKE) -f $(ARCH_POSTLINK) $@, true)

vmlinux: scripts/link-vmlinux.sh vmlinux_prereq $(vmlinux-deps) FORCE
    +$(call if_changed,link-vmlinux)

이 타겟은 이미 익숙해진 if_changed 함수를 사용합니다. 어떤 prerequisite이 수정될 때마다 cmd_link-vmlinux 커맨드가 실행됩니다. 이러한 커맨드는 scripts/link-vmlinux.sh 스크립트를 실행합니다( cmd_link-vmlinux 커맨드안에 $< 자동 변수의 사용를 유의하세요). 또한 아키텍처 특정된 postlink script 를 실행하지만 이것에 대해 딱히 살펴보지 않겠습니다.

  • scripts/link-vmlinux.sh가 실행되면 모든 필요한 오브젝트 파일들이 이미 빌드가 되고 위치들이 3 변수:KBUILD_VMLINUX_INIT, KBUILD_VMLINUX_MAIN, KBUILD_VMLINUX_LIBS에 저장되었다고 가정합니다.
  • link-vmlinux.sh 스크립트는 우선 모든 가능한 오브젝트 파일에서 thin archive를 생성합니다. thin archive 는 오브젝트 파일의 집합 뿐만 아니라 그들과 결합된 심볼 테이블에 대한 참조를 가지는 특별한 오브젝트입니다. archive_builtin 함수안에 수행됩니다. thin archive를 생성하기 위해서 이 함수는 ar 유틸리티를 사용합니다. 생성된 thin archivebuilt-in.o 파일로 저장되고 링커가 이해할 수 있는 포맷을 가지기 때문에 어떤 다른 평범한 오브젝트 파일 처럼 사용될 수 있습니다.
  • 다음으로 modpost_link가 호출됩니다. 이 함수는 링커를 호출하고 vmlinux.o 오브젝트 파일을 생성합니다. Section mismatch analysis를 수행하기 위해 이 오브젝트 파일이 필요합니다. 이 분석은 modpost 프로그램에 의해 수행되고 여기 이 코드로 시작됩니다.
  • 다음으로 커널 심볼 테이블이 생성됩니다. 커널 심볼 테이블은 모든 함수와 전역 변수 그리고 vmlinux 바이너리 안에 그들의 위치에 대한 정보들을 포함합니다. 주요 작업은 kallsyms 함수 안에서 수행됩니다. 이 함수는 우선 vmlinux 바이너리에서 심볼을 추출하기 위해서 nm을 사용합니다. 그 후 리눅스 커널이 이해할 수 있는 특별한 포맷에 모든 심볼을 포함하는 특별한 어셈블러 파일을 생성하기 위해서 scripts/kallysms 유틸리티를 사용합니다. 다음으로 이 어셈블러 파일은 컴파일 되고 원래 바이너리와 링크됩니다. 마지막 링크 이후 몇 심볼의 주소가 변경될 수 있기 때문에 이 과정은 몇 번 반복됩니다. 커널 심볼 테이블에서의 정보는 실행시 ‘/proc/kallsyms’파일을 생성하기 위해 사용됩니다.
  • 마지막으로 vmlinux 바이너리는 준비가 되고 System.map이 빌드됩니다. System.map/proc/kallsyms처럼 같은 정보를 포함하지만 실행시 생성되는 /proc/kallsyms와 달리 정적인 파일입니다. System.map은 대부분 kernel oops 동안 심볼 이름에 주소를 해결하기 위해 사용됩니다. 같은 nm 유틸리티가 System.map을 빌드하기 위해 사용됩니다. 이것은 여기서 수행됩니다.

Build stage

  • 이제 한 발 뒤로 물러서서 어떻게 소스코드가 오브젝트 파일로 컴파일 되는 지 알아봅시다. vmlinux 타겟의 prerequisites 중 하나가 $(vmlinux-deps) 변수임을 기억하실 수 있을 겁니다. 이제 이 변수가 어떻게 빌드 되는 지 보여드리기 위해 메인 리눅스 Makefile에서 몇개의 연관있는 코드를 복사하겠습니다.

  • init-y        := init/
    drivers-y    := drivers/ sound/ firmware/
    net-y        := net/
    libs-y        := lib/
    core-y        := usr/
      
    core-y        += kernel/ certs/ mm/ fs/ ipc/ security/ crypto/ block/
      
    init-y        := $(patsubst %/, %/built-in.o, $(init-y))
    core-y        := $(patsubst %/, %/built-in.o, $(core-y))
    drivers-y    := $(patsubst %/, %/built-in.o, $(drivers-y))
    net-y        := $(patsubst %/, %/built-in.o, $(net-y))
      
    export KBUILD_VMLINUX_INIT := $(head-y) $(init-y)
    export KBUILD_VMLINUX_MAIN := $(core-y) $(libs-y2) $(drivers-y) $(net-y) $(virt-y)
    export KBUILD_VMLINUX_LIBS := $(libs-y1)
    export KBUILD_LDS          := arch/$(SRCARCH)/kernel/vmlinux.lds
      
    vmlinux-deps := $(KBUILD_LDS) $(KBUILD_VMLINUX_INIT) $(KBUILD_VMLINUX_MAIN) $(KBUILD_VMLINUX_LIBS)
    

    모든 변수들이 init-y, core-y와 같은 일으므로 시작합니다. 이것들은 빌드할 수 잇는 소스코드를 포함하는 모든 리눅스 커널의 하위 폴더를 포함합니다. 그 후 built-in.o가 모든 하위 폴더 이름에 붙여집니다. 예를 들어 drivers/drivers/built-in.o가 됩니다. vmlinux-deps는 모든 결과 값을 종합합니다. 이것은 어떻게 vmlinux가 최종적으로 모든 built-in.o 파일에 의존적으로 되는지 설명합니다.

  • 다음 질문은 어떻게 모든 built-in.o 오브젝트들이 생성되는 가 입니다. 다시 한 번 모든 연관 있는 코드를 복사하고 어떻게 동작하는 지 살펴봅시다.

  • $(sort $(vmlinux-deps)): $(vmlinux-dirs) ;
      
    vmlinux-dirs    := $(patsubst %/,%,$(filter %/, $(init-y) $(init-m) \
                 $(core-y) $(core-m) $(drivers-y) $(drivers-m) \
                 $(net-y) $(net-m) $(libs-y) $(libs-m) $(virt-y)))
      
    build := -f $(srctree)/scripts/Makefile.build obj               #Copied from `scripts/Kbuild.include`
      
    $(vmlinux-dirs): prepare scripts
        $(Q)$(MAKE) $(build)=$@
    

    첫 번째 줄은 vmlinux-depsvmlinux-dirs에 의존한다는 것을 말해줍니다. 다음으로 vmlinux-dirs가 끝에 /가 없는 모든 직접적인 루트 하위 폴더들을 포함하는 변수라는 것을 보실 수 있습니다. 그리고 여기서 가장 중요한 코드는 $(vmlinux-dirs) 타겟을 필드하는 receipe입니다. 모든 변수들을 대체 후 이 receipe는 아래와 비슷해 질 것입니다.(drivers 폴더를 예시로 사용했지만 이 규칙은 모든 루트 하위 폴더에서 실행 될 것입니다).

    make -f scripts/Makefile.build obj=drivers
    
  • 다음 논리적인 단계는 scripts/Makefile.build를 살펴보는 것입니다. 실행된 이후 첫 번째로 중요한 것은 Makefile또는 Kbuild 파일의 현재 디렉토리에서 정의된 모든 변수들이 포함된다는 것입니다. 현재 디렉토리는 obj 변수에 의해 참조되는 디렉토리를 의미합니다. 이러한 포함하는 것은 아래 3줄에서 수행됩니다.

  • kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
    kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
    include $(kbuild-file)
    

    내장된 makefile들은 대부분 obj-y 같은 변수를 초기화 하는 역할을 맡습니다. 빠르게 회상하면 obj-y 변수는 현재 디렉토리에 위치한 모든 소스 코드 파일의 리스트를 가지고 있어야 합니다. 내장된 makefile로 초기화 되는 다른 변수는 subdir-y입니다. 이 변수는 현재 디렉토리안에 소스코드가 빌드 되기 전에 방문되어야 하는 모든 하위 폴더의 리스트를 포함합니다. subdir-y는 재귀적인 하위 폴더로 하향하는 것을 구현하기 위해 사용된다.

  • make가 타겟을 특정 짓지 않은 상태에서 호출되면 첫 번째 타겟을 사용합니다. scripts/Makefile.build를 위한 첫 번째 타겟은 __build로 불리고 여기서 찾아보실 수 있습니다. 같이 살펴 봅시다.

  • __build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
         $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
         $(subdir-ym) $(always)
        @:
    

    보이는 바와 같이 __build 타겟은 receipe이 없지만 다른 타겟에 의존 적입니다. 우리는 오로지 built-in.o 파일을 생성하는 $(builtin-target) 파일과 내부 디렉토리 안으로 하향하는 $(subdir-ym)에 관심이 있습니다.

  • subdir-ym을 살펴봅시다. 이 변수는 여기서 초기화 되고 subdir-ysubdir-m 변수를 잇는 것일 뿐입니다. (subdir-m 변수는 subdir-y 변수와 비슷합니다, 하지만 subdir-m은 다른 커널 모듈안에 포함되어야하는 하위 폴더들을 정의합니다. 지금은 계속 집중하기 위해서 모듈 논의는 넘어가겠습니다)

  • subdir-ym 타겟은 여기에 정의되 있고 익숙해지셔야 합니다.

  • $(subdir-ym):
        $(Q)$(MAKE) $(build)=$@
    

    이 타겟은 내부 하위 폴더의 하나에서 scripts/Makefile.build의 실행을 시작 시킬 뿐입니다.

  • 이제 builtin-target 타겟을 살펴보겠습니다. 관련된 코드만 가져왔습니다.

  • cmd_make_builtin = rm -f $@; $(AR) rcSTP$(KBUILD_ARFLAGS)
    cmd_make_empty_builtin = rm -f $@; $(AR) rcSTP$(KBUILD_ARFLAGS)
      
    cmd_link_o_target = $(if $(strip $(obj-y)),\
                  $(cmd_make_builtin) $@ $(filter $(obj-y), $^) \
                  $(cmd_secanalysis),\
                  $(cmd_make_empty_builtin) $@)
      
    $(builtin-target): $(obj-y) FORCE
        $(call if_changed,link_o_target)
    

    이 타겟은 $(obj-y) 타겟에 의존하고 obj-y은 현재 폴더에서 빌드되어야 할 모든 오브젝트 파일의 목록입니다. 이 파일들이 준비가 된 후 cmd_link_o_target 커맨드가 실행됩니다. 만약 obj-y 변수가 비어 있는 경우 cmd_make_empty_builtin이 호출됩니다. 다른 경우에는 cmd_make_builtin 커맨드가 실행됩니다; built-in.o thin archive를 생성하기 위해 ar 도구를 사용하는 것은 익숙합니다.

  • 마지막으로 어떤 것을 컴파일해야 하는 지점을 가졋습니다. 아직 살펴보지 않은 마지막 의존이 $(obj-y)이고 obj-y는 단순한 오브젝트 파일의 목록이라는 것을 기억하실 겁니다. 각 .c파일에 맞는 모든 오브젝트 파일을 컴파일하는 타겟은 여기 정의되 있습니다. 이 타겟을 이해하는데 필요한 모든 코드를 살펴봅시다.

  • cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $<
      
    define rule_cc_o_c
        $(call echo-cmd,checksrc) $(cmd_checksrc)              \
        $(call cmd_and_fixdep,cc_o_c)                      \
        $(cmd_modversions_c)                          \
        $(call echo-cmd,objtool) $(cmd_objtool)                  \
        $(call echo-cmd,record_mcount) $(cmd_record_mcount)
    endef
      
    $(obj)/%.o: $(src)/%.c $(recordmcount_source) $(objtool_dep) FORCE
        $(call cmd,force_checksrc)
        $(call if_changed_rule,cc_o_c)
    

    이 receipe 안에서는 이 타겟은 rule_cc_o_c를 호출합니다. 이 규칙은 어떤 에러(cmd_checksrc)를 확인하는 소스코드를 확인하거나 수출된 모듈 심볼(cmd_modversions_c)를 버전화하거나 생성된 오브젝트 파일의 어떤 측면을 검증하기 위해 objtool을 사용하거나 mcount 함수를 호출하는 목록을 만들어 ftrace 가 쉽게 찾을 수 있게 하는 것과 같은 많은 일들을 맡습니다. 그러나 가장 중요하거는 모든 .c파일을 오브젝트 파일로 실질적으로 컴파일을 수행하는 cmd_cc_o_c 커맨드를 호출합니다.

Conclusion

와우, 커널 빌드 시스템 내부를 살펴보는 긴 여행이엿습니다. 많은 상세 사항을 넘어갔지만 이 주제에 대해 더 공부하고 싶으신 분들에게 다음과 같은 문서를 읽기를 권하고 계속 Makefile 소스 코드를 읽기를 권합니다. 이제 이번 챕터에서 숙제로 가져가야할 중요한 점들을 강조하겠습니다.

  1. .c 파일들이 어떻게 오브젝트 파일로 컴파일 되는가?
  2. 어떻게 오브젝트 파일들이 build-in.o 파일에 결합되는가?
  3. 어떻게 재귀적인 빌드가 모든 자식 build-in.o 파일을 알아보고 하나로 결합하는가?
  4. 어떻게 vmlinux가 모든 최상위 build-in.o파일로 부터 링크 되는가?

이 챕터를 읽고 저자의 주요 목적은 여러분들이 모든 위의 점들에 대한 일반적인 이해를 가지게 되는 것입니다.

태그:

카테고리:

업데이트:

댓글남기기