Skip to content

dyna-bytes/sleep_from_scratch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

16 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

C언어와 시스템 호출만으로 'sleep' 프로그램 처음부터 다시 만들기

소개: 왜 라이브러리 없이 만들어 볼까요?

최근 트위터에서 성능이 좋지 않은 GitHub의 sleep 구현을 조롱하는 글을 올린 적이 있습니다. 바쁜 대기(busy-wait)를 사용하고, 일광 절약 시간제 등으로 언제든 바뀔 수 있는 시스템 시계에 의존하는 등 문제가 많았죠. 어떤 사람들은 리눅스 배포판이 제공하는 sleep 바이너리 대신 이식성 있는 무언가를 원했을 것이라고 추측했습니다.

그래서 저는 오늘 새로운 것을 배워보기로 했습니다. 오직 리눅스 커널에만 의존하는, 완전히 독립적인 sleep 프로그램을 처음부터 만들어 보는 것입니다.

우리의 출발점은 표준 라이브러리(libc)를 사용하는 간단한 C언어 sleep 프로그램입니다.

// libc를 사용하는 기본 sleep 프로그램
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("사용법: %s <초>\n", argv[0]);
        return 1;
    }

    int seconds = atoi(argv[1]);
    printf("%d초 동안 대기합니다...\n", seconds);
    sleep(seconds);
    printf("완료.\n");

    return 0;
}

이 코드는 명령줄 인자로 받은 숫자(초)만큼 프로그램을 멈추게 합니다. 컴파일 후 ldd 명령어로 의존성을 확인해 보면, 당연히 libc에 의존하고 있음을 알 수 있습니다.

$ gcc sleep.c -o sleep_libc
$ ldd ./sleep_libc
    linux-vdso.so.1 (0x...)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (...) # libc에 의존!
    /lib64/ld-linux-x86-64.so.2 (0x...)

이 튜토리얼의 목표는 libc 의존성을 하나씩 제거하여 아무것에도 의존하지 않는 프로그램을 만드는 것입니다. 이 여정을 통해 여러분은 다음과 같은 저수준 개념을 배우게 될 것입니다.

  • 프로그램의 진정한 시작점(_start)
  • 커널과 직접 소통하는 방법, 즉 시스템 호출(System Calls)
  • C 코드 안에서 하드웨어를 제어하는 인라인 어셈블리

이제 libc라는 안전망을 제거했을 때 어떤 일이 벌어지는지 살펴보며 본격적인 여정을 시작하겠습니다.


1단계: 안전망 제거하기 - libc 없이 컴파일하기

가장 먼저 할 일은 컴파일러에게 libc를 연결하지 말라고 지시하는 것입니다. GCC에서는 -nostdlib 플래그를 사용합니다.

$ gcc -nostdlib sleep.c -o sleep_no_libc

이 명령을 실행하면 컴파일러는 다음과 같은 오류 폭탄을 쏟아냅니다.

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to ...
/usr/bin/ld: /tmp/ccXXXXXX.o: in function `main`:
sleep.c:(.text+0x2d): undefined reference to `printf`
/usr/bin/ld: sleep.c:(.text+0x44): undefined reference to `atoi`
/usr/bin/ld: sleep.c:(.text+0x61): undefined reference to `printf`
/usr/bin/ld: sleep.c:(.text+0x6c): undefined reference to `sleep`

이 오류 메시지는 단순한 실패가 아니라, 우리가 앞으로 구현해야 할 기능들의 로드맵입니다.

  1. _start: 프로그램의 실제 진입점
  2. printf: 화면 출력 기능
  3. atoi: 문자열을 정수로 변환하는 기능
  4. sleep: 프로그램을 일정 시간 멈추는 기능

이처럼 컴파일러가 친절하게 우리가 가야 할 길을 안내해 줄 것입니다. 가장 먼저 해결할 독립적인 함수, 즉 문자열을 숫자로 파싱하는 함수부터 만들어 봅시다.


2단계: 문자열을 숫자로 - atoi 직접 구현하기

atoi는 명령줄에서 받은 문자열(예: "5")을 실제 숫자 5로 변환하는 함수입니다. 그런데 저는 atoi의 원래 이름인 'A2L'은 도저히 참을 수가 없네요. 도대체 왜 그렇게 부르는 걸까요? 아마 아무도 모를 겁니다. 그래서 저는 pars_int라는 이름으로 함수를 만들겠습니다.

pars_int 함수 구현

핵심 원리는 아스키(ASCII) 코드에 있습니다. 아스키 코드에서 숫자 '0'부터 '9'까지는 순서대로 배치되어 있습니다. 예를 들어, '2'의 아스키 값에서 '0'의 아스키 값을 빼면 숫자 2가 나옵니다. (50 - 48 = 2)

중간 검증

한 번에 모든 것을 만들기보다, 각 단계를 검증하며 나아가는 것이 좋습니다. 지금은 -nostdlib 플래그를 잠시 빼고 libc를 다시 링크하여 pars_int 함수가 잘 작동하는지 테스트해 보겠습니다.

컴파일 후 실행하면 이전과 동일하게 잘 작동하는 것을 확인할 수 있습니다. 이제 우리는 의존성 하나를 성공적으로 제거했습니다. 이제 프로그램의 가장 근본적인 부분, 즉 모든 실행 파일의 진짜 시작점을 다룰 차례입니다.


3단계: 프로그램의 진짜 시작점 - _start 진입점 만들기

main은 시작점이 아니다

많은 개발자가 main 함수가 프로그램의 시작점이라고 생각하지만, 사실이 아닙니다. 실제 진입점은 _start라는 심볼(symbol)입니다. 우리가 평소에 C 프로그램을 컴파일할 때 libc가 이 _start를 자동으로 제공해 줍니다. _start는 운영체제로부터 인자를 받아 스택을 설정한 뒤, 우리가 작성한 main 함수를 호출하는 준비 작업을 합니다.

이론적 배경: System V ABI

_start가 어떻게 main 함수에 argc(인자 개수)와 argv(인자 배열)를 전달하는지 알려면, 시스템의 저수준 규칙인 System V Application Binary Interface (ABI) 문서를 참조해야 합니다. 이런 저수준 매뉴얼은 접근하기 어렵다는 평판이 있지만, 실제로는 매우 정확하고 이해하기 쉬운 경우가 많습니다.

x86-64 아키텍처용 ABI 문서를 보면, 프로그램이 시작될 때 스택의 상태는 다음과 같이 정의됩니다.

  • 스택 포인터 레지스터 RSP가 가리키는 주소에는 argc 값이 저장됩니다.
  • 그 바로 뒤(RSP+8)부터는 argv 문자열 포인터들이 차례로 위치합니다.
  • 또한, ABI는 "가장 깊은 스택 프레임을 표시하기 위해 프레임 포인터(RBP)를 0으로 설정해야 한다"고 명시합니다.

첫 번째 시도와 실패

이 정보를 바탕으로, _start에서 C 포인터를 사용해 RSP 레지스터의 값을 읽어 argcargv를 가져오는 순진한 시도를 해볼 수 있습니다.

// 잘못된 _start 구현
void _start(); // 선언

int main(int argc, char *argv[]) {
    // argc와 argv가 올바른지 확인하기 위해 출력
    printf("argc: %d\n", argc);
    printf("argv[0]: %s\n", argv[0]);
    printf("argv[1]: %s\n", argv[1]);
    return 0;
}

void _start() {
    long *rsp;
    // 인라인 어셈블리로 RSP 값을 C 변수로 가져옴
    __asm__ volatile("mov %%rsp, %0" : "=r"(rsp));

    long argc = *(long *)rsp;
    char **argv = (char **)(rsp + 8);

    main(argc, argv);
}

이제 libc를 임시로 다시 링크해서 컴파일하고 실행해 봅시다.

$ gcc -nostdlib sleep.c -o sleep_test -lc
$ ./sleep_test 5
argc: 140732895235824  # 쓰레기 값
argv[0]: (null)         # 쓰레기 값
argv[1]: (null)         # 쓰레기 값

왜 이런 쓰레기 값이 나올까요? objdump -d 명령어로 실행 파일의 어셈블리 코드를 들여다보면 답을 찾을 수 있습니다.

$ objdump -d ./sleep_test
...
00000000004010e0 <_start>:
  4010e0:   55                      push   %rbp
  4010e1:   48 89 e5                mov    %rsp,%rbp
  ...
  4010ea:   48 89 e0                mov    %rsp,%rax
...

_start 함수가 시작되자마자 컴파일러가 자동으로 함수 프롤로그(prologue) 코드(push %rbp, mov %rsp,%rbp)를 삽입했습니다. 이 코드 때문에 우리가 RSP 레지스터 값을 읽으려 할 때, 그 값은 이미 변경된 후입니다. 우리가 필요했던 초기 RSP 값은 사라진 것이죠.

해결책: Naked 함수와 인라인 어셈블리

이 문제를 해결하려면 컴파일러가 프롤로그 코드를 삽입하지 못하게 막아야 합니다. 함수에 __attribute__((naked)) 속성을 부여하면 됩니다. "Naked" 함수는 컴파일러가 어떠한 코드도 추가하지 않는 순수한 어셈블리 컨테이너가 됩니다.

이제 인라인 어셈블리로 ABI 규칙을 직접 구현해 봅시다.

AT&T Style (Default GCC)

__attribute__((naked)) void _start() {
    __asm__ volatile (
        // 1. 프레임 포인터를 0으로 초기화 (ABI 규칙)
        "xor %rbp, %rbp\n"

        // 2. main의 첫 번째 인자(argc) 설정
        // rsp가 가리키는 메모리 주소의 값(argc)을 rdi 레지스터로 옮깁니다.
        // 첫 번째 함수 인자는 rdi 를 통해 전달됩니다.
        "mov (%rsp), %rdi\n"

        // 3. main의 두 번째 인자(argv) 설정
        // rsp에서 8바이트 떨어진 주소 (argv의 시작 주소)를 rsi로 옮깁니다.
        // 두 번째 함수 인자는 rsi 를 통해 전달됩니다.
        "lea 8(%rsp), %rsi\n"

        // 4. 스택을 16바이트로 정렬 (ABI 규칙)
        // SSE 같은 최신 명령어 세트가 16바이트 정렬된 메모리에 접근할 때
        // 성능상 이점을 얻기 위해 필요한 규칙입니다.
        "and $-16, %rsp\n"

        // 5. main 함수 호출
        "call main\n"
     );
}

Intel Style

gcc -nostdlib -masm=intel sleep.c -o sleep_test -lc 로 빌드합니다.

__attribute__((naked)) void _start() {
    __asm__ volatile (
        // 1. 프레임 포인터를 0으로 초기화 (ABI 규칙)
        // rbp 레지스터를 자신과 XOR 연산하여 0으로 만듭니다.
        "xor rbp, rbp\n"

        // 2. main의 첫 번째 인자(argc) 설정
        // rsp가 가리키는 메모리 주소의 값(argc)을 rdi 레지스터로 옮깁니다.
        // 첫 번째 함수 인자는 rdi를 통해 전달됩니다.
        "mov rdi, [rsp]\n"

        // 3. main의 두 번째 인자(argv) 설정
        // rsp에서 8바이트 떨어진 주소 자체(argv의 시작 주소)를 rsi로 옮깁니다.
        // 두 번째 함수 인자는 rsi를 통해 전달됩니다.
        "lea rsi, [rsp+8]\n"
        
        // 4. 스택을 16바이트로 정렬 (ABI 규칙)
        // SSE 같은 최신 명령어 세트가 16바이트 정렬된 메모리에 접근할 때
        // 성능상 이점을 얻기 위해 필요한 규칙입니다.
        "and rsp, -16\n"

        // 5. main 함수 호출
        "call main\n"
    );
}

이제 _start를 구현하여 main 함수를 성공적으로 호출할 수 있게 되었습니다. 하지만 main 함수가 끝나면 프로그램이 "Illegal instruction" 오류와 함께 비정상적으로 종료됩니다. 다음 단계에서는 프로그램을 안전하게 종료하는 방법을 구현해 보겠습니다.


4단계: 깔끔하게 끝내기 - exit 시스템 호출 구현

프로그램이 작업을 마치면 운영체제에게 "이제 끝났습니다"라고 알려줘야 합니다. 이 역할을 하는 것이 바로 시스템 호출(System Call) 입니다. 시스템 호출은 사용자 프로그램이 운영체제 커널의 기능을 요청하는 유일한 공식적인 창구입니다. 프로그램을 종료하기 위해서는 커널에 exit 시스템 호출을 요청해야 합니다.

Syscall 래퍼 함수 만들기

시스템 호출을 사용하려면 특정 레지스터에 값을 설정하고 syscall 명령어를 실행해야 합니다. 이 과정을 편리하게 해 줄 래퍼(wrapper) 함수를 만들겠습니다.

  • x86-64 아키텍처에서 exit의 시스템 호출 번호는 60입니다.
  • 시스템 호출 번호는 rax 레지스터에 전달합니다.
  • 첫 번째 인자(종료 코드)는 rdi 레지스터에 전달합니다.
// 시스템 호출 번호 정의
#define SYS_exit 60

// 인자 1개를 받는 범용 시스템 호출 래퍼
long syscall1(long syscall_num, long arg1) {
    long result;
    __asm__ volatile (
        "syscall"
        : "=a" (result) // 출력: rax 레지스터의 값을 result 변수에 저장
        : "a" (syscall_num), "D" (arg1) // 입력: rax에 syscall_num, rdi에 arg1 저장
        : "rcx", "r11", "memory" // syscall이 변경할 수 있는 레지스터 및 메모리
    );
    return result;
}

_exit 함수 구현 및 적용

규칙에 따라 main 함수가 반환하는 값(종료 코드)은 rax 레지스터에 저장됩니다. 우리는 이 값을 exit 시스템 호출의 인자로 사용해야 합니다. 이를 위해 _exit이라는 C 함수를 만들고, _start에서는 main 호출 후 이 함수를 호출하도록 수정하겠습니다. 이 방식은 어셈블리와 C의 역할을 깔끔하게 분리합니다.

void _exit(int exit_code) {
    syscall1(SYS_exit, exit_code);
    // GCC는 exit 함수가 절대 반환하지 않는다는 것을 알고 있습니다.
    // 컴파일러 경고를 없애기 위해 무한 루프를 추가합니다.
    while(1);
}

void _start() {
    __asm__ volatile (
        "xor rbp, rbp\n"
        "mov rdi, [rsp]\n"
        "lea rsi, [rsp+8]\n"
        "and rsp, -16\n"
        "call main\n"

        // 6. main의 반환값(rax)을 _exit의 첫 인자(rdi)로 이동
        "mov rdi, rax\n"
        // 7. _exit C 함수 호출
        "call _exit\n"
    );
}

이제 main 함수가 끝나면 그 반환값이 rdi로 옮겨지고, _exit 함수가 호출되어 exit 시스템 호출을 실행합니다. 프로그램을 실행해 보면 더 이상 비정상 종료 없이 올바른 종료 코드를 반환하며 깔끔하게 끝나는 것을 확인할 수 있습니다.

프로그램의 입구와 출구를 모두 만들었습니다. 이제 사용자에게 메시지를 보여주는 출력 기능을 구현할 차례입니다.


5단계: 세상과 소통하기 - print 함수와 write 시스템 호출

libc의 printf는 서식 지정 등 복잡한 기능이 많습니다. 우리는 단순히 문자열을 표준 출력(stdout) 에 쓰는 간단한 print 함수를 만들 것입니다.

write 시스템 호출

콘솔에 글자를 출력하려면 write 시스템 호출을 사용해야 합니다. 이 호출은 3개의 인자를 받습니다.

  1. 파일 디스크립터(File Descriptor): 어디에 쓸 것인가? (표준 출력은 1)
  2. 버퍼(Buffer): 무엇을 쓸 것인가? (출력할 문자열의 주소)
  3. 크기(Count): 얼마나 쓸 것인가? (문자열의 길이)

이를 위해 인자 3개를 받는 syscall3 래퍼 함수를 작성합니다. 인자는 순서대로 rdi, rsi, rdx 레지스터에 매핑됩니다.

// 인자 3개를 받는 범용 시스템 호출 래퍼
long syscall3(long syscall_num, long arg1, long arg2, long arg3) {
    long result;
    __asm__ volatile (
        "syscall"
        : "=a" (result)
        : "a" (syscall_num), "D" (arg1), "S" (arg2), "d" (arg3)
        : "rcx", "r11", "memory"
    );
    return result;
}

필요 함수 구현

write 호출에는 문자열의 길이가 필요하므로, strlen을 대체할 st_rlen 함수를 먼저 만듭니다.

// 문자열 길이를 계산하는 함수
long st_rlen(char *str) {
    char *cursor = str;
    while (*cursor != '\0') {
        cursor++;
    }
    return cursor - str;
}

이제 st_rlensyscall3를 사용하여 print 함수를 완성합니다.

// 시스템 호출 번호 정의
#define SYS_write 1

// 문자열을 표준 출력에 쓰는 함수
void print(char *str) {
    long len = st_rlen(str);
    // syscall3(호출번호, 파일디스크립터, 버퍼, 길이)
    syscall3(SYS_write, 1, (long)str, len);
}

적용 및 테스트

기존 코드의 모든 printf 호출을 새로 만든 print 함수로 교체합니다. 예를 들어, printf("사용법: %s <초>\n", argv[0]);는 다음과 같이 바꿉니다.

print("사용법: ");
print(argv[0]);
print(" <초>\n");

코드를 수정한 뒤 다시 컴파일하면, 이제 화면 출력 기능까지 libc 없이 구현된 것을 확인할 수 있습니다. 이제 마지막 남은 핵심 기능, 프로그램을 잠시 멈추게 하는 sleep을 구현할 시간입니다.


6단계: 대단원의 막 - nanosleep으로 sleep 구현하기

핵심 Syscall 소개

흥미롭게도 리눅스 커널에는 sleep이라는 이름의 시스템 호출이 없습니다. libc의 sleep 함수는 내부적으로 **nanosleep**이라는 시스템 호출을 사용합니다. nanosleep은 이름처럼 나노초 단위의 정밀한 시간 동안 프로세스를 대기시킬 수 있으며, 시스템 시간 변경의 영향을 받지 않는 단조 시계(monotonic clock)를 사용합니다.

timespec 구조체 정의

nanosleep 시스템 호출은 timespec이라는 구조체의 포인터를 인자로 받습니다. 우리는 <time.h> 헤더를 포함할 수 없으므로, 이 구조체를 직접 정의해야 합니다. glibc의 time.h 헤더를 보면 왜 직접 정의하고 싶어지는지 알 수 있습니다. 온갖 아키텍처를 지원하려는 시도 때문에 끔찍하게 읽기 어렵습니다.

timespec은 본질적으로 초(tv_sec)와 나노초(tv_nsec) 두 멤버로 구성된 간단한 구조체입니다.

// timespec 구조체 직접 정의
typedef struct {
    long tv_sec;  // 초
    long tv_nsec; // 나노초
} timespec;

sleep 함수 구현

nanosleep은 2개의 인자를 받으므로, 이를 위한 syscall2 래퍼 함수가 필요합니다. 또한, nanosleep의 시스템 호출 번호는 35입니다.

// 시스템 호출 번호 정의
#define SYS_nanosleep 35

// 인자 2개를 받는 범용 시스템 호출 래퍼
long syscall2(long syscall_num, long arg1, long arg2) {
    long result;
    __asm__ volatile (
        "syscall"
        : "=a" (result)
        : "a" (syscall_num), "D" (arg1), "S" (arg2)
        : "rcx", "r11", "memory"
    );
    return result;
}

// 우리가 만들 최종 sleep 함수
void sleep(long seconds) {
    // timespec 구조체 변수 선언 및 초기화
    timespec duration;
    duration.tv_sec = seconds;
    duration.tv_nsec = 0;

    // nanosleep 호출
    // 첫 번째 인자: 대기 시간 구조체 포인터
    // 두 번째 인자: 인터럽트 시 남은 시간 저장용 (사용 안함, 0 전달)
    syscall2(SYS_nanosleep, (long)&duration, 0);
}

이제 사용자로부터 받은 초(seconds) 값을 timespec 구조체에 할당하고, 이 구조체의 포인터를 nanosleep 시스템 호출에 전달하여 sleep 함수를 완성했습니다. 모든 조각이 맞춰졌습니다. 최종적으로 컴파일하고 결과를 확인할 시간입니다.


7단계: 최종 조립 및 결과 확인

이제 모든 코드를 하나로 합쳐 마지막으로 컴파일합니다.

최종 컴파일과 마지막 장애물

이 명령을 실행하면 예상치 못한 오류가 발생할 수 있습니다. 이것은 최신 컴파일러가 스택 오버플로우 공격을 막기 위해 자동으로 추가하는 스택 보호 기능 때문입니다. 우리는 libc를 사용하지 않으므로 이 보호 기능의 구현체도 없습니다. -fno-stack-protector 플래그를 추가하여 이 기능을 비활성화해야 합니다.

결과 분석

드디어 아무 오류 없이 독립적인 실행 파일 sleep_final이 생성되었습니다.

의존성 확인

ldd 명령어로 의존성을 확인해 봅시다. "statically linked"라는 메시지는 이 프로그램이 동적 라이브러리에 전혀 의존하지 않는 완전한 독립 실행 파일임을 의미합니다.

파일 크기 확인

실행 파일의 크기는 약 14KB에 불과합니다. 매우 작고 효율적인 프로그램이 완성된 것입니다.


결론: 우리는 무엇을 배웠는가?

이 튜토리얼을 통해 우리는 libc의 편리함 뒤에 숨겨진 컴퓨터의 동작 원리를 깊숙이 들여다보았습니다. 여러분은 다음과 같은 핵심 개념들을 직접 경험했습니다.

  • 프로그램의 진짜 시작점: main이 아닌 _start가 프로그램의 진정한 진입점이며, 운영체제가 어떻게 스택을 통해 인자를 전달하는지 배웠습니다.
  • 시스템 호출: exit, write, nanosleep 같은 시스템 호출을 통해 커널의 기능을 직접 사용하는 방법을 익혔습니다.
  • 인라인 어셈블리: C 코드 내에서 레지스터를 조작하고 저수준 하드웨어를 제어하는 강력한 도구를 사용해 보았습니다.
  • 독립 실행 파일: 라이브러리 의존성이 전혀 없는 작고 빠른 프로그램을 직접 제작했습니다.

이 과정이 처음에는 복잡하고 어려워 보일 수 있습니다. 하지만 약간의 명세서(ABI 문서)를 읽는 노력과 인내심만 있다면, 누구나 컴퓨터가 어떻게 작동하는지 더 깊은 수준에서 이해할 수 있습니다.


부록: 전체 소스 코드

지금까지 작성한 모든 코드를 하나로 합친 전체 소스 코드입니다. 복사하여 직접 컴파일하고 실행해 보세요.

// 시스템 호출 번호 정의 (x86-64)
#define SYS_write 1
#define SYS_nanosleep 35
#define SYS_exit 60

// timespec 구조체 직접 정의
typedef struct {
    long tv_sec;
    long tv_nsec;
} timespec;

// --- 유틸리티 함수 ---

long st_rlen(char *str) {
    char *cursor = str;
    while (*cursor != '\0') {
        cursor++;
    }
    return cursor - str;
}

long pars_int(char *raw_int) {
    long result = 0;
    char *cursor = raw_int;
    while (*cursor >= '0' && *cursor <= '9') {
        result = result * 10 + (*cursor - '0');
        cursor++;
    }
    return result;
}

// --- 시스템 호출 래퍼 ---

long syscall1(long syscall_num, long arg1) {
    long result;
    __asm__ volatile (
        "syscall"
        : "=a" (result)
        : "a" (syscall_num), "D" (arg1)
        : "rcx", "r11", "memory"
    );
    return result;
}

long syscall2(long syscall_num, long arg1, long arg2) {
    long result;
    __asm__ volatile (
        "syscall"
        : "=a" (result)
        : "a" (syscall_num), "D" (arg1), "S" (arg2)
        : "rcx", "r11", "memory"
    );
    return result;
}

long syscall3(long syscall_num, long arg1, long arg2, long arg3) {
    long result;
    __asm__ volatile (
        "syscall"
        : "=a" (result)
        : "a" (syscall_num), "D" (arg1), "S" (arg2), "d" (arg3)
        : "rcx", "r11", "memory"
    );
    return result;
}

// --- 직접 구현한 라이브러리 함수 ---

void print(char *str) {
    syscall3(SYS_write, 1, (long)str, st_rlen(str));
}

void sleep(long seconds) {
    timespec duration;
    duration.tv_sec = seconds;
    duration.tv_nsec = 0;
    syscall2(SYS_nanosleep, (long)&duration, 0);
}

void _exit(int exit_code) {
    syscall1(SYS_exit, exit_code);
    while(1);
}

// --- 프로그램의 메인 로직 ---

int main(int argc, char *argv[]) {
    if (argc < 2) {
        print("사용법: ");
        print(argv[0]);
        print(" <초>\n");
        return 1;
    }

    long seconds = pars_int(argv[1]);
    
    print("대기 시작...\n");
    sleep(seconds);
    print("대기 완료.\n");

    return 0;
}

// --- 프로그램의 진정한 진입점 ---

void _start() {
    __asm__ volatile (
        // 프레임 포인터 초기화
        "xor rbp, rbp\n"
        // argc, argv 설정
        "mov rdi, [rsp]\n"
        "lea rsi, [rsp+8]\n"
        // 스택 정렬
        "and rsp, -16\n"
        // main 호출
        "call main\n"
        // main의 반환값을 _exit의 인자로 전달
        "mov rdi, rax\n"
        "call _exit\n"
    );
}

About

C언어와 시스템 호출만으로 'sleep' 프로그램 처음부터 다시 만들기

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages