업로드 안한 기간동안 OS는 꾸준히 공부하는 중에 있다.
양이 생각보다 많아, 모든 정보를 언제 다 업로드할지 걱정되지만
꾸준히 정리하여 업로드를 진행해보겠다.
Ⅰ. C 소스 파일 추가와 보호 모드 엔트리 포인트 통합
- 보호 모드 커널 디렉터리에 C 소스 파일을 추가하고 자동으로 포함하여 빌드하는 방법에 대해 작성되어 있다.
- 환영 메시지를 출력하는 C코드를 추가하여, 보호 모드 엔트리 포인트와 통합해볼 예정이다.
1. C 소스 파일 추가
- 여러 소스 파일에서 공통으로 사용한 헤더 파일 생성
- 해당 헤더 파일은 보호 모드 커널 전반에 걸쳐 사용한다.
- 기본 데이터 타입과 자료구조를 정의하는데 사용한다.
- 1.Kernel32의 하위 디렉토리인 Source에 Types.h 파일을 생성한다.
#ifndef __TYPES_H__
#define __TYPES_H__
#define BYTE unsigned char
#define WORD unsigned short
#define DWORD unsigned int
#define QWORD unsigned long
#define BOOL unsigned char
#define TRUE 1
#define FALSE 0
#define NULL 0
#pragma pack ( push, 1 )
//비디오 모드 중 텍스트 모드 화면을 구조하는 자료구조
typedef struct kCharactorStruct{
BYTE bCharactor;
BYTE bAttribute;
} CHARACTER;
#pragma pack( pop )
#endif /*__TYPES_H__*/
- 1.Kernel32의 Source 디렉터리에 Main.c파일을 생성한다.
- Main() 함수는 C코드의 엔트리 포인트 함수로, 0x10200 주소에 위치한다.
- 이전에 작성한 보호 모드 엔트리 포인트(EntryPoint.s) 코드에서 최초로 실행되는 C코드이다.
- Main( ) 함수를 가장 앞쪽에 위치시켜, 컴파일 시에 코드 섹션의 가장 앞쪽에 위치하게 한 것을 알 수 있다.
- Main( ) 함수의 내부는 kPrintString( )함수를 사용해서 메시지를 표시하고 무한 루프를 수행하게 작성하였다.
- kPrintString( )함수는 화면의 특정 위치(X, Y)에 문자열을 출력해주는 함수로, 텍스트 모드용 비디오 메모리 어드레스(0xB8000)에 문자를 갱신한다.
#include "Types.h"
void kPrintString (int iX, int iY, const char* pcString);
//Main 함수
void Main(void){
kPrintString( 0, 3, "C Language Kernel Started~!!!");
while(1);
}
//문자열 출력 함수
void kPrintString(int iX, int iY, const char* pcString){
CHARACTER* pstScreen = (CHARACTER*) 0xB8000;
int i;
pstScreen += (iY * 80) + iX;
for(i = 0; pcString[i] != 0; i++){
pstScreen[i].bCharactor = pcString [i];
}
}
2. 보호 모드 엔트리 포인트 코드 수정
- 이전에 작성한 보호 모드 커널의 엔트리 포인트 코드(EntryPoint.s)를 수정한다.
- 보호 모드 엔트리 포인트 이후, C커널 코드가 있으므로 무한 루프 수행하는 코드를 수정하여 0x10200으로 이동하게 변경한다.
- 리얼모드에서 보호 모드로 전환할 때처럼 CS세그먼트 셀렉터와 이동할 선형주소를 jmp명령에 같이 지정해주면 된다.
- 아래는 C커널 코드로 이동하기 위해, 수정한 부분이다.
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; 보호 모드로 진입 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; [BITS 32] ; 이하의 코드는 32bit 코드로 설정 PROTECTEDMODE: ``` 생략 ``` push ( SWITCHSUCCESSMESSAGE - $$ + 0x10000 ) ; 출력할 메시지의 어드레스르 스택에 삽입 push 2 ; 화면 Y 좌표(2)를 스택에 삽입 push 0 ; 화면 X 좌표(0)를 스택에 삽입 call PRINTMESSAGE ; PRINTMESSAGE 함수 호출 add esp, 12 ; 삽입한 파라미터 제거 ; 수정한 부분 jmp dword 0x08: 0x10200 ; C언어 커널이 존재하는 0x10200 어드레스로 이동하여 C언어 커널 수행 ; CS 세그먼트 셀렉터를 커널 코드 디스크립터(0x08)로 변경 ``` 생략 ```
3. makefile 수정
- 이전 작성한 makefile은 EntryPoint.s 파일만 사용하여, 보호 모드 커널 이미지(Kernel32.bin)을 생성하였다.
- 해당 부분에서는 다수의 파일을 컴파일 하고 링크해야 하므로 좀 더 편한 makefile로 수정한다.
- make의 몇가지 유용한 기능을 사용하여, Source 디렉터리에 .c 확장자의 파일만 추가하면 자동으로 포함하여 빌드하도록 수정한다.
- .c 파일을 자동으로 빌드 목록에 추가하려면, 매번 빌드를 수행할 때마다 Source디렉터리에 있는 *.c 파일을 검색하여 소스 파일 목록에 추가해야 한다.
- make에서는 이를 위해, 디렉터리에 있는 파이을 검색하는 와일드 카드 기능을 제공한다.
- Soucre 디렉토리에 있는 *.c 파일을 모두 검색해서 CSOURCEFILES라는 변수에 넣고 싶다면 와일드 카드를 사용하여, 아래와 같이 작성할 수 있따.
CSOURCEFILES = $(wildcard Source/*.c)
- 디렉터리에 있는 모든 C 파일을 검색하였으니, 이제 해당 파일에 대한 빌드 룰만 정해주면 자동으로 빌드할 수 있다.
- 파일 패턴에 대해, 동일한 룰을 적용하여 간단히 처리할 수 있다.
- 모든 .c 파일은 “gcc -c”라는 컴파일 과정을 통해 .o 파일로 변환된다면 아래와 같이 작성할 수 있다.
- %.o : %.c ; .c 파일을 .o 파일로 컴파일할 수 있음 gcc -c $< ; $<은 Dependency의 첫 번째 항목을 의미함
- 검색된 C 파일을 이용하여, 링크할 파일 목록을 생성한다.
- 일반적으로, 오브젝트 파일은 소스파일과 같은 이름이며 확장자만 .o로 변경된다. 따라서, 소스 파일 목록에 포함된 파일 확장자를 .c에서 .o로 수정하면 된다.
- 특정 문자를 치환하려면, patsubst 기능을 사용하면 된다.
- patsubst는 $(patsubst 수정할 패턴, 교체할 패턴, 입력 문자열) 형식으로 사용한다.
- CSOURCEFILES의 내용에서 확장자 .c를 .o로 수정하고 싶으면, 아래와 같다.
- COBJECTFILES = $(patsubst %.c, %.o, $(CSOURCEFILES))
- C 커널 엔트리 포인트 함수를 가장 앞쪽에 배치하면, 엔트리 포인트 오브젝트 파일을 COBJECTFILES의 맨 앞에 둬야 한다.
- 만일 C커널의 엔트리 포인트를 포함하는 오브젝트 파일 이름이 Main.o라고 가정한다. Main.o 파일을 COBJECTFILES에서 맨 앞에 두려면 아래와 같이 subst를 사용할 수 있다.
- CENTRYPOINTOBJECTFILE = Main.o COBJECTFILES = $(patsubst %.c, %.o, $(CSOURCEFILES)) COTHEROBJECTFILES = $(subst Main.o, , $(COBJECTFILES)) ; subst는 문자열 치환 함수로, COBJECTFILES에 있는 문자열 중에 Main.o를 공백으로 치환 Kernel32.elf: $(CENTRYPOINTOBJECTFILE) $(COBJECTFILES) ; Main.o 오브젝트 파일을 가장 앞쪽으로 이동 x86_64-pc-linux-ld.exe -o $@ $^
- 이와 같은 규칙은 어셈블리어 파일에도 적용할 수 있다.
- 보호모드 커널과 IA-32e 모드 커널에서 사용할 어셈블리어 파일은 .asm으로 생성할 예정이므로 이를 고려하여 수정한다.
- 앞서 작성한 makefile에서 .c부분만 .asm으로 수정하고 gcc컴파일 옵션 대신 NASM을 사용하게 변경하면 된다.
- But, 컴파일된 어셈블리어 오브젝트 파일과 C언어 오브젝트 파일은 같이 링크되어야 하므로 이를 고려하여 컴파일 옵션을 설정해야 한다.
- GCC 오브젝트 파일은 ELF32파일 포맷 형태를 따르므로 NASM의 오브젝트 파일 역시 동일한 포맷으로 생성되게 컴파일 옵션에 -f elf32 를 추가해야 한다.
- C언어는 헤더 파일을 정의하여, 소스 파일에서 공통으로 사용하는 데이터 타입이나 함수의 선언을 모아두고 이를 참조할 수 있다.
- 이는 소스 파일의 내용 뿐 아니라 헤더 파일이 수정되어도 소스 파일을 다시 빌드해야 하는 것을 의미한다.
- 이를 위해, 소스 파일을 모두 검사하여, 포함하는 헤더 파일을 모두 makefile의 Dependency에 기록해야 한다.
- GCC 옵션 중에 makefile용 규칙을 만들어주는 전처리기 관련 옵션 (-M옵션)을 사용하면 자동으로 헤더 파일을 추출할 수 있다.
- 그 중, -MM 옵션을 사용하면 stdio.h와 같은 시스템 헤더 파일을 제외한 나머지 헤더 파일에 대한 의존 관계를 출력할 수 있다.
- 따라서, -MM옵션을 이용하여, 소스 파일을 모두 검사하고 그 결과를 파일에 저장하면 소스 파일별 헤더 파일의 의존 관계를 확인할 수 있다.
- 아래는 Main.c와 Test.c 소스 파일의 의존관계를 구해, Dependency.dep 파일로 저장하는 예이다.
gcc -MM Main.c > Dependency.dep
- 빌드 시, Dependency.dep 파일이 없으면 빌드 에러가 발생한다.
- 이를 피하기 위해, 현재 디렉터리를 검사해서 Dependency.dep 파일이 있을 때만 포함해야 한다.
- 이러한 작업은 make의 조건문과 wildcard 함수를 조합하면 된다.
ifeq (Dependency.dep, $(wildcard Dependency.dep)) ;wildcard 함수의 결과가 Dependency.dep와 같으면 endif까지의 구문 수행 include Dependency.dep endif
4. 최종 makefile code
#####################################
# 빌드 환경 및 규칙 설정
#####################################
# 컴파일러 및 링커 정의
NASM32 = nasm
GCC32 = gcc -c -m32 -ffreestanding
LD32 = ld -melf_i386 -T ../elf_i386.x -nostdlib -e Main -Ttext 0x10200
OBJCOPY32 = objcopy -j .text -j .data -j .rodata -j .bss -S -O binary
OBJECTDIRECTORY = Temp
SOURCEDIRECTORY = Source
#####################################
# 빌드 항목 및 빌드 방법 설정
#####################################
all: prepare Kernel32.bin
prepare:
mkdir -p $(OBJECTDIRECTORY)
$(OBJECTDIRECTORY)/EntryPoint.bin: $(SOURCEDIRECTORY)/EntryPoint.s
$(NASM32) -o $@ $<
dep:
@echo === Make Dependancy File ===
make -C $(OBJECTDIRECTORY) -f ../Makefile InternalDependency
@echo === Dependancy Search Complete ===
ExecuteInternalBuild: dep
make -C $(OBJECTDIRECTORY) -f ../Makefile Kernel32.elf
$(OBJECTDIRECTORY)/Kernel32.elf.bin: ExecuteInternalBuild
$(OBJCOPY32) $(OBJECTDIRECTORY)/Kernel32.elf $@
Kernel32.bin: $(OBJECTDIRECTORY)/EntryPoint.bin $(OBJECTDIRECTORY)/Kernel32.elf.bin
cat $^ > $@
clean:
rm -f *.bin
rm -f $(OBJECTDIRECTORY)/*.*
################################################################
# Make에 의해 다시 호출되는 부분, Temp 디렉터리를 기준으로 수행됨
################################################################
CENTRYPOINTOBJECTFILE = Main.o
CSOURCEFILES = $(wildcard ../$(SOURCEDIRECTORY)/*.c)
ASSEMBLYSOURCEFILES = $(wildcard ../$(SOURCEDIRECTORY)/*.asm)
COBJECTFILES = $(subst Main.o, , $(notdir $(patsubst %.c,%.o,$(CSOURCEFILES))))
ASSEMBLYOBJECTFILES = $(notdir $(patsubst %.asm,%.o,$(ASSEMBLYSOURCEFILES)))
# .c 파일을 .o 파일로 바꾸는 규칙 정의
%.o: ../$(SOURCEDIRECTORY)/%.c
$(GCC32) -c $<
# .asm 파일을 .o 파일로 바꾸는 규칙 정의
%.o: ../$(SOURCEDIRECTORY)/%.asm
$(NASM32) -f elf32 -o $@ $<
InternalDependency:
$(GCC32) -MM $(CSOURCEFILES) > Dependency.dep
Kernel32.elf: $(CENTRYPOINTOBJECTFILE) $(COBJECTFILES) $(ASSEMBLYOBJECTFILES)
$(LD32) -o $@ $^
ifeq (Dependency.dep, $(wildcard Dependency.dep))
include Dependency.dep
endif
Ⅱ. 커널 빌드와 실행
-
- Kernel32 디렉터리에서 이전에 만든 Makefile을 실행시면, Kernel32.bin파일이 생성되는 것을 확인할 수 있다.
- 해당 크기는 약 646byte이므로 2섹터에 못 미치는 크기이다.
- 보호 모드 커널을 정상적으로 메모리에 로딩하고 실행하려면, 부트 로더에 포함된 “TOTALSECTORCOUNT” 값을 변경해야 한다.
- 따라서, 0.BootLoader디렉토리에 존재하는 BootLoader.asm 파일을 열어서 TOTALSECTORCOUNT의 값을 2로 수정한 후 다시 빌드한다.
jmp 0x07C0:START ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; MINT64 OS에 관련된 환경설정 값 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; TOTALSECTORCOUNT: dw 0x02
- QEMU가 섹터 단위로 정렬된 디스크 이미지만 처리할 수 있다.
- 따라서, 현재 QEMU를 실행해도 정상적으로 동작하지 않는다.
- 현재 커널 이미지가 약 646byte정도이므로, 2섹터에 못 미치기 때문에 마지막 섹터를 로딩하는데 문제가 발생한 것이다.
- 해당 문제는 디스크 이미지를 512byte 크기로 정렬하고, 모자란 부분을 0x00과 같은 임의의 값으로 채워주면 해결할 수 있다.
1. 이미지 메이커 프로그램 작성
- 부트 로더와 커널 이미지를 통합하여, 섹터(512byte) 크기로 정렬하고 부트 로더의 “TOTALSECTORCOUNT”에 커널의 총 섹터 수를 업데이트하는 이미지 메이커(Image Maker)프로그램을 작성한다.
- 이미지 생성하는 프로그램 작성 전, 부트 로더 이미지 파일(BootLoader.bin)에 있는 “TOTALSECTORCOUNT”의 오프셋을 확인한다.
- 파일 시작으로 부터 5byte 떨어진 2byte가 TOTALSECTORCOUNT인 것을 확인할 수 있다.
- 아래 코드는 위치 정보를 바탕으로 만든 이미지 메이커의 소스파일이다.
- 4.Utility 디렉터리에 0.ImageMaker디렉터리를 생성한 후, ImageMaker.c파일을 생성한다.
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <sys/io.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #define BYTESOFSECTOR 512 #define BYTESOFSECTOR 512 #ifndef O_BINARY #define O_BINARY 0 #endif #ifndef S_IREAD #define S_IREAD 0x400 #endif #ifndef S_IWRITE #define S_IWRITE 0x200 #endif // 함수 선언 int AdjustInSectorSize( int iFd, int iSourceSize ); void WriteKernelInformation( int iTargetFd, int iKernelSectorCount ); int CopyFile( int iSourceFd, int iTargetFd ); // Main 함수 int main(int argc, char* argv[]) { int iSourceFd; int iTargetFd; int iBootLoaderSize; int iKernel32SectorCount; int iSourceSize; // 커맨드 라인 옵션 검사 if( argc < 3 ) { fprintf( stderr, "[ERROR] ImageMaker.exe BootLoader.bin Kernel32.bin\\n" ); exit( -1 ); } // Disk.img 파일을 생성 if( ( iTargetFd = open( "Disk.img", O_RDWR | O_CREAT | O_TRUNC | O_BINARY, S_IREAD | S_IWRITE ) ) == -1 ) { fprintf( stderr , "[ERROR] Disk.img open fail.\\n" ); exit( -1 ); } //------------------------------------------------------------ // 부트 로더 파일을 열어서 모든 내용을 디스크 이미지 파일로 복사 //------------------------------------------------------------ printf( "[INFO] Copy boot loader to image file\\n" ); if( ( iSourceFd = open( argv[ 1 ], O_RDONLY | O_BINARY ) ) == -1 ) { fprintf( stderr, "[ERROR] %s open fail\\n", argv[ 1 ] ); exit( -1 ); } iSourceSize = CopyFile( iSourceFd, iTargetFd ); close( iSourceFd ); // 파일 크기를 섹터 크기인 512바이트로 맞추기 위해 나머지 부분을 0x00 으로 채움 iBootLoaderSize = AdjustInSectorSize( iTargetFd , iSourceSize ); printf( "[INFO] %s size = [%d] and sector count = [%d]\\n", argv[ 1 ], iSourceSize, iBootLoaderSize ); //------------------------------------------------------------- // 32비트 커널 파일을 열어서 모든 내용을 디스크 이미지 파일로 복사 //------------------------------------------------------------- printf( "[INFO] Copy protected mode kernel to image file\\n" ); if( ( iSourceFd = open( argv[ 2 ], O_RDONLY | O_BINARY ) ) == -1 ) { fprintf( stderr, "[ERROR] %s open fail\\n", argv[ 2 ] ); exit( -1 ); } iSourceSize = CopyFile( iSourceFd, iTargetFd ); close( iSourceFd ); // 파일 크기를 섹터 크기인 512바이트로 맞추기 위해 나머지 부분을 0x00 으로 채움 iKernel32SectorCount = AdjustInSectorSize( iTargetFd, iSourceSize ); printf( "[INFO] %s size = [%d] and sector count = [%d]\\n", argv[ 2 ], iSourceSize, iKernel32SectorCount ); //---------------------------------- // 디스크 이미지에 커널 정보를 갱신 //---------------------------------- printf( "[INFO] Start to write kernel information\\n" ); // 부트섹터의 5번째 바이트부터 커널에 대한 정보를 넣음 WriteKernelInformation( iTargetFd, iKernel32SectorCount ); printf( "[INFO] Image file create complete\\n" ); close( iTargetFd ); return 0; } // 현재 위치부터 512바이트 배수 위치까지 맞추어 0x00으로 채움 int AdjustInSectorSize( int iFd, int iSourceSize ) { int i; int iAdjustSizeToSector; char cCh; int iSectorCount; iAdjustSizeToSector = iSourceSize % BYTESOFSECTOR; cCh = 0x00; if( iAdjustSizeToSector != 0 ) { iAdjustSizeToSector = 512 - iAdjustSizeToSector; printf( "[INFO] File size [%lu] and fill [%u] byte\\n", iSourceSize, iAdjustSizeToSector ); for( i = 0 ; i < iAdjustSizeToSector ; i++ ) { write( iFd , &cCh , 1 ); } } else { printf( "[INFO] File size is aligned 512 byte\\n" ); } // 섹터 수를 되돌려줌 iSectorCount = ( iSourceSize + iAdjustSizeToSector ) / BYTESOFSECTOR; return iSectorCount; } // 부트 로더에 커널에 대한 정보를 삽입 void WriteKernelInformation( int iTargetFd, int iKernelSectorCount ) { unsigned short usData; long lPosition; // 파일의 시작에서 5바이트 떨어진 위치가 커널의 총 섹터 수 정보를 나타냄 lPosition = lseek( iTargetFd, 5, SEEK_SET ); if( lPosition == -1 ) { fprintf( stderr, "lseek fail. Return value = %d, errno = %d, %d\\n", lPosition, errno, SEEK_SET ); exit( -1 ); } usData = ( unsigned short ) iKernelSectorCount; write( iTargetFd, &usData, 2 ); printf( "[INFO] Total sector count except boot loader [%d]\\n", iKernelSectorCount ); } // 소스 파일(Source FD)의 내용을 목표 파일(Target FD)에 복사하고 그 크기를 되돌려줌 int CopyFile( int iSourceFd, int iTargetFd ) { int iSourceFileSize; int iRead; int iWrite; char vcBuffer[ BYTESOFSECTOR ]; iSourceFileSize = 0; while( 1 ) { iRead = read( iSourceFd, vcBuffer, sizeof( vcBuffer ) ); iWrite = write( iTargetFd, vcBuffer, iRead ); if( iRead != iWrite ) { fprintf( stderr, "[ERROR] iRead != iWrite.. \\n" ); exit(-1); } iSourceFileSize += iRead; if( iRead != sizeof( vcBuffer ) ) { break; } } return iSourceFileSize; }
- 0.ImageMaker 디렉터리에 아래와 같이 makefile을 간단히 작성하여 make를 실행한다.
#기본적으로 빌드를 수행할 목록 all: ImageMaker.exe # ImageMaker 빌드 ImageMaker.exe: ImageMaker.c gcc -o $@ $< # 소스 파일을 제외한 나머지 파일 정리 clean: rm -f ImageMaker.exe
2. 커널 이미지 생성과 실행
- 생성한 이미지 메이커 프로그램으로 디스크 이미지를 생성한다.
- 방금 만든 ImageMaker.exe 파일을 프로젝트 최상위 디렉터리인 MINT64 디렉터리에 복사한 후, 해당 디렉터리에 있는 makefile을 수정한다.
``` 생략 ```
Disk.img: 0.BootLoader/BootLoader.bin 1.Kernel32/Kernel32.bin
@echo
@echo =================== Disk Image Build Start ===================
@echo
./ImageMaker.exe $^
@echo
@echo ==================== All Build Complete ====================
@echo
``` 생략 ```
'MINT64 OS 개발' 카테고리의 다른 글
9장. 페이징 기능을 활성화하여, 64비트 전환 준비 (0) | 2023.07.20 |
---|---|
8장. A20 게이트를 활성화하여 1MB이상 영역에 접근 (0) | 2023.07.20 |
6. 32비트 보호 모드로 전환 (0) | 2023.05.06 |
5. OS이미지 메모리에 복사 (0) | 2023.05.06 |
4. 32비트 보호 모드로 전환 (0) | 2023.04.13 |