- 프로그램이 실행되기 전, 커널이 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() 호출 전까지의 전체 흐름
- 전체 과정은 다음과 같이 요약됨
-
execve 호출 → 커널이 ELF 파일 로드
- ELF 해석 → 코드/데이터 섹션 매핑, 인터프리터 지정
- 스택 구성 → 인자, 환경 변수, 보조 벡터 저장
- 엔트리포인트 _start 실행
- 런타임 초기화 후 main() 호출
- 이 일련의 과정은 운영체제 커널, ELF 포맷, 언어 런타임의 협력 구조를 보여줌
- 실제 Linux 커널은 주소 공간, 프로세스 테이블, 그룹 관리 등 추가적인 내부 로직을 포함하지만, 본 글은 그 전 단계의 핵심 흐름을 설명
결론 및 교정
-
main() 이전의 실행 과정은 커널 수준의 초기화와 런타임 설정의 결합체임
- 단순한 “Hello, World!” 프로그램조차 복잡한 ELF 구조와 런타임 초기화를 거쳐 실행됨
- 글의 초기 버전에서는 일부 섹션 로딩 로직을 커널에 귀속시켰으나, 실제로는 ELF 인터프리터의 역할임이 교정됨
- 이 분석은 시스템 프로그래밍, 컴파일러, OS 아키텍처 이해에 유용한 기초 자료로 기능