[1000줄 OS 구현하기] C 표준 라이브러리

2025. 2. 3. 23:21CS/OS

 

 

C 표준 라이브러리 | OS in 1,000 Lines

 

operating-system-in-1000-lines.vercel.app

 


1. C 표준 라이브러리

 

기본 타입과 메모리 조작, 문자열 조작 함수를 직접 구현해 본다.

 


가. 기본 타입들

 

  • common.h
typedef int bool;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef uint32_t size_t;
typedef uint32_t paddr_t;
typedef uint32_t vaddr_t;

#define true  1
#define false 0
#define NULL  ((void *) 0)
#define align_up(value, align)   __builtin_align_up(value, align)
#define is_aligned(value, align) __builtin_is_aligned(value, align)
#define offsetof(type, member)   __builtin_offsetof(type, member)
#define va_list  __builtin_va_list
#define va_start __builtin_va_start
#define va_end   __builtin_va_end
#define va_arg   __builtin_va_arg

void *memset(void *buf, char c, size_t n);
void *memcpy(void *dst, const void *src, size_t n);
char *strcpy(char *dst, const char *src);
int strcmp(const char *s1, const char *s2);
void printf(const char *fmt, ...);
  • #define NULL ((void *) 0)
    : (void *)는 어떤 타입의 데이터든 가리킬 수 있는 범용 포인터 타입
    : 메모리 주소 0을 의미하며, 이는 운영체제에서 일반적으로 접근이 허용되지 않는 예약된 메모리 영역
    : 포인터가 아무것도 가리키지 않는다는 것을 나타내는 특별한 값

  • paddr_t: 물리 메모리 주소를 나타내는 타입.

  • vaddr_t: 가상 메모리 주소를 나타내는 타입. 표준 라이브러리의 uintptr_t에 해당.

  • align_up: valuealign의 배수로 맞춰 올림. 여기서 align은 2의 거듭제곱.

  • is_aligned: valuealign의 배수인지 확인. 여기서 align은 2의 거듭제곱.

  • offsetof: 구조체 내에서 특정 멤버가 시작되는 위치(바이트 단위)를 반환.

예를 들어 align_up(0x1234, 0x1000)0x2000이 되고, is_aligned(0x2000, 0x1000)은 true를 반환하지만 is_aligned(0x2f00, 0x1000)은 false를 반환한다고 한다.

 

메모리 정렬(alignment)은 시스템의 성능과 안정성을 위해 필요하다.

  • 하드웨어 접근 효율성: CPU가 메모리에 접근할 때 정렬된 주소에 더 효율적으로 접근할 수 있다.
  • 메모리 접근 오류 방지: 일부 CPU 아키텍처에서는 정렬되지 않은 메모리 접근 시 오류가 발생할 수 있음.
  • 운영체제 메모리 관리: 페이지 단위의 메모리 관리를 위해 주소 정렬이 필수적.

 

순수 C 코드로 구현할 수도 있지만 교제에서는 Clang의 확장 기능인 __builtin_ 함수를 사용함.

 


나. 메모리 조작 함수

 

common.c에 구현한다.

 

1) memcpy

void *memcpy(void *dst, const void *src, size_t n) {
    uint8_t *d = (uint8_t *) dst;
    const uint8_t *s = (const uint8_t *) src;
    while (n--)
        *d++ = *s++;
    return dst;
}

src에서 dstn 바이트를 복사.

 

uint8_t*로 캐스팅하는 이유는 1바이트 단위로 메모리에 접근하기 위해서다.

 

uint32_t와 같이 4바이트 큰 단위를 사용하면 메모리 경계를 넘어설 수 있고, 정렬되지 않은 접근으로 인한 문제가 발생할 수 있습니다.

 

추가적으로, 1바이트 단위로 접근하면 문자열이나 네트워크 패킷 처리 시 바이트 단위 조작이 용이하다.

 

문자열은 기본적으로 1바이트 단위로 처리되며, 네트워크 패킷도 바이트 스트림 형태로 전송되기 때문이다.

 

4바이트를 단위로 설정하면 1바이트를 위해서 3바이트가 낭비될 수 있다.

 

따라서 일반적인 32비트 시스템에서는…

  • 주소 버스가 32비트이므로 2^32개의 고유한 메모리 주소를 가질 수 있다.
  • 각 주소는 1바이트(8비트)를 나타내므로, 최대 메모리 용량은 2^32 bytes = 4,294,967,296 bytes = 4GB가 된다!!! 오오옷!!

 

이것이 32비트 운영체제가 일반적으로 4GB의 물리적 메모리만 직접 주소 지정할 수 있는 이유다.

 

4GB 메모리는 부족하기 때문에 64비트 운영체제를 사용하고 이는 최대 16 엑사바이트(EB), 즉 16,384,000GB의 메모리를 이론적으로 주소 지정할 수 있다.

 

2) memset

void *memset(void *buf, char c, size_t n) {
    uint8_t *p = (uint8_t *) buf;
    while (n--)
        *p++ = c;
    return buf;
}

buf의 시작 위치부터 n바이트를 문자 c로 채운다.

 

4장 Boot chapter에서 BSS 섹션 초기화 용도로 kernel.c에 구현한 코드를 common.c로 옮긴다.

 


다. 문자열 조작 함수

 

1) strcpy

char *strcpy(char *dst, const char *src) {
    char *d = dst;
    while (*src)
        *d++ = *src++;
    *d = '\0';
    return dst;
}

src 문자열을 dst로 복사.

 

원본 포인터 dst를 직접 수정하는 대신 임시 포인터 d를 사용하는 이유.

 

  1. 원본 포인터 dst의 시작 주소 보존하기 위해서. 함수가 끝날 때 반환해야 한다.
  2. 코드 안정성: 포인터 연산 중에 발생할 수 있는 실수나 부작용을 방지.

 

2) strcmp

Condition Return
s1 == s2 0
s1 > s2 양수
s1 < s2 음수
int strcmp(const char *s1, const char *s2) {
    while (*s1 && *s2) {
        if (*s1 != *s2)
            break;
        s1++;
        s2++;
    }

    return *(unsigned char *)s1 - *(unsigned char *)s2;
}

 


'CS > OS' 카테고리의 다른 글

[1000줄 OS 구현하기] 예외  (0) 2025.02.05
[1000줄 OS 구현하기] 커널 패닉  (0) 2025.02.03
[1000줄 OS 구현하기] Hello World!  (1) 2025.02.03
[1000줄 OS 구현하기] Boot  (0) 2025.01.21
[1000줄 OS 구현하기] RISC-V Assembly  (0) 2025.01.17