First steps in the kernel

커널 초기화. Part 1

커널 코드의 첫 단계

이전 포스트는 Linux 커널 부팅 프로세스챕터의 마지막 부분이었습니다. 그리고 이제 Linux 커널의 초기화 과정을 시작합니다. Linux 커널 이미지가 압축 해제되고 메모리의 올바른 위치에 배치되면 작동하기 시작합니다. 이전의 모든 부분에서는 Linux 커널 코드의 첫 바이트가 실행되기 전에 준비하는 Linux 커널 설정 코드의 작업에 대해 설명합니다. 이제 우리는 커널에 있으며 이 장에서는 pid 1로 프로세스를 시작하기 전에 커널의 초기화 과정에 집중 할 것입니다. 커널이 init프로세스를 시작하기 전에 해야 할 일이 많이 있습니다. 이 큰 장에서 커널이 시작되기 전에 모든 준비 과정을 보게되기를 바랍니다. arch/x86/kernel/head_64.S에 있는 커널 진입 점에서 시작하겠습니다. init/main.c에서 start_kernel 함수를 호출 되는 것을 보기 전에 초기 페이지 테이블 초기화, 커널 공간에서 새 디스크립터로 전환하는 등과 같은 첫 번째 준비를 살펴볼 것입니다.

jmp    *%rax

현재 rax 레지스터에는 arch/x86/boot/compressed/misc.c 소스 코드 파일에서 decompress_kernel 함수를 호출 한 결과 얻은 Linux 커널 진입 점의 주소가 포함되어 있습니다. 커널 설정 코드의 마지막 명령은 커널 진입 점을 뛰어 넘는 것입니다. 우리는 이미 Linux 커널의 진입 점이 정의되어 있다는 것을 알고 있으므로 Linux 커널이 무엇을하는지 배울 수 있습니다.

커널의 첫 단계

압축 해제 된 커널 이미지의 주소를 decompress_kernel 함수에서 rax레지스터로 가져 와서 바로 점프 하였습니다. 우리가 이미 알고 있듯이 압축 해제 된 커널 이미지의 시작점은 arch/x86/kernel/head_64.S어셈블리 소스 코드 파일에서 시작하고 그 시작 부분에서 다음 정의를 볼 수 있습니다:

    .text
    __HEAD
    .code64
    .globl startup_64
startup_64:
    ...
    ...
    ...

실행 가능한 .head.text섹션의 정의로 확장되는 매크로 인 __HEAD섹션에 정의 된 startup_64루틴의 정의를 볼 수 있습니다:

#define __HEAD        .section    ".head.text","ax"

이 섹션의 정의를 arch/x86/kernel/vmlinux.lds.S 링커 스크립트에서 볼 수 있다:

.text : AT(ADDR(.text) - LOAD_OFFSET) {
    _text = .;
    ...
    ...
    ...
} :text = 0x9090

.text섹션의 정의 외에도 링커 스크립트에서 기본 가상 주소와 물리적 주소를 이해할 수 있습니다. _text의 주소는 다음과 같이 정의되는 위치 카운터입니다:

. = __START_KERNEL;

x86_64 위해. __START_KERNEL매크로의 정의는 arch/x86/include/asm/page_types.h헤더 파일에 있고 커널 매핑의 기본 가상 주소와 물리적 시작의 합으로 표시됩니다:

#define __START_KERNEL    (__START_KERNEL_map + __PHYSICAL_START)

#define __PHYSICAL_START  ALIGN(CONFIG_PHYSICAL_START, CONFIG_PHYSICAL_ALIGN)

혹은 다른 표현으로:

  • 리눅스 커널의 기본 물리 주소 - 0x1000000;

  • 리눅스 커널의 기본 가상 주소 - 0xffffffff81000000.

CPU구성을 삭제한 후, arch/x86/kernel/head64.c에 정의된 __startup_64함수를 호출한다:

    leaq    _text(%rip), %rdi
    pushq    %rsi
    call    __startup_64
    popq    %rsi
unsigned log __head __startup_64(unsigned long physaddr,
                 struct boot_params *bp)
{
    unsigned long load_delta, *p;
    unsigned long pgtable_flags;
    pgdval_t *pgd;
    p4dval_t *p4d;
    pudval_t *pud;
    pmdval_t *pmd, pmd_entry;
    pteval_t *mask_ptr;
    bool la57;
    int i;
    unsigned int *next_pgt_ptr;
    ...
    ...
    ...
}

kASLR을 사용하도록 설정되었으므로 로드 된 'startup_64'루틴 주소가 컴파일 된 주소와 다를 수 있으므로 델타를 다음 코드를 통해 계산해야합니다:

    load_delta = physaddr - (unsigned long)(_text - __START_KERNEL_map);

결과적으로, load_delta는 컴파일 된 주소와 실제로 로드 된 주소 사이의 델타를 포함합니다.

델타를 얻은 후 _text 주소가 2 메가 바이트에 올바르게 정렬되어 있는지 확인합니다. 다음 코드를 사용하여 수행합니다:

    if (load_delta & ~PMD_PAGE_MASK)
        for (;;);

_text 주소가 2 메가 바이트로 정렬되지 않으면 무한 루프가 됩니다. PMD_PAGE_MASKPage middle directory에 대한 마스크를 나타냅니다(Paging 참조) 그리고 다음과 같이 정의됩니다:

#define PMD_PAGE_MASK           (~(PMD_PAGE_SIZE-1))

where PMD_PAGE_SIZE macro is defined as:

#define PMD_PAGE_SIZE           (_AC(1, UL) << PMD_SHIFT)
#define PMD_SHIFT        21

쉽게 계산할 수있는 PMD_PAGE_SIZE2MB입니다.

SME가 지원되고 허용 된 경우 이를 활성화하고 load_delta에 SME 암호화 마스크를 포함시킵니다:

    sme_enable(bp);
    load_delta += sme_get_me_mask();

자, 우리는 몇 가지 체크를 하였고 계속 진행할 수 있습니다.

페이지 테이블의 기본 주소 수정

다음 단계에서는 페이지 테이블의 실제 주소를 수정합니다:

    pgd = fixup_pointer(&early_top_pgt, physaddr);
    pud = fixup_pointer(&level3_kernel_pgt, physaddr);
    pmd = fixup_pointer(level2_fixmap_pgt, physaddr);

전달 된 인자의 물리적 주소를 반환하는 fixup_pointer함수의 정의를 봅시다:

static void __head *fixup_pointer(void *ptr, unsigned long physaddr)
{
    return ptr - (void *)_text + (void *)physaddr;
}

다음으로 우리는 early_top_pgt와 위에서 본 다른 페이지 테이블 심볼에 초점을 맞출 것입니다. 이 기호들이 무엇을 의미하는지 이해하려고 노력합시다. 우선 그들의 정의를 봅시다:

NEXT_PAGE(early_top_pgt)
    .fill    512,8,0
    .fill    PTI_USER_PGD_FILL,8,0

NEXT_PAGE(level3_kernel_pgt)
    .fill    L3_START_KERNEL,8,0
    .quad    level2_kernel_pgt - __START_KERNEL_map + _KERNPG_TABLE_NOENC
    .quad    level2_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC

NEXT_PAGE(level2_kernel_pgt)
    PMDS(0, __PAGE_KERNEL_LARGE_EXEC,
        KERNEL_IMAGE_SIZE/PMD_SIZE)

NEXT_PAGE(level2_fixmap_pgt)
    .fill    506,8,0
    .quad    level1_fixmap_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC
    .fill    5,8,0

NEXT_PAGE(level1_fixmap_pgt)
    .fill    512,8,0

어려워 보이지만 그렇지 않습니다. 우선 early_top_pgt를 봅시다. 4096바이트의 0으로 (CONFIG_PAGE_TABLE_ISOLATION이 활성화 된 경우 8192바이트로) 시작합니다. 이는 첫 번째 512항목을 사용하지 않음을 의미합니다. 그리고 나서 level3_kernel_pgt항목을 볼 수 있습니다. 정의의 시작 부분에서 4080바이트의 0으로 채워져 있음을 알 수 있습니다(L3_START_KERNEL510). 이어서 커널 공간을 매핑하는 두 개의 항목을 저장합니다. level2_kernel_pgtlevel2_fixmap_pgt에서 __START_KERNEL_map을 뺍니다. 우리가 알고있는 것처럼 __START_KERNEL_map은 커널 텍스트의 기본 가상 주소이므로 __START_KERNEL_map을 빼면 level2_kernel_pgtlevel2_fixmap_pgt의 물리적 주소를 얻게됩니다.

다음으로 _KERNPG_TABLE_NOENC_PAGE_TABLE_NOENC를 봅시다. 이들은 페이지 항목 액세스 권한입니다:

#define _KERNPG_TABLE_NOENC   (_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED | \
                   _PAGE_DIRTY)
#define _PAGE_TABLE_NOENC     (_PAGE_PRESENT | _PAGE_RW | _PAGE_USER | \
                   _PAGE_ACCESSED | _PAGE_DIRTY)

level2_kernel_pgt는 커널 공간을 매핑하는 페이지 중간 디렉토리에 대한 포인터를 포함하는 페이지 테이블 항목입니다. 커널 .text에 대한 __START_KERNEL_map에서 512 메가 바이트를 생성하는 PDMS 매크로를 호출합니다. (이 512메가 바이트는 모듈 메모리 공간이됩니다)

level2_fixmap_pgt는 커널 공간에서도 물리적 주소를 참조 할 수있는 가상 주소입니다. 그것들은 4048 바이트의 0, level1_fixmap_pgt 엔트리, vsyscalls매핑을 위해 예약 된 8메가 바이트와 2 메가 바이트의 홀로 표시됩니다.

자세한 내용은 Paging부분에서 읽을 수 있습니다.

이제이 심볼의 정의를 확인한 후 코드로 돌아가 보겠습니다. 다음으로 level3_kernel_pgtpgd의 마지막 엔트리를 초기화합니다:

    pgd[pgd_index(__START_KERNEL_map)] = level3_kernel_pgt - __START_KERNEL_map + _PAGE_TABLE_NOENC;

startup_64가 기본 0x1000000 주소와 같지 않으면 모든 p d 주소가 잘못되었을 수 있습니다. load_delta는 커널 링킹 동안 얻은 startup_64심볼의 주소와 실제 주소 사이의 델타를 포함합니다. 따라서 `p d`의 특정 항목에 델타를 추가합니다.

    pgd[pgd_index(__START_KERNEL_map)] += load_delta;
    pud[510] += load_delta;
    pud[511] += load_delta;
    pmd[506] += load_delta;

이 모든 후에 다음 결과를 얻게 됩니다:

early_top_pgt[511] -> level3_kernel_pgt[0]
level3_kernel_pgt[510] -> level2_kernel_pgt[0]
level3_kernel_pgt[511] -> level2_fixmap_pgt[0]
level2_kernel_pgt[0]   -> 512 MB kernel mapping
level2_fixmap_pgt[506] -> level1_fixmap_pgt

우리는 early_top_pgt의 기본 주소와 일부 다른 페이지 테이블 디렉토리를 고치지 않았습니다. 이 페이지 테이블의 building/filling 구조때 이것이 보이기 때문입니다. 페이지 테이블의 기본 주소를 수정하면 빌드를 시작할 수 있습니다.

아이디 매핑 설정

이제 초기 페이지 테이블의 아이디 매핑 설정을 볼 수 있습니다. Identity Mapped Paging에서 가상 주소는 물리적 주소에 동일하게 매핑됩니다. 자세히 살펴 보겠습니다. 우선 pudpmdearly_dynamic_pgts의 첫 번째와 두 번째 항목에 대한 포인터로 바꿉니다:

    next_pgt_ptr = fixup_pointer(&next_early_pgt, physaddr);
    pud = fixup_pointer(early_dynamic_pgts[(*next_pgt_ptr)++], physaddr);
    pmd = fixup_pointer(early_dynamic_pgts[(*next_pgt_ptr)++], physaddr);

early_dynamic_pgts 정의를 봅시다:

NEXT_PAGE(early_dynamic_pgts)
    .fill    512*EARLY_DYNAMIC_PAGE_TABLES,8,0

초기 커널에 대한 임시 페이지 테이블을 저장합니다.

다음으로 p*d 항목을 초기화 할 때 사용되는 pgtable_flags를 초기화합니다:

    pgtable_flags = _KERNPG_TABLE_NOENC + sme_get_me_mask();

sme_get_me_mask함수는 sme_enable함수에서 초기화 된sme_me_mask를 반환합니다.

다음으로 pudpgtable_flags로 위에서 두 개의 pgd 항목을 채운다:

    i = (physaddr >> PGDIR_SHIFT) % PTRS_PER_PGD;
    pgd[i + 0] = (pgdval_t)pud + pgtable_flags;
    pgd[i + 1] = (pgdval_t)pud + pgtable_flags;

`PGDIR_SHFT`는 가상 주소의 페이지 글로벌 디렉토리 비트에 대한 마스크를 나타냅니다. 여기에서는 512보다 큰 인덱스에 액세스하지 않도록 PTRS_PER_PGD(512로 확장)로 모듈로를 계산합니다. 모든 유형의 페이지 디렉토리에 대한 매크로가 있습니다:

#define PGDIR_SHIFT     39
#define PTRS_PER_PGD    512
#define PUD_SHIFT       30
#define PTRS_PER_PUD    512
#define PMD_SHIFT       21
#define PTRS_PER_PMD    512

다음에도 위에서와 같은 일을 합니다:

    i = (physaddr >> PUD_SHIFT) % PTRS_PER_PUD;
    pud[i + 0] = (pudval_t)pmd + pgtable_flags;
    pud[i + 1] = (pudval_t)pmd + pgtable_flags;

다음으로 pmd_entry를 초기화하고 지원되지 않는 __PAGE_KERNEL_ * 비트를 걸러냅니다:

    pmd_entry = __PAGE_KERNEL_LARGE_EXEC & ~_PAGE_GLOBAL;
    mask_ptr = fixup_pointer(&__supported_pte_mask, physaddr);
    pmd_entry &= *mask_ptr;
    pmd_entry += sme_get_me_mask();
    pmd_entry += physaddr;

다음으로 모든 pmd 항목을 채워서 커널의 전체 크기를 다룹니다:

    for (i = 0; i < DIV_ROUND_UP(_end - _text, PMD_SIZE); i++) {
        int idx = i + (physaddr >> PMD_SHIFT) % PTRS_PER_PMD;
        pmd[idx] = pmd_entry + i * PMD_SIZE;
    }

다음으로 커널 텍스트 + 데이터 가상 주소를 수정합니다. 커널을 재배치 할 때 유효하지 않은 pmd를 작성할 수 있습니다 (cleanup_highmap 함수는 _end 이외의 매핑과 함께 이를 수정합니다).

    pmd = fixup_pointer(level2_kernel_pgt, physaddr);
    for (i = 0; i < PTRS_PER_PMD; i++) {
        if (pmd[i] & _PAGE_PRESENT)
            pmd[i] += load_delta;
    }

다음으로 메모리 암호화 마스크를 제거하여 실제 주소를 얻습니다(load_delta에 마스크가 포함되어 있음을 기억하십시오):

    *fixup_long(&phys_base, physaddr) += load_delta - sme_get_me_mask();

phys_baselevel2_kernel_pgt의 첫 번째 항목과 일치해야합니다.

__startup_64 함수의 마지막 단계로 커널을 암호화하고 (SME가 활성화 된 경우) cr3 레지스터에 프로그래밍 된 초기 페이지 디렉토리 항목의 제어자로 사용할 SME 암호화 마스크를 반환합니다:

    sme_encrypt_kernel(bp);
    return sme_get_me_mask();

이제 어셈블리 코드로 돌아가 봅시다. 다음 코드를 사용하여 다음을 준비합니다:

    addq    $(early_top_pgt - __START_KERNEL_map), %rax
    jmp 1f

early_top_pgt의 물리적 주소를 rax 레지스터에 추가하여 rax 레지스터에 주소와 SME 암호화 마스크의 합이 포함되도록합니다.

지금은 여기까지입니다. 초기 페이징이 준비되었으므로 커널 준비 지점으로 이동하기 전에 마지막 준비를 마치면됩니다.

커널 진입 점에서 점프하기 전에 마지막 준비

그런 다음 레이블 1로 이동하여 PAE, PGE (Paging Global Extension)를 활성화하고 phys_base(위 참조)의 내용을 rax 레지스터에 넣고cr3 레지스터를 채웁니다:

1:
    movl    $(X86_CR4_PAE | X86_CR4_PGE), %ecx
    movq    %rcx, %cr4

    addq    phys_base(%rip), %rax
    movq    %rax, %cr3

다음 단계에서 CPU가 다음을 사용하여 NX비트를 지원하는지 확인합니다:

    movl    $0x80000001, %eax
    cpuid
    movl    %edx,%edi

0x80000001 값을 eax에 넣고 확장 프로세서 정보 및 기능 비트를 얻기 위해 cpuid 명령을 실행합니다. 결과는 edi에 넣은 edx 레지스터에 있을 것이다.

이제 우리는 0xc0000080 또는 MSR_EFERecx에 넣고 판독 모델 특정 레지스터에 대해 rdmsr 명령을 실행합니다.

    movl    $MSR_EFER, %ecx
    rdmsr

결과는 edx : eax에 나타납니다. EFER의 일반적인 견해는 다음과 같습니다:

63                                                                              32
 --------------------------------------------------------------------------------
|                                                                               |
|                                Reserved MBZ                                   |
|                                                                               |
 --------------------------------------------------------------------------------
31                            16  15      14      13   12  11   10  9  8 7  1   0
 --------------------------------------------------------------------------------
|                              | T |       |       |    |   |   |   |   |   |   |
| Reserved MBZ                 | C | FFXSR | LMSLE |SVME|NXE|LMA|MBZ|LME|RAZ|SCE|
|                              | E |       |       |    |   |   |   |   |   |   |
 --------------------------------------------------------------------------------

여기서 모든 필드를 자세히 볼 수는 없지만 이에 대한 특별한 부분에서 이 필드와 다른 MSR에 대해 알아 봅니다. edx : eaxEFER을 읽으면서, btsl 명령으로 _EFER_SCE 또는 System Call Extensions 인 0 비트를 체크하고 1로 설정합니다. SCE 비트를 설정함으로써 우리는 SYSCALLSYSRET 명령을 활성화합니다. 다음 단계에서 우리는 edi에서 20 번째 비트를 검사합니다. 이 레지스터는cpuid의 결과를 저장한다는 것을 기억하십시오 (위 참조). 만약 20 비트가 설정되면 (NX 비트) 우리는 EFER_SCE를 모델 특정 레지스터에 씁니다.

    btsl    $_EFER_SCE, %eax
    btl    $20,%edi
    jnc     1f
    btsl    $_EFER_NX, %eax
    btsq    $_PAGE_BIT_NX,early_pmd_flags(%rip)
1:    wrmsr

NX비트가 지원되는 경우 _EFER_NX를 활성화하고 wrmsr명령을 사용하여 쓰십시오. NX비트가 설정된 후, 우리는 다음 어셈블리 코드를 사용하여 cr0 제어 레지스터에 있는 몇몇 비트들을 설정합니다:

    movl    $CR0_STATE, %eax
    movq    %rax, %cr0

특히 다음 비트들:

  • X86_CR0_PE - system is in protected mode;

  • X86_CR0_MP - controls interaction of WAIT/FWAIT instructions with TS flag in CR0;

  • X86_CR0_ET - on the 386, it allowed to specify whether the external math coprocessor was an 80287 or 80387;

  • X86_CR0_NE - enable internal x87 floating point error reporting when set, else enables PC style x87 error detection;

  • X86_CR0_WP - when set, the CPU can't write to read-only pages when privilege level is 0;

  • X86_CR0_AM - alignment check enabled if AM set, AC flag (in EFLAGS register) set, and privilege level is 3;

  • X86_CR0_PG - enable paging.

우리는 코드를 실행하고 어셈블리에서 더 많은 C 코드를 실행하려면 스택을 설정해야 한다는 것을 이미 압니다. 항상 그렇듯이 우리는 스택 포인터를 메모리의 올바른 위치로 설정하고 플래그를 재설정하여 이 작업을 수행합니다:

    movq initial_stack(%rip), %rsp
    pushq $0
    popfq

여기서 가장 흥미로운 것은 initial_stack입니다. 이 심볼은 소스 코드 파일코드 파일에 정의되어 있으며 다음과 같습니다.

GLOBAL(initial_stack)
    .quad  init_thread_union + THREAD_SIZE - SIZEOF_PTREGS

THREAD_SIZE 매크로는 arch/x86/include/asm/page_64_types.h헤더 파일에 정의되어 있습니다. KASAN_STACK_ORDER 매크로의 값에 따라 다릅니다:

#ifdef CONFIG_KASAN
#define KASAN_STACK_ORDER 1
#else
#define KASAN_STACK_ORDER 0
#endif

#define THREAD_SIZE_ORDER       (2 + KASAN_STACK_ORDER)
#define THREAD_SIZE  (PAGE_SIZE << THREAD_SIZE_ORDER)

kasan이 비활성화되고 PAGE_SIZE4096 바이트 인 경우를 고려합니다. 따라서 THREAD_SIZE16킬로바이트로 확장되며 스레드 스택의 크기를 나타냅니다. 왜 스레드일까요? 각 프로세스부모 프로세스자식 프로세스가 있을 것을 이미 알고 있을 것입니다. 실제로 부모 프로세스와 자식 프로세스는 스택에서 다릅니다. 새로운 프로세스에 새로운 커널 스택이 할당됩니다. 리눅스 커널에서 이 스택은 thread_info 구조를 가진 union으로 표현됩니다.

init_thread_unionthread_union으로 표시됩니다. thread_union은 다음과 같이 include/linux/sched.h 파일에 정의되어 있습니다:

union thread_union {
#ifndef CONFIG_ARCH_TASK_STRUCT_ON_STACK
    struct task_struct task;
#endif
#ifndef CONFIG_THREAD_INFO_IN_TASK
    struct thread_info thread_info;
#endif
    unsigned long stack[THREAD_SIZE/sizeof(long)];
};

CONFIG_ARCH_TASK_STRUCT_ON_STACK 커널 구성 옵션은 ia64아키텍처에서만 활성화되며 CONFIG_THREAD_INFO_IN_TASK 커널 구성 옵션은 x86_64 아키텍처에서 활성화됩니다. 따라서 `thread_info`구조체는 `thread_union`공용체 대신 `task_struct`구조체에 배치됩니다.

init_thread_unioninclude/asm-generic/vmlinux.lds.h 파일에 다음과 같은 INIT_TASK_DATA매크로의 일부로 배치됩니다:

#define INIT_TASK_DATA(align)  \
    . = ALIGN(align);      \
    ...                    \
    init_thread_union = .; \
    ...

이 매크로는 arch/x86/kernel/vmlinux.lds.S파일에서 다음과 같이 사용됩니다:

.data : AT(ADDR(.data) - LOAD_OFFSET) {
    ...
    INIT_TASK_DATA(THREAD_SIZE)
    ...
} :data

즉, init_thread_union16킬로바이트인 THREAD_SIZE에 정렬 된 주소로 초기화됩니다.

이제 우리는 이 표현을 이해할 수 있습니다:

GLOBAL(initial_stack)
    .quad  init_thread_union + THREAD_SIZE - SIZEOF_PTREGS

initial_stack 기호는 16 킬로바이트인 thread_union.stack 배열 +THREAD_SIZE의 시작과 커널 내 언와인더가 스택의 끝을 안정적으로 감지하는 데 도움이되는 규칙 인SIZEOF_PTREGS를 나타냅니다.

초기 부팅 스택을 설정 한 후,lgdt명령으로 글로벌 디스크립터 테이블을 업데이트합니다:

lgdt    early_gdt_descr(%rip)

여기서 early_gdt_descr은 다음과 같이 정의됩니다:

early_gdt_descr:
    .word    GDT_ENTRIES*8-1
early_gdt_descr_base:
    .quad    INIT_PER_CPU_VAR(gdt_page)

커널은 낮은 사용자 공간 주소에서 작동하지만 곧 커널은 자체 공간에서 작동하기 때문에 글로벌 디스크럽터 테이블을 다시 로드해야합니다.

이제 early_gdt_descr의 정의를 봅시다. GDT_ENTRIES32로 확장되어 글로벌 디스크럽터 테이블에 커널 코드, 데이터, 스레드 로컬 스토리지 세그먼트 등에 대한 32 항목이 포함됩니다.

이제 early_gdt_descr_base의 정의를 봅시다. gdt_page구조체는 arch/x86/include/asm/desc.h에 정의되어 있습니다:

struct gdt_page {
    struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));

여기에는 다음과 같이 정의되는 desc_struct 구조체의 배열인 하나의 필드 gdt가 포함됩니다:

struct desc_struct {
         union {
                 struct {
                         unsigned int a;
                         unsigned int b;
                 };
                 struct {
                         u16 limit0;
                         u16 base0;
                         unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
                         unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
                 };
         };
 } __attribute__((packed));

친숙한 GDT 디스크립터가 보입니다. gdt_page 구조는 4096바이트 인 PAGE_SIZE에 맞춰집니다. 이것은 gdt가 한 페이지를 차지한다는 것을 의미합니다. which looks familiar GDT descriptor. Note that gdt_page structure is aligned to PAGE_SIZE which is 4096 bytes. Which means that gdt will occupy one page.

이제 INIT_PER_CPU_VAR이 무엇인지 이해해 봅시다. INIT_PER_CPU_VARarch/x86/include/asm/percpu.h에 정의 된 매크로입니다. 그리고 주어진 매개 변수로 init_per_cpu__를 연결합니다.

#define INIT_PER_CPU_VAR(var) init_per_cpu__##var

INIT_PER_CPU_VAR 매크로가 확장 된 후, 우리는 init_per_cpu__gdt_page를 갖게됩니다. 링커 스크립터에서 init_per_cpu__gdt_page의 초기화를 볼 수 있습니다.

#define INIT_PER_CPU(x) init_per_cpu__##x = x + __per_cpu_load
INIT_PER_CPU(gdt_page);

INIT_PER_CPU_VARinit_per_cpu__gdt_page가 있고 링커 스크립트에서 INIT_PER_CPU 매크로가 확장되면 __per_cpu_load에서 오프셋을 얻습니다. 이 계산 후에는 새 GDT의 정확한 기본 주소를 갖게됩니다.

일반적으로 CPU 별 변수는 2.6 커널 기능입니다. 이름에서 무엇인지 이해할 수 있습니다. 우리가 per-CPU '변수를 만들면, 각 CPU는 이 변수의 자체 복사본을 갖게됩니다. 여기서 우리는 CPU 당gdt_page를 만들고 있습니다. 각 CPU가 고유한 변수 사본 등으로 작동하므로 이 타입의 변수에는 잠금이 없다는 것과 같은 많은 이점이 있습니다. 따라서 멀티 프로세서의 모든 코어에는 자체GDT테이블과 코어에서 실행 된 스레드에서 액세스 할 수있는 메모리 세그먼트를 나타내는 테이블의 모든 항목이 있습니다. [Concepts/per-cpu](https://0xax.gitbooks.io/linux-insides/content/Concepts/linux-cpu-1.html) 포스트에서per-CPU`변수에 대한 자세한 내용을 읽을 수 있습니다.

새로운 글로벌 디스크럽터 테이블을 로드 할 때마다 매번 해왔듯이 세그먼트를 다시로드합니다:

    xorl %eax,%eax
    movl %eax,%ds
    movl %eax,%ss
    movl %eax,%es
    movl %eax,%fs
    movl %eax,%gs

이 모든 단계 후에 gs 레지스터를 설정하여 interrupts가 처리 될 특별한 스택을 나타내는 irqstack에 게시합니다:

    movl    $MSR_GS_BASE,%ecx
    movl    initial_gs(%rip),%eax
    movl    initial_gs+4(%rip),%edx
    wrmsr

여기서 MSR_GS_BASE는 다음과 같습니다:

#define MSR_GS_BASE             0xc0000101

MSR_GS_BASEecx 레지스터에 넣고 wrmsr 명령으로 eaxedx(initial_gs를 가리킴)에서 데이터를 로드해야합니다. 64 비트 모드 주소 지정에서 cs, fs, dsss 세그먼트 레지스터를 사용하지 않지만 fsgs 레지스터를 사용할 수 있습니다. fsgs는 숨겨진 부분이 있고 (cs의 실제 모드에서 본 것처럼) 이 부분에는 Model Specific Registers에 매핑 된 설명자가 들어 있습니다. 위의 0xc0000101gs.base MSR 주소입니다. 시스템 호출 또는 인터럽트가 발생하면 진입점에 커널 스택이 없습니다. 따라서 MSR_GS_BASE의 값은 인터럽트 스택의 주소를 저장합니다.

다음 단계에서 우리는 리얼 모드 bootparam 구조체의 주소를 rdi에 넣고 (rsi는 처음부터 이 구조에 대한 포인터를 가지고 있음을 기억하세요) 다음과 같이 C 코드로 점프합니다:

    pushq    $.Lafter_lret    # put return address on stack for unwinder
    xorq    %rbp, %rbp    # clear frame pointer
    movq    initial_code(%rip), %rax
    pushq    $__KERNEL_CS    # set correct cs
    pushq    %rax        # target address in negative space
    lretq
.Lafter_lret:

여기서 우리는 initial_code의 주소를 rax에 넣고 리턴 주소 __KERNEL_CSinitial_code의 주소를 스택에 푸시합니다. 이 후 우리는 lretq 명령을 볼 수 있는데, 이는 반환 주소가 스택에서 추출되고(이제 initial_code의 주소가 있음) 그곳으로 점프함을 의미합니다. initial_code는 동일한 소스 코드 파일에 정의되어 있으며 다음과 같습니다:

    .balign    8
    GLOBAL(initial_code)
    .quad    x86_64_start_kernel
    ...
    ...
    ...

보시다시피 initial_codearch/x86/kerne/head64.c에 정의 되어있는 x86_64_start_kernel주소를 포함합니다. 다음과 같습니다:

asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{
    ...
    ...
    ...
}

여기에는 하나의 전달인자인 real_mode_data가 있습니다.(전에 리얼 모드 데이터의 주소를 rdi 레지스터에 전달한 것을 기억하세요).

start_kernel

"커널 진입점"-init/main.c에서 start_kernel 함수를 보기 전 마지막 준비를 해야합니다.

우선 우리는 x86_64_start_kernel 함수에서 몇 가지 확인를 할 수 있습니다:

BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map);
BUILD_BUG_ON(MODULES_VADDR - __START_KERNEL_map < KERNEL_IMAGE_SIZE);
BUILD_BUG_ON(MODULES_LEN + KERNEL_IMAGE_SIZE > 2*PUD_SIZE);
BUILD_BUG_ON((__START_KERNEL_map & ~PMD_MASK) != 0);
BUILD_BUG_ON((MODULES_VADDR & ~PMD_MASK) != 0);
BUILD_BUG_ON(!(MODULES_VADDR > __START_KERNEL));
MAYBE_BUILD_BUG_ON(!(((MODULES_END - 1) & PGDIR_MASK) == (__START_KERNEL & PGDIR_MASK)));
BUILD_BUG_ON(__fix_to_virt(__end_of_fixed_addresses) <= MODULES_END);

모듈 공간의 가상 주소가 커널 텍스트의 기본 주소보다 작지 않은 __STAT_KERNEL_map과 같은 다른 것들에 대한 검사가 있습니다, 모듈이 있는 커널 텍스트는 커널 이미지보다 작지 않습니다. BUILD_BUG_ON은 다음과 같은 매크로:

#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))

이 트릭이 어떻게 작동하는지 이해해 봅시다. 첫 번째 조건 인 MODULES_VADDR <__START_KERNEL_map을 예로 들어 보겠습니다. !! conditionscondition! = 0과 같습니다. 따라서 MODULES_VADDR <__START_KERNEL_map이 true이면 !! (condition)에서 1을 얻거나 그렇지 않으면 0을 얻습니다. 2 * !! (조건)후에 우리는 2또는 0을 얻습니다. 계산이 끝나면 두 가지 다른 동작을 얻을 수 있습니다.

  • 음수 인덱스를 가진 char 배열의 크기를 얻으려고 시도하기 때문에 컴파일 오류가 발생합니다(우리의 경우 처럼 MODULES_VADDR__START_KERNEL_map보다 작을 수 없기 때문에);

  • 컴파일 오류가 없습니다.

그게 다입니다. 일부 상수에 따라 컴파일 오류가 발생하는 흥미로운 C 트릭입니다.

다음 단계에서는 CPU 당 cr4의 쉐도우 복사본을 저장하는 cr4_init_shadow 함수의 호출을 볼 수 있습니다. 컨텍스트 스위치는 cr4의 비트를 변경할 수 있으므로 각 CPU마다 cr4를 저장해야합니다. 그리고 나서 모든 페이지 글로벌 디렉토리 엔트리를 재설정하고 cr3에서 PGT에 대한 새로운 포인터를 작성하는 reset_early_page_tables 함수의 호출을 볼 수 있습니다.

    memset(early_top_pgt, 0, sizeof(pgd_t)*(PTRS_PER_PGD-1));
    next_early_pgt = 0;
    write_cr3(__sme_pa_nodebug(early_top_pgt));

곧 새로운 페이지 테이블을 만들 것입니다. 여기서 모든 페이지 글로벌 디렉토리 항목이 0임을 알 수 있습니다. 그런 다음 next_early_pgt를 0으로 설정하고(다음 포스트에서 이에 대한 세부 사항을 볼 것입니다) early_top_pgt의 물리적 주소를 cr3에 씁니다.

그런 다음 _bss__bss_stop부터 __bss_start까지 지우고 init_top_pgt도 지웁니다. init_top_pgt는 다음과 같이 arch/x86/kerne/head_64.S에 정의되어 있습니다:

NEXT_PGD_PAGE(init_top_pgt)
    .fill    512,8,0
    .fill    PTI_USER_PGD_FILL,8,0

이것은 early_top_pgt와 정확히 같은 정의입니다.

다음 단계는 초기 IDT핸들러의 설정이지만, 큰 개념이므로 다음 장에서 볼 것입니다.

결론

이것이 리눅스 커널 초기화에 대한 첫 번째 부분입니다.

이것이 리눅스 커널 부팅 과정의 네 번째 부분입니다. 질문이나 제안이 있으면 트위터 0xAXemail을 보내거나 issue를 만드십시오.

다음 부분에서는 초기 인터럽트 처리기, 커널 공간 메모리 매핑 등의 초기화를 볼 수 있습니다.

모국어가 영어가 아니면 죄송합니다. 실수를 발견하면 PR을 linux-insides로 보내주십시오.

링크

Last updated