어플리케이션
2025년 7월 31일
터미널에서 프로그램 실행 #
프로그램 만들기 #
일단 hlt 명령만 실행하는 아주 간단한 프로그램을 작성한다.
1TARGET = onlyhlt
2
3.PHONY: all
4all: $(TARGET)
5
6onlyhlt: onlyhlt.asm Makefile
7 nasm -f bin -o $@ $<
1bits 64
2section .text
3
4loop:
5 hlt
6 jmp loop
nasm -f bin 옵션으로 컴파일하면 헤더 없는 순수 플랫 바이너리 포맷으로 생성되며, 실행파일 포맷 없이 명령어만 컴파일된다.
터미널 명령어 추가 #
터미널에서 입력한 문자열이 있지만, 어떤 내장 명령어에도 매칭되지 않을때 파일시스템에서 찾아서 실행한다.
1void Terminal::ExecuteLine() {
2 char* command = &linebuf_[0];
3 if (strcmp(command, "echo") == 0) {
4 // ...
5 } else if (command[0] != 0) {
6 auto file_entry = fat::FindFile(command);
7 if (!file_entry) {
8 Print("no such command: ");
9 Print(command);
10 Print("\n");
11 } else {
12 ExecuteFile(*file_entry);
13 }
14 }
15}
파일을 실제로 실행하는 코드인데, 내용은 간단하다.
클러스터 단위로 파일이 저장되기 때문에 파일 전체 사이즈가 채워질때까지 다음 클러스터를 찾아가며 복사해서 가져온다.
flat binary 형식이기 때문에 읽어온 파일의 시작위치를 호출하면 바로 명령어를 실행시킬 수 있다.
1void Terminal::ExecuteFile(const fat::DirectoryEntry& file_entry) {
2 auto cluster = file_entry.FirstCluster();
3 auto remain_bytes = file_entry.file_size;
4
5 std::vector<uint8_t> file_buf(remain_bytes);
6 auto p = &file_buf[0];
7
8 while (cluster != 0 && cluster != fat::kEndOfClusterchain) {
9 const auto copy_bytes = fat::bytes_per_cluster < remain_bytes ?
10 fat::bytes_per_cluster : remain_bytes;
11 memcpy(p, fat::GetSectorByCluster<uint8_t>(cluster), copy_bytes);
12
13 remain_bytes -= copy_bytes;
14 p += copy_bytes;
15 cluster = fat::NextCluster(cluster);
16 }
17
18 using Func = void ();
19 auto f = reinterpret_cast<Func*>(&file_buf[0]);
20 f();
21}
이제 빌드한 바이너리를 볼륨이미지를 생성할 때 추가하도록 스크립트를 수정하고 부트해보면 파일시스템에 앱이 표시되며 실행해보면 hlt에 의해 터미널이 프리즈되는것을 볼 수 있다.
터미널 태스크는 이 hlt 루프만 돌기 때문에 계속해서 프리즈되지만, 명령어 실행 전에 sti 를 실행해줬기 때문에 다른 태스크들은 인터럽트로 깨어나며 정상적으로 동작하는 것을 볼 수 있다. (Text Box의 커서가 깜빡이고 마우스를 움직일 수 있다.)
ELF 지원 #
이제 컴파일러를 통해서 ELF 파일을 만들고, 실행을 하려고하면 이전에 부트로더에서 커널을 로드했던 것과 비슷한 문제들이 생기게 된다. 부트로더문제
현재 로직을 확인해보면 파일을 메모리버퍼(벡터)에 읽어와서 메모리버퍼 시작위치 + EntryPoint 로 ELF 파일이 실행될 것 같지만, ELF 헤더에 있는 EntryPoint는 메모리상 주소(vaddr)이기 때문에 PHDR에 맞게 섹션을 배치해줘야 한다.
코드에서 참조하는 문자열리터럴도 문제가 있다. 컴파일할 때 image_base 기준으로 위치가 고정되어 참조하도록 컴파일되는데, 로드된 위치가 image_base가 아니기 때문에 잘못된 위치를 참조하게된다.
가상메모리 페이징 #
현재 커널 메모리는 UEFI에서 관리되던 물리 메모리 공간 중 사용가능한 공간만을 넘겨받아서 메모리매니저로 관리하고 있다. 참고
현재 커널 메모리는 long mode를 진입을 위해 identity 페이징 방식으로 물리주소 페이지 테이블을 생성 후 CR3 레지스터에 세팅해뒀다.
그래서 커널에서는 MMU에 전달되는 메모리주소가 물리주소와 동일한 상태인데, 어플리케이션을 실행하기 위해서 가상주소를 사용하여 원래의 MMU 역할처럼 사용하게된다.
물리메모리를 그대로 사용해도 되지만, 메모리공간 격리나 더 넓은 주소공간 사용 등 여러 장점으로 인해 가상메모리를 사용하는것이 기본이 된다.
ExecuteFile 변형 #
터미널에서 입력받은 문자열은 command와 나머지(first_arg)로 나뉜다. command는 파일시스템에서 찾아와서 file_entry로 전달받았지만, 프로그램 실행 시 인자로 넘겨줄 수 있도록 command와 first_arg 모두 인자 벡터(argv)로 변경해서 실행할 프로그램에 전달한다.
실행할 파일이 ELF가 아니라면(플랫파일) 기존처럼 실행하고, ELF헤더로 시작된다면 PHDR에 맞게 다시 로드 후 엔트리포인트를 실행한다.
1void Terminal::ExecuteFile(const fat::DirectoryEntry& file_entry, char* command, char* first_arg) {
2 auto cluster = file_entry.FirstCluster();
3 auto remain_bytes = file_entry.file_size;
4
5 std::vector<uint8_t> file_buf(remain_bytes);
6 auto p = &file_buf[0];
7
8 while (cluster != 0 && cluster != fat::kEndOfClusterchain) {
9 const auto copy_bytes = fat::bytes_per_cluster < remain_bytes ?
10 fat::bytes_per_cluster : remain_bytes;
11 memcpy(p, fat::GetSectorByCluster<uint8_t>(cluster), copy_bytes);
12
13 remain_bytes -= copy_bytes;
14 p += copy_bytes;
15 cluster = fat::NextCluster(cluster);
16 }
17
18 auto elf_header = reinterpret_cast<Elf64_Ehdr*>(&file_buf[0]);
19 if (memcmp(elf_header->e_ident, "\x7f" "ELF", 4) != 0) {
20 using Func = void ();
21 auto f = reinterpret_cast<Func*>(&file_buf[0]);
22 f();
23 return;
24 }
25
26 // ELF인 경우 파싱 후 실행
27 auto argv = MakeArgVector(command, first_arg);
28 auto entry_addr = elf_header->e_entry;
29 entry_addr += reinterpret_cast<uintptr_t>(&file_buf[0]);
30 using Func = int (int, char**);
31 auto f = reinterpret_cast<Func*>(entry_addr);
32 auto ret = f(argv.size(), &argv[0]);
33
34 char s[64];
35 sprintf(s, "app exited. ret = %d\n", ret);
36 Print(s);
37}
빌드 #
빌드할때 -ffreestanding 옵션을 사용하는데 표준 OS 처럼 멀티스레드, 표준입출력, 파일시스템 조작 등의 작업이 지원되지 않기 때문에 프리스탠딩 환경으로 빌드한다.
마찬가지로 익셉션이나 rtti도 제거된다.
1CPPFLAGS += -I.
2CXXFLAGS += -O2 -Wall -g --target=x86_64-elf -ffreestanding \
3 -fno-exceptions -fno-rtti -std=c++17
4LDFLAGS += --entry main -z norelro --image-base 0 --static
5
6rpn: rpn.o Makefile
7 ld.lld $(LDFLAGS) -o rpn rpn.o
8
9%.o: %.cpp Makefile
10 clang++ $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@