tmxklab

Linux Binary Execution Flow(main함수 호출 및 종료 과정) 본문

OS/02 Linux

Linux Binary Execution Flow(main함수 호출 및 종료 과정)

tmxk4221 2020. 9. 15. 17:11

main()가 호출되고 종료되는 과정에 대한 지식이 필요하여 이번에 정리하게 되었다.

우리가 흔히 .c파일을 작성하여 컴파일 과정을 마치면 결과물로 linux실행파일인 elf파일이 생성된다.

이제 elf파일 실행할 때 내부적으로 어떻게 실행되는지 단계별로 살펴보자

목표 :

  • elf파일(바이너리 파일)이 실행되어 종료되는 과정을 살펴본다
  • 특히 main함수가 호출되고 종료되는 과정에 중점을 둔다.
  • 너무 깊게 들어가지는 않을꺼다....

1. elf파일 실행

(이쪽 부분은 커널 영역이다 보니 자세하게 다루지 않음... 나중에 리눅스 커널쪽을 배우고 커널 디버깅을 할 수 있을 때 따로 정리하도록 하겠다. )

 

 

1) 쉘에서 ./a.out와 같이 elf파일을 실행시키면 새로운 프로세스를 fork

 

 

2) 새로운 프로세스가 execve()를 호출하면서 유저 모드에서 커널 모드로 전환

  • 이 때, 파일 경로, 명령 인자, 환경 변수 등 프로그램 동작 시 필요한 정보를 넘겨 준다.
  • ex) execve("./a.out", *argv[], *envp[])

 

3) system call handler에 의해 sys_execve()호출

 

ex) system call henadler

출처 : http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch05lev1sec3.html

 

4) do_execve()호출

  • 데이터 구조체(linux_binprm)에 필요한 정보(argv, envp, uid, gid..)를 설정
  • search_binary_handler() 호출

+) struct linux_binprm

  • 리눅스가 동시에 여러 개의 실행파일 포맷을 지원하기 위한 구조체
  • 해당 구조체에는 실행파일 포맷 로더에 관한 함수 포인터 포함

do_execve flow :

 

do_execve code flow in linux kernel

Implementation of execve •        The entry point of the system call is the architecture-dependent sys_execve function. This function q...

venkateshabbarapu.blogspot.com

 

5) search_binary_handler()

  • 특정 파일에 적합한 바이너리 포맷을 찾기 위해 do_execve()끝에 사용
  • 바이너리 포맷에 맞는 처리기 호출
    • elf 파일인 경우 load_elf_binary() 호출

 

6) load_elf_binary()

  • elf 파일 실행 준비를 위해 여러가지 작업을 수행

    → elf 파일의 architecture 및 type 확인

    → elf 파일을 메모리에 매핑

    → 등등...(자세한 것은 찾아보길 바란다..)

  • load_elf_binay()를 실행한 후 start_thread()를 호출

  • start_thread()가 프로세스 실행을 시작

  • _start 루틴이 커널에서 실행의 제어를 넘겨 받게 됨

 

 

참고 자료)

 


2. _start()

_start루틴 시작 전에 위와 같이 커널에서 몇 가지 작업을 수행하고 파일을 로드한 후 커널 모드에서 유저 모드로 넘어와 _start루틴이 시작된다.

 

_start루틴

따로 설정을 하지 않아도 main함수가 호출되기 전에 가장 먼저 실행되는 함수로 csu 또는 crt(C run time)루틴이라고 부른다. _start루틴은 커널로부터 받은 argc, argv인자를 저장하고 스택을 초기화한 후 glibc내에 정의된 __libc_start_main()를 호출한다.

 

glibc에서 제공하는 crt1.o는 elf파일의 entry point인 _start함수를 구현하고 elf형식의 .init, .fini섹션을 지원한다. 참고로 gcc 4.7 이상 버전부터 .ctors(constructor), .dtors(destructor)대신에 .init, .fini를 사용하고 이들은 main함수 호출 직전 및 종료 후에 실행되는 생성자, 소멸자이다. (.init / .fini섹션에 관련된 작업은 __libc_csu_init, fini함수에서 처리)

 

+) .init / .fini섹션

.init섹션(main()보다 먼저 실행)

  • 오브젝트 파일 로드할 때 실행되는 코드(프로세스 초기화 코드)
  • _init() 함수가 저장되어 있다.

.fini섹션(main()가 반환된 후 실행)

  • 프로세스 종료 전에 실행되는 코드(프로세스 종료 코드)
  • _fini() 함수가 저장되어 있다. → 특별한 일을 하지 않음

 

디버깅은 glibc 2.27 환경에서 진행하였고 glibc 버젼에 따라 약간의 차이가 존재할 수 있음

  • $elfheader명령을 통해 각 섹션들의 주소를 확인할 수 있다.
  • .init섹션 : 0x4008f8 → _init()주소 / .fini섹션 : 0x4012e4 → _fini()주소

(참고로 예전에 FTZ문제(level 11)에서 FSB를 사용하여 쉘 코드 주소를 뭔지도 모르고 .dtors에 넣었는데 이제 처음 알았음.. ㄷㄷ)

 

 

1) elf header를 통해 _start 확인

  • Entry point address(0x400a60) : _start() address
  • 해당 바이너리 파일을 시작하면 가장 먼저 Entry Point가 가리키는 _start함수를 호출한다.

 

2) _start함수 확인

  • _libc_start_main()을 호출하기 전에 필요한 인자들을 레지스터에 셋업한다.
  • _libc_start_main_ptr : _libc_start_main@got

  • _libc_start_main@got에 __libc_start_main()의 실제 주소가 들어있다.

 


3. __libc_start_main()

_start()에서 필요한 인자들을 셋업해주고 __libc_start_main()를 호출하였다.

 

_libc_start_main()은 glibc에 존재하는 함수로 바이너리 실행과정에 필요한 여러 요소들을 초기화하고 실제로 main()를 호출해주는 역할을 한다. 추가로 _start에서 언급했던 .init / .fini섹션에 관련된 작업을 하는 __libc_csu_init, fini 를 __libc_start_main()에서 호출한다.

 

 

1) call __GI__cxa_atexit()

__libc_csu_init()을 호출하기전에 call하는 함수로 파라미터를 _dl_fini()를 받는다.(뒤에서 _dl_fini()나옴)

공유 라이브러리가 언로드될 때 호출할 함수를 등록하는 함수라는 정도만 알고 넘어가자

 

 

2) call __libc_csu_init()

먼저, main()을 호출하기 전에 __libc_csu_init()을 호출하여 초기화 과정을 거친다.

 

 


4. __libc_csu_init()

(이전에 가젯 제한으로 인해 RTC(Return to csu)기법을 이용한 문제를 풀면서 한 번 봤었던 함수이다. )

 

 

1) __libc_csu_init() 소스 코드

/* These functions are passed to __libc_start_main by the startup code.
   These get statically linked into each program.  For dynamically linked
   programs, this module will come from libc_nonshared.a and differs from
   the libc.a module in that it doesn't call the preinit array.  */
void
__libc_csu_init (int argc, char **argv, char **envp)
{
  /* For dynamically linked executables the preinit array is executed by
     the dynamic linker (before initializing any shared object).  */
#ifndef LIBC_NONSHARED
  /* For static executables, preinit happens right before init.  */
  {
    const size_t size = __preinit_array_end - __preinit_array_start;
    size_t i;
    for (i = 0; i < size; i++)
      (*__preinit_array_start [i]) (argc, argv, envp);
  }
#endif
#ifndef NO_INITFINI
  _init ();
#endif
  const size_t size = __init_array_end - __init_array_start;
  for (size_t i = 0; i < size; i++)
      (*__init_array_start [i]) (argc, argv, envp);
}

출처 : https://code.woboq.org/userspace/glibc/csu/elf-init.c.html#__libc_csu_init

 

 

__libc_csu_init()가 수행하는 주요 작업은 _init()를 호출하는 일이다. 추가적으로 __preinit_array 와 __init_array에 설정된 함수 포인터를 읽어서 함수를 호출하는 것을 확인할 수 있다. 그리고 _init()는 call_gmon_start()를 호출하고 call_gmon_start()는 _gmon_start_()가 존재할 경우 이거를 호출한다고만 알고 넘어가자

 

 

2) challenge파일 확인

challenge파일에서는 .init_array섹션 사이즈를 계산해서 .init_array에 저장된 함수 포인터들을 for문을 통해 순차적으로 호출하는 것을 확인할 수 있다. (RTC기법을 사용할 때 위 로직이 사용됨)

추가로 바이너리가 종료되는 시점에도 이와 비슷한 방식으로 .fini_array가 참조된다.(여기서 init_proc()는 _init()와 동일)

 

 

+) .init_array / .fini_array 섹션

.init_array 섹션

  • 오브젝트 파일 로드할 때 실행되는 함수의 포인터 배열

.fini_array 섹션

  • 프로세스 종료 전에 실행되는 함수의 포인터 배열

 

현재 challenge파일에서 각 .init_array와 .fini_array에 저장된 함수들을 확인할 수 있다.

 

 

_init()를 호출하고 .init_array에 저장된 함수들을 순차적으로 호출하는 것을 확인할 수 있다. 여기서는 .init_array배열 처음에 들어있는 frame_dummy() 하나만 호출하고 리턴된다.

 

 

__libc_csu_init()루틴이 종료되면 다시 __libc_start_main()으로 돌아오고 드디어 main()를 호출한다.

 

 


5. .fini_array섹션

__libc_csu_init()이 main()실행 전에 호출되는 함수로 프로그램 실행 전 초기화를 위해 .init섹션에 저장된 _init()를 호출하고 .init_array배열에 들어있는 함수들을 순차적으로 호출하는 것이라고 하였다.

 

반대로 __libc_csu_fini()는 main()가 리턴되고 프로그램을 정상적으로 종료시키기 위해서 .fini섹션과 .fini_array에 대한 처리를 하는 역할을 한다.

 

그런데 실제로 메인함수가 종료되면 __GI_exit()를 통해 .fini_array섹션을 참조한다.

main함수가 리턴되고 __libc_start_main()으로 돌아와 __GI_exit()를 호출한다.

이 때, 파라미터로 0x0을 받는 것을 알 수 있는데 우리가 흔히 .c파일을 작성할 때 main() 마지막에 return 0; 으로 작성하는데 여기서 return값 0이 exit()의 파라미터로 들어가게 되는 것이다.

 

 

_GI_exit()루틴 내부에서는 __run_exit_handlers()를 호출한다.

 

 

__run_exit_handlers()루틴 내부에서는 rdx에 저장된 _dl_fini()를 호출하게 된다.

__run_exit_handlers()는 exit_function구조체 멤버 변수인 flavor값에 따라 함수를 호출한다고 하는데 기본적으로 glibc내부에 존재하는 _dl_fini()를 호출한다.

 

 

_dl_fini()내부 루틴이 쭉쭉 실행되다가 r15(0x601e00)에 들어있는 함수를 호출한다.

 

 

r15는 .fini_array섹션을 가리키며 .fini_array배열에 들어있는 __do_global_dtors_aux()를 호출하는 것을 확인할 수 있다.

 

 

.fini_array배열에 있는 함수들을 호출하고 다시 _dl_fini()로 돌아와 _fini()를 호출하는 것을 볼 수 있다.

 

 

그런데 아까 위에서 말했듯이 딱히 _fini()에서 하는 일이 없다.

 

 

_dl_fini()가 리턴되고 다시 __run_exit_handlers()루틴으로 돌아오면

 

__GI__exit()를 호출한다.(처음에 실행한 __GI_exit()랑 다름, 가운데에 '_'가 2개임)

 

 

__GI__exit()에서는 rax값을 0xe7, rdi를 0x0으로 세팅하고

syscall하면서 이제 완전히 프로세스가 종료된다.

 

 


6. 정리

 

1) 쉘을 통해서 elf파일을 실행하면 user단에서 kernel에게 요청을 하게 되고 kernel은 elf파일을 실행시키기 위해 몇 가지 복잡한 작업을 거치게 된다.

 

2) kernel에서 작업을 마치면 파일을 메모리에 로드하고 elf파일의 entry point가 가리키는 _start()부터 호출하여 시작하게 된다.

 

3) _start루틴(crt(C run time))에서는 커널로부터 받은 argc, argv인자를 저장하고 스택을 초기화한 후 glibc내에 정의된 __libc_start_main()를 호출한다.

 

4) __libc_start_main()에서는 .init / .fini섹션 작업과 관련된 함수들을 호출하고 메인함수를 호출한다.(.init → main() → exit())

 

 

 

지금까지 쉘을 통해 elf파일을 실행시키고 main함수가 호출되기 전부터 종료되기까지 과정을 살펴봤는데 진짜 너무 복잡한 것 같다.ㅋㅋㅋㅋ 나중에 기회되면 리눅스 커널이랑 커널 디버깅도 공부해보고 싶다. 

(혹시 틀린 내용있으면 말씀해주세여.. ) 

 


참고자료

Comments