[1000줄 OS 구현하기] 애플리케이션
2025. 3. 9. 21:58ㆍCS/OS
애플리케이션 | OS in 1,000 Lines
operating-system-in-1000-lines.vercel.app
앞서 가상 메모리를 paging을 통해서 관리하도록 기능을 구현했다.
이제 커널 위에서 애플리케이션을 동작시키기 위해서 고민해 보자.
1. 메모리 레이아웃
애플리케이션을 위한 주소 공간을 배치하기 위해서 애플리케이션용 링커 스크립트를 추가한다.
내용은 커널의 링커 스크립트와 거의 비슷하다.
- user.ld
ENTRY(start)
SECTIONS {
. = 0x1000000;
/* machine code */
.text :{
KEEP(*(.text.start));
*(.text .text.*);
}
/* read-only data */
.rodata : ALIGN(4) {
*(.rodata .rodata.*);
}
/* data with initial values */
.data : ALIGN(4) {
*(.data .data.*);
}
/* data that should be zero-filled at startup */
.bss : ALIGN(4) {
*(.bss .bss.* .sbss .sbss.*);
. = ALIGN(16);
. += 64 * 1024; /* 64KB */
__stack_top = .;
ASSERT(. < 0x1800000, "too large executable");
}
}
ENTRY(start)
: 프로그램 실행 시start
로 진입. (커널은ENTRY(boot)
였다.)SECTIONS { . = 0x1000000;
: 기본 주소를 지정. (커널은0x80200000
였다.)KEEP(*(.text.start))
:.text.start
섹션을 항상 시작 부분에 배치.
: 중요한 부팅 코드가 링커의 최적화 과정에서 실수로 제거되거나 재배치되는 것을 방지.
: 커널은.text.boot
였다.. = ALIGN(16);
: 현재 주소를 16바이트 경계에 맞춰 배치.
: 많은 현대 프로세서들은 16바이트 정렬된 메모리 접근에서 최적의 성능을 발휘하기 때문이라고 함.. += 64 * 1024; /* 64KB */
: 스택을 위한 64KB 크기의 공간을 할당
: 실제 상용 운영체제들은 프로세스당 1MB~8MB 정도의 스택을 할당함.ASSERT(. < 0x1800000, "too large executable");
:.bss
섹션 끝(즉, 애플리케이션 메모리 끝)이0x1800000
을 초과하지 않도록 제한.
프로그램이 시작될 때 스택 영역도 0
으로 초기화된 상태에서 시작되어야 하기 때문에, 스택을 .bss
섹션에 포함시키는 것 같다.
또 이렇게 하면 커널과 달리 실행 파일의 크기를 예측하기 쉽고, 메모리 레이아웃도 단순함.
2. 사용자 프로그램 라이브러리
사용자 애플리케이션 구동에 필요한 최소한의 기능만 우선 추가한다.
- user.c
#include "user.h"
extern char __stack_top[];
__attribute__((noreturn)) void exit(void) {
for (;;);
}
void putchar(char c) {
/* TODO */
}
__attribute__((section(".text.start")))
__attribute__((naked))
void start(void) {
__asm__ __volatile__(
"mv sp, %[stack_top] \n"
"call main \n"
"call exit \n"
:: [stack_top] "r" (__stack_top)
);
}
for (;;);
: 무한 루프를 도는 코드"mv sp, %[stack_top] \n"
: 애플리케이션의 stack point 설정."call main \n"
: 애플리케이션의main
함수를 호출함."call exit \n"
:main
함수의 실행이 끝나면exit
함수를 호출하여 프로그램을 종료.
- user.h
#pragma once
#include "common.h"
__attribute__((noreturn)) void exit(void);
void putchar(char ch);
3. 첫 번째 애플리케이션
- shell.c
#include "user.h"
void main(void) {
for (;;);
}
아직 문자를 화면에 표시하는 방법이 없으므로 단순 무한 루프를 도는 코드를 작성.
4. 애플리케이션 빌드하기
- run.sh
#!/bin/bash
set -xue
QEMU=qemu-system-riscv32
# Path to clang and compiler flags
# mac Users:
# CC=/opt/homebrew/opt/llvm/bin/clang
# Ubuntu users:
CC=clang
CFLAGS="-std=c11 -O2 -g3 -Wall -Wextra --target=riscv32 -ffreestanding -nostdlib"
# Build the kernel
$CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf kernel.c common.c
# Start QEMU
$QEMU -machine virt -bios default -nographic -serial mon:stdio --no-reboot -kernel kernel.elf
커널을 빌드하기 위해서 사용한 기존의 run.sh
다.
애플리케이션을 빌드하기 위해서 기존의 run.sh
를 수정한다.
- run.sh
#!/bin/bash
set -xue
QEMU=qemu-system-riscv32
CFLAGS="-std=c11 -O2 -g3 -Wall -Wextra --target=riscv32 -ffreestanding -nostdlib"
# Path to clang and compiler flags
# mac Users:
# CC=/opt/homebrew/opt/llvm/bin/clang
# Ubuntu users:
CC=clang
# Path to llvm-objcopy
# mac Users:
# OBJCOPY=/opt/homebrew/opt/llvm/bin/llvm-objcopy
# Ubuntu users:
OBJCOPY=llvm-objcopy # 추가
# Build the shell (application)
$CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf shell.c user.c common.c # 추가
$OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin # 추가
$OBJCOPY -Ibinary -Oelf32-littleriscv shell.bin shell.bin.o # 추가
# Build the kernel
$CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf \
kernel.c common.c shell.bin.o # 수정
# Start QEMU
$QEMU -machine virt -bios default -nographic -serial mon:stdio --no-reboot -kernel kernel.elf
OBJCOPY=llvm-objcopy
: objcopy는 바이너리 파일의 형식을 변환하는 도구.$CC $CFLAGS -Wl,-Tuser.ld -Wl,-Map=shell.map -o shell.elf shell.c user.c common.c
: 애플리케이션의 C 파일들을 컴파일하고,user.ld
링커 스크립트를 사용해 링킹한다.
: ELF 파일을 생성한다.$CC $CFLAGS -Wl,-Tkernel.ld -Wl,-Map=kernel.map -o kernel.elf \ kernel.c common.c shell.bin.o
: 커널 빌드 시shell.bin.o
추가.
$OBJCOPY --set-section-flags .bss=alloc,contents -O binary shell.elf shell.bin
$OBJCOPY -Ibinary -Oelf32-littleriscv shell.bin shell.bin.o
shell.o
를 단순화하기 위해서 shell.bin
로 변환 후 shell.bin.o
로 재변환.
ELF 형식에서 메모리 매핑 정보를 제거하고 실제 메모리 내용만 남겨 단순화한다.
이 과정에서 파일 크기가 줄어든다.
또한 바이너리 형태는 메모리에 직접 로드하기 더 쉽다.
일반적인 OS에서는 그냥 ELF를 사용한다고 하니 중요하진 않은 것 같다.
5. 실행 파일 디스어셈블
$ llvm-objdump -d shell.elf
shell.elf: file format elf32-littleriscv
Disassembly of section .text:
01000000 <start>:
1000000: 37 05 01 01 lui a0, 0x1010
1000004: 13 05 05 26 addi a0, a0, 0x260
1000008: 2a 81 mv sp, a0
100000a: 11 20 jal 0x100000e <main>
100000c: 11 20 jal 0x1000010 <exit>
...
01000000 <start>:
:start
함수가0x1000000
에 배치된 것을 알 수 있다.
'CS > OS' 카테고리의 다른 글
[1000줄 OS 구현하기] 시스템 콜 (0) | 2025.03.11 |
---|---|
[1000줄 OS 구현하기] 유저 모드 (0) | 2025.03.11 |
[1000줄 OS 구현하기] 페이지 테이블 (0) | 2025.03.07 |
[1000줄 OS 구현하기] 프로세스 (0) | 2025.03.06 |
[1000줄 OS 구현하기] 메모리 할당 (0) | 2025.02.11 |