본문 바로가기

MINT64 OS 개발

7장. C언어로 커널 작성

업로드 안한 기간동안 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

Ⅱ. 커널 빌드와 실행

    1. 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

``` 생략 ```