카테고리 없음

[프로그램] C에서 main 보다 먼저 호출되는 함수 만들기

오늘도힘차게 2013. 2. 7. 21:34
728x90

최호진님(누군지 잘모름)의 블러그에서 퍼운 글입니다.

원본은: http://pynoos.byus.net/tt/index.php?pl=134


C에서 main 보다 먼저 호출되는 함수 만들기  

C++ 언어는 그 특성상, 전역 개체의 초기화가 main 보다 먼저 이루어지므로 전역 개체의 생성자에 들어 있는 코드는 main 보다 먼저 호출된다.
이것은 여러가지 트릭으로 사용될 수 있는데, C에는 과연 그런 것이 없을까? 표준 명세에는 없다.
하지만, gcc의 __attribute__에는 그러한 일을 가능하게 해주는 지시자가 있는데, 바로 다음과 같이 사용된다.


void __attribute__((constructor)) before_main( void )
{
     /* Things to do before main function */
}


또한 main 뒤에 호출되는 전형적인 방식은 atexit에 등록하는 것인데, 이것또한


void __attribute__((destructor)) after_main( void )
{
     /* Things to do after main function */
}


와 같은 방식으로 호출된다.

간단한 샘플을 돌려보자면,

$ more c.c
#ifdef STRIP_ATTR
#define __attribute__(x)
#endif
void __attribute__((constructor)) before_main( void )
{
     printf("I miss you Lorthlorien ever beauty.\n");
}

void __attribute__((constructor)) before_main_2nd( void ) 
{
     printf("Bombadil, where have you been in the morning?\n");
}

void __attribute__((destructor)) after_main( void ) 
{
     printf("Mithlandir, help me!\n");
}

int main()
{
     printf("I am working, no touch!\n");
     return 0;
}

$ gcc -o c1 -save-temps c.c
$ ./c1
Bombadil, where have you been in the morning?
I miss you Lorthlorien ever beauty.
I am working, no touch!
Mithlandir, help me!




근사하지 않은가? 다만 의심가는 것은 __attribute((constructor))로 지정한 함수들이 두 개 이상일때의 순서는 어떠하냐는 것인데, 외관상 stack형식으로 먼저 발견되는 것이 나중에 실행되는 것 같다.
자세한 것은 추후에 설명하지도 모르겠다.

이것이 수행되는 원리를 좀더 파헤쳐보자면,

우선 위 코드상에 STRIP_ATTR이라고 되어 있는 부분이 수행되도록하면 평범한 함수가 되는데, 그렇게 생성된 바이너리(c2)의 심볼들을 살펴보자.
nm 은 -n 옵션을 주어 번지로 정렬되도록 하였다.
두 nm 결과를 비교하는데는 unified diff를 사용하여 보자.
그리고 -save-temps 를 통해 생성되는 c.s 라는 어셈블리어 파일은 각각 c1.s c2.s로 이름을 바꾸는 과정이 들어가 있다.


$ mv c.s c1.s
$ gcc -o c1 c.c
$ gcc -o c2 -DSTRIP_ATTR -save-temps c.c
$ mv c.s c2.s
$ nm -n c1 > c1.nm
$ nm -n c2 > c2.nm
$ diff -u c1.nm c2.nm         
--- c1.nm     Thu Jan 5 13:29:16 2006
+++ c2.nm     Thu Jan 5 13:29:20 2006
@@ -27,14 +27,14 @@
0804952c r __FRAME_END__
08049530 D _DYNAMIC
080495f8 d __CTOR_LIST__
-08049604 d __CTOR_END__
-08049608 d __DTOR_LIST__
-08049610 d __DTOR_END__
-08049614 d __JCR_END__
-08049614 d __JCR_LIST__
-08049618 D _GLOBAL_OFFSET_TABLE_
-08049630 A __bss_start
-08049630 A _edata
-08049630 b completed.1
-08049634 b object.2
-0804964c A _end
+080495fc d __CTOR_END__
+08049600 d __DTOR_LIST__
+08049604 d __DTOR_END__
+08049608 d __JCR_END__
+08049608 d __JCR_LIST__
+0804960c D _GLOBAL_OFFSET_TABLE_
+08049624 A __bss_start
+08049624 A _edata
+08049624 b completed.1
+08049628 b object.2
+08049640 A _end


이것을 잘 보아하니 __CTOR_LIST__ 라는 값까지는 같고 그 뒤가 달라지는 것을 볼 수가 있다.
아! __CTOR_LIST__에 내용이 채워지면서 조금씩 그 뒤로 밀려나는구나!

그럼 이 둘의 어셈블코드의 차이는 어떠할까?


$ diff -u c1.s c2.s     
--- c1.s     Thu Jan 5 13:31:04 2006
+++ c2.s     Thu Jan 5 13:31:13 2006
@@ -15,9 +15,6 @@
     leave
     ret
     .size  before_main, .-before_main
-     .section     .ctors,"aw",@progbits
-     .align 4
-     .long  before_main
     .section     .rodata
     .align 32
.LC1:
@@ -34,9 +31,6 @@
     leave
     ret
     .size  before_main_2nd, .-before_main_2nd
-     .section     .ctors
-     .align 4
-     .long  before_main_2nd
     .section     .rodata
.LC2:
     .string "Mithlandir, help me!\n"
@@ -52,9 +46,6 @@
     leave
     ret
     .size  after_main, .-after_main
-     .section     .dtors,"aw",@progbits
-     .align 4
-     .long  after_main
     .section     .rodata
.LC3:
     .string "I am working, no touch!\n"


아앗! 이것은!
단지 section .ctor, .dtor에 함수 포인터만 추가하는 일을 하는 것 아닌가.
그렇다면, .ctor를 찾아서 하나씩 호출하는 부분은 어디에 있다는 것이지? ((계속))

지난 기사에서 .ctor 섹션에 있는 코드들이 어떻게 실행되는지에 대한 호기심만 자극한 채 벌써 일주일이 지났다. 오래 참으시었다.

지난번의 코드를 다시 인용해 보자면, 

$ more c.c
#ifdef STRIP_ATTR
#define __attribute__(x)
#endif
void __attribute__((constructor)) before_main( void )
{
     printf("I miss you Lorthlorien ever beauty.\n");
}

void __attribute__((constructor)) before_main_2nd( void ) 
{
     printf("Bombadil, where have you been in the morning?\n");
}

void __attribute__((destructor)) after_main( void ) 
{
     printf("Mithlandir, help me!\n");
}

int main()
{
     printf("I am working, no touch!\n");
     return 0;
}

$ gcc -o c1 -save-temps c.c
$ ./c1
Bombadil, where have you been in the morning?
I miss you Lorthlorien ever beauty.
I am working, no touch!
Mithlandir, help me!




그리고 그 때 만들었던 심볼 덤프는 다음과 같다.


$ more c1.nm
      w _Jv_RegisterClasses
      w __deregister_frame_info_bases
      w __gmon_start__
      U __libc_start_main@@GLIBC_2.0
      w __register_frame_info_bases
      U printf@@GLIBC_2.0
08048230 T _init
08048280 T _start
080482a4 t call_gmon_start
080482a4 t gcc2_compiled.
080482d0 t __do_global_dtors_aux
08048330 t frame_dummy
08048394 T before_main
080483a8 T before_main_2nd
080483bc T after_main
080483d0 T main
08048400 t __do_global_ctors_aux
08048430 T _fini
08048430 t gcc2_compiled.
08048460 R _fp_hw
08048464 R _IO_stdin_used
08049520 D __data_start
08049520 W data_start
08049524 d __dso_handle
08049528 d p.0
0804952c r __EH_FRAME_BEGIN__
0804952c r __FRAME_END__
08049530 D _DYNAMIC
080495f8 d __CTOR_LIST__
08049604 d __CTOR_END__
08049608 d __DTOR_LIST__
08049610 d __DTOR_END__
08049614 d __JCR_END__
08049614 d __JCR_LIST__
08049618 D _GLOBAL_OFFSET_TABLE_
08049630 A __bss_start
08049630 A _edata
08049630 b completed.1
08049634 b object.2
0804964c A _end


저 코드 중에서 함수들만 추려서 gdb의 브레이크 포인트 구문으로 만들어 보자.


$ cat c1.nm | grep -i " t " | grep -v gcc2 | awk '{print "b", $NF}'     
b _init
b _start
b call_gmon_start
b __do_global_dtors_aux
b frame_dummy
b before_main
b before_main_2nd
b after_main
b main
b __do_global_ctors_aux
b _fini



그리고, 클립보드에 복사한 뒤 다음 명령을 수행한다.


$ gdb c1
GNU gdb 6.1.1
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB. Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnu"...Using host libthread_db library "/lib/libthread_db.so.1".

(gdb) b _init
Breakpoint 1 at 0x8048236
(gdb) b _start
Breakpoint 2 at 0x8048280
(gdb) b call_gmon_start
Breakpoint 3 at 0x80482b5
(gdb) b __do_global_dtors_aux
.
.
.
(gdb) b _fini
Breakpoint 11 at 0x8048441
(gdb) r
Starting program: /home/pynoos/test/crt0/c1 
Breakpoint 11 at 0x4013cab0: file soinit.c, line 66.

Breakpoint 2, 0x08048280 in _start ()
(gdb) continue
Continuing.

Breakpoint 1, 0x08048236 in _init ()
(gdb) continue
Continuing.
(이하 continue 명령으로 계속...)
Breakpoint 3, 0x080482b5 in call_gmon_start ()
Breakpoint 5, 0x08048330 in frame_dummy ()
Breakpoint 10, 0x08048404 in __do_global_ctors_aux ()
Breakpoint 7, 0x080483ae in before_main_2nd ()

Bombadil, where have you been in the morning?

Breakpoint 6, 0x0804839a in before_main ()

I miss you Lorthlorien ever beauty.

Breakpoint 9, 0x080483d6 in main ()

I am working, no touch!

Breakpoint 4, 0x080482d6 in __do_global_dtors_aux ()
Breakpoint 8, 0x080483c2 in after_main ()

Mithlandir, help me!

Breakpoint 11, _fini () at soinit.c:66

(gdb) c
Continuing.

Program exited normally.
(gdb) 


약간 호출 순서대로 살펴보면


_start ()
_init ()
call_gmon_start ()
frame_dummy ()
__do_global_ctors_aux ()
before_main_2nd ()
before_main ()
main ()
__do_global_dtors_aux ()
after_main ()
_fini ()



main 앞에 실행되는 것들이 상당히 많다. 실험용으로 제작한 함수를 제외하면, _start, _init, call_gmon_start, frame_dummy, __do_global_ctors_aux 가 실행된다.
그리고 대칭으로 main 다음에 __do_global_dtors_aux 가 먼저 실행되고, _fini를 끝으로 실행한다.

예상컨데, __do_global_ctors_aux가 실험용으로 제작한 main 함수 이전 함수들을, __do_global_dtors_aux가 실험용으로 제작한 main 함수 이후 함수들을 실행하는 것임을 알 수 있다.

이 놈들이 어디서 흘러왔는지 알아보자. gcc에 -v 옵션을 주어 link에 참여하는 모든 파일들을 나열한 다음 절대 경로로 주어지는 파일들만 골라서 nm 으로 속내를 들여다 보자.


$ gcc -v -o c3 c.c 2>&1 | xargs -n1 echo | grep "^/" | xargs nm -A 2>/dev/null | grep __do_global_ctors_aux
No -c option found for c.c
/usr/local/bin/cc1:083c2c70 t __do_global_ctors_aux
/usr/local/bin/as:08123410 t __do_global_ctors_aux
/usr/local/bin/collect2:080a48f0 t __do_global_ctors_aux
/usr/lib/gcc-lib/i386-redhat-linux/2.96/crtend.o:00000000 t __do_global_ctors_aux



아싸.. crtend.o 에서 발견되는 놈이 정답인거 같다. 나머지는 모두 빌드에 필요한 실행파일들이니까.

자세히 보니 gcc와 같이 딸려오는 라이브러리에 있는것 같다.
그럼 gcc 소스를 봐야하는군.


$ grep -lr __do_global_ctors_aux /usr/src/gcc-2.95.3
/usr/src/gcc-2.95.3/gcc/FSFChangeLog
/usr/src/gcc-2.95.3/gcc/FSFChangeLog.11
/usr/src/gcc-2.95.3/gcc/crtstuff.c
/usr/src/gcc-2.95.3/gcc/config/alpha/crtend.asm
/usr/src/gcc-2.95.3/gcc/tags



대층 보니까 crtstuff.c 에 있는 것 같다. 그 파일에서 인용하자면,


   328 #ifdef CTOR_LIST_BEGIN
   329 CTOR_LIST_BEGIN;
   330 #else
   331 asm (CTORS_SECTION_ASM_OP);   /* cc1 doesn't know that we are switching! */
   332 STATIC func_ptr __CTOR_LIST__[1] __attribute__ ((__unused__))
   333  = { (func_ptr) (-1) };
   334 #endif
   335
   336 #ifdef DTOR_LIST_BEGIN
   337 DTOR_LIST_BEGIN;
   338 #else
   339 asm (DTORS_SECTION_ASM_OP);   /* cc1 doesn't know that we are switching! */
   340 STATIC func_ptr __DTOR_LIST__[1] = { (func_ptr) (-1) };
   341 #endif
   342
   343 #ifdef EH_FRAME_SECTION_ASM_OP
   344 /* Stick a label at the beginning of the frame unwind info so we can register
   345   and deregister it with the exception handling library code. */
   346
   347 asm (EH_FRAME_SECTION_ASM_OP);
   348 #ifdef INIT_SECTION_ASM_OP
   349 STATIC
   350 #endif
   351 char __EH_FRAME_BEGIN__[] = { };
   352 #endif /* EH_FRAME_SECTION_ASM_OP */
   353
   354 #endif /* defined(CRT_BEGIN) */
   355
   356 #ifdef CRT_END
   357
   358 #ifdef INIT_SECTION_ASM_OP
   359
   360 #ifdef OBJECT_FORMAT_ELF
   361
   362 static func_ptr __CTOR_END__[];
   363 static void
   364 __do_global_ctors_aux (void)
   365 {
   366  func_ptr *p;
   367  for (p = __CTOR_END__ - 1; *p != (func_ptr) -1; p--)
   368   (*p) ();
   369 }
   471 #ifdef CTOR_LIST_END
   472 CTOR_LIST_END;
   473 #else
   474 asm (CTORS_SECTION_ASM_OP);   /* cc1 doesn't know that we are switching! */
   475 STATIC func_ptr __CTOR_END__[1] = { (func_ptr) 0 };
   476 #endif




332, 333을 보면 __CTOR_LIST__ 가 가리키는 첫번째에는 -1 값이 들어가고, 그리고 362의 __CTOR_END__는 선언이며, 실제 정의는 475에 존재한다. 이들은 모두 앞부분에 있는 asm (CTORS_SECTION_ASM_OP); 에 의해 .ctor 섹션에 존재하도록 지시된다. Makefile을 잘보니 이 파일이 crtbegin.o와 crtend.o 를 만드는데 사용되며, 각각을 만들 때, __CTOR_LIST__와 __CTOR_END__가 두 파일에 나뉘어 들어가게 되어 있다.

전통적으로 링크할 때는 인자에 넘겨지는 순서대로 심볼들이 배치되므로 그 중간에 .ctor 섹션에 뭔가를 넣고 링커를 호출하면 __CTOR__LIST__에서 출발하여 __CTOR_END__에 이르는 배열을 만들 수 있게된다.

실제로 gcc -v 옵션을 넣어 확인해보면, 


   /usr/lib/gcc-lib/i386-redhat-linux/2.96/collect2 -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o c3 /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crt1.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crti.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtbegin.o -L/usr/lib/gcc-lib/i386-redhat-linux/2.96 -L/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../.. /tmp/ccfOmTjx.o -lgcc -lc -lgcc /usr/lib/gcc-lib/i386-redhat-linux/2.96/crtend.o /usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crtn.o



이렇게 되어 있다. 따라서 .ctor 섹션은 하나의 배열을 만들게 되며,
__do_global_ctors_aux 함수를 살펴보면
__CTOR_END__가 가리키는 윗번지에서부터, -1을 만나기 전까지 거꾸로 차례차례 함수를 호출하도록 되어 있다. 

그러니 before_main_2nd 부터 수행되겠다!!, 에~~ 그런것이었구만.


728x90