0x07_C언어로 커널을 작성하자
0x0500 : 기술, 분석 문서/0x0503 : OS 원리와 구조실행 가능한 C언어 코드 커널 생성 방법
이번 장에서는 C 소스 파일을 추가하고, 이를 빌드하여 보호 모드 커널 이미지에 통합하는 것이다. C언어로 작성한 커널을 보호 모드 엔트리 포인트의 뒷 부분에 연결하고 엔트리 포인트에서는 C커널의 시작 부분으로 이동시켜보자.
C코드는 어셈블리어 코드와 달리 컴파일과 링크 과정을 거쳐서 최종 결과물이 생성된다. 컴파일 과정은 소스 파일을 중간 단계인 오브젝트 파일(Object file)로 변환하는 과정을 소스 파일을 해석하여 코드 영역과 데이터 영역으로 나누고, 이러한 메모리 영역에 대한 정보를 생성하는 단계이다. 링크 단계는 오브젝트 파일들의 정보를 취합하여 실행 파일에 통합하며, 필요한 라이브러리 등을 연결해주는 역할을 하는 단계이다.
빌드 조건과 제약 사항
1. C언어 라이브러리를 참조하지 않고 빌드한다.
2. 0x10200 위치에서 실행하도록 빌드한다.
/*
0x10000의 위치는 6장에서 작성한 섹터크기의 보호 모드 엔트리 포인트가 있으므로, 결합된 C코드는 512바이트 이후인 0x10200 위치부터 로딩된다. 따라서 C로 작성한 커널 부분은 빌드할때 0x10200 위치에서 실행되는 것을 전제로 해야하며, 해당 위치의 코드는 C 코드 중에 가장 먼저 실행되어야 하는 함수(엔트리 포인트)가 위치해야한다.
*/
위 코드를 보면 메모리에 로딩되는 어드레스에 따라 전역 변수의 어드레스에 접근하는 부분이 변한다는 것을 알 수 있다. 이러한 이유로 커널이 0x10200의 어드레스에서 실행되게 빌드하는 것이 필요하다.
3. 코드나 데이터 외에 기타 정보를 포함하지 않는 순수 바이너리 형태여야 한다.
일반적으로 GCC를 통해 실행파일을 생성하면 ELF 파일 포맷이나 PE파일 포맷과 같이 특정 OS에서 실행할 수 있는 포맷으로 생성된다. 이러한 파일 포맷들은 실행하는데 필요한 코드와 데이터 정보 이외의 불필요한 정보를 포함하고 있다. 해당 파일 포맷을 그대로 사용하게되면 엔트리 포인트에서 파일 포맷을 해석하여 해당 정보에 따라 처리하는 기능이 포함되어야 하므로 코드가 복잡해진다. 만일 부트 로더나 보호 모드 엔트리포인트처럼 코드와 데이터만 포함된 바이너리 파일 형태를 사용한다면, 엔트리 포인트에서 해당 어드레스로 점프(jmp)하는 것만으로 C언어를 실행할 수 있다.
소스파일 컴파일 라이브러리 사용 없이 코딩
// gcc -c -m32 -ffreestanding Main.c
-ffreestanding = 라이브러리 사용을 안한다 라는 의미..?
-m32 = 32비트 코드
-c = 코딩
오브젝트 파일 링크 - 라이브러리를 사용하지 않고 특정 어드레스에서 실행 가능한 커널 이미지 파일 생성 방법
오브젝트 파일을 링크하여 실행 파일을 만드는 방법은 소스 파일을 컴파일 하는 방법보다 까다롭다. 실행 파일을 구성하는 섹션의 배치와 로딩될 어드레스, 코드 내에서 가장 먼저 실행될 코드인 엔트리 포인트를 지정해줘야 하기 때문이다. 특히 섹션을 배치하는 작업은 오브젝트 파일이나 실행파일 구조와 관련이 있으므로 다른 작업보다 좀 더 까다로울 수 있다. 하지만, 섹션을 배치하는 방식과 크기 정렬 방식에 따라서 OS 메모리 구조와 크기가 달라지므로, 한번 아래 내용을 읽고 넘어가길을 권장한다.
섹션 배치를 다시 하는 이유는 실행 파일이 링크될때 코드나 데이터 이외의 디버깅 관련 정보와 심볼(Symbol, 함수나 변수의 이름) 정보가 포함되기 때문이다. 이러한 정보는 커널을 실행할때 불필요하므로, 최종 바이너리 파일을 생성할 때 이를 제거하려고 섹션을 재배치하는것이다. 섹션을 재배치하여 코드와 데이터를 실행 파일 앞쪽으로 이동시키면 손쉽게 나머지 부분을 제거할 수 있다.
섹션 배치와 링커 스크립트, 라이브러리를 사용하지 않은 링크
섹션은 실행 파일 또는 오브젝트 파일에 있으며 공통된 속성(코드, 데이터, 각종 심볼과 디버깅 정보 등)을 담는 영역을 뜻한다. 실행 파일이나 오브젝트 파일에는 무수히 많은 섹션이 있지만 핵심 역할을 하는 섹션은 3가지가 있다.
1. 실행 가능한 코드가 들어있는 .text 섹션
2. 초기화 된 데이터가 들어있는 .data 섹션
3. 세 번째 초기화되지않은 데이터가 들어있는 .bss 섹션
소스코드를 컴파일하여 생성한 오브젝트 파일은 각 섹션의 크기와 파일 내에 있는 오프셋 정보만 들어있다. 오브젝트 파일은 중간 단계의 생성물로, 다른 오브젝트 파일과 합쳐지기 때문이다. 합쳐지는 순서에 다라서 섹션의 어드레스는 얼마든지 변경될 수 있다.
오브젝트 파일들을 결합하여 정리하고 실제 메모리에 로딩될 위치를 결정하는 것이 바로 링커(Linker)이며, 이러한 과정을 링크(Link) 또는 링킹(Linking)이라고 부른다.
링커의 주된 역할은 오브젝트 파일 모아 섹션을 통합하고 그에 따라 어드레스를 조정하며, 외부 라이브러리에 있는 함수를 연결해주는 것이다. 하지만, 두드리기만 하면 금은보화가 나오는 요술 방망이가 아니므로 링커가 실행파일을 만들려면 파일 구성에 대한 정보가 필요하다. 바로 이때 사용하느것이 링커 스크립트(Linker Script)이다.
/* Default linker script, for normal executables */ OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386") OUTPUT_ARCH(i386) ENTRY(_start) SEARCH_DIR("/opt/cross/i386-pc-linux/lib32"); SEARCH_DIR("/opt/cross/i386-pc-linux/lib"); SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x08048000)); . = SEGMENT_START("text-segment", 0x08048000) + SIZEOF_HEADERS; .interp : { *(.interp) } .note.gnu.build-id : { *(.note.gnu.build-id) } .hash : { *(.hash) } .gnu.hash : { *(.gnu.hash) } .dynsym : { *(.dynsym) } .dynstr : { *(.dynstr) } .gnu.version : { *(.gnu.version) } .gnu.version_d : { *(.gnu.version_d) } .gnu.version_r : { *(.gnu.version_r) } .rel.init : { *(.rel.init) } .rel.text : { *(.rel.text .rel.text.* .rel.gnu.linkonce.t.*) } .rel.fini : { *(.rel.fini) } .rel.rodata : { *(.rel.rodata .rel.rodata.* .rel.gnu.linkonce.r.*) } .rel.data.rel.ro : { *(.rel.data.rel.ro .rel.data.rel.ro.* .rel.gnu.linkonce.d.rel.ro.*) } .rel.data : { *(.rel.data .rel.data.* .rel.gnu.linkonce.d.*) } .rel.tdata : { *(.rel.tdata .rel.tdata.* .rel.gnu.linkonce.td.*) } .rel.tbss : { *(.rel.tbss .rel.tbss.* .rel.gnu.linkonce.tb.*) } .rel.ctors : { *(.rel.ctors) } .rel.dtors : { *(.rel.dtors) } .rel.got : { *(.rel.got) } .rel.bss : { *(.rel.bss .rel.bss.* .rel.gnu.linkonce.b.*) } .rel.ifunc : { *(.rel.ifunc) } .rel.plt : { *(.rel.plt) PROVIDE_HIDDEN (__rel_iplt_start = .); *(.rel.iplt) PROVIDE_HIDDEN (__rel_iplt_end = .); } .init : { KEEP (*(SORT_NONE(.init))) } .plt : { *(.plt) *(.iplt) } .text : { *(.text.unlikely .text.*_unlikely .text.unlikely.*) *(.text.exit .text.exit.*) *(.text.startup .text.startup.*) *(.text.hot .text.hot.*) *(.text .stub .text.* .gnu.linkonce.t.*) /* .gnu.warning sections are handled specially by elf32.em. */ *(.gnu.warning) } .fini : { KEEP (*(SORT_NONE(.fini))) } PROVIDE (__etext = .); PROVIDE (_etext = .); PROVIDE (etext = .); .rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) } .rodata1 : { *(.rodata1) } .eh_frame_hdr : { *(.eh_frame_hdr) } .eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) } .gcc_except_table : ONLY_IF_RO { *(.gcc_except_table .gcc_except_table.*) } /* These sections are generated by the Sun/Oracle C++ compiler. */ .exception_ranges : ONLY_IF_RO { *(.exception_ranges .exception_ranges*) } /* Adjust the address for the data segment. We want to adjust up to the same address within the page on the next page up. */ . = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE)); /* Exception handling */ .eh_frame : ONLY_IF_RW { KEEP (*(.eh_frame)) } .gcc_except_table : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) } .exception_ranges : ONLY_IF_RW { *(.exception_ranges .exception_ranges*) } /* Thread Local Storage sections */ .tdata : { *(.tdata .tdata.* .gnu.linkonce.td.*) } .tbss : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) } .preinit_array : { PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array)) PROVIDE_HIDDEN (__preinit_array_end = .); } .init_array : { PROVIDE_HIDDEN (__init_array_start = .); KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array )) PROVIDE_HIDDEN (__init_array_end = .); } .fini_array : { PROVIDE_HIDDEN (__fini_array_start = .); KEEP (*(SORT(.fini_array.*))) KEEP (*(.fini_array )) PROVIDE_HIDDEN (__fini_array_end = .); } .ctors : { /* gcc uses crtbegin.o to find the start of the constructors, so we make sure it is first. Because this is a wildcard, it doesn't matter if the user does not actually link against crtbegin.o; the linker won't look for a file to match a wildcard. The wildcard also means that it doesn't matter which directory crtbegin.o is in. */ KEEP (*crtbegin.o(.ctors)) KEEP (*crtbegin?.o(.ctors)) /* We don't want to include the .ctor section from the crtend.o file until after the sorted ctors. The .ctor section from the crtend file contains the end of ctors marker and it must be last */ KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors)) KEEP (*(SORT(.ctors.*))) KEEP (*(.ctors)) } .dtors : { KEEP (*crtbegin.o(.dtors)) KEEP (*crtbegin?.o(.dtors)) KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors)) KEEP (*(SORT(.dtors.*))) KEEP (*(.dtors)) } .jcr : { KEEP (*(.jcr)) } .data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) } .dynamic : { *(.dynamic) } .got : { *(.got) *(.igot) } . = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 12 ? 12 : 0, .); .got.plt : { *(.got.plt) *(.igot.plt) } .data : { *(.data .data.* .gnu.linkonce.d.*) SORT(CONSTRUCTORS) } .data1 : { *(.data1) } _edata = .; PROVIDE (edata = .); . = .; __bss_start = .; .bss : { *(.dynbss) *(.bss .bss.* .gnu.linkonce.b.*) *(COMMON) /* Align here to ensure that the .bss section occupies space up to _end. Align after .bss to ensure correct alignment even if the .bss section disappears because there are no input sections. FIXME: Why do we need it? When there is no .bss section, we don't pad the .data section. */ . = ALIGN(. != 0 ? 32 / 8 : 1); } . = ALIGN(32 / 8); . = SEGMENT_START("ldata-segment", .); . = ALIGN(32 / 8); _end = .; PROVIDE (end = .); . = DATA_SEGMENT_END (.); /* Stabs debugging sections. */ .stab 0 : { *(.stab) } .stabstr 0 : { *(.stabstr) } .stab.excl 0 : { *(.stab.excl) } .stab.exclstr 0 : { *(.stab.exclstr) } .stab.index 0 : { *(.stab.index) } .stab.indexstr 0 : { *(.stab.indexstr) } .comment 0 : { *(.comment) } /* DWARF debug sections. Symbols in the DWARF debugging sections are relative to the beginning of the section so we begin them at 0. */ /* DWARF 1 */ .debug 0 : { *(.debug) } .line 0 : { *(.line) } /* GNU DWARF 1 extensions */ .debug_srcinfo 0 : { *(.debug_srcinfo) } .debug_sfnames 0 : { *(.debug_sfnames) } /* DWARF 1.1 and DWARF 2 */ .debug_aranges 0 : { *(.debug_aranges) } .debug_pubnames 0 : { *(.debug_pubnames) } /* DWARF 2 */ .debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) } .debug_abbrev 0 : { *(.debug_abbrev) } .debug_line 0 : { *(.debug_line .debug_line.* .debug_line_end ) } .debug_frame 0 : { *(.debug_frame) } .debug_str 0 : { *(.debug_str) } .debug_loc 0 : { *(.debug_loc) } .debug_macinfo 0 : { *(.debug_macinfo) } /* SGI/MIPS DWARF 2 extensions */ .debug_weaknames 0 : { *(.debug_weaknames) } .debug_funcnames 0 : { *(.debug_funcnames) } .debug_typenames 0 : { *(.debug_typenames) } .debug_varnames 0 : { *(.debug_varnames) } /* DWARF 3 */ .debug_pubtypes 0 : { *(.debug_pubtypes) } .debug_ranges 0 : { *(.debug_ranges) } /* DWARF Extension. */ .debug_macro 0 : { *(.debug_macro) } .gnu.attributes 0 : { KEEP (*(.gnu.attributes)) } /DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) } }
// 내용이 많아 가져다 씀...
GCC 크로스 컴파일러을 열어보면, 아래와 같은 구조가 반복되는 것을 알 수 있다. 링커 스크립터의 구조를 아래에 표시된 기본 형식에 대입해보면 SectionName과 그 내부 오브젝트 파일에서 통합할 섹션의 이름과 정렬할 기준값, 그리고 섹션의 초깃값을 쉽게 찾을 수 있다.
위의 내용을 이용하여 GCC를 크로스 컴파일한 후 생성된 32비트용 링커 스크립트 파일을 정리해 보겠다. 32비트용 링커 스크립트 파일은 CrossCompiler/x86_64-pc-linux/lib/ldscripts/elf_i386.x이다. 이 파일을 01.Kernel32/elf_i386.x라는 이름으로 저장하여 재배치 작업을 준비한다.
섹션의 재배치는 텍스트나 데이터와 관계없는 섹션(.tdata, .tbss, .ctors, .got 등)의 기본 구조, 즉 'SectionName{...}' 부분 전체를 코드 및 데이터 섹션의 뒷부분으로 이동하거나, 코드 및 데이터에 관련된 섹션(.text, .data, .bss, .rodata)을 가장 앞으로 이동하는 것이 수월하므로 관련된 섹션을 링커 스크립트의 가장 앞쪽으로 이동하겠다. 섹션 크기 정렬 부분은 ALIGN() 부분의 값을 수정함으로써 변경할 수 있다. 크기 정렬 값은 임의 값으로 설정해도 괜찮지만, 편의상 데이터 섹션의 시작을 섹터 크기(512바이트)에 맞추겠다. 이후에 커널의 공간이 부족하다면 이 값을 더 작게 줄임으로써 보호 모드 커널이 차지하는 비중을 줄일 수 있다.
// 수정코드 생략.
이후 다음과 같은 빌드 명령어를 사용하여 실행파일을 생성하자.
./64bit-Multicore-OS/util/CrossCompiler/bin/x86_64-pc-linux-ld -melf_i386 -T elf_i386.x -nostdlib Main.o -o Main.elf
명령어 |
설명 |
-melf_i386 |
기본적으로 64비트 코드를 생성하므로 32비트 실행 파일을 만들기 위해 설정한 옵션 |
-T elf_i386.x |
elf_i386.x 링커 스크립트를 이용해서 링크 수행 |
-nostdlib |
표준 라이블러리 Standard Library 를 사용하지 않고 링크 수행 |
-o Main.elf |
링크하여 생성할 파일 이름 |
로딩할 메모리 어드레스와 엔트리 포인트 지정
어셈블리어로 작성된 부트로더나 보호 모드 엔트리 포인트처럼 C 코드 역시 로딩될 메모리를 미리 예측하고 그에 맞춰 이미지를 생성하는 것이 중요하다. 만약 이미지를 로딩할 어드레스에 맞춰서 생성하지 않는다면 전역변수와 같이 선형 어드레스를 직접 참조하는 코드는 모두 잘못된 어드레스에 접근하기 때문이다.
메모리에 로딩하는 어드레스를 지정하는 링커 스크립트를 수정하는 방법과 링커(LD) 프로그램의 명령줄(Command Line) 옵션으로 지정하는 방식 2가지가 있다. 링커 스크립트를 통해 수정하려면 스크립트 파일의 '.text' 섹션을 아래와 같이 수정한다. '.text' 섹션의 어드레스를 수정하면 그 이후에 있는 '.data'와 '.bss'같은 섹션은 자동으로 '.text'가 로딩되는 어드레스 이후로 계산되므로 다른 섹션들은 수정하지 않아도 된다. 보호 모드 커널은 부트 로더에 의해 0x10000에 로디오디며, 0x10000의 어드레스에는 512 바이트 크기의 보호 모드 엔트리 포인트(EntryPoint.s) 코드가 있으니 C코드는 0x10200 어드레스 부터 시작할 것이다.
링커 스크립트를 수정해 로딩할 메모리 어드레스를 지정하려면..
엔트리 포인트 역시 링커 스크립트 또는 커맨드 라인 옵션으로 지정할 수 있다.
사실 엔트리 포인트를 링커에 지정하는 작업은 빌드의 결과물이 OS에 의해 실행 가능한 파일 포맷(리눅스의 elf 파일 포맷, 윈도우의 PE 파일 포맷 등)일때만 의미가 있다. 실행 파일을 바이너리 형태로 변환하는 BASH64 OS의 경우는 엔트리 포인트 정보가 제거되므로 엔트리 포인트는 큰 의미가 없으며, 단순히 링크 시에 발행하는 경고(Warning)를 피하려고 설정한 것이다. 하지만 앞서 설명햇듯이 0x10000 어드레스에 존재하는 보호 모드 엔트리 포인트는 0x10200 어드레스로 이동(jmp)하므로, C 코드의 엔트리 포인트를 해당 어드레스에 강제로 위치시킬 필요가 없다.
그럼 어떻게 해야 특정 함수를 가장 앞쪽에 위치시킬 수 있을까? 특정 함수를 실행 파일의 가장 앞쪽에 두려면 두가지 순서를 조작해야 한다.
1. 오브젝트 파일 내의 함수간의 순서
2. 실행 파일 내의 함수간의 순서
objcopy는 실행 파일 또는 오브젝트 파일을 다른 포맷으로 변환하거나 특정 섹션을 추출하여 파일로 생성해주는 프로그램으로 binutils에 포함되어 있다. objcopy는 옵션이 굉장히 많지만 섹션을 추출하여 바이너리로 바꾸는 작업만 수행하므로 -j,-S, -O 옵션에 대해서만 알아보겠다.
-j : 실행 파일에서 해당 섹션만 추출하는 옵션
-S : 실행 파일에서 재배치 정보와 심볼을 제거하는 옵션
-O : 새로 생성할 파일의 포맷을 지정하는 옵션
C소스 파일 추가와 보호 모드 엔트리 포인트 통합
C소스 파일 추가
C 커널의 엔트리 포인트가 될 Main.c 소스 파일을 생성하기에 앞서, 여러 소스 파일에서 공통으로 사용할 헤더 파일부터 생서하겠다. 이 헤더 파일은 보호 모드 커널 전반에 걸쳐 사용할 것으로, 기본 데이터 타입과 자료구조를 정의하는데 사용한다.
CHARACTER타입은 텍스트 모드 화면을 구성하는 문자 하나를 나타내는 구조체로 텍스트 모드용 비디오 메모리(0xB8000)에 문자를 편하게 출력할 목적으로 추가했다.
Main() 함수는 C코드의 엔트리 포인트 함수로써 0x10200 어드레스에 위치하며, 6장에서 작성한 보호 모드 엔트리 포인트코드에서 최초로 실행되는 코드이다. 코드를 보혐 Main() 함수를 가장 앞쪽으로 위치시켜, 컴파일 시에 코드 섹션의 가장 앞쪽에 위치하게 한 것을 알 수 있다. Main()함수의 내부는 kPrintString()함수를 사용해서 메시지를 표시하고 무한 루프를 수행하게 작성되었다.
보호 모드 엔트리 포인트 코드 수정
makefile 수정
다수의 파일을 컴파일하고 링크해야하므로 makefile이 좀 더 편리하게 수정할 필요가 있다. 따라서 make의 몇 가지 유용한 기능을 사용하여 Source 디렉터리에 .c 확장자의 파일만 추가하면 자동으로 포함하여 빌드하게 수정할 것이다.
.c 파일을 자동으로 빌드 목록에 추가려면, 매번 빌드 때마다 Source 디렉터리에 있는 *.c파일을 검색하여 소스 파일 목록에 추가해야한다. make에서 이러한 작업을 위해 디렉터리에 있는 파일을 검색하는 와일드 카드 기능을 제공한다.
디렉터리에 있는 모든 C파일을 검색했으니, 이제 이파일들에 대한 빌드 룰만 정해주면 자동으로 빌드할 수 있다. 지금까지의 makefile은 각 파일에 대해 빌드 룰을 개별적으로 기술했다. 하지만 빌드에 필요한 파일이 수백개쯤 된다면 관리하기 힘들것이다. 또한 파일이 추가되고 삭제될때마다 룰을 변경해야하는데 실수하면 오류나 실행 도중 예기치 못한 오류가 발생할 수 있다.
> ./64bit-Multicore-OS/util/CrossCompiler/bin/x86_64-pc-linux-gcc -MM Main.c Test.c > Dependency.dep
이렇게 생성한 Dependency.dep 파일을 makefile에 포함해야 각 파일의 의존 관계를 분석하여 정확한 빌드를 수행할 수 있다. make는 수행 시 다른 makefile 포함하는 기능을 제공하며, include 지시어가 바로 그러한 역할을 담당한다. 무조건 include Dependency.dep 를 수행하면 안된다. include 지시어는 해당 파일이 없으면 에러를 발생시킨다. 따라서 최초 빌드시나 오브젝트 파일을 정리하고 나서 다시 빌드할때 Dependency.dep 파일이 없으면 빌드 에러가 발생한다. 이를 피하기위해 현재 디렉터리를 검사해서 파일이 있을때만 포함해야한다. 이러한 작업은 make의 조건문과 wildcard 함수를 조합하면 된다.
커널 디렉터리는 소스 디렉터리(Source)와 임시 디렉터리(Temp)로 다시 구분되며, 커널 빌드 작업은 임시 디렉터리를 기준으로 수행한다. 따라서 Dependancy.dep 파일의 내용과 경로를 같게 하려면 make르 수행하는 디렉터리를 변경하는 옵션 -C를 이용하여 임시 디렉터리로 변경한 후 makefile을 수행한다. 최종 결과물인 보호 모드 커널 이미지는 컴파일과 링크 과정이 끝난 후에 보호 모드 엔트리 포인트와 바이너리로 변환된 C 커널을 결합하여 생성한다.
// 이후 makefile은...생략...?
'0x0500 : 기술, 분석 문서 > 0x0503 : OS 원리와 구조' 카테고리의 다른 글
0x06_32비트 보호모드 전환 (0) | 2016.10.12 |
---|---|
0x05_플로피 디스크에서 OS이미지 로딩 (0) | 2016.10.10 |
0x04_BootLoader (0) | 2016.10.02 |
0x03_프로세서 모드 및 레지스터 (0) | 2016.10.01 |
0x02_환경 구축 (0) | 2016.10.01 |