Egloos | Log-in
F/OSS study
F/OSS study
[glibc] CSU (C start up) routines
glibc: 2.10.1
gcc: 4.4.3
arch: x86


보통 프로그램이 시작될 때 main() 함수가 먼저 수행되는 것으로 알고 있지만
실상은 그보다 먼저 실행되어 main 함수가 동작하기 위한 환경을 만들어주는 역할을 하는 여러 루틴들이 있다.
(사실 ELF 표준에서 별다른 설정 없이 가장 먼저 실행되는 함수의 이름은 _start이다.)
이들을 csu 혹은 crt (C run-time?) 루틴이라고 부른다.

앞서 gcc로 컴파일 시 -v 옵션을 주었을 때 링크 과정에서 여러 start/end file들과 함께
링크되는 것을 보았을 것이다. 이들이 바로 이러한 crt 루틴에 해당한다.
정확히는 다음 파일들에 해당한다.
  • /usr/lib/crt1.o
  • /usr/lib/crti.o
  • /usr/lib/gcc/i686-pc-linux-gnu/4.4.3/crtbegin.o
  • /usr/lib/gcc/i686-pc-linux-gnu/4.4.3/crtend.o
  • /usr/lib/crtn.o
이 중 crt{1,i,n}.o 파일은 glibc에서 제공하는 것으로 ELF 형식의 .init/.fini 섹션을 지원하며
crt{begin,end}.o 파일은 gcc에서 제공하는 것으로 constructor/destructor를 지원하기 위해 사용되며
정확한 디렉토리 명은 gcc 설정과 버전에 따라 약간 달라질 수 있다.

crt1.o은 ELF 실행 파일의 시작점(entry point)인 _start 함수를 구현하며
실제로 커널이 exec() 시스템 콜 처리 과정에서 해당 파일을 로드한 후에 이 함수를 호출한다.
(사실 이는 -static 옵션을 통해 링크된 실행 파일의 경우에 해당한다.
동적 링크된 (일반적인) 실행 파일의 경우에는 동적 링커(interpreter)가 이보다 먼저 실행되어
필요한 라이브러리를 모두 로드한 후에 _start 함수를 호출한다.)

_start 루틴은 glibc/sysdeps/i386/elf/start.S 파일에 어셈블리어로 구현되어 있으며
커널로부터 넘겨받은 argc, argv 인자를 저장하고 스택을 적절히 초기화한 후
glibc 내에 정의된 __libc_start_main() 함수를 호출하도록 작성되어 있다.
이 때 인자로 main() 함수의 주소, argc, argv 및 초기화/종료 처리 루틴의 주소 등이 주어진다.
__libc_start_main() 함수에 대해서는 조금 후에 자세히 살펴보기로 한다.

crti.o와 crtn.o는 서로 짝을 이루는 파일인데 약간 특이한 방식으로 작성되어졌다.
이를 구현한 파일은 glibc/sysdeps/generic/initfini.c 파일이다.
이 파일에는 .init 섹션에 저장되는 _init() 함수와 .fini 섹션에 저장되는 _fini() 함수가 존재한다.
_init 함수는 call_gmon_start() 함수를 호출하는 것 뿐이고, _fini 함수는 특별한 일을 하지 않는다.
call_gmon_start() 함수는 (프로파일링 시에 정의되는) __gmon_start__ 함수가 존재하는 경우
이를 호출한다.

도대체 이것이 무슨 역할을 하는 것일까?
ELF 표준에서는 main() 함수 실행 전에 .init 섹션 내의 코드가 먼저 수행되어야 한다고 규정하고 있다.
또한 프로그램이 (정상) 종료 시 .fini 섹션 내의 코드가 마지막으로 수행되어야 한다.
initfini.c 파일에서는 바로 이러한 섹션에 포한되는 _init/_fini 함수를 구현한 것인데
함수 중간에 @_{init,fini}_{PROLOG,EPILOG}_{BEGINS,ENDS}와 같은 표시를 해 두고
어셈블리 파일을 생성한 뒤 sed & awk 스크립트를 이용하여 함수를 둘로 나눈다.
나누어진 앞쪽 부분은 crti.o 파일에 저장되고 뒤쪽 부분은 crtn.o 파일에 저장되는데
이들은 링크 시 다른 모든 파일의 앞/뒤에 위치하므로 다른 파일에서 해당 섹션에 포함될 코드를 작성하면
자연스럽게 동일한 함수 내로 포함되도록 되어 있다.
이를 이용하여 construct/destructor를 구현하는 것이 바로 crt{begin,end}.o 파일의 역할이다.

먼저 _fini() 함수가 어떻게 구성되는지 살펴보자. (_init 함수도 동일한 방식이다.)
원래의 함수는 정상적인 컴파일 과정에서 다음과 같이 어셈블리어로 변환될 것이다.

00000000 <_fini>:
   0:    55                       push   %ebp         /* @_fini_PROLOG_BEGINS */
   1:    89 e5                    mov    %esp,%ebp
   3:    53                       push   %ebx
   4:    83 ec 04                 sub    $0x4,%esp
   7:    e8 00 00 00 00           call   c <_fini+0xc>
   c:    5b                       pop    %ebx
   d:    81 c3 03 00 00 00        add    $0x3,%ebx    /* @_fini_PROLOG_ENDS */
  13:    59                       pop    %ecx         /* @_fini_EPILOG_BEGINS */
  14:    5b                       pop    %ebx
  15:    c9                       leave  
  16:    c3                       ret                 /* @_fini_EPILOG_ENDS */

0xd 부근에서 (call & pop 후에) %ebx에 3을 더하는 것은 GOT 오프셋을 계산하기 위한 것으로
실행 시 relocation 과정에서 다른 값으로 바뀌게된다는 것에 주의하자.
아무튼 이렇게 컴파일된 어셈블리 파일은 sed 스크립트를 통해 PROLOG와 EPILOG로 나누어져
각각 crti.o와 crtn.o 파일에 저장된다. 다음을 실행하면 이를 확인해 볼 수 있다.

$ objdump -d /usr/lib/crtn.o

/usr/lib/crtn.o:     file format elf32-i386


Disassembly of section .init:

00000000 <.init>:
   0:    58                       pop    %eax
   1:    5b                       pop    %ebx
   2:    c9                       leave  
   3:    c3                       ret    

Disassembly of section .fini:

00000000 <.fini>:
   0:    59                       pop    %ecx
   1:    5b                       pop    %ebx
   2:    c9                       leave  
   3:    c3                       ret    

crtbegin.o 파일과 crtend.o 파일도 서로 짝을 이루며, gcc/crtstuff.c 파일로부터 추출된다.
해당 파일 컴파일 시 각각 CRT_BEGIN 및 CRT_END 매크로를 정의하여 구분한다.
이들은 각각 __do_global_{ctors,dtors}_aux() 함수를 구현하고 있는데
이 함수는 컴파일 시 소스 파일 내에 정의된 constructor와 destructor를 실행하는 역할을 수행한다.
이러한 생성자/소멸자도 main() 함수보다 먼저, 그리고 main()이 종료된 후에 실행되어야 하기 때문에
이를 구현하기 위해 위에서 언급한 _init()와 _fini() 함수를 이용하고 있다.
즉, _init 내에서 __do_global_ctors_aux()를 호출하고
_fini 내에서 __do_global_dtors_aux()를 호출하도록 하면 간단히 해결된다.
gcc/crtstuff.c 파일에는 다음과 같은 내용이 포함되어 있다.

CRT_CALL_STATIC_FUNCTION (FINI_SECTION_ASM_OP, __do_global_dtors_aux)

이것은 다음과 같은 인라인 어셈블리 함수로 확장된다.

asm (".section .fini\n\t"
     "call __do_global_dtors_aux\n\t"
     ".text");

결국 .fini 섹션에는 함수 호출하는 call 명령 만이 포함될 것이다.
실제로 빌드된 실행 파일의 .fini 섹션을 살펴보면 위의 부분이 포함된 것을 볼 수 있다.

$ objdump -d -j .fini a.out

a.out:     file format elf32-i386


Disassembly of section .fini:

0804845c <_fini>:
 804845c:    55                       push   %ebp
 804845d:    89 e5                    mov    %esp,%ebp
 804845f:    53                       push   %ebx
 8048460:    83 ec 04                 sub    $0x4,%esp
 8048463:    e8 00 00 00 00           call   8048468 <_fini+0xc>
 8048468:    5b                       pop    %ebx
 8048469:    81 c3 8c 1b 00 00        add    $0x1b8c,%ebx
 804846f:    e8 bc fe ff ff           call   8048330 <__do_global_dtors_aux>
 8048474:    59                       pop    %ecx
 8048475:    5b                       pop    %ebx
 8048476:    c9                       leave  
 8048477:    c3                       ret    

일반 프로그램 작성 시 main()이 종료된 후에 호출해야 할 함수가 있다면
위와 같이 인라인 어셈블리 함수를 이용하여 해당 함수를 호출하는 부분을 .fini 섹션에 저장하면 되지만
이는 매우 불편하며 관리하기도 힘든 방식이기 때문에 gcc에서는 destructor 속성을 통해 이를 처리하는 것이다.
특정 함수에 destructor 속성이 주어지면 해당 함수의 주소가 .dtors 섹션에 저장되며
_fini()에서 호출하는 __do_global_dtors_aux() 함수는 .dtors 섹션 내의 모든 항목들을
순서대로 호출하도록 구현된다. (이는 constructor에서도 동일하게 적용된다.)

아래의 예제에서는 이들을 따로 사용하여 동일한 효과를 얻을 수 있음을 보여준다.

#include <stdio.h>

#define DEFINE_FUNC(name)  name(void) { puts(#name); }

void __attribute__((constructor)) DEFINE_FUNC(ctor);
void __attribute__((destructor))  DEFINE_FUNC(dtor);
void DEFINE_FUNC(init);
void DEFINE_FUNC(fini);

int main(void)
{
  puts("main");
  asm (".section .init\n\tcall init\n\t.text");
  asm (".section .fini\n\tcall fini\n\t.text");
  return 0;
}

이를 컴파일하여 실행하면 다음과 같은 결과를 얻을 수 있다.

$ ./a.out
init
ctor
main
dtor
fini

위에서 보듯이 crt{begin,end}.o 파일이 링크되는 순서에 의해
constructor와 destructor는 main() 함수의 바로 전후에 호출된다.

이제 __libc_start_main() 함수에 대해서 살펴보도록 하자.
이 함수는 glibc/csu/libc-start.c 파일에 정의되어 있으며
제일 먼저 커널로부터 넘겨받은 argv를 통해 envp를 계산하고 다시 이를 통해 auxv를 찾는다.
그 후 vdso 내의 정보나 uname() 시스템 콜을 통해 커널 버전이 호환 가능한지를 확인하고
TLS를 초기화 한 후 SSP에서 사용할 canary 값을 결정한다.

그리고 .init/.fini 섹션에 관련된 작업을 수행하는데 각각은 __libc_csu_{init,fini} 함수에서 처리한다.
__libc_csu_init() 함수의 주요한 작업은 (당연히) _init 함수를 호출하는 일이다.
부가적으로 __preinit_array와 __init_array에 설정된 함수 포인터들을 읽어 호출하는 일도 수행한다.
이는 constructor와 비슷한 효과이지만 (.preinit_array와 .init_array 섹션을 이용한다)
대신 main 함수와 같이 argc, argv, envp 인자를 모두 받을 수 있다는 차이가 있다.
마찬가지로 __libc_csu_fini()에서는 _fini와 __fini_array에 대한 처리를 수행한다.

현재 main 함수가 호출되기 전이므로 __libc_csu_init() 함수는 현시점에서 호출할 수 있지만
__libc_csu_fini()의 경우는 main() 호출 뒤에 수행되어야 하며
main() 내에서 exit()를 직접 호출하는 경우도 가능하므로 atexit() 함수를 통해 등록된다.
마지막으로 main() 함수를 수행한 뒤 리턴값을 이용하여 exit()를 호출하고 종료한다.

by namhyung | 2010/03/31 20:39 | System | 트랙백(1) | 덧글(4)
트랙백 주소 : http://studyfoss.egloos.com/tb/5283161
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Tracked from at 2014/03/11 00:38
Commented by 이창주 at 2011/07/07 10:00
잘읽었습니다~:)
Commented by namhyung at 2011/07/09 00:47
답글 남겨주셔서 감사합니다. ^^
Commented by 나그네 at 2012/01/29 22:17
도움이 많이 되었습니다. 정말로 감사드립니다 (__)
Commented by namhyung at 2012/02/03 09:51
도움이 되셨다니 다행입니다. ^^

:         :

:

비공개 덧글

◀ 이전 페이지 다음 페이지 ▶

카테고리
General
Application
System
Kernel
Book
Tips
태그
x86 kernel gcc binutils blktrace bash vcs documentation CAaQA3 computer-architecture glibc block-layer elf perf C sed patch algorithm emacs compiler memory script SMP scm git CARM linux build synchronization awk
전체보기
이글루 파인더

최근 등록된 덧글
informsi yang bagus dan b..
by agen qnc at 06/22
informasi yang bagus dan b..
by agen qnc at 06/22
informasi yang bagus dan b..
by agen qnc at 06/22
최근 등록된 트랙백
Tod's Ferrari Homme
by Tods Pas Cher,Kodak did ..
Mocassin Femme
by Mocassins Homme, I got so..
natural garcinia cambogia
by
rss

skin by jiinny


X