on
[번역]main 함수 없이 Hello World 출력하기
이 글은 Executing main() in C/C++ – behind the scene을 번역한 글입니다.
main 함수 없이 Hello World 출력하기
어떻게 main 함수 없이 “Hello world”를 출력할 수 있을까? 먼저, main 함수가 모든 프로그램의 시작점이기 때문에 main 함수 없이 프로그램을 실행하는 것은 불가능해 보인다.
우선, 리눅스에서 C 프로그램을 실행하는 동안에 어떤 일이 일어나고, main 함수가 어떻게 호출되고, main 함수 없이 프로그램을 실행할 수 있는지 알아보자.
다음과 같은 환경에서 진행하였다.
- Ubuntu 16.4 LTS operating system
- GCC 5.4.0 compiler
- objdump utility
C 프로그래밍 관점에서 main 함수는 프로그램의 시작점이다. 그러나 프로그램의 실행 관점에서는 그렇지 않다. main 함수에 도달하기 전에 인자를 설정하는 몇 가지 함수 호출이 이루어지고 프로그램 실행을 위한 환경 변수들을 준비한다.
C 소스 코드를 컴파일하여 생성되는 실행 파일은 Executable and Linkable Format(ELF) 파일이다. 모든 ELF 파일은 프로그램이 시작되는 메모리 주소를 가진 e_entry가 있는 ELF 헤더를 가지고 있다. 이 메모리 주소는 _start 함수를 가리킨다. 프로그램을 로드한 이후, 로더는 e_entry 필드를 ELF 헤더에서 찾아낸다. ELF는 실행 파일, 목적 코드, 동적 라이브러리, 코어 덤프를 위한 UNIX 시스템에서 사용되는 표준 파일 포맷이다.
다음 예시를 통해서 보자.
아래와 같은 파일을 만들었다.
example.c:
int main() {
return 0;
}
그리고 다음과 같은 명령어로 컴파일하자.
gcc -o example example.c
그러면 example이라는 실행 파일이 만들어졌고, 이 실행 파일을 objdump로 관찰하자.
objdump -f example
그러면 내 컴퓨터에서 실행하는데 중요한 정보가 나온다. start address에 있는 주소가 _start 함수의 주소이다.
example: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x00000000004003e0
실행 파일을 분해해서 이 주소를 재차 확인할 수 있다. 출력이 너무 길어서 0x00000000004003e0가 가리키는 부분만을 가져왔다.
objdump --disassemble example
Output:
00000000004003e0 <_start>:
4003e0: 31 ed xor %ebp,%ebp
4003e2: 49 89 d1 mov %rdx,%r9
4003e5: 5e pop %rsi
4003e6: 48 89 e2 mov %rsp,%rdx
4003e9: 48 83 e4 f0 and $0xfffffffffffffff0,%rsp
4003ed: 50 push %rax
4003ee: 54 push %rsp
4003ef: 49 c7 c0 60 05 40 00 mov $0x400560,%r8
4003f6: 48 c7 c1 f0 04 40 00 mov $0x4004f0,%rcx
4003fd: 48 c7 c7 d6 04 40 00 mov $0x4004d6,%rdi
400404: e8 b7 ff ff ff callq 4003c0
400409: f4 hlt
40040a: 66 0f 1f 44 00 00 nopw 0x0(%rax,%rax,1)
이것이 _start 함수를 가리킨다는 사실을 분명히 알 수 있다.
_start 함수의 역할
_start 함수는 다음에 실행될 _libc_start_main 함수를 위한 입력 인자 값들을 준비한다. 다음은 _libc_start_main 함수의 프로토타입니다. 여기서 우리는 _start 함수가 준비한 인자들을 볼 수 있다.
int __libc_start_main(
int (*main) (int, char * *, char * *), /* address of main function*/
int argc, /* number of command line args*/
char ** ubp_av, /* command line arg array*/
void (*init) (void), /* address of init function*/
void (*fini) (void), /* address of fini function*/
void (*rtld_fini) (void), /* address of dynamic linker fini function */
void (* stack_end) /* end of the stack address*/
);
_libc_start_main 함수의 역할
- 프로그램 실행을 위한 환경 변수들을 준비한다.
- main 함수의 시작 전에 초기화를 실행하는 init 함수를 호출한다.
- 프로그램 종료 이후에 정리(?)를 하는 _fini 와 _rtld_fini 함수를 등록한다.
이 모든 행동이 끝나면, main 함수를 호출한다.
main 없이 프로그램 작성하기
이재 우리는 main 함수 호출이 어떻게 이루어지는지 알았다. 다시한번 말하자면, main은 코드 작성을 시작하기 위한 약속된 단어일 뿐이다. 꼭 main이 아니라 어떠한 이름도 괜찮다. 기본적으로 _start 함수가 main을 호출하기 때문에, 우리만의 시작 코드를 실행하기 위해서 이것을 바꿔야 한다. main이 아니라 우리의 시작 코드를 호출하기 위해서 _start를 덮어쓸 수 있다.
nomain.c
#include<stdio.h>
#include<stdlib.h>
void _start()
{
int x = my_fun(); //calling custom main function
exit(x);
}
int my_fun() // our custom main function
{
printf("Hello world!\n");
return 0;
}
이제 -nostarfiles 옵션을 통해 컴파일러가 _start 함수를 자동으로 생성하지 않게 해주어야 한다.
gcc -nostartfiles -o nomain nomain.c
./nomain
Output:
Hello world!
References
- http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html
- Advanced C/C++ Compiling by Milan Stevanovic