4 minute read

compile process 위 그림에서 알 수 있듯이 source code에서 executable이 되기까지 4단계로 구성된다. 하나씩 알아보자.

예제코드(main.c)

#include <stdio.h>
#define MACRO 1;

int main(){
	int a=3;
	int b;
	b=MACRO+a;
	return 0;
}

Pre-Process(main.i)

source code인 c파일은 사용자가 사용하기 편하게 만들어져있다. 따라서 컴퓨터를 위해 약간의 전처리를 해주는데 헤더파일(#include)을 포함해주고 매크로 변수(#define)를 바꾸어준다. 이 과정을 gcc의 cpp가 담당한다. 전처리만 하는 명령어는 아래와 같다.

gcc -E main.c -o main.i

-E 옵션을 이용해 전처리만 하고 -o 옵션으로 출력파일의 이름을 지정할 수 있다.

# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "main.c"
# 1 "/usr/include/stdio.h" 1 3 4
# 27 "/usr/include/stdio.h" 3 4
...
typedef unsigned char __u_char;
typedef unsigned short int __u_short;
typedef unsigned int __u_int;
typedef unsigned long int __u_long;
...
# 873 "/usr/include/stdio.h" 3 4

# 2 "main.c" 2

# 4 "main.c"
int main(){
 int a=3;
 int b;
 b=1;+a;
 return 0;
}

위의 코드를 보면 stdio.h 헤더파일에 있는 변수와 함수의 프로토타입을 추가하고 MACRO를 직접 값을 치환한 것을 확인할 수 있다.

Compile(main.s)

컴파일은 전처리된 코드를 어셈블리어로 변환하는 것이다. cc1에 의해 진행되고 명령어는 아래와 같다.

gcc -S main.i -o main.s

	.file	"main.c"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$3, -8(%rbp)
	movl	$1, -4(%rbp)
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	 1f - 0f
	.long	 4f - 1f
	.long	 5
0:
	.string	 "GNU"
1:
	.align 8
	.long	 0xc0000002
	.long	 3f - 2f
2:
	.long	 0x3
3:
	.align 8
4:

Assemble(main.o)

이 과정에서는 어셈블리어를 기계어인 오브젝트 코드로 변환해준다. 이 과정을 통해 기계어로 변환되었기 때문에 아키텍처만 동일하다면 어느 환경에서든지 돌아가게 된다. 이 과정은 as가 담당하며 명령어는 아래와 같다.

as main.s -o main.o

이때부터는 일반 텍스트 파일이 아닌 바이너리(바이트코드)이므로 readelf 명령어로 구조만 확인해보자.

readelf -S main.o

There are 12 section headers, starting at offset 0x260:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000001d  0000000000000000  AX       0     0     1
  [ 2] .data             PROGBITS         0000000000000000  0000005d
       0000000000000000  0000000000000000  WA       0     0     1
  [ 3] .bss              NOBITS           0000000000000000  0000005d
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .comment          PROGBITS         0000000000000000  0000005d
       000000000000002b  0000000000000001  MS       0     0     1
  [ 5] .note.GNU-stack   PROGBITS         0000000000000000  00000088
       0000000000000000  0000000000000000           0     0     1
  [ 6] .note.gnu.propert NOTE             0000000000000000  00000088
       0000000000000020  0000000000000000   A       0     0     8
  [ 7] .eh_frame         PROGBITS         0000000000000000  000000a8
       0000000000000038  0000000000000000   A       0     0     8
  [ 8] .rela.eh_frame    RELA             0000000000000000  000001e0
       0000000000000018  0000000000000018   I       9     7     8
  [ 9] .symtab           SYMTAB           0000000000000000  000000e0
       00000000000000f0  0000000000000018          10     9     8
  [10] .strtab           STRTAB           0000000000000000  000001d0
       000000000000000d  0000000000000000           0     0     1
  [11] (.shstrtab         STRTAB           0000000000000000  000001f8
       0000000000000067  0000000000000000           0     0     1

-S 옵션을 통해 section의 구조를 보면 .text, .data, .bss 등의 section으로 나누어져 있는 것을 볼 수 있다.

readelf -l main.o

There are no program headers in this file.

하지만 Segment구조를 보는 -l 옵션은 아직 segment를 찾지 못하는데 link과정 후에 비교해보자.

다른 방법으로는 assemble하기 전으로 돌아가서 어셈블리 코드를 보는 방법도 있다. 이 과정을 disassemble이라고 한다.

objdump -d main.o

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	f3 0f 1e fa          	endbr64
   4:	55                   	push   %rbp
   5:	48 89 e5             	mov    %rsp,%rbp
   8:	c7 45 f8 03 00 00 00 	movl   $0x3,-0x8(%rbp)
   f:	c7 45 fc 01 00 00 00 	movl   $0x1,-0x4(%rbp)
  16:	b8 00 00 00 00       	mov    $0x0,%eax
  1b:	5d                   	pop    %rbp
  1c:	c3                   	retq

Link(main)

이 과정은 외부 라이브러리를 연결하는 과정이다. libc에 있는 함수를 사용하면 libc.so 파일을 연결하게 되는데 이런 과정을 ld가 담당한다. 이때 linking을 static하게하면 파일에 모든 라이브러리 함수를 저장하고 dynamic하게 하면 라이브러리 주소를 통해 함수를 불러올 수 있다. 마찬가지로 readelf로 확인하면 아래와 같다.

readelf -l main

Elf file type is DYN (Shared object file)
Entry point 0x1040
There are 13 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040
                 0x00000000000002d8 0x00000000000002d8  R      0x8
  INTERP         0x0000000000000318 0x0000000000000318 0x0000000000000318
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000005c8 0x00000000000005c8  R      0x1000
  LOAD           0x0000000000001000 0x0000000000001000 0x0000000000001000
                 0x00000000000001d5 0x00000000000001d5  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000002000 0x0000000000002000
                 0x0000000000000130 0x0000000000000130  R      0x1000
  LOAD           0x0000000000002df0 0x0000000000003df0 0x0000000000003df0
                 0x0000000000000220 0x0000000000000228  RW     0x1000
  DYNAMIC        0x0000000000002e00 0x0000000000003e00 0x0000000000003e00
                 0x00000000000001c0 0x00000000000001c0  RW     0x8
  NOTE           0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000020 0x0000000000000020  R      0x8
  NOTE           0x0000000000000358 0x0000000000000358 0x0000000000000358
                 0x0000000000000044 0x0000000000000044  R      0x4
  GNU_PROPERTY   0x0000000000000338 0x0000000000000338 0x0000000000000338
                 0x0000000000000020 0x0000000000000020  R      0x8
  GNU_EH_FRAME   0x0000000000002004 0x0000000000002004 0x0000000000002004
                 0x000000000000003c 0x000000000000003c  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000002df0 0x0000000000003df0 0x0000000000003df0
                 0x0000000000000210 0x0000000000000210  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn
   03     .init .plt .plt.got .text .fini
   04     .rodata .eh_frame_hdr .eh_frame
   05     .init_array .fini_array .dynamic .got .data .bss
   06     .dynamic
   07     .note.gnu.property
   08     .note.gnu.build-id .note.ABI-tag
   09     .note.gnu.property
   10     .eh_frame_hdr
   11
   12     .init_array .fini_array .dynamic .got

-l 옵션을 이용하면 segment구성을 볼 수 있는데 Program Header와 Section to Segment mapping이 있는 것을 볼 수 있다. 따라서 link를 하기 전에는 section만 존재하고 link를 한 후에는 Segment가 존재한다. 이 프로그램이 메모리로 적재될 때도 이 구조가 그대로 메모리에 적재되기 때문에 메모리에 Segment가 존재하는 것이다.

Categories:

Updated: