← System Programming DUJINLABS.COM

Syscall / ELF · Linux userspace / kernel ABI

ELF 적재와 _start

execve 이후 ELF program header, interpreter, 초기 stack, _start, __libc_start_main, main이 이어지는 순서를 봅니다.

Series
04 / 37
Build
cc -std=c17 -Wall -Wextra -O2 elf_start.c -o elf_start
Run
./elf_start one two
Kernel
Linux 6.18.37 LTS

main()보다 먼저 실행되는 코드는 어디에서 오는가?

ELF loader는 section name을 보고 실행 파일을 적재하지 않는다. PT_LOAD program header가 지정한 file offset, virtual address, permission을 기준으로 VMA를 만들고, PT_INTERP가 있으면 dynamic linker도 함께 적재한다.

새 process image의 첫 userspace instruction은 main이 아니라 ELF entry point다. 일반적인 glibc 실행 파일에서는 crt1의 _start가 초기 stack에서 argc, argv를 꺼내 __libc_start_main에 넘긴다.

구조 그림

그림 1. execve 직후 ELF process의 가상 주소 배치
높은 가상 주소낮은 가상 주소
user stackargc · argv · envp · auxv
vDSO / vvarkernel이 제공하는 userspace helper
ld-linux + shared objectsPT_INTERP · relocation · libc
mmap areaanonymous/file mappings
heapbrk와 allocator arena
main ELF PT_LOADR-- headers · R-X text · RW- data/bss

PT_LOAD가 실행 시 mapping을 만들고 section table은 이 배치를 직접 결정하지 않는다. 주소는 ASLR 때문에 실행마다 달라질 수 있다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
execve pathname/argv/envp
ELF phdr PT_LOAD/PT_INTERP
ld-linux relocation과 dependency
_start crt startup
main C runtime 준비 완료

section table은 link/debug 정보에 가깝고, 실행 시 mapping은 program header가 결정한다. readelf -l 출력과 /proc/PID/maps를 서로 맞춰 본다.

그림 3. 커널 내부에서 지나가는 주요 지점
do_execveat_common linux_binprm 준비
search_binary_handler format 선택
load_elf_binary VMA와 stack
start_thread IP/SP 설정
EL0 ELF entry 실행

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

Linux 6.18.37 LTS 소스 위치

glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.

파일함수·구조체여기서 볼 것
fs/exec.c do_execveat_common(), begin_new_exec() 기존 주소 공간을 폐기하고 새 binary를 확정하는 지점
fs/binfmt_elf.c load_elf_binary(), create_elf_tables() PT_LOAD mapping과 초기 stack 구성
arch/x86/include/asm/processor.h start_thread() 새 instruction pointer와 stack pointer 설정

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 elf_start.c -o elf_start
01#include <stdio.h>
02#include <stdlib.h>
03
04static void before_main(void) __attribute__((constructor));
05static void after_main(void) __attribute__((destructor));
06
07static void before_main(void)
08{
09    puts("constructor: runtime is ready");
10}
11
12static void after_main(void)
13{
14    puts("destructor: normal exit path");
15}
16
17int main(int argc, char **argv, char **envp)
18{
19    printf("main: argc=%d argv0=%s\n", argc, argv[0]);
20    printf("argv=%p envp=%p\n", (void *)argv, (void *)envp);
21    return EXIT_SUCCESS;
22}

코드 조각별 설명

실제 코드 4행__attribute__((constructor))

linker가 함수 주소를 .init_array에 넣고 C runtime이 main 전에 배열을 순회한다. ELF entry point 자체가 이 함수로 바뀌는 것은 아니다.

실제 코드 5행__attribute__((destructor))

정상적인 exit 경로에서 .fini_array를 통해 호출된다. _exit(), fatal signal, 전원 차단에는 실행되지 않는다.

실제 코드 9행puts("constructor

이 시점에는 dynamic relocation과 libc 초기화가 끝났으므로 stdio를 사용할 수 있다.

실제 코드 17행int main(int argc

argc/argv/envp 원본은 kernel이 만든 초기 stack에서 시작하지만, main 호출은 C runtime이 ABI에 맞춰 다시 구성한다.

실제 코드 21행return EXIT_SUCCESS

main의 반환은 __libc_start_main 내부에서 exit() 호출로 이어져 atexit handler와 stdio flush를 수행한다.

세부 동작

01

PT_LOAD가 VMA의 초안이다

p_offset와 p_vaddr는 page offset 관계가 맞아야 한다. loader는 file-backed 구간을 map하고 p_memsz가 p_filesz보다 큰 끝부분을 0으로 채워 .bss를 만든다.

W^X 정책은 program header permission과 mprotect relocation 과정에서 확인한다.

02

초기 stack에는 문자열만 있는 것이 아니다

argc, argv pointer array, envp pointer array 뒤에 auxiliary vector가 놓인다. AT_PHDR, AT_ENTRY, AT_RANDOM, AT_SYSINFO_EHDR 같은 항목을 libc와 dynamic linker가 소비한다.

getauxval()을 사용하면 /proc/self/auxv를 직접 parsing하지 않고 값을 확인할 수 있다.

03

exec 성공에는 반환 경로가 없다

execve가 성공하면 호출 process의 code, data, stack이 교체되므로 이전 instruction 다음 줄로 돌아오지 않는다. 실패했을 때만 -1과 errno가 돌아온다.

multithread process에서 호출하면 다른 thread는 사라지고 호출한 thread만 새 image의 초기 thread가 된다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
linux_binprmexec 준비 중 임시로 존재하며 binary handler가 소비한다file, buf, argc/envc
PT_LOAD VMAexec 때 만들어지고 munmap/다음 exec/process exit까지 유지된다offset, protection, p_filesz/p_memsz
initial stackcreate_elf_tables가 만들고 _start와 libc가 소비한다argc, argv, envp, auxv

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
ENOEXECELF magic/architecture/format 불일치file, readelf -h, kernel log
ENOENT인데 파일은 존재PT_INTERP가 가리키는 dynamic linker 없음readelf -l의 interpreter 확인
main 전 crashrelocation, constructor, stack/ABI 문제LD_DEBUG, gdb starti, core dump

직접 확인

  1. readelf -l 출력의 LOAD 주소와 실행 중 /proc/$PID/maps 주소를 PIE/비PIE 빌드로 비교한다.
  2. gdb에서 starti를 사용해 _start 첫 instruction에서 stack을 확인하고 x/32gx $rsp로 argc와 pointer 배열을 찾는다.
  3. _exit(0)를 호출하도록 바꿔 destructor와 stdio flush가 실행되지 않는지 확인한다.
실행./elf_start one two
추적readelf -h -l ./elf_start && strace -f -e execve,mmap,mprotect ./elf_start

원문