모두의 코드
씹어먹는 C++ - <20 - 4. 코드 부터 실행 파일 까지 - main 으로의 여정>

작성일 : 2020-11-30 이 글은 1510 번 읽혔습니다.

이번 강좌에서는

  • main 함수가 호출되는 경로

  • 전역 객체들의 생성자는 어떻게 호출되는지

  • 링커 스크립트

  • crt 라이브러리들

에 대해서 다루어 보겠습니다.

안녕하세요! 지난 강의에서 링킹이 어떠한 과정으로 이루어지는지 배웠습니다. 그렇다면 이번 강의에서는 생성된 ELF 파일을 실행 할 때 어떠한 경로로 main 함수가 호출되는 것인지 살펴겠습니다.

참고 사항

아래 서술하는 모든 내용은 리눅스 상에서 GCC 컴파일러 기준으로 ELF 형식의 실행 파일을 생성함을 기준으로 작성된 것입니다. 윈도우의 경우 이와 다를 수 있습니다.

세상에서 제일 간단한 C++ 프로그램

이 세상에서 가장 간단한 C++ 프로그램을 만들어봅시다.

int main() {}

GCC 컴파일러를 사용해서 만들어진 ELF 실행 파일의 크기를 보면

$ ls -l test
-rwxrwxr-x 1 jaebum jaebum 17392 Nov 29 22:35 test

놀랍게도 무려 17392 바이트나 된다는 것을 알 수 있습니다. 도대체 이 완성된 실행 파일에 뭐가 들어있길래 이렇게 큰 용량을 차지할까요? 한 번 objdump 로 실행파일이 어떠한 코드로 이루어져 있는지 살펴보겠습니다.

 objdump -S test

test:     file format elf64-x86-64


Disassembly of section .init:

0000000000001000 <_init>:
    1000:	f3 0f 1e fa          	endbr64 
    1004:	48 83 ec 08          	sub    $0x8,%rsp
    1008:	48 8b 05 d9 2f 00 00 	mov    0x2fd9(%rip),%rax        # 3fe8 <__gmon_start__>
    100f:	48 85 c0             	test   %rax,%rax
    1012:	74 02                	je     1016 <_init+0x16>
    1014:	ff d0                	callq  *%rax
    1016:	48 83 c4 08          	add    $0x8,%rsp
    101a:	c3                   	retq   

Disassembly of section .plt:

0000000000001020 <.plt>:
    1020:	ff 35 a2 2f 00 00    	pushq  0x2fa2(%rip)        # 3fc8 <_GLOBAL_OFFSET_TABLE_+0x8>
    1026:	f2 ff 25 a3 2f 00 00 	bnd jmpq *0x2fa3(%rip)        # 3fd0 <_GLOBAL_OFFSET_TABLE_+0x10>
    102d:	0f 1f 00             	nopl   (%rax)

Disassembly of section .plt.got:

0000000000001030 <__cxa_finalize@plt>:
    1030:	f3 0f 1e fa          	endbr64 
    1034:	f2 ff 25 bd 2f 00 00 	bnd jmpq *0x2fbd(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    103b:	0f 1f 44 00 00       	nopl   0x0(%rax,%rax,1)

Disassembly of section .text:

0000000000001040 <_start>:
    1040:	f3 0f 1e fa          	endbr64 
    1044:	31 ed                	xor    %ebp,%ebp
    1046:	49 89 d1             	mov    %rdx,%r9
    1049:	5e                   	pop    %rsi
    104a:	48 89 e2             	mov    %rsp,%rdx
    104d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
    1051:	50                   	push   %rax
    1052:	54                   	push   %rsp
    1053:	4c 8d 05 56 01 00 00 	lea    0x156(%rip),%r8        # 11b0 <__libc_csu_fini>
    105a:	48 8d 0d df 00 00 00 	lea    0xdf(%rip),%rcx        # 1140 <__libc_csu_init>
    1061:	48 8d 3d c1 00 00 00 	lea    0xc1(%rip),%rdi        # 1129 <main>
    1068:	ff 15 72 2f 00 00    	callq  *0x2f72(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    106e:	f4                   	hlt    
    106f:	90                   	nop

0000000000001070 <deregister_tm_clones>:
    1070:	48 8d 3d 99 2f 00 00 	lea    0x2f99(%rip),%rdi        # 4010 <__TMC_END__>
    1077:	48 8d 05 92 2f 00 00 	lea    0x2f92(%rip),%rax        # 4010 <__TMC_END__>
    107e:	48 39 f8             	cmp    %rdi,%rax
    1081:	74 15                	je     1098 <deregister_tm_clones+0x28>
    1083:	48 8b 05 4e 2f 00 00 	mov    0x2f4e(%rip),%rax        # 3fd8 <_ITM_deregisterTMCloneTable>
    108a:	48 85 c0             	test   %rax,%rax
    108d:	74 09                	je     1098 <deregister_tm_clones+0x28>
    108f:	ff e0                	jmpq   *%rax
    1091:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)
    1098:	c3                   	retq   
    1099:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

00000000000010a0 <register_tm_clones>:
    10a0:	48 8d 3d 69 2f 00 00 	lea    0x2f69(%rip),%rdi        # 4010 <__TMC_END__>
    10a7:	48 8d 35 62 2f 00 00 	lea    0x2f62(%rip),%rsi        # 4010 <__TMC_END__>
    10ae:	48 29 fe             	sub    %rdi,%rsi
    10b1:	48 89 f0             	mov    %rsi,%rax
    10b4:	48 c1 ee 3f          	shr    $0x3f,%rsi
    10b8:	48 c1 f8 03          	sar    $0x3,%rax
    10bc:	48 01 c6             	add    %rax,%rsi
    10bf:	48 d1 fe             	sar    %rsi
    10c2:	74 14                	je     10d8 <register_tm_clones+0x38>
    10c4:	48 8b 05 25 2f 00 00 	mov    0x2f25(%rip),%rax        # 3ff0 <_ITM_registerTMCloneTable>
    10cb:	48 85 c0             	test   %rax,%rax
    10ce:	74 08                	je     10d8 <register_tm_clones+0x38>
    10d0:	ff e0                	jmpq   *%rax
    10d2:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)
    10d8:	c3                   	retq   
    10d9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

00000000000010e0 <__do_global_dtors_aux>:
    10e0:	f3 0f 1e fa          	endbr64 
    10e4:	80 3d 25 2f 00 00 00 	cmpb   $0x0,0x2f25(%rip)        # 4010 <__TMC_END__>
    10eb:	75 2b                	jne    1118 <__do_global_dtors_aux+0x38>
    10ed:	55                   	push   %rbp
    10ee:	48 83 3d 02 2f 00 00 	cmpq   $0x0,0x2f02(%rip)        # 3ff8 <__cxa_finalize@GLIBC_2.2.5>
    10f5:	00 
    10f6:	48 89 e5             	mov    %rsp,%rbp
    10f9:	74 0c                	je     1107 <__do_global_dtors_aux+0x27>
    10fb:	48 8b 3d 06 2f 00 00 	mov    0x2f06(%rip),%rdi        # 4008 <__dso_handle>
    1102:	e8 29 ff ff ff       	callq  1030 <__cxa_finalize@plt>
    1107:	e8 64 ff ff ff       	callq  1070 <deregister_tm_clones>
    110c:	c6 05 fd 2e 00 00 01 	movb   $0x1,0x2efd(%rip)        # 4010 <__TMC_END__>
    1113:	5d                   	pop    %rbp
    1114:	c3                   	retq   
    1115:	0f 1f 00             	nopl   (%rax)
    1118:	c3                   	retq   
    1119:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

0000000000001120 <frame_dummy>:
    1120:	f3 0f 1e fa          	endbr64 
    1124:	e9 77 ff ff ff       	jmpq   10a0 <register_tm_clones>

0000000000001129 <main>:
    1129:	f3 0f 1e fa          	endbr64 
    112d:	55                   	push   %rbp
    112e:	48 89 e5             	mov    %rsp,%rbp
    1131:	b8 00 00 00 00       	mov    $0x0,%eax
    1136:	5d                   	pop    %rbp
    1137:	c3                   	retq   
    1138:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
    113f:	00 

0000000000001140 <__libc_csu_init>:
    1140:	f3 0f 1e fa          	endbr64 
    1144:	41 57                	push   %r15
    1146:	4c 8d 3d a3 2c 00 00 	lea    0x2ca3(%rip),%r15        # 3df0 <__frame_dummy_init_array_entry>
    114d:	41 56                	push   %r14
    114f:	49 89 d6             	mov    %rdx,%r14
    1152:	41 55                	push   %r13
    1154:	49 89 f5             	mov    %rsi,%r13
    1157:	41 54                	push   %r12
    1159:	41 89 fc             	mov    %edi,%r12d
    115c:	55                   	push   %rbp
    115d:	48 8d 2d 94 2c 00 00 	lea    0x2c94(%rip),%rbp        # 3df8 <__do_global_dtors_aux_fini_array_entry>
    1164:	53                   	push   %rbx
    1165:	4c 29 fd             	sub    %r15,%rbp
    1168:	48 83 ec 08          	sub    $0x8,%rsp
    116c:	e8 8f fe ff ff       	callq  1000 <_init>
    1171:	48 c1 fd 03          	sar    $0x3,%rbp
    1175:	74 1f                	je     1196 <__libc_csu_init+0x56>
    1177:	31 db                	xor    %ebx,%ebx
    1179:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)
    1180:	4c 89 f2             	mov    %r14,%rdx
    1183:	4c 89 ee             	mov    %r13,%rsi
    1186:	44 89 e7             	mov    %r12d,%edi
    1189:	41 ff 14 df          	callq  *(%r15,%rbx,8)
    118d:	48 83 c3 01          	add    $0x1,%rbx
    1191:	48 39 dd             	cmp    %rbx,%rbp
    1194:	75 ea                	jne    1180 <__libc_csu_init+0x40>
    1196:	48 83 c4 08          	add    $0x8,%rsp
    119a:	5b                   	pop    %rbx
    119b:	5d                   	pop    %rbp
    119c:	41 5c                	pop    %r12
    119e:	41 5d                	pop    %r13
    11a0:	41 5e                	pop    %r14
    11a2:	41 5f                	pop    %r15
    11a4:	c3                   	retq   
    11a5:	66 66 2e 0f 1f 84 00 	data16 nopw %cs:0x0(%rax,%rax,1)
    11ac:	00 00 00 00 

00000000000011b0 <__libc_csu_fini>:
    11b0:	f3 0f 1e fa          	endbr64 
    11b4:	c3                   	retq   

Disassembly of section .fini:

00000000000011b8 <_fini>:
    11b8:	f3 0f 1e fa          	endbr64 
    11bc:	48 83 ec 08          	sub    $0x8,%rsp
    11c0:	48 83 c4 08          	add    $0x8,%rsp
    11c4:	c3                   	retq   

생각보다 실행 파일에 많은 코드들이 들어가 있습니다. 실제로 우리가 작성한 코드는 딱 이부분 하나 입니다.

0000000000001129 <main>:
    1129:	f3 0f 1e fa          	endbr64 
    112d:	55                   	push   %rbp
    112e:	48 89 e5             	mov    %rsp,%rbp
    1131:	b8 00 00 00 00       	mov    $0x0,%eax
    1136:	5d                   	pop    %rbp
    1137:	c3                   	retq   
    1138:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
    113f:	00 

그렇다면 나머지 부분은 도대체 뭘까요? 일단 위에서 보시다 시피 main 함수에선 다른 정의된 함수들을 호출하지 않기 때문에 다른 함수들이 도무지 왜 필요할까요? 그런데 여기엔 사실 비밀이 있습니다. 바로 프로그램 실행 시에 가장 먼저 호출되는 함수가 main 이 아니기 때문이죠!

가장 먼저 시작되는 함수는 main 이 아니다

여태까지 프로그램 시작 시에 가장 먼저 호출되는 함수가 main 이라고 알고 계셨곗지만 이는 사실이 아닙니다. main 함수를 실행하기 전에 여러가지 준비해야 될 것들이 많기 때문이죠. 가장 먼저 호출되어야 할 함수의 위치는 실행 파일 정보에 써져 있습니다.

$ readelf -h test
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1040
  Start of program headers:          64 (bytes into file)
  Start of section headers:          14608 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

위는 ELF 실행 파일의 정보를 보여주는 readelf 라는 유틸리티를 사용한 것입니다. 위 처럼 우리가 만든 실행 파일의 여러가지 정보를 보여주고 있는데, 여기서 주목할 부분은 Entry point address 라고 쓰여져 있는 부분입니다.

우리가 프로그램을 실행하면 프로그램 로더는 ELF 파일의 Entry point address 를 읽고 해당 위치로 RIP 를 이동시킵니다. 그렇다면 위 프로그램의 0x1040 에는 뭐가 있을까요?

0000000000001040 <_start>:
    1040:	f3 0f 1e fa          	endbr64 
    1044:	31 ed                	xor    %ebp,%ebp
    1046:	49 89 d1             	mov    %rdx,%r9
    1049:	5e                   	pop    %rsi
    104a:	48 89 e2             	mov    %rsp,%rdx
    104d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
    1051:	50                   	push   %rax
    1052:	54                   	push   %rsp
    1053:	4c 8d 05 56 01 00 00 	lea    0x156(%rip),%r8        # 11b0 <__libc_csu_fini>
    105a:	48 8d 0d df 00 00 00 	lea    0xdf(%rip),%rcx        # 1140 <__libc_csu_init>
    1061:	48 8d 3d c1 00 00 00 	lea    0xc1(%rip),%rdi        # 1129 <main>
    1068:	ff 15 72 2f 00 00    	callq  *0x2f72(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    106e:	f4                   	hlt    
    106f:	90                   	nop

바로 _start 함수가 있는 것을 볼 수 있습니다. 그렇습니다. 사실 main 이 아니라 _start 함수가 프로그램 시작 시 가장 먼저 호출되는 함수라고 보시면 됩니다.

_start 함수 자체에서는 그닥 별로 하는 것이 없습니다. 0x1068 에서 __libc_start_main 함수를 호출하기 위해 전달할 인자들을 준비해주는 것 뿐입니다. __libc_start_main 함수는 C++ 로 작성된 프로그램을 시작하기 위해서 필요한 여러가지 환경들을 준비하는 함수 입니다. 하는 일로;

  1. 프로그램의 Effective User IdReal User Id 와 다를 때 필요한 보안 관련 체크를 수행

  2. 쓰레드 사용을 위한 준비

  3. 프로그램 실행 시 실행해야 하는 루틴들을 등록

  4. 프로그램 시작 시에 실행해야 할 초기화 함수를 호출

  5. main 함수 호출

  6. main 함수 리턴 시, 해당 리턴값으로 exit 을 호출

등등 여러가지 일들을 하고 있습니다. 전체 코드를 보고 싶으신 분들은 여기 에서 보시면 됩니다. 물론 라이브러리 코드라 읽기 썩 쉽지만은 않습니다.

__libc_start_main 는 아래와 같이 생겼습니다.

int __libc_start_main(int *(main)(int, char **, char **), int argc,
                      char **ubp_av, void (*init)(void), void (*fini)(void),
                      void (*rtld_fini)(void), void(*stack_end));

이 때 위 처럼 첫 번째 인자로 호출할 main 함수를 전달하고 있고, 네 번째 인자 (init) 에 초기화를 해줄 함수를 전달하고 있는데, 우리의 경우 보시다시피, __libc_csu_init 함수를 초기화 함수로 전달되고 있습니다.

__libc_csu_init 함수의 코드를 보면 간단합니다. 아래처럼 __init_array_start 부터 __init_array_end 까지 정의되어 있는 라는 함수 포인터 배열에 있는 함수들을 하나씩 돌아가면서 호출하고 있을 뿐입니다.

extern void (*__init_array_start[])(int, char **, char **) attribute_hidden;

void __libc_csu_init(int argc, char **argv, char **envp) {
  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);
  }
}

한 가지 재미있는 점은 __init_array_start__init_array_end 의 값은 코드 어디에서도 제공되는 것이 아닙니다 (__init_array_start = 0x1234 와 같은 코드를 볼 수 없다는 것입니다). 이 변수들의 값은 링커에 의해서 직접 제공됩니다. 따라서 만약에 우리가 임의로 __init_array_start 라는 이름의 배열을 정의한다면 프로그램 실행 시에 위 for loop 이 제대로 작동하지 않겠죠.

아무튼 __init_array_start 에 등록된 함수들을 통해서 main 함수 시작 전에 필요한 함수들을 호출할 수 있게 됩니다. 예를 들어서 아래와 같은 코드를 생각해봅시다.

#include <iostream>

class A {
 public:
  A() { std::cout << "A constructor \n"; }
};

A a;
int main() { std::cout << "main! \n"; }

성공적으로 컴파일 하였다면

실행 결과

A constructor 
main! 

와 같이 나옵니다. 위 코드에서 사용되고 있는 전역 객체는 두 가지가 있습니다. 하나는 우리가 정의한 A 클래스의 a 라는 이름의 전역 변수가 있고, 그리고 std::cout 이 있습니다. 이 두 객체들은 main 호출 전에 반드시 초기화가 이루어져야 합니다.

어떤 식으로 초기화가 이루어지는지 확인하기 위해서는 __init_array_start__init_array_end 가 어떠한 값을 가지는지 알아야 합니다. 그런데 위에서 말했듯이 해당 심볼들의 값은 링커에 의해서 정의된다고 했었죠? 이를 위해선 링커가 어떠한 식으로 심볼들을 배치하는지 알아야 합니다.

링커 스크립트 (Linker script)

링커 스크립트(Linker script)는 링커가 심볼들을 실행 파일 내부에 여러 섹션에 배치할 때 어떤 식으로 배치할지 알려주는 스크립트 입니다. GCC 의 링커의 경우 우리가 따로 명시 하지 않을 경우디폴트로 사용하고 있는 스크립트가 있습니다. 이를 보기 위해서는 아래와 같이 링커에 verbose 옵션을 주면 됩니다.

$ gcc test.cc -g -o test -Wl,--verbose
.... (생략) ....
SECTIONS
{
  .... (생략) ....
  .init_array    :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
    KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
    PROVIDE_HIDDEN (__init_array_end = .);
  }

링커 스크립트 문법은 간단합니다. (정확한 문법을 이해하고 싶으신 분들은 여기를 참조해주세요.) 먼저 원하는 섹션 이름을 명시한 뒤에, 중괄호 안에 어떠한 심볼들을 배치할 지 써 넣으면 됩니다.

그 다음에

    PROVIDE_HIDDEN (__init_array_start = .);

. 은 현재의 위치라는 의미를 가집니다. 따라서 __init_array_start 에는 init_array 섹션의 시작 주소값이 들어가게 되고, 마찬가지로 __init_array_end 에는 init_array 섹션의 마지막 주소값이 들어가겠죠. 아무튼 __init_array_start__init_array_end 의 위치를 알고 싶다면 실행 파일에서 init_array 를 보면 된다는 것입니다.

자 이제 init_array 에 뭐가 있는지 볼까요?

$ objudmp -s test
Contents of section .init_array:
 3d88 80110000 00000000 04120000 00000000  ................

objdump 에 -s 옵션을 주면 각 섹션들의 내용을 볼 수 있습니다. 리틀 엔디안임을 감안해서 보자면, 첫 번째 원소로 0x1180 이 있고, 두 번째 원소로 0x1204 가 있습니다. 첫 번째 원소는 frame_dummy 를 호출하는 것인데 이는 나중에 살펴보기로 하고, 두 번째 원소인 0x1204 를 호출하는 것을 봅시다. 0x1204 에는 뭐가 있냐면;

0000000000001204 <_GLOBAL__sub_I_a>:
    1204:	f3 0f 1e fa          	endbr64 
    1208:	55                   	push   %rbp
    1209:	48 89 e5             	mov    %rsp,%rbp
    120c:	be ff ff 00 00       	mov    $0xffff,%esi
    1211:	bf 01 00 00 00       	mov    $0x1,%edi
    1216:	e8 90 ff ff ff       	callq  11ab <_Z41__static_initialization_and_destruction_0ii>
    121b:	5d                   	pop    %rbp
    121c:	c3                   	retq   
    121d:	90                   	nop

위 처럼 _Z41__static_initialization_and_destruction_0ii 함수를 호출하는 부분이 있습니다. 이름만 보아도, C++ 컴파일러가 프로그램 내에 전역 및 static 객체들의 초기화를 위해서 만들어준 함수 임을 알 수 있죠. 그럼 해당 함수로 가봅시다.

00000000000011ab <_Z41__static_initialization_and_destruction_0ii>:
    11ab:	f3 0f 1e fa          	endbr64 
    11af:	55                   	push   %rbp
    11b0:	48 89 e5             	mov    %rsp,%rbp
    11b3:	48 83 ec 10          	sub    $0x10,%rsp
    11b7:	89 7d fc             	mov    %edi,-0x4(%rbp)
    11ba:	89 75 f8             	mov    %esi,-0x8(%rbp)
    11bd:	83 7d fc 01          	cmpl   $0x1,-0x4(%rbp)
    11c1:	75 3e                	jne    1201 <_Z41__static_initialization_and_destruction_0ii+0x56>
    11c3:	81 7d f8 ff ff 00 00 	cmpl   $0xffff,-0x8(%rbp)
    11ca:	75 35                	jne    1201 <_Z41__static_initialization_and_destruction_0ii+0x56>
  extern wostream wclog;	/// Linked to standard error (buffered)
#endif
  //@}

  // For construction of filebuffers for cout, cin, cerr, clog et. al.
  static ios_base::Init __ioinit;
    11cc:	48 8d 3d 7f 2f 00 00 	lea    0x2f7f(%rip),%rdi        # 4152 <_ZStL8__ioinit>
    11d3:	e8 b8 fe ff ff       	callq  1090 <_ZNSt8ios_base4InitC1Ev@plt>
    11d8:	48 8d 15 29 2e 00 00 	lea    0x2e29(%rip),%rdx        # 4008 <__dso_handle>
    11df:	48 8d 35 6c 2f 00 00 	lea    0x2f6c(%rip),%rsi        # 4152 <_ZStL8__ioinit>
    11e6:	48 8b 05 0b 2e 00 00 	mov    0x2e0b(%rip),%rax        # 3ff8 <_ZNSt8ios_base4InitD1Ev@GLIBCXX_3.4>
    11ed:	48 89 c7             	mov    %rax,%rdi
    11f0:	e8 7b fe ff ff       	callq  1070 <__cxa_atexit@plt>
A a;
    11f5:	48 8d 3d 55 2f 00 00 	lea    0x2f55(%rip),%rdi        # 4151 <a>
    11fc:	e8 1d 00 00 00       	callq  121e <_ZN1AC1Ev>
}
    1201:	90                   	nop
    1202:	c9                   	leaveq 
    1203:	c3                   	retq   

보시다 시피, 먼저 ios_base 에 정의된 Init 함수를 호출함으로써 cout, cin 등등의 입출력 객체들을 정의하는 것을 볼 수 있고 (0x11d3), 밑에 0x11fc에서 A 의 생성자인 _ZN1AC1Ev 를 호출하고 있습니다. (맹글링된 이름임을 염두!) 실제로 _ZN1AC1Ev 에 가보면

000000000000121e <_ZN1AC1Ev>:
  A() { std::cout << "A constructor \n"; }
    121e:	f3 0f 1e fa          	endbr64 
    1222:	55                   	push   %rbp
    1223:	48 89 e5             	mov    %rsp,%rbp
    1226:	48 83 ec 10          	sub    $0x10,%rsp
    122a:	48 89 7d f8          	mov    %rdi,-0x8(%rbp)
    122e:	48 8d 35 d0 0d 00 00 	lea    0xdd0(%rip),%rsi        # 2005 <_ZStL19piecewise_construct+0x1>
    1235:	48 8d 3d 04 2e 00 00 	lea    0x2e04(%rip),%rdi        # 4040 <_ZSt4cout@@GLIBCXX_3.4>
    123c:	e8 3f fe ff ff       	callq  1080 <_ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@plt>
    1241:	90                   	nop
    1242:	c9                   	leaveq 
    1243:	c3                   	retq   
    1244:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
    124b:	00 00 00 
    124e:	66 90                	xchg   %ax,%ax

위 처럼 A constructor 을 출력하고 있습니다.

register_tm_clones

앞서 init_array 에 첫 번째 원소로 frame_dummy 의 주소값이 있었습니다. frame_dummy 에 뭐가 있냐면;

0000000000001180 <frame_dummy>:
    1180:	f3 0f 1e fa          	endbr64 
    1184:	e9 77 ff ff ff       	jmpq   1100 <register_tm_clones>

그냥 위와 같이 register_tm_clones 함수를 호출하고 있습니다. register_tm_clones 는 C 와 C++ 에서 Transactional Memory (TM) 를 지원하기 위해서 필요한 여러가지 작업들을 수행합니다. TM 이란 한 블록 안에 있는 일련의 작업들을 atomic 하게 실행하기 위한 도구인데, 예를 들어서

int f() {
  static int i = 0;
  synchronized {  // begin synchronized block
    std::cout << i << " -> ";
    ++i;  // each call to f() obtains a unique value of i
    std::cout << i << '\n';
    return i;  // end synchronized block
  }
}

와 같이 __synchronized 블록을 정의한다면 해당 블록 안에서 일어나는 일은 모두 atomic 하게 일어납니다. 다시말해 f() 를 여러 쓰레드에서 실행하였을 경우 출력 결과가 0 -> 1, 1 -> 2, .. 으로만 나온다는 것이죠. 따라서 mutex 와 같은 다른 동기화 장치 없이도 아주 편하게 작업을 atomic 하게 할 수 있습니다.

하지만 현재 해당 표준은 아직 C++ 에서 정식으로 지원하지 않고 있지만 GCC 컴파일러의 경우 fgnu-tm 옵션을 주면 사용 가능합니다. 만약에 TM 을 사용하지 않는다면 register_tm_clones 함수는 아무런 작업도 수행하지 않습니다.

컴파일러는 어디서 이 정보를 가지고 오는 것일까?

그럼 한 가지 궁금한 것이 있습니다. 컴파일러는 도대체 어떻게 이러한 식으로 실행파일을 구성할 수 있는 것일까요? 이를 위해서는 실제로 컴파일러와 링커가 실행 파일을 생성할 때 어떠한 라이브러리 파일들을 사용하고 있는지 봐야 합니다. 이를 위해서 GCC 에 -v 옵션을 줘서 컴파일을 해봅시다.

$ g++ test.cc -g -v -o test
... (생략) ...
COLLECT_GCC_OPTIONS='-g' '-v' '-o' 'test' '-shared-libgcc' '-mtune=generic' '-march=x86-64'
 /usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/ccvHYI13.res -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lgcc --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o test /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/9/../../.. /tmp/cc11vpo2.o --verbose -lstdc++ -lm -lgcc_s -lgcc -lc -lgcc_s -lgcc /usr/lib/gcc/x86_64-linux-gnu/9/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o

와우. 보시다 시피 엄청나게 많은 파일들이 링크되어서 들어가고 있음을 알 수 있습니다. -o 다음에 오는 파일들이 링킹 되는 파일들이라 보시면 됩니다. 이 때 -L 의 경우 그냥 -lstdc++ 와 같이 명확한 경로가 지정되지 않은 라이브러리 파일들을 찾을 디렉토리 위치를 지정해주는 것이라 보시면 됩니다. 따라서 정확히 링크되고 있는 파일들을 뽑아보면 다음과 같습니다.

  • Scrt1.o

  • crti.o

  • crtbeginS.o

  • libstdc++ (C++ 라이브러리)

  • libm (수학 관련 라이브러리)

  • libgcc_s

  • libgcc

  • libc (C 라이브러리)

  • libgcc_s

  • crtendS.o

  • crtn.o

생각보다 많은 수의 파일들이 링킹되고 있음을 알 수 있습니다. 먼저 살펴볼 라이브러리 파일의 경우 crt 로 시작하는 부류입니다. crtC Runtime 의 약자로, C 로 작성된 프로그램이 실행될 수 있도록 준비해주는 역할을 합니다.

crt1.o

가장 먼저 crt1 부터 봅시다. crt1 에는 _start 가 정의되어 있고, libc 를 사용하기 위한 준비를 수행하고 있습니다. 실제로 그 내용을 objdump 로 까보면;

 objdump -S ./x86_64-linux-gnu/crt1.o

./x86_64-linux-gnu/crt1.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <_start>:
   0:	f3 0f 1e fa          	endbr64 
   4:	31 ed                	xor    %ebp,%ebp
   6:	49 89 d1             	mov    %rdx,%r9
   9:	5e                   	pop    %rsi
   a:	48 89 e2             	mov    %rsp,%rdx
   d:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
  11:	50                   	push   %rax
  12:	54                   	push   %rsp
  13:	4c 8b 05 00 00 00 00 	mov    0x0(%rip),%r8        # 1a <_start+0x1a>
  1a:	48 8b 0d 00 00 00 00 	mov    0x0(%rip),%rcx        # 21 <_start+0x21>
  21:	48 8b 3d 00 00 00 00 	mov    0x0(%rip),%rdi        # 28 <_start+0x28>
  28:	ff 15 00 00 00 00    	callq  *0x0(%rip)        # 2e <_start+0x2e>
  2e:	f4                   	hlt    
  2f:	90                   	nop

0000000000000030 <_dl_relocate_static_pie>:
  30:	f3 0f 1e fa          	endbr64 
  34:	c3                   	retq   

위 처럼 _start 가 정의되어 있음을 알 수 있습니다. 참고로 우리가 실제로 링크한 파일은 Scrt1.o 인데, 이는 그냥 crt1 과 거의 동일한데, PIE (Position Indepenent Executable) 실행 파일을 작성하는데 사용한다고 보시면 됩니다. 참고로 PIE 란, 이전에 다루었던 공유 라이브러리 처럼 메모리 임의의 위치에 로드 되어도 실행 가능한 파일이라 보시면 됩니다. 제가 사용하는 운영체제 (우분투 20.04) 에선 기본으로 GCC 가 모든 실행 파일을 PIE 형태로 만들게 되어 있습니다.

crtbegin.o

그 다음으로 링크되는 파일로 crtbegin.o 이 있습니다. 이 파일은 __libc_start_main 에서 호출되는 여러가지 초기화 관련 함수들을 정의하고 있습니다.

 objdump -S crtbegin.o
   0:	b8 00 00 00 00       	mov    $0x0,%eax
   5:	48 3d 00 00 00 00    	cmp    $0x0,%rax
   b:	74 13                	je     20 <deregister_tm_clones+0x20>
   d:	b8 00 00 00 00       	mov    $0x0,%eax
  12:	48 85 c0             	test   %rax,%rax
  15:	74 09                	je     20 <deregister_tm_clones+0x20>
  17:	bf 00 00 00 00       	mov    $0x0,%edi
  1c:	ff e0                	jmpq   *%rax
  1e:	66 90                	xchg   %ax,%ax
  20:	c3                   	retq   
  21:	66 66 2e 0f 1f 84 00 	data16 nopw %cs:0x0(%rax,%rax,1)
  28:	00 00 00 00 
  2c:	0f 1f 40 00          	nopl   0x0(%rax)

0000000000000030 <register_tm_clones>:
  30:	be 00 00 00 00       	mov    $0x0,%esi
  35:	48 81 ee 00 00 00 00 	sub    $0x0,%rsi
  3c:	48 89 f0             	mov    %rsi,%rax
  3f:	48 c1 ee 3f          	shr    $0x3f,%rsi
  43:	48 c1 f8 03          	sar    $0x3,%rax
  47:	48 01 c6             	add    %rax,%rsi
  4a:	48 d1 fe             	sar    %rsi
  4d:	74 11                	je     60 <register_tm_clones+0x30>
  4f:	b8 00 00 00 00       	mov    $0x0,%eax
  54:	48 85 c0             	test   %rax,%rax
  57:	74 07                	je     60 <register_tm_clones+0x30>
  59:	bf 00 00 00 00       	mov    $0x0,%edi
  5e:	ff e0                	jmpq   *%rax
  60:	c3                   	retq   
  61:	66 66 2e 0f 1f 84 00 	data16 nopw %cs:0x0(%rax,%rax,1)
  68:	00 00 00 00 
  6c:	0f 1f 40 00          	nopl   0x0(%rax)

0000000000000070 <__do_global_dtors_aux>:
  70:	f3 0f 1e fa          	endbr64 
  74:	80 3d 00 00 00 00 00 	cmpb   $0x0,0x0(%rip)        # 7b <__do_global_dtors_aux+0xb>
  7b:	75 13                	jne    9은
  8d:	5d                   	pop    %rbp
  8e:	c3                   	retq   
  8f:	90                   	nop
  90:	c3                   	retq   
  91:	66 66 2e 0f 1f 84 00 	data16 nopw %cs:0x0(%rax,%rax,1)
  98:	00 00 00 00 
  9c:	0f 1f 40 00          	nopl   0x0(%rax)

00000000000000a0 <frame_dummy>:
  a0:	f3 0f 1e fa          	endbr64 
  a4:	eb 8a                	jmp    30 <register_tm_clones>

objdump 로 까보면 실제로 frame_dummyregister_tm_clones 와 같은 앞서 __libc_csu_init 에서 호출되는 함수들이 정의되어 있는 것을 볼 수 있습니다.

그 외에 crtn.ocrtend.S 에는 소멸자 관련 함수들이 정의되어 있습니다.

libgcc

마지막으로 살펴볼 라이브러리는 libgcc 입니다. libgcc 의 경우 GCC 에서 컴파일 되는 코드들에 반드시 링크되어야 하는 라이브러리 입니다. GCC 를 통해 컴파일 된 코드는 특정 작업을 위해서 해당 라이브러리에 정의되어 있는 함수들을 호출할 수 있습니다. 최적화 레벨 옵션이 켜져있을 경우 libgcc 의 함수를 호출하는 대신 해당 코드로 아예 치환해버릴 수 있겠지만 최적화 옵션을 끌 경우 libgcc 안에 정의되어 있는 함수를 호출하게 될 것입니다.

libgcc 에 어떠한 루틴들이 정의되어 있는지 궁금하신 분들은 여기 를 참조하시기 바랍니다.

덧붙여 초기화 시 필요한 라이브러리들을 좀 더 자세히 알고 싶다면 여기 를 참조해주세요!

마무리

이것으로 4 개의 강의를 거쳐 우리가 작성한 소스 코드에서 어떠한 방식으로 실행 파일이 생성되는지 살펴보았습니다. 특히 마지막 두 강좌는 GCC 컴파일러의 ELF 실행 파일 생성과정에 국한된 것으로 다른 환경 (예를 들어 윈도우나 맥) 과 다른 형태의 실행 파일 (예를 들어 윈도우의 PE 파일) 은 다른 형태의 실행 파일을 구성할 것입니다. 하지만 내가 다른 환경에서 작업한다고 해서 여태까지 다룬 내용이 무의미 한 것은 아닙니다! 그래도 큰 틀에서 작동하는 방식은 비슷하기 때문에 다른 방식들을 이해하는데에는 큰 무리가 없으리라 생각합니다.

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면 꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <씹어먹는 C++ - <20 - 4. 코드 부터 실행 파일 까지 - main 으로의 여정>> 입니다. 이번 강좌의 모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요
댓글이 2 개 있습니다!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요