[1000줄 OS 구현하기] 애플리케이션

2025. 3. 9. 21:58CS/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에 배치된 것을 알 수 있다.