gcc attribute: constructor & destructor
Explore the internal process and usage of gcc attributes: constructor (pre-main execution) and destructor (post-main execution).
March 9, 2026
gcc attribute: constructor & destructor
Reqruies
- Compiler:
gcc 2.7.2이상
C++에서는 클래스 인스턴스가 생성될 때 호출되는
constructor와, 라이프 사이클이 끝날 때 호출되는 destructor의 개념이 존재합니다.안타깝게도 C 언어에는 객체 지향 언어처럼 생성과 소멸에 따른
constructor와 destructor의 개념이 기본적으로 존재하지 않습니다.💡 참고: C 언어에서도weak심볼과LD_PRELOAD기법을 활용하면 우회적으로 함수를 후킹(Hooking)하여 유사한 효과를 낼 수는 있습니다.
하지만, gcc 컴파일러는 이를 보완하기 위한 확장(Extension) 기능으로써 main 함수 실행 전에 호출되는 constructor attribute과, main 함수 종료 후에 호출되는 destructor attribute 기능을 제공합니다.
gcc-4.7.0/Function-AttributesDocconstructor destructor constructor (priority) destructor (priority)
constructor 속성은 프로그램이
main()에 진입하기 전에 함수가 자동으로 호출되도록 합니다. 마찬가지로 destructor 속성은main()이 완료되거나exit()이 호출된 후 함수가 자동으로 호출되도록 합니다.이러한 속성이 적용된 함수는 프로그램 실행 중 암시적으로 사용될 데이터를 초기화하는 데 유용합니다.
선택적으로 정수
우선순위(priority)를 제공하여 constructor와 destructor 함수의 실행 순서를 제어할 수 있습니다.
constructor는 우선순위 번호가 작을수록 더 먼저 실행되며,destructor는 그 반대의 관계를 가집니다.따라서 리소스를 할당하는
constructor와 동일한 리소스를 해제하는destructor가 있다면 두 함수는 일반적으로 같은 우선순위를 가집니다.
constructor와destructor함수의 우선순위는 C++ 객체에서 지정된 것과 동일합니다.
사용 방법은 매우 간단합니다.
usage.cc
Compile & Runbash
attribute((constructor))를 사용하여 정의된
ctor_func가 먼저 호출되고, attribute((destructor))를 사용하여 정의된 dtors_func가 main 함수 이후에 호출되는 것을 볼 수 있습니다.이제
constructor와 destructor가 내부적으로 어떻게 호출되는지 살펴보겠습니다.Internal Process of Constructor and Destructor
먼저 gcc가 이를 어떻게 관리하고 호출하는지 확인해 보겠습니다.
gcc 4.7 이전에는
constructors가 ELF의 .ctors 섹션을 사용하여 관리되었고, destructors는 .dtors 섹션을 사용하여 관리되었습니다.반면에 gcc 4.7 이후부터는 TARGET_ASM_CONSTRUCTOR가 default_elf_init_array_asm_out_constructor로 정의되어 있으며,
이는 get_elf_initfini_array_priority_section을 호출하고 함수들을 ELF의
.init_array 섹션과 .fini_array 섹션에 저장합니다.이러한 호출 과정을 자세히 살펴보겠습니다.
기준은
먼저 앞서 생성한 샘플 코드의 시작 주소(start address)를 확인합니다.
.init_array 로 합니다.먼저 앞서 생성한 샘플 코드의 시작 주소(start address)를 확인합니다.
Check start addressbash
위에서 시작 주소(start address)를 확인할 수 있습니다.
이 값은 ELF 헤더의
e_entry 값입니다.objdump를 사용하여 시작 주소값에 위치한 함수를 찾아보면, 그것이 _start 함수라는 것을 확인할 수 있습니다.The starting position of ELFbash
이
_start 함수를 살펴보면, 내부적으로 __libc_csu_init, __libc_csu_fini 및 main 함수의 주소를 레지스터 레벨에 저장하고 _libc_start_main을 호출하는 구조를 띠고 있습니다.__libc_start_main은 프로그램의 진입점인
main()을 호출하기 전에, glibc 내부에 정의된 csu (C start up) 혹은 crt (C runtime) 루틴들을 처리하는 중요한 함수입니다.참고로, gcc를 빌드할 때
-v 옵션을 사용하면 링크 과정을 볼 수 있습니다.Check link processbash
gcc의
sysroot 디렉터리에 위치한 crt*.o 오브젝트 파일들이 이러한 startup 루틴 역할을 수행하는 데 포함됩니다.glibc의 LIBC_START_MAIN 내부에서는 call_init()과 call_fini()가 호출됩니다.
call_init()은 __init_array_start를 연이어 호출하고 call_fini()는 __fini_array_start를 연이어 호출합니다.
check libc-start.c in glibcc
Call init and fini sequenceMermaidsequenceDiagram participant LIBC_START_MAIN participant call_init participant init_array as __init_array_start[] participant main participant call_fini participant fini_array as __fini_array_start[] LIBC_START_MAIN->>call_init: Call call_init() activate call_init loop i = 0 to size call_init->>init_array: (*__init_array_start[i])(argc, argv, envp) Note right of init_array: Execute functions with constructor attribute end call_init-->>LIBC_START_MAIN: Return deactivate call_init LIBC_START_MAIN->>main: Call main() activate main Note right of main: Execute user program main-->>LIBC_START_MAIN: Return deactivate main LIBC_START_MAIN->>call_fini: Call call_fini() activate call_fini loop i = size down to 0 call_fini->>fini_array: (*__fini_array_start[i])() Note right of fini_array: Execute functions with destructor attribute (reverse order) end call_fini-->>LIBC_START_MAIN: Return deactivate call_fini
그렇다면 이번에는 실제로 어떤 함수들이 init_array 및 fini_array에 등록되어 실행되는지 확인해 보겠습니다.
샘플 프로그램의 ELF 동적 섹션(dynamic section)을 살펴보면,
INIT_ARRAY / FINI_ARRAY 및 INIT_ARRAYSZ / FINI_ARRAYSZ가 있습니다.Check ELF dynamic sectionbash
디버거(gdb)를 사용하여 INIT_ARRAYSZ 크기만큼 실제로 INIT_ARRAY 메모리 영역을 덤프해 보겠습니다.
Inside INIT_ARRAYbash
덤프된 x/2g 명령의 메모리 결과값을 디스어셈블해보면, 명시적으로 선언했던 ctor_func 의 함수 시작 주소가 INIT_ARRAY 배열에 등록되어 있음을 확인할 수 있습니다.
이 원리는 FINI_ARRAY에서도 동일하게 적용됩니다.
Inside FINI_ARRAYbash
함수 주소가 배열 형태로 관리된다는 것은 곧 여러 개의 함수를 한 번에 등록하여 순서대로 호출하게 만들 수 있다는 점을 의미합니다.
따라서 priorities를 개별 속성에 명기하여 프로그래머가 원하는 실행 순서를 구체적으로 지정할 수도 있습니다.
Priorities
Supporting priorities: ctor_pri.cc
Testing Priority valuesbash
생성자(constructor)의 경우에는 속성에 명시적으로 전달된 우선순위 숫자 값이 작을수록 더 빠르게 앞서서 호출됩니다.
반대로 소멸자(destructor)의 경우에는 우선순위 값이 클수록 후반이 아닌 초반에 더 빠르게 호출되어 뒷정리 과정에서의 우선권을 갖습니다.
우선순위 값은 101 이상의 값을 자유롭게 지정하여 사용할 수 있습니다.
그렇다면, 동일한 우선순위를 가진 여러 함수들이 등록될 경우에는 어떻게 동작할까요?
How are the same priorities handled?c
Testing Identical Prioritiesbash
위의 출력 결과를 통해, 우선순위의 숫자 값이 완전히 동일한 상황에서는 소스코드가 컴파일러에 의해
파싱(Parsing)되어 분석된 순서 그대로 배열에 차례로 등록되고, 또 그 순차적인 구조에 따라 실행되는 것을 알 수 있습니다.마지막으로 호출되는 소멸자(destructor) 역시 스택(Stack)의 LIFO 원리에 충실하게, 가장 먼저 등록된 생성자의 소멸자가 가장 나중에 불리도록 역순으로 안전하게 호출되는 것도 완벽히 보장됩니다.
Behavior on Abnormal Termination
만약 프로그램이
main 함수의 정상적인 종료(Return) 과정을 거치지 않고, Signal Handler와 exit 시스템 콜 등에 의해 강제로 종료되는 예외 상황이 발생하면 어떻게 될까요?이 특수 상황에서도 스크립트 내부에 할당된 소멸자(destructor)가 올바르게 호출되는지 추가로 검증해 보겠습니다.
Does it work in signal handler too?c
Testing Signal Handlerbash
프로그램이
while (1)에 의해 블로킹 상태로 무한 루프를 돌고 있다가 SIGINT 외부 인터럽트 시그널 등을 수신하여 중간에 루프가 중단되더라도, 시그널 핸들러 내부에 exit(0) 호출을 명시적으로 적어줌으로써 OS의 개입과 함께 정상적으로 dtors_func() 소멸자의 호출을 보장할 수 있게 됩니다.Leveraging in Shared Libraries
이 생성자/소멸자 속성 제어 기능은 단순히 단일 실행 파일에서뿐만 아니라, 외부 동적 컴파일 라이브러리(Shared/Dynamic Library)와 함께 맞물려 동작할 때 진정한 위력을 발휘하게 됩니다.
이미 컴파일되어 바이너리로 배포된 타겟 애플리케이션일지라도 해당 애플리케이션을 다시 빌드하거나 소스 코드를 수정할 필요가 전혀 없습니다.
단순히 생성자와 소멸자가 구비된 외부 라이브러리를 메모리에 상주(LD_PRELOAD 등)시키는 것만으로, 메인 함수 진입 전후에 원하는 코드 로직을 언제든 간편하게 추가 주입할 수 있기 때문입니다.
단순히 생성자와 소멸자가 구비된 외부 라이브러리를 메모리에 상주(LD_PRELOAD 등)시키는 것만으로, 메인 함수 진입 전후에 원하는 코드 로직을 언제든 간편하게 추가 주입할 수 있기 때문입니다.
Testing Shared Library (hooklib.c)c
Testing LD_PRELOADbash
물론, LD_PRELOAD 뿐만 아니라 개발 중인 애플리케이션에서 직접 링크되어 사용 중인 일반 동적 라이브러리에서도 바로 사용할 수 있습니다.
Testing Shared Library Link (hooklib.c)c
sample.cc
Testing Shared Library Executionbash
Conclusion
이처럼 gcc에서 기본 제공하는 이러한 컴파일러 속성(attribute)을 잘 응용하면, 다채로운 테크닉 시도가 가능해집니다.
예를 들면, 이미 소스가 유실되거나 빌드가 끝난 블랙박스 바이너리를 우회 디버깅하기 위한 안전한 백도어 후킹(Hooking) 인터페이스 용도로 가볍게 사용할 수 있으며, 무거운 전역 변수(Global Variable)의 동적인 메모리 할당 및 해제 제어로 인한 치명적인 메모리 누수 방지 같은 최적화 목적으로도 폭넓게 사용할 수 있습니다.