# main() 함수가 실행되기 전의 여정

> Clean Markdown view of GeekNews topic #23927. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=23927](https://news.hada.io/topic?id=23927)
- GeekNews Markdown: [https://news.hada.io/topic/23927.md](https://news.hada.io/topic/23927.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-10-26T22:34:00+09:00
- Updated: 2025-10-26T22:34:00+09:00
- Original source: [amit.prasad.me](https://amit.prasad.me/blog/before-main)
- Points: 1
- Comments: 1

## Topic Body

- 프로그램이 실행되기 전, **커널이 execve 시스템 호출을 통해 프로세스를 생성하고 초기화하는 과정**을 탐구한 기술 분석  
- 이 호출은 실행 파일 경로, 인자, 환경 변수를 전달하며, 커널은 이를 기반으로 **ELF 형식의 실행 파일을 로드**함  
- ELF 파일은 코드, 데이터, 심볼, 동적 링크 정보 등을 포함하며, 커널은 이를 해석해 **메모리 매핑과 스택 초기화**를 수행  
- 이후 커널은 `_start` 엔트리포인트로 제어를 넘기며, 언어별 런타임이 초기화된 뒤에야 **사용자 정의 main 함수**가 호출됨  
- 이 과정은 **운영체제, 컴파일러, 런타임의 협업 구조**를 보여주며, 시스템 수준에서 프로그램 실행이 어떻게 이루어지는지 이해하는 데 중요함  

---

### 프로그램 실행의 시작점: execve 호출
- Linux에서 프로그램 실행은 **`execve` 시스템 호출**을 통해 시작됨  
  - `execve(const char *filename, char *const argv[], char *const envp[])` 형태로, 실행 파일 이름, 인자 목록, 환경 변수 목록을 전달  
  - 커널은 이를 통해 어떤 프로그램을 어떤 환경에서 실행할지 결정  
- 고수준 언어에서는 이 호출이 **표준 라이브러리의 프로세스 실행 API**로 감싸져 있음  
  - 예: Rust의 `std::process::Command`는 내부적으로 `execve`를 호출  
  - 쉘의 PATH 탐색과 유사하게, 명령 이름을 전체 경로로 변환하는 과정을 수행  
- **Shebang(`#!`)** 이 있는 스크립트의 경우, 커널은 지정된 인터프리터를 사용해 프로그램을 실행  
  - 예: `#!/usr/bin/python3` → Python 인터프리터로 실행  

### ELF: 실행 파일의 구조
- Linux의 실행 파일은 **ELF(Executable and Linkable Format)** 형식을 따름  
  - ELF는 코드, 데이터, 심볼, 재배치 정보 등을 포함하는 **표준 실행 파일 포맷**  
  - 다른 OS는 Mach-O(macOS), PE(Windows) 등 별도 포맷 사용  
- ELF 헤더에는 파일의 구조와 메모리 배치 정보가 포함됨  
  - 예시 항목: `ELF Magic`, `Class`, `Entry point address`, `Program headers`, `Section headers`  
  - `Entry point address`는 프로그램이 처음 실행될 명령어의 주소  
- 예시 ELF 헤더에서는 **RISC-V 아키텍처용 ELF32 실행 파일**로, `0x10358` 주소가 엔트리포인트로 지정됨  

### ELF 내부 구성 요소
- ELF 파일은 여러 **섹션(section)** 으로 구성되어 있음  
  - `.text`: 실행 코드  
  - `.data`: 초기화된 전역 변수  
  - `.bss`: 초기화되지 않은 전역 변수  
  - `.plt`: 공유 라이브러리 호출용 테이블  
  - `.symtab`, `.strtab`: 심볼 및 문자열 테이블  
- **PLT(Procedure Linkage Table)** 는 공유 라이브러리 함수 호출을 지원  
  - 예: `libc`의 `printf`, `malloc` 등  
  - ELF의 `PT_INTERP` 섹션은 동적 링커(interpreter)를 지정  
- 커널은 ELF를 읽어 **로드 가능한 섹션을 메모리에 배치**하고, 필요한 경우 **ASLR, NX bit** 등 보안 기능을 적용  

### 심볼 테이블과 런타임 링크
- ELF의 **심볼 테이블(symtab)** 은 함수와 변수의 주소 정보를 포함  
  - 예시: `_start`, `main`, `__libc_start_main` 등의 엔트리 존재  
  - 단순한 “Hello, World!” 프로그램도 2300개 이상의 심볼을 포함할 수 있음  
- 이는 대부분 **표준 라이브러리와 런타임 초기화 코드**에서 비롯됨  
  - `musl`이나 `glibc` 같은 `libc` 구현체가 연결되어 있기 때문  
- 커널은 ELF의 각 섹션을 로드한 뒤, **인터프리터(동적 링커)** 로 제어를 넘김  
  - 인터프리터는 재배치(relocation), 주소 무작위화(ASLR), 실행 권한 설정(NX bit) 등을 처리  

### 스택 초기화 과정
- 커널은 프로그램 실행 전 **스택(stack)** 을 직접 구성해야 함  
  - 스택은 지역 변수, 함수 호출 프레임, 인자 전달 등에 사용  
- `execve` 호출 시 전달된 **argv, envp**는 스택에 저장됨  
  - 프로그램은 이를 통해 명령행 인자와 환경 변수에 접근  
- 커널은 또한 **ELF 보조 벡터(auxv)** 를 스택에 포함  
  - 페이지 크기, ELF 메타데이터, 시스템 정보 등 30여 개 항목 포함  
  - 예: `AT_PAGESZ`는 메모리 페이지 크기(예: 4KiB)를 지정  
- RISC-V 에뮬레이터 예시에서는 스택 포인터(`sp`)를 높은 주소에서 시작해 인자, 환경 변수, 보조 벡터를 역순으로 쌓음  

### 엔트리포인트와 `_start` 함수
- ELF의 **엔트리포인트**는 `_start` 함수의 주소로 지정됨  
  - `_start`는 커널이 제어를 넘기는 최초의 사용자 공간 코드  
- 대부분의 언어는 `_start`에서 **런타임 초기화**를 수행한 뒤 `main`을 호출  
  - 예: Rust의 `std::rt::lang_start`, C의 `__libc_start_main`  
- Rust 예시에서는 `#![no_std]`, `#![no_main]` 속성을 사용해 런타임 없이 직접 `_start`를 정의 가능  
  - `_start` 내에서 스택에서 `argc`, `argv`, `envp`를 읽고 `main` 포인터를 호출  
- 언어별 런타임은 전역 생성자, 스레드 로컬 저장소, 예외 처리 등 **언어 특화 초기화 작업**을 수행  

### main() 호출 전까지의 전체 흐름
- 전체 과정은 다음과 같이 요약됨  
  1. `execve` 호출 → 커널이 ELF 파일 로드  
  2. ELF 해석 → 코드/데이터 섹션 매핑, 인터프리터 지정  
  3. 스택 구성 → 인자, 환경 변수, 보조 벡터 저장  
  4. 엔트리포인트 `_start` 실행  
  5. 런타임 초기화 후 `main()` 호출  
- 이 일련의 과정은 **운영체제 커널, ELF 포맷, 언어 런타임의 협력 구조**를 보여줌  
- 실제 Linux 커널은 주소 공간, 프로세스 테이블, 그룹 관리 등 추가적인 내부 로직을 포함하지만, 본 글은 그 전 단계의 핵심 흐름을 설명  

### 결론 및 교정
- `main()` 이전의 실행 과정은 **커널 수준의 초기화와 런타임 설정의 결합체**임  
- 단순한 “Hello, World!” 프로그램조차 복잡한 ELF 구조와 런타임 초기화를 거쳐 실행됨  
- 글의 초기 버전에서는 일부 섹션 로딩 로직을 커널에 귀속시켰으나, 실제로는 **ELF 인터프리터의 역할**임이 교정됨  
- 이 분석은 **시스템 프로그래밍, 컴파일러, OS 아키텍처 이해**에 유용한 기초 자료로 기능

## Comments



### Comment 45483

- Author: neo
- Created: 2025-10-26T22:34:01+09:00
- Points: 1

###### [Hacker News 의견](https://news.ycombinator.com/item?id=45706380) 
- ELF 파일의 **동적 링크 과정**에 대해 설명함  
  커널은 ELF의 PT_LOAD 세그먼트를 매핑하고, PT_INTERP로 지정된 **동적 링커(ld.so)** 를 로드한 뒤 제어를 넘김  
  이후 동적 링커가 스스로 재배치(relocation)하고 필요한 공유 객체를 mmap/mprotect로 로드함  
  이 구조는 스크립트의 **shebang(#!)** 메커니즘과 유사하다고 비유함  
  - 커널은 섹션 정보에는 전혀 관심이 없고, 오직 PT_LOAD 세그먼트만 처리함  
    예전에 objcopy로 ELF에 임의 파일을 삽입하려다 커널이 로드하지 않아 혼란스러웠던 경험을 공유함  
    결국 직접 **프로그램 헤더 테이블 패치 도구**를 만들었고, mold 링커에도 이 기능이 추가되었다고 함  
    관련 글: [Self-contained Lone Lisp Applications](https://www.matheusmoreira.com/articles/self-contained-lone-lisp-applications)  
  - 작성자가 이전에 내용을 잘못 수정해 올렸음을 인정하고 수정하겠다고 함  
  - 리눅스에서는 로더가 사용자 공간에서 동작하는데, 왜 더 다양한 로더가 없는지 항상 궁금했다고 함  

- 전체 코드를 **main() 이전** 혹은 main() 없이 패킹하는 실험을 했다고 함  
  관련 글: [Packing a codebase into a single function](https://joshua.hu/packing-codebase-into-single-function-disrupt-reverse-engineering)  
  - 읽어보니 의외로 단순하고 취약하지 않아서 흥미로웠다고 함  
    모든 함수를 main(100+n, ...) 형태로 바꾸면 된다고 농담함  

- 이 주제에 흥미가 있다면 자신이 만든 [cpu.land](https://cpu.land/)를 참고하라고 함  
  메모리 레이아웃보다는 **멀티태스킹과 코드 로딩 과정**을 다룸  
  - cpu.land를 정말 좋아한다고 감사 인사를 전함  

- C 프로젝트 중 표준 라이브러리를 피하고 **Linux syscall**만 직접 호출하는 경우가 얼마나 될지 궁금하다고 함  
  이렇게 코드를 짜는 게 훨씬 재미있다고 느낌  
  - 직접 syscall을 쓰는 건 오히려 비효율적이라고 주장함  
    ALSA, DRM 같은 기능은 커널 syscall 대신 **시스템 라이브러리**를 통해 접근하는 게 이점이 많음  
    이 방식이 이식성과 유지보수성 면에서 **Windows 스타일 접근**보다 낫다고 설명함  
  - Windows에서는 Win32 API만 사용하면 C 런타임을 링크하지 않아도 된다고 덧붙임  
  - 자신도 예전에 **liblinux** 프로젝트를 만들어 syscall만으로 프로그램을 작성했었다고 함  
    지금은 Linux의 nolibc 헤더가 잘 되어 있어 중단했지만,  
    현재는 syscall 기반의 **Lisp 인터프리터 언어**를 개발 중이라고 함  
    시스템 호출로 직접 Linux 유저 스페이스를 구성하는 실험이라 매우 흥미로운 여정이었다고 함  
  - 이식성을 유지하려 하지만, **파일 디스크립터**는 너무 편리해서 포기하기 어렵다고 함  
  - 많은 드라이버 코드가 실제로 syscall만 사용한다고 덧붙임  

- ELF 인터프리터(ld.so)가 초기 ELF 세그먼트를 매핑한 후 모든 로딩을 담당한다고 설명함  
  execve는 PT_LOAD 세그먼트를 매핑하고 aux vector를 스택에 채운 뒤  
  ELF 인터프리터의 엔트리 포인트로 점프함  
  커널은 PLT/GOT에 대해 아무것도 모름  

- 대학에서 이 주제를 가르치는 사람으로서, 학생들이 **메모리 다이어그램** 때문에 혼란스러워한다고 함  
  교재는 주소가 높을수록 위쪽에 그려지지만, 실제 Linux 프로세스는  
  **낮은 주소가 위, 높은 주소가 아래**로 출력됨  
  `/proc/&lt;pid&gt;/maps`를 보면 스크롤을 아래로 내릴수록 주소가 커짐  
  즉, “heap은 위로 자라고(stack은 아래로 자란다)”는 표현은 숫자상의 방향일 뿐,  
  시각적으로는 오히려 반대임  
  IDE처럼 아래로 갈수록 주소가 커지는 식으로 그리면 훨씬 직관적이라고 제안함  
  - 스택은 어쨌든 **스택 포인터가 감소**하면서 자라므로 “아래로 자란다”는 표현이 여전히 맞다고 함  
    다만 시각화는 가로 방향으로 하는 게 더 자연스럽다고 제안함  
  - 자신도 예전에 같은 혼란을 겪었고, **리틀엔디언 주소 표기**가 헷갈렸다고 회상함  
  - 실제 사물의 쌓이는 방향을 생각하면 “스택이 아래로 자란다”는 표현이 직관적이지 않다고 반박함  

- 오래된 **PIC16 마이크로컨트롤러**로 이런 실험을 하는 걸 좋아한다고 함  
  스택 포인터, 타이머, 변수 설정 등을 직접 다루는 게 재미있다고 느낌  

- **shebang(#!)** 관련 경험을 공유함  
  Java 애플리케이션이 실행 스크립트를 찾지 못한다는 오류를 냈는데,  
  실제 문제는 스크립트의 shebang 경로가 잘못된 것이었음  
  로컬에서는 잘 실행됐지만, 원격 서버의 인터프리터 경로가 달라서 생긴 문제였음  
  - 이건 Java만의 문제가 아니라, ENOENT 오류가 발생하는 모든 프로그램에서 생길 수 있다고 함  
    **strace**로 실행하면 어떤 syscall에서 오류가 났는지 바로 확인 가능하다고 조언함  
  - shebang의 구조를 분석한 글을 공유함: [What the #! means](https://blog.foletta.net/post/2021-04-19-what-the/)  
  - 커널에서 shebang을 지원하려면 **CONFIG_BINFMT_SCRIPT=y** 설정이 필요하다고 덧붙임  

- 디버깅 중에 메인 바이너리의 **재배치 순서**가 언제 적용되는지 항상 헷갈린다고 함  
  링커가 자신의 심볼을 해결하기 전인지 후인지가 마치 블랙매직 같다고 표현함  

- 마크다운 내 “lang_start function (defined here)” 부분의 **링크가 깨져 있음**을 지적함
