이번 글도 꾀나 장문이 될 것 같다.
셸, 쉘? 셸 Shell 이다.
드디어 내 0SOS의 CLI 의 초기버전이 완성이 되었다.
이제 무언가 OS같은 모습을 가지게 되었달까…
그와 더불어 소스 트리가 커지고 있다. 무서울정도로…
현제 0SOS의 소스 트리이다. 다룰것도 많고 다룬것도 많다.
아무튼, 이번건 개발을 이곳 저곳에서 했는데,
얼마전에는 또 저번에 혼자 밤을 보냈던 24시간 카페에서 아는 지인들과 떠들면서 코딩하면서
Printf 함수와 SPrintf 함수를 구현했다.
근데 사실, 떠든게 더 많아서 그런지 카페에선 밍기적밍기적 코딩했다.
그러고 다음날도 코딩하고 다다음날도 코딩…
막, 폭풍처럼 개발하고 쉬엄 쉬엄 블로그 포스팅하며 정리하는거도 참 소소한 즐거움 인것 같다.
이 글도 아마 한번에 다 쓴글이 아니라 중간중간 틈틈히 쓸거 같긴 하다.
아무튼 아무튼, 0SOS는 콘솔 셸을 가지게 되었다.
멋지게 스크롤 하는 모습도 멋지다.
이미지로만 보면 별롤 지도 몰라서
이번엔 영상으로 준비했다.
저렇게 명령어 셸로 돌리기 위해 얼마나 많은 코드와 준비를 거쳤는가….
16비트 어셈코드로 이뤄진 부트로더부터 시작하여, 16비트 모드에서 열씸히 준비해서 32비트 커널로 넘어가고 32비트에선 또 64비트로 넘어가기 위해 준비해서 (말이 준비지 별별게 다 필요한… 쿨럭) 64비트로 올라오고, 키보드 드라이버, 인터럽트핸들링을 위해 준비하고, 인터럽트를 활용하고..
이번에는 Printf함수를 만들고(나중에 쓰려고 SPrintf함수도 만들고), GetCh 함수를 만들어서 입력을 받는다. 그리고 셸 코드쪽에서 처리하는 형식.
이번에도 책은 잘 안봤다. 동작만 똑같이 하면 되지 하며…(게다가 무거워서 저번과 똑같이 책을 안들고 갔었다 ㅋㅋ)
일단, 가변인자를 사용하는거야 뭐… 많은 C언어 예제들이 있으니까 크게 걱정 없었다.
다만, 이걸 OS개발시 써먹을 수 있는지에 대해 좀 찾아보긴 했었다.
문제없이 사용 가능해 보인다.
일단 가변인자는 해결 완료.
그럼 그걸 쓸 쪽인 출력 포멧과 이스케이프 시퀀스를 구현해야 한다.
결과적으로, %d 와 %x, %o는 숫자->문자열 화를 거친뒤 화면에 또는 메모리에 출력해야 한다.
itoa 같은 표준에서 제공하는 함수는 분명히 효율적인 알고리즘을 가진 상태로 있을 것 이라고 생각했고, 구글링을 좀 해본 결과 아주 멋진 코드를 발견했다.
BOOL _itoa(long _value, char* _result, int _base)
{
if (_base < 2 || _base > 36) { *_result = '\0'; return FALSE; }
char* ptr = _result, *ptr1 = _result, tmp_char;
int tmp_value;
//음수 양수 상관없이 처리가 가능함.
do {
tmp_value = _value;
_value /= _base;
*ptr++ = "zyxwvutsrqponmlkjihgfedcba9876543210123456789abcdefghijklmnopqrstuvwxyz"[35 + (tmp_value - _value * _base)];
} while (_value);
//Sign 처리
if (tmp_value < 0) *ptr++ = '-';
*ptr-- = '\0';
//Reverse
while (ptr1 < ptr) {
tmp_char = *ptr;
*ptr-- = *ptr1;
*ptr1++ = tmp_char;
}
return TRUE;
}
약간 내 코드로 변경하긴 했지만 동작 구조는 똑같다.
gcc에서 사용하던 itoa 라고 한다.
문자열 테이블을 만들고, 양수 음수에 상관없게끔 reverse테이블을 준비해둔다.
그리고 그걸 이용해서 숫자에서 문자로 뽑아낸다.
이제 2진수에서 36진수까지 지원하는 itoa함수가 있으니, 문자를 숫자로 바꾸는 함수인 atoi 함수를 구현하기로 한다.
이 함수는 굳이 안찾아봐도 효율적인 방법이 보여서 걍 구현했다.
BOOL _atoi(const char* _number, long* _value, int _base)
{
if (!_number)
return FALSE;
if (_base < 2 || _base > 36)
return FALSE;
int sign = 1;
int i = 0;
if (_number[0] == '-')
{
sign = -1;
i = 1;
}
long result = 0;
char c = 0;
while ((c = _number[i++]) != '\0')
{
int num = _ctoi(c);
if (num > _base)
return FALSE;
result = result * _base + num;
}
*_value = sign*result;
return TRUE;
}
위 코드에서 _ctoi 함수는 문자와 대응하는 숫자를 반환한다 ex) ‘a’ 는 10
받은 숫자를 가지고 r = r * base + v 형태로 자릿수마다 반복했다.
예를 들어, ‘202’ 라는 10진수 숫자가 들어오면
1) r = 0 * 10 + 2
r = 2
2) r = 2 * 10 + 0
r = 20
3) r = 20 * 10 + 2
r = 202
종료
해서 202를 뽑아낸다,
16진수, 8진수라고 해서 다르지 않다.
아무튼, 활용을 위해 두 함수를 Utility/String.c 에 구현했다.
이제 문자를 숫자로 변환하는 함수가 준비되었으니
대망의 Printf와 SPrintf 함수를 구현해야한다.
그전에, 출력시 사용하는 attribute에 대한 정보와, 커서위치정보등을 해더파일에 정의해두었다.
//Console.h
#ifndef __CONSOLE_H__
#define __CONSOLE_H__
#include <Types.h>
#include <stdarg.h>
#define FORMAT_BUFFER_SIZE 200
#define PRINT_MEMORY 0x01
#define PRINT_OUTPUT 0x02
#define CONSOLE_WIDTH 80
#define CONSOLE_HEIGHT 25
#define CONSOLE_VIDEO_MEMORY 0xB8000
#define CONSOLE_BACKGROUND_BLACK 0x00
#define CONSOLE_BACKGROUND_BLUE 0x10
#define CONSOLE_BACKGROUND_GREEN 0x20
#define CONSOLE_BACKGROUND_CYAN 0x30
#define CONSOLE_BACKGROUND_RED 0x40
#define CONSOLE_BACKGROUND_MAGENTA 0x50
#define CONSOLE_BACKGROUND_BROWN 0x60
#define CONSOLE_BACKGROUND_WHITE 0x70
#define CONSOLE_BACKGROUND_BLINK 0x80
#define CONSOLE_FOREGROUND_DARKBLACK 0x00
#define CONSOLE_FOREGROUND_DARKBLUE 0x01
#define CONSOLE_FOREGROUND_DARKGREEN 0x02
#define CONSOLE_FOREGROUND_DARKCYAN 0x03
#define CONSOLE_FOREGROUND_DARKRED 0x04
#define CONSOLE_FOREGROUND_DARKMAGENTA 0x05
#define CONSOLE_FOREGROUND_DARKBROWN 0x06
#define CONSOLE_FOREGROUND_DARKWHITE 0x07
#define CONSOLE_FOREGROUND_BLACK 0x08
#define CONSOLE_FOREGROUND_BLUE 0x09
#define CONSOLE_FOREGROUND_GREEN 0x0A
#define CONSOLE_FOREGROUND_CYAN 0x0B
#define CONSOLE_FOREGROUND_RED 0x0C
#define CONSOLE_FOREGROUND_MAGENTA 0x0D
#define CONSOLE_FOREGROUND_BROWN 0x0E
#define CONSOLE_FOREGROUND_WHITE 0x0F
#define COLOR_DEFAULT (CONSOLE_BACKGROUND_BLACK|CONSOLE_FOREGROUND_WHITE)
#pragma pack(push, 1)
typedef struct _Struct_ConsoleCursor
{
int cursor_offset;
DWORD current_attribute;
} CONSOLESYSTEM;
typedef struct _Charactor_Struct
{
BYTE bCharactor;
BYTE bAttribute;
} CHARACTER_MEMORY;
#pragma pack(pop)
static CONSOLESYSTEM g_console_system = {0,COLOR_DEFAULT};
void _PrintStringXY(int _x, int _y, BYTE _Attribute ,const char* _str);
void _PrintChar_offset(int _offset, BYTE _attribute, char _ch);
void _Printf(char* _str, ...);
void _SPrintf(void* _dst, char* _str, ...);
char _GetCh();
void __VSPrintf(BYTE _type, const void* _dst, char* str, va_list _arg);
void __NextLine();
void __NextScroll();
void __SetConsole_System(CONSOLESYSTEM value);
CONSOLESYSTEM __GetConsole_System();
#endif /*__CONSOLE_H__*/
define이 많다
하지만 자세히보면 그냥 Framebuffer를 구성할때 사용하는 attribute의 속성값이다.
CONSOLESYSTEM g_console_system 변수가 이제 커서의 위치, 출력 색상등을 담당한다.
//Console.c
void __UpdateWithCheckConsoleCursor()
{
if(g_console_system.cursor_offset + 1 >=
CONSOLE_HEIGHT * CONSOLE_WIDTH)
{
__NextScroll();
g_console_system.cursor_offset = CONSOLE_WIDTH*(CONSOLE_HEIGHT - 1);
}
else
{
g_console_system.cursor_offset++;
}
}
void __putch(char _ch, BYTE _attribute)
{
_PrintChar_offset(g_console_system.cursor_offset, _attribute, _ch);
__UpdateWithCheckConsoleCursor();
}
void __PrintOutInteger(long _value, char* _buffer, int _base)
{
_itoa(_value, _buffer, _base);
for (int i = 0; _buffer[i] != '\0'; i++)
{
__putch(_buffer[i], g_console_system.current_attribute);
}
}
char* __PrintMemInteger(long _value, char* _dst, int _base)
{
char Buffer[FORMAT_BUFFER_SIZE];
_itoa(_value, Buffer, _base);
int length = __StringLength(Buffer);
_MemCpy(_dst, Buffer, length);
return _dst + length;
}
void __VSPrintf(BYTE _type, const void* _dst, char* str, va_list _arg)
{
char* ptr = str;
char Buffer[FORMAT_BUFFER_SIZE];
char* dst = (char*)_dst;
while (*ptr != '\0')
{
char output = *ptr;
int value = 0;
char ch = 0;
char* str = 0;
QWORD qvalue = 0;
if (*ptr == '%')
{
ptr++;
switch (*ptr)
{
case 'd':
value = (int)(va_arg(_arg, int));
if (_type == PRINT_OUTPUT)
__PrintOutInteger(value, Buffer, 10);
else if (_type == PRINT_MEMORY)
dst = __PrintMemInteger(value, dst, 10);
break;
case 'o':
value = (int)(va_arg(_arg, int));
if (_type == PRINT_OUTPUT)
__PrintOutInteger(value, Buffer, 8);
else if (_type == PRINT_MEMORY)
dst = __PrintMemInteger(value, dst, 8);
break;
case 'x':
value = (int)(va_arg(_arg, int));
if (_type == PRINT_OUTPUT)
__PrintOutInteger(value, Buffer, 16);
else if (_type == PRINT_MEMORY)
dst = __PrintMemInteger(value, dst, 16);
break;
//case 'f':
//case 'g':
//TODO: Floating Point
case 'p':
qvalue = (QWORD)(va_arg(_arg, void*));
if (_type == PRINT_OUTPUT)
{
__putch('0', g_console_system.current_attribute);
__putch('x', g_console_system.current_attribute);
__PrintOutInteger(qvalue, Buffer, 16);
}
else if (_type == PRINT_MEMORY)
{
dst[0] = '0';
dst[1] = 'x';
dst = __PrintMemInteger(qvalue, dst+ 2, 16);
}
break;
case 'c':
ch = (char)(va_arg(_arg,int));
if (_type == PRINT_OUTPUT)
__putch(ch, g_console_system.current_attribute);
else if (_type == PRINT_MEMORY)
{
dst[0] = ch;
dst++;
}
break;
case 's':
str = (char*)(va_arg(_arg, char*));
if (_type == PRINT_OUTPUT)
_Printf(str);
else if (_type == PRINT_MEMORY)
{
_SPrintf(dst, str);
int len = __StringLength(str);
dst += len;
}
break;
default:
break;
}
}
Printf 함수와 SPrintf 함수의 심장이 되는 VSPrintf 내부 모습이다.
위엔 VSPrintf 함수 구현을 돕기위해 정의한 함수들이다.
책과는 아마 많이 다르게 구현되있을탠데, 책에서는 Printf 에서 Buffer를 중간에 두고 있는점때문에 버퍼를 안두고 바로 쓰게끔 했다.
그중에 __putch도 있다.
아무튼 자세히 보면 %문자를 만나면 처리하는 형태로 출력문자를 처리하고,
Sprintf 냐 Printf냐에 따라 구현을 조금씩 다르게 했다.
잘 보면 %s 에 대해 다시 _Printf 를 하건 _SPrintf 를 하건 한다. 그 이유는 문자열안에 이스케이프 시퀀스가 등장하는것에 대한 처리때문이다.
이어서 Printf와 SPrintf함수의 구현을 보면 단순히 _VSPrintf를 타입에 맞추어 호출하고
커서위치를 조정해 주는 역할을 한다.
//Console.c
void _Printf(char* _str, ...)
{
va_list _arg;
va_start(_arg, _str);
__VSPrintf(PRINT_OUTPUT, 0, _str, _arg);
va_end(_arg);
_SetCursor(g_console_system.cursor_offset % CONSOLE_WIDTH,
g_console_system.cursor_offset / CONSOLE_WIDTH);
}
void _SPrintf(void* _dst, char* _str, ...)
{
va_list _arg;
va_start(_arg, _str);
__VSPrintf(PRINT_MEMORY, _dst, _str, _arg);
va_end(_arg);
}
void _PrintStringXY(int _x, int _y, BYTE _Attribute ,const char* _str)
{
CHARACTER_MEMORY* Address = ( CHARACTER_MEMORY* ) CONSOLE_VIDEO_MEMORY;
int i = 0;
Address+= ( _y * 80 ) + _x;
for ( i = 0; _str[i] != 0; i++)
{
Address[i].bCharactor = _str[i];
Address[i].bAttribute = _Attribute;
}
}
void _PrintChar_offset(int _offset, BYTE _attribute, char _ch)
{
CHARACTER_MEMORY* Address = ( CHARACTER_MEMORY* ) CONSOLE_VIDEO_MEMORY;
int i = 0;
Address+= _offset;
Address[0].bCharactor = _ch;
Address[0].bAttribute = _attribute;
}
void __NextLine()
{
int addoffset =CONSOLE_WIDTH - (g_console_system.cursor_offset%CONSOLE_WIDTH);
if( g_console_system.cursor_offset + addoffset >= CONSOLE_HEIGHT * CONSOLE_WIDTH)
{
__NextScroll();
g_console_system.cursor_offset = CONSOLE_WIDTH*(CONSOLE_HEIGHT - 1);
}
else
g_console_system.cursor_offset += addoffset;
}
void __NextScroll()
{
_MemCpy(CONSOLE_VIDEO_MEMORY,
CONSOLE_VIDEO_MEMORY + CONSOLE_WIDTH* sizeof(CHARACTER_MEMORY),
(CONSOLE_HEIGHT-1) * CONSOLE_WIDTH * sizeof(CHARACTER_MEMORY) );
CHARACTER_MEMORY* target = (CHARACTER_MEMORY*)(CONSOLE_VIDEO_MEMORY) + (CONSOLE_HEIGHT-1) * CONSOLE_WIDTH ;
for(int i = 0; i <CONSOLE_WIDTH; i++)
target[i].bCharactor = ' ';
}
라인을 넘기는거라던가, 스클을 넘기는거라던가
좀 까다롭긴 해도 원리는 단순하다.
스크롤 내리는건, 비디오메모리에서, 한칸을 밀어버린다고 생각하면 된다(마지막줄은 초기화)
그러곤 커서위치를 정한다.
이렇게 까지 오고 마지막으론 _GetCh 함수를 구현했다.
잘 알듯이 단순히 문자 하나 받아오는 함수이다.
//Console.c
char _GetCh()
{
KEYDATA keydata;
while(1)
{
if(GetKeyData(&keydata) == TRUE)
{
if(keydata.Flags & KEY_DOWN)
{
return keydata.ASCIICode;
}
}
}
}
이렇게 셸 만들 준비가 되었다.
일단, 좀 복잡할지도 모르지만 셸의 해더파일을 보면
//Shell.c
#ifndef __SHELL_H__
#define __SHELL_H__
#include <Types.h>
#define SHELL_INPUT_BUFFER_SIZE 500
#define SHELL_PROMPT_MESSAGE "0SOS>"
typedef void (*CommandCallBack) (const char* parameter);
#pragma pack(push, 1)
typedef struct __Struct_ShellCommandEntry
{
char* Command;
char* Comment;
CommandCallBack command_callback;
} SHELLCOMMAND;
typedef struct __Struct_Shell_Parameters
{
const char* Buffer;
int Length;
int CurrentPosition;
} PARAMETERLIST;
#pragma pack(pop)
//--------------------------------------------------- *
void Command_Help(const char* _Parameter);
void Command_Clear(const char* _Parameter);
void Command_ShutDown(const char* _Parameter);
void Command_TotalRamSize(const char* _Parameter);
void Command_StringToNumber(const char* _Parameter);
//---------------------------------------------------*
static SHELLCOMMAND g_ShellCommandTable[] = {
{"clear", "clear the consol\n-f {white, green, cyan,black} front color\n-b {black, white, blue} back ground\n"
, Command_Clear},
{"help", "help for 0SOS", Command_Help},
{"shutdown", "Shutdown PC", Command_ShutDown},
{"strtod", "String To Hex or Decimal", Command_StringToNumber},
{"totalram", "Show Ram Size", Command_TotalRamSize}
};
void Clear();
void SetAttribute(BYTE _attribute);
void StartShell();
void ExecuteCommand(const char * CommandBuffer);
void InitalizeParameter(PARAMETERLIST* _List, const char* _Parameter);
int GetNextParameter(PARAMETERLIST* _List, char* _Parameter_Out);
#endif /*__SHELL_H__*/
셸 해더파일이다.
(…)
대충 구조는 커널 엔트리에서 StartShell 함수를 호출해서 무한 루프를 돌며 명령어를 받고 출력하고 해주는 역할을 한다.
여기서 눈에 띄는 친구가 있다.
typedef void (*CommandCallBack) (const char* parameter);
짜잔, 함수 포인터다.
이친구는 SHELLCOMMNAD 구조체 안에 들어가 있으면서
g_ShellCommandTable 을 통해 명령어와 함수를 이어줬다.
그리고 InitalzieParameter 과 GetNextParameter 함수는 각각 파라미터 문자열을 등록하고, 그 문자열에서 파라미터 하나씩 때오는 함수다.
그 구조를 위해 PARAMETERLIST 구조체가 있다.
그리고 Command_로 시작하는 함수는 죄다 명령어 처리를 위한 콜백함수.
//Shell.c
void StartShell()
{
__InitializeMemoryCheck();
char CommandBuffer[SHELL_INPUT_BUFFER_SIZE];
_SetCursor(0, 17);
int CommandBufferIndex = 0;
_Printf(SHELL_PROMPT_MESSAGE);
const int Prompt_length = sizeof(SHELL_PROMPT_MESSAGE);
while(1)
{
BYTE c = _GetCh();
if(c == KEY_BACKSPACE)
{
if(CommandBufferIndex > 0)
{
int x, y;
_GetCursor(&x, &y);
int dx = x - Prompt_length;
for(int i = dx; i < CommandBufferIndex; i++)
{
CommandBuffer[i] = CommandBuffer[i + 1];
}
for(int i = 0; i< CONSOLE_WIDTH; i++)
{
_PrintStringXY(i,y, __GetConsole_System().current_attribute, " ");
}
_SetCursor(0, y);
_Printf(SHELL_PROMPT_MESSAGE);
_Printf("%s", CommandBuffer);
if((x-1) >= Prompt_length)
_SetCursor(x-1, y);
else
_SetCursor(x, y);
CommandBufferIndex --;
if(CommandBufferIndex == 0)
{
_SetCursor(Prompt_length-1, y);
}
}
}
else if(c == KEY_ENTER)
{
_Printf("\n");
if(CommandBufferIndex > 0)
{
CommandBuffer[CommandBufferIndex] = '\0';
ExecuteCommand(CommandBuffer);
}
_Printf(SHELL_PROMPT_MESSAGE);
CommandBufferIndex = 0;
_MemSet(CommandBuffer,0 , SHELL_INPUT_BUFFER_SIZE);
}
else if( (c == KEY_LSHIFT) || (c == KEY_RSHIFT) || (c == KEY_CAPSLOCK)
|| (c == KEY_NUMLOCK) || (c == KEY_SCROLLLOCK))
{
;
}
else if (c == KEY_ARROW_LEFT)
{
int x,y;
_GetCursor(&x, &y);
if((x - 1) >= Prompt_length)
{
_SetCursor(x - 1, y);
}
}
else if(c == KEY_ARROW_RIGHT)
{
int x,y;
_GetCursor(&x, &y);
if((x + 1) <= CommandBufferIndex)
{
_SetCursor(x + 1, y);
}
}
else
{
if(c == KEY_TAB)
c = ' ';
if(CommandBufferIndex < SHELL_INPUT_BUFFER_SIZE)
{
CommandBuffer[CommandBufferIndex ++ ] = c;
_Printf("%c", c);
}
}
}
}
Start Shell 함수이다. 초반에 __InitalieMemoryCheck 함수는 Utility/Memory 에 구현되어있으며 메모리 사이즈를 체크해 글로벌 변수에 넣어두고
__GetTotalRamSize() 함수를 통해 가져온다.
뭐, 별거 없음.
밑에 코드를 보면, 대충 화살표나, 쉬프트, 캡스락 키에 대해 무시하게끔 만들었다.
지금까지는, 쉬프트나 그런 키를 입력할때 이상한 문자가 같이 딸려왔었다.
저 코드로 완전히 해결되었다. (다만 저기에 없는 ALT 키같은경우는 여전히 생긴다, 나중에 싹 추가해야지)
뭐, 백스페이스로 지우는거 처리를 하고 그런 역할을 한다.
그리고 가장 중요한 엔터를 누를때 명령어에 대한 처리는, ExecuteCommand함수를 통해 처리된다.
void ExecuteCommand(const char * CommandBuffer)
{
int command_index = 0;
const int length = __StringLength(CommandBuffer);
for(command_index = 0; command_index < length; command_index++)
{
if(CommandBuffer[command_index] == ' ')
break;
}
const int CommandTableSize = sizeof(g_ShellCommandTable)/sizeof(SHELLCOMMAND);
for(int i = 0; i <CommandTableSize; i++)
{
int command_length = __StringLength(g_ShellCommandTable[i].Command);
if((command_length == command_index) && (_MemCmp(g_ShellCommandTable[i].Command, CommandBuffer, command_index) == 0))
{
g_ShellCommandTable[i].command_callback(CommandBuffer + command_index + 1);
return;
}
}
_Printf("\'%s\' command not found\n",CommandBuffer);
}
이 함수는 간단하게, 명령어와 파라미터를 분리시키고, 명령어 테이블에서 명령어에 맞는 함수를 호출한다. 호출하면서 파라미터를 문자열로 넘겨주는 역할을 한다.
커멘드에 있는 모든 함수를 다 소개하기엔 너무 긴거같으니까
help 함수만 보고, 나머지는 깃헙에서 봐주면 좋겠다!
//Shell.c
void Command_Help(const char* _Parameter)
{
_Printf("\n");
_Printf("============================= Help ===========================\n");
const int command_table_size = sizeof(g_ShellCommandTable) / sizeof(SHELLCOMMAND);
int max_command_length = 0;
for(int i = 0; i <command_table_size; i ++)
{
int length = __StringLength(g_ShellCommandTable[i].Command);
if(length > max_command_length)
max_command_length = length;
}
int x,y;
for(int i = 0; i < command_table_size; i++)
{
_Printf("%s", g_ShellCommandTable[i].Command);
_GetCursor(&x, &y);
_SetCursor(max_command_length, y);
_Printf(" : %s\n", g_ShellCommandTable[i].Comment);
}
_Printf("============================= Help ===========================\n");
_Printf("\n");
}
간단히 말하면 만들어둔 테이블 덕분에 help 땅 치면 위 함수가 호출이 된다.
셸까지 와보니까 슬슬 현대의 OS가 얼마나 대단한건지 보인다.
실제로 개발해보면 운영체제 개론에서 흔히 말하는 부팅과정과는 차원이 다르게 준비할 게 많다.
게다가 부트로더는 저장매체마다 다르게 동작해서 그것도 준비해야하고 ….
하지만 뭐랄까, 눈이 트인다고 해야하나? 로우레벨 프로그래밍에 대해서 눈이 확 트이기 시작하는 느낌이다.
다만, 시간이 많이 들어가긴 한다.
이제 다음은.. 타이머 디바이스 드라이버와,
다다음은 대망의… 테스크 기능으로 넘어가는… 쿨럭
아무튼.. 많이 오긴 왔지만, 갈길이 더 멀다! 힘내서 개발하자!