[번역]main 함수 없이 Hello World 출력하기

이 글은 Executing main() in C/C++ – behind the scene을 번역한 글입니다.


main 함수 없이 Hello World 출력하기

어떻게 main 함수 없이 “Hello world”를 출력할 수 있을까? 먼저, main 함수가 모든 프로그램의 시작점이기 때문에 main 함수 없이 프로그램을 실행하는 것은 불가능해 보인다.

우선, 리눅스에서 C 프로그램을 실행하는 동안에 어떤 일이 일어나고, main 함수가 어떻게 호출되고, main 함수 없이 프로그램을 실행할 수 있는지 알아보자.

다음과 같은 환경에서 진행하였다.

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 함수를 호출한다.

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


main