Last preparations before the kernel entry point

커널 진입 포인트 전에 마지막 준비

이것은 리눅스 커널 초기화 프로세스 시리즈의 세번째 파트 입니다. 이전 파트에서는 early 인터럽트 및 예외처리를 봤으며 현재 파트에서는 리눅스 커널 초기화 프로세스를 계속할 것입니다. 다음 포인트는 커널 진입 포인트입니다. start_kernel함수는 init/main.c소스 코드 파일에 있습니다. 예, 기술적으로는 커널 진입 포인트가 아니라 특정 아키텍처에 의존하지 않는 일반 커널 코드의 시작입니다. start_kernel함수를 호출하기 전에 준비해야 할 것이 있습니다. 계속합시다.

다시 boot_params으로

이전 파트에서 인터럽트 디스크립터 테이블을 설정을 중지하고 IDTR레지스터를 불러왔습니다. 다음 단계에서 copy_bootdata함수를 호출합니다:

copy_bootdata(__va(real_mode_data));

이 함수는 하나의 인자-가상 주소real_mode_data를 사용합니다. arch/x86/include/uapi/asm/bootparam.hboot_params구조체의 주소를 arch/x86/kernel/head_64.S의 첫번째 인자 x86_64_start_kernel함수에 넘긴것을 기억하십시오:

    /* rsi는 C에서 넘겨진 흥미로운 정보의 
        리얼 모드 구조체 포인터 입니다.*/
    movq    %rsi, %rdi

이제 __va매크로를 봅니다. 이 매크로는 init/main.c에 정의되었습니다:

#define __va(x)                 ((void *)((unsigned long)(x)+PAGE_OFFSET))

PAGE_OFFSET0xffff880000000000및 모든 물리 메모리의 직접 매핑 가상 주소의 기반인 __PAGE_OFFSET에 위치합니다. 그래서 boot_params구조의 가상주소를 얻고 그것을 arch/x86/include/asm/setup.h에 선언된 boot_paramsreal_mod_data를 복사한 곳의 copy_bootdata함수에 넘깁니다.

extern struct boot_params boot_params;

copy_boot_data의 구현을 봅시다:

static void __init copy_bootdata(char *real_mode_data)
{
    char * command_line;
    unsigned long cmd_line_ptr;

    memcpy(&boot_params, real_mode_data, sizeof boot_params);
    sanitize_boot_params(&boot_params);
    cmd_line_ptr = get_cmd_line_ptr();
    if (cmd_line_ptr) {
        command_line = __va(cmd_line_ptr);
        memcpy(boot_command_line, command_line, COMMAND_LINE_SIZE);
    }
}

우선 이 함수는 __init접두사로 선언됩니다. 이것은 이 함수가 초기화 중에만 사용되고 사용된 메모리가 해제되는 것을 의미합니다.

커널 커맨드 라인의 두 변수를 선언하고 memcpy함수의 boot_params에서 real_mode_data를 복사하는 것을 볼 수 있습니다. boot_params이 0이어서 알수없는 필드의 초기화에 실패한 부트로더의 경우 boot_params구조체와 같은 ext_ramdisk_image등의 영역이 약간 채워진 sanitize_boot_params함수를 호출합니다. 다음으로 get_cmd_line_ptr함수를 호출해 커맨드 라인의 주소를 얻습니다:

unsigned long cmd_line_ptr = boot_params.hdr.cmd_line_ptr;
cmd_line_ptr |= (u64)boot_params.ext_cmd_line_ptr << 32;
return cmd_line_ptr;

커널 부트헤더에서 커맨드 라인의 64비트 주소를 얻고 이것을 반환합니다. 마지막으로 cmd_line_ptr을 확인하고 이것의 가상주소를 받아 바이트 배열 boot_command_line에 복사합니다:

extern char __initdata boot_command_line[];

커널 커맨드라인과 boot_params구조체가 복사됩니다. 다음 단계에서는 load_ucode_bsp 프로세서 마이크로 코드를 호출하는 함수를 볼 수 있지만, 여기서는 아닙니다.

마이크로코드가 불려진 다음 console_loglevelKernel Alive 문자열을 출력하는 early_printk함수를 확인하는 것을 볼 수 있습니다. 그러나 아직 early_printk가초기화되지 않아서 출력은 볼 수 없습니다. 이것은 커널의 사소한 버그로 패치를 보냈습니다 - 커밋하면 메인라인에서 볼 수 있습니다. 따라서 이 코드를 건너 뛸 수 있습니다.

초기화 페이지로 이동

다음 단계에서는 boot_params구체조가 복사됬으니 초기화 프로세스를 위해 초기 페이지 테이블에서 페이지 테이블로 이동해야 합니다. 이미 전환을 위해 초기 페이지 테이블을 설정했습니다. 이전 파트에서 읽을 수 있으며 reset_early_page_tables함수에서 모두 떨어트릴 수 있고(이전 파트에서 볼 수 있음) 커널의 높은 매핑을 유지합니다. 그 다음 호출합니다:

    clear_page(init_level4_pgt);

함수 및 arch/x86/kernel/head_64.S에 정의된 init_level4_pgt을 넘깁니다:

NEXT_PAGE(init_level4_pgt)
    .quad   level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
    .org    init_level4_pgt + L4_PAGE_OFFSET*8, 0
    .quad   level3_ident_pgt - __START_KERNEL_map + _KERNPG_TABLE
    .org    init_level4_pgt + L4_START_KERNEL*8, 0
    .quad   level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE

커널 코드, 데이터, bss에 대해 처음에 2기가바이트와 512메가바이트를 매핑합니다. clear_page함수는 arch/x86/lib/clear_page_64.S에 정의됬습니다. 이 함수를 봅시다:

ENTRY(clear_page)
    CFI_STARTPROC
    xorl %eax,%eax
    movl $4096/64,%ecx
    .p2align 4
    .Lloop:
    decl    %ecx
#define PUT(x) movq %rax,x*8(%rdi)
    movq %rax,(%rdi)
    PUT(1)
    PUT(2)
    PUT(3)
    PUT(4)
    PUT(5)
    PUT(6)
    PUT(7)
    leaq 64(%rdi),%rdi
    jnz    .Lloop
    nop
    ret
    CFI_ENDPROC
    .Lclear_page_end:
    ENDPROC(clear_page)

함수 이름에서 알 수 있듯이 0페이지 테이블을 지우거나 채웁니다. 우선 이 함수는 CFI_STARTPROC 및 GNU 어셈블리 명령어로 확장된 CFI_ENDPROC로 시작합니다.

#define CFI_STARTPROC           .cfi_startproc
#define CFI_ENDPROC             .cfi_endproc

그리고 디버깅이 쓰입니다. CFI_STARTPROC매크로 후에 eax레지스터를 0으로 만들고 ecx(이것은 counter이 됩니다)에 64를 넣습니다. 다음으로 .Lloop레이블로 시작하는 루프를 볼 수 있고 이것은 ecx의 감소에서 시작됩니다. 다음으로 init_level4_pgt의 기본 주소를 포함하는 rdirax레지스터에 0을 넣고 동일한 절차를 7번 수행합니다. 하지만 rdi오프셋은 매번 8로 이동합니다. 이후 init_level4_pgt의 첫 64바이트는 0으로 채워집니다. 다음 단계에서 init_level4_pgt 64-바이트 오프셋의 주소를 rdi에 넣고 ecx가 0이 될때까지 모든 작업을 반복합니다. 결국 init_level4_pgt은 0으로 채워집니다.

따라서 0으로 채워진 init_level4_pgt을 가집니다. 마지막 init_level4_pgt엔트리의 커널 하이 매핑을 다음으로 설정합니다.

init_level4_pgt[511] = early_top_pgt[511];

reset_early_page_table함수의 모든 early_top_pgt엔트리를 지우고 커널 하이 매핑을 유지한 것을 기억하십시오.

x86_64_start_kernel함수의 마지막 단계는 다음을 호출합니다:

x86_64_start_reservations(real_mode_data);

real_mode_data같은 인자와 함수. x86_64_start_reservations함수는 같은 소스 코드 파일x86_64_start_kernel 함수에 정의됬습니다. 봅시다:

void __init x86_64_start_reservations(char *real_mode_data)
{
    if (!boot_params.hdr.version)
        copy_bootdata(__va(real_mode_data));

    reserve_ebda_region();

    start_kernel();
}

start_kernel함수가 커널 진입 포인트에 들어가기전 마지막 함수임을 알 수 있습니다. 그것이 무엇을 하고 어떻게 작동하는지 알아봅시다.

커널 진입 포인트 전 마지막 단계

우선 x86_64_start_reservations함수에서 boot_params.hdr.version을 확인하십시오:

if (!boot_params.hdr.version)
    copy_bootdata(__va(real_mode_data));

그리고 그것이 0이라면 real_mode_data(구현에 대한 읽기)의 가상주소에서 copy_bootdata함수를 다시 호출하십시오.

다음 단계는 arch/x86/kernel/head.c에 정의된 reserve_ebda_region함수를 호출합니다. 이 함수는 EBDA 또는 확장된 BIOS 데이터 영역에 대한 메모리 블록을 예약합니다. 확장된 BIOS 데이터 영역은 기존 메모리 상단에 있으며 포트, 디스크 매개변수 등의 데이터를 포함합니다.

reserve_ebda_region함수를 봅시다. 반가상화가 활성화됐는지 확인합니다:

if (paravirt_enabled())
    return;

In the next step we need to get the end of the low memory: 반가상화가 활성화됐을 경우 활성화된 확장 bios 데이터 영역이 없기때문에 reserve_ebda_region함수에서 빠져나옵니다. 다음 단계에서 low 메모리의 끝을 찾아야합니다:

lowmem = *(unsigned short *)__va(BIOS_LOWMEM_KILOBYTES);
lowmem <<= 10;

BIOS의 low 메모리의 가상 주소를 킬로바이트 단위로 가져오고 그것을 10개(즉, 1024를 곱하기)씩 바이트로 변환합니다. 그 다음, 다음과 같이 확장된 BIOS 데이터 주소를 가져옵니다:

ebda_addr = get_bios_ebda();

get_bios_ebda함수는 arch/x86/include/asm/bios_ebda.h에 정의됐으며 다음과 같습니다:

static inline unsigned int get_bios_ebda(void)
{
    unsigned int address = *(unsigned short *)phys_to_virt(0x40E);
    address <<= 4;
    return address;
}

어떻게 작동하는지 이해해봅시다. 여기서 확장된 BIOS 데이터 영역의 기본 주소를 포함하는 세그먼트인 0x0040:0x000e에서 물리주소 0x40E를 가상으로 변환하는 것을 볼 수 있습니다. 물리주소를 가상주소로 변환하는 phys_to_virt함수를 사용한다고 걱정하지 마십시오. 이전에는 같은 점에 대해 __va매크로를 사용했지만 phys_to_virt는 같습니다:

static inline void *phys_to_virt(phys_addr_t address)
{
         return __va(address);
}

한 가지 차이점이 있습니다: CONFIG_PHYS_ADDR_T_64BIT에 의존해 인자phys_addr_t를 전달합니다:

#ifdef CONFIG_PHYS_ADDR_T_64BIT
    typedef u64 phys_addr_t;
#else
    typedef u32 phys_addr_t;
#endif

이 구성 옵션은 CONFIG_PHYS_ADDR_T_64BIT에 의해 활성화됩니다. 확장된 BIOS 데이터 영역의 기본 주소를 저장하는 세그먼트의 가상주소를 얻고 4만큼 이동해 반환합니다. 그 다음 ebda_addr변수는 확장된 BIOS 데이터 영역의 기본주소를 포함합니다.

다음 단계는 확장된 BIOS 데이터 영역과 low 메모리의 주소가 INSANE_CUTOFF매크로 이상인지 확인합니다:

if (ebda_addr < INSANE_CUTOFF)
    ebda_addr = LOWMEM_CAP;

if (lowmem < INSANE_CUTOFF)
    lowmem = LOWMEM_CAP;

이것은:

#define INSANE_CUTOFF        0x20000U

또는 128킬로바이트 입니다. 마지막 단계에서 low 메모리의 낮은 부분과 확장된 bios 데이터 영역을 얻고 low메모리와 1메가바이트 사이의 확장된 bios 데이터를 위한 메모리 영역을 예약하는 memblock_reserve함수를 호출합니다.

lowmem = min(lowmem, ebda_addr);
lowmem = min(lowmem, LOWMEM_CAP);
memblock_reserve(lowmem, 0x100000 - lowmem);

memblock_reserve함수는 mm/block.c에 정의되었고 두 매개변수를 가집니다:

  • 기본 물리적 주소;

  • 영역 크기

그리고 주어진 기본 주소와 크기의 메모리 영역을 예약합니다. memblock_reserve는 리눅스 커널 메모리 관리자 프레임워크 책의 첫번째 함수입니다. 곧 메모리 관리자를 자세히 살펴볼 것이고 지금은 구현을 보겠습니다.

리눅스 커널 메모리 관리자 프레임워크의 첫 터치

이전 단락에서 memblock_reserve함수의 호출에서 멈췄고 앞서 말했듯이 메모리 관리자 프레임워크의 첫번째 함수입니다. 이것이 어떻게 작동하는지 이해해봅시다. memblock_reserve함수 호출:

memblock_reserve_region(base, size, MAX_NUMNODES, 0);

함수에 4개의 매개변수를 전달합니다:

  • 메모리 영역의 물리 기본 주소;

  • 메모리 영역의 크기;

  • numa 노드의 최대 개수;

  • 플래그;

memblock_reserve_region본체의 시작부분에서 memblock_type구조체의 정의를 볼 수 있습니다:

struct memblock_type *_rgn = &memblock.reserved;

메모리 블록의 형식을 제시합니다. 보십시오:

struct memblock_type {
         unsigned long cnt;
         unsigned long max;
         phys_addr_t total_size;
         struct memblock_region *regions;
};

확장된 bios 데이터 영역을 위한 메모리 블록을 예약해야하므로 현재 메모리 영역의 형식은 다음과 같은 memblock구조체로 예약됩니다:

struct memblock {
         bool bottom_up;
         phys_addr_t current_limit;
         struct memblock_type memory;
         struct memblock_type reserved;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
         struct memblock_type physmem;
#endif
};

그리고 일반 메모리 블록을 설명합니다. memblock.reserved의 주소에 할당하면 _rgn가 초기화되는 것을 볼 수 있습니다. memblock은 전역변수입니다:

struct memblock memblock __initdata_memblock = {
    .memory.regions        = memblock_memory_init_regions,
    .memory.cnt        = 1,
    .memory.max        = INIT_MEMBLOCK_REGIONS,
    .reserved.regions    = memblock_reserved_init_regions,
    .reserved.cnt        = 1,
    .reserved.max        = INIT_MEMBLOCK_REGIONS,
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
    .physmem.regions    = memblock_physmem_init_regions,
    .physmem.cnt        = 1,
    .physmem.max        = INIT_PHYSMEM_REGIONS,
#endif
    .bottom_up        = false,
    .current_limit        = MEMBLOCK_ALLOC_ANYWHERE,
};

이 변수에 대해 자세히 설명하지는 않지만 메모리 관리자 파트에서 모든 세부 정보를 볼 수 있습니다. memblock변수는 __initdata_memblock로 정의되는 것을 참고하십시오:

#define __initdata_memblock __meminitdata

__meminit_data는:

#define __meminitdata    __section(.meminit.data)

이것으로 모든 메모리 블록이 .meminit.data섹션에 있다고 결론 내릴 수 있습니다. _rgn을 정의한 후 memblock_dbg매크로의 정보를 출력합니다. memblock=debug을 커널 커맨드 라인에 전달함으로써 활성화할 수 있습니다.

디버깅 라인이 출력된 후 다음 함수가 호출됩니다:

memblock_add_range(_rgn, base, size, nid, flags);

.meminit.data섹션에 새로운 메모리 블록 영역을 추가합니다. 따라서 _rgn을 초기화하지는 않지만 이것은 &memblock.reserved을 포함합니다. 전달된 _rgn을 확장된 BIOS 데이터 지역의 기본주소, 이 지역의 크기와 플래그로 채웁니다:

if (type->regions[0].size == 0) {
    WARN_ON(type->cnt != 1 || type->total_size);
    type->regions[0].base = base;
    type->regions[0].size = size;
    type->regions[0].flags = flags;
    memblock_set_region_node(&type->regions[0], nid);
    type->total_size = size;
    return 0;
}

지역을 채운 후 두 매개변수로 memblock_set_region_node함수를 호출할 수 있습니다:

  • 채워진 메모리 지역의 주소;

  • NUMA 노드 아이디.

여기서 지역은 memblock_region구조체로 표현됩니다:

struct memblock_region {
    phys_addr_t base;
    phys_addr_t size;
    unsigned long flags;
#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP
    int nid;
#endif
};

NUMA 노드 아이디는 include/linux/numa.h에 정의된 MAX_NUMNODES매크로에 의존합니다:

#define MAX_NUMNODES    (1 << NODES_SHIFT)

NODES_SHIFT는 구성 매개변수 CONFIG_NODES_SHIFT에 의존하며 다음과 같이 정의됩니다:

#ifdef CONFIG_NODES_SHIFT
  #define NODES_SHIFT     CONFIG_NODES_SHIFT
#else
  #define NODES_SHIFT     0
#endif

memblick_set_region_node함수는 memblock_region에서 주어진 값으로 nid필드를 채웁니다:

static inline void memblock_set_region_node(struct memblock_region *r, int nid)
{
         r->nid = nid;
}

그 다음 먼저 .meminit.data섹션의 확장된 bios 데이터 영역의 memblock를 예약합니다. reserve_ebda_region함수는 이 단계에서 작업을 완료하고 arch/x86/kernel/head64.c로 돌아갈 수 있습니다.

커널 진입 포인트 전의 모든 준비가 끝났습니다! 마지막으로 x86_64_start_reservations함수를 호출합니다:

start_kernel()

함수는 init/main.c파일에 있습니다.

이것이 이 파트의 전부입니다.

결론

리눅스 커널 내부에 대한 세 번째 파트의 끝입니다. 다음 파트에서는 커널 진입 포인트 - start_kernel함수의 첫 번째 초기화 단계를 볼 것입니다. 이것은 첫 번째 init프로세스가 실행되기 전 첫 번째 단계가 될 것입니다.

질문이나 제안사항이 있다면 코멘트를 남기거나 트위터로 보내주십시오.

영어는 모국어가 아니며 모든 불편한 점은 정말 죄송합니다. 실수를 발견하면 linux-insides에서 수정 사항이 포함된 PR을 보내주십시오.

링크

Last updated