4 minute read

Computer Systems : A Programmer’s Perspective (Chapter 7)

1. 전처리기, 컴파일러, 어셈블러

간단한 두 소스 코드로 시작해 보자.

우리는 보통

gcc -o main main.c

같은 커맨드를 입력해서 main.c 소스 코드를 main이라는 이름의 executable object file로 바꾼다.

./main

만든 실행 파일은 이렇게 실행시킬 수 있다.

명령어 한 줄로 끝나는 간단한 과정이지만 많은 중간 과정이 포함되어 있다. 이 글에서는 전처리기를 간단하게 살펴본다.

위 명령어에서 gcc 드라이버는 가장 먼저 C preprocessor(전처리기) cpp를 실행한다. 아래 커맨드로 딱 전처리 과정만 수행할 수 있다.

cpp main.c main.i
## 또는 아래 명령어 (stdout으로 출력됨)
gcc -E main.c

전처리기는 각종 directive들, 즉 #include <stdio.h> 같은 헤더 파일과 #define INF 10000000 같은 매크로의 처리를 담당한다.
전처리기가 무슨 일을 하는지에 대해서는 여기 에서 자세한 설명을 볼 수 있다.

main.i를 열어 보니 기존 소스코드는 다 그대로 있고, 맨 앞에 뭐가 잔뜩 붙었다. 형식은 # linenum filename flags 이고, 명칭은 linemarkers 이다.
설명서 를 보아하니 그 의미는 “filename 파일의 linenum 번째 줄에서 flag 하는 일이 일어났다” 정도가 되는 것 같다.

‘1’ : This indicates the start of a new file.
‘2’ : This indicates returning to a file (after having included another file).
‘3’ : This indicates that the following text comes from a system header file, so certain warnings should be suppressed.
‘4’ : This indicates that the following text should be treated as being wrapped in an implicit extern “C” block.

flag에 대한 설명은 이렇게 나와 있다. 참고해서 한줄한줄 의미를 살펴보면,

# 1 "main.c"

main.c의 첫 번째 줄에서 시작된다.

# 1 "<built-in>"

스택오버플로우의 글을 보니 built-in c pre-procssor directive를 처리하는 것 같다. built-in directive는 __FILE__ 같이 기본으로 정의돼 있는 매크로들인데, GCC가 맨 처음에 얘네들을 처리하고 시작하는 것으로 보인다.

# 1 "<command-line>"

이 부분은 command line directive 를 처리하는 부분으로 보인다. Shell에서 directive로 define되는 것들을 처리하는 과정인 걸까?

# 1 "/usr/include/stdc-predef.h" 1 3 4

저 파일이 뭘지 들어가 보면 이렇게 되어 있다.

#ifndef	_STDC_PREDEF_H
#define	_STDC_PREDEF_H	1

/* This header is separate from features.h so that the compiler can
   include it implicitly at the start of every compilation.  It must
   not itself include <features.h> or any other header that includes
   <features.h> because the implicit include comes before any feature
   test macros that may be defined in a source file before it first
   explicitly includes a system header.  GCC knows the name of this
   header in order to preinclude it.  */

// ...

무슨 소린지 잘 모르겠지만, 모든 컴파일 과정의 처음에 항상 하는 과정으로 보인다. flag가 1, 3, 4 세개로 되어 있는데 3,4번은 “지금부터 읽을 거는 system header file의 것이니 경고를 띄우지 않아도 되고, extern C block 같은 거에 wrap된 걸로 생각해 주라!” 라는 의미이다. 사실 무슨 말인지 아직 잘 모르겠다.

# 1 "<command-line>" 2
# 1 "main.c"

command-line으로 돌아오고, 곧이어 main.c로 돌아온다.

지금은 아무것도 include도 안 해서 추가된 게 없는데, #include <stdio.h> 하나만 해도 많은 양의 코드가 추가된다. stdio.h와 관련된 여러 헤더 파일들을 돌아다니면서 헤더 파일에 있는 내용을 현재 소스 코드로 옮겨 오는 과정으로 보인다. extern int printf (const char *__restrict __format, ...); 같은 익숙한 것들도 눈에 띈다.

전처리기의 작업이 끝난 파일을 ASCII intermediate file이라고 부르고, 확장자는 .i를 사용한다.

그 다음은 컴파일러의 등장이다. 전처리기가 준비해 놓은 파일을 파싱하고 syntax대로 처리해서 ASCII assembly-language file로 바꿔 준다.

gcc -S main.i

위 명령어를 쓰면 main.s 파일이 생성된다(gcc -S main.c 를 해도 바로 main.s가 생성된다). 열어보면 너무 재밌어 보이는 x86-64 어셈블리어를 볼 수 있다. 아쉽지만 RISC-V 어셈블리어밖에 모르기 때문에 해석은 다음에 하도록 하자.

다음 차례는 어셈블러이다. 어셈블러는 알다시피 어셈블리어를 기계어, 즉 binary file로 변환한다.

as -o main.o main.s
## 또는
gcc -c main.s

위 명령어를 통해 ASCII assembly-language file(.s)을 relocatable object file(.o)로 변환할 수 있다(역시, gcc -c main.c를 해도 바로 main.o가 만들어진다). 이제부터는 binary 파일이라 열어서 읽어볼 수는 없다.
이 파일은 linking을 기다리는 파일이라고 볼 수 있다. 소스 코드는 .c 파일 혼자서 실행되는 것도 있지만 보통 다른 소스 코드의 것들을 #include 해서 사용하는데, .c 파일이 단독으로 전처리와 컴파일, 어셈블을 거쳐서 relocatable object file이 되면 그 .c파일 혼자서는 아직 뜻을 모르는 것들에 “모른다”라고 표시해 놓고 언제든지 relocate할 수 있도록 만들어 놓은 것이 바로 relocatable object file이다.

main.c와 sum.c 파일 두 개를 각각 gcc -c main.c, gcc -c sum.c를 통해 main.o와 sum.o를 만들면 이들을 linking할 준비가 끝났다.

ld -o prog main.o sum.o

이렇게 하면 오류가 난다. gcc를 쓰면 알아서 linking을 할 때 필요한 숨겨진 각종 셋팅들을 해 주는데, ld로 하려면 직접 다 쳐 줘야 한다. 뭘 쳐 줘야 하는지는 gcc -v main.o sum.o를 치면 나오는 것 같은데, 너무 많으니 일단은 gcc를 사용한다.

gcc -o prog main.o sum.o
## 둘 다 된다
gcc -o prog main.c sum.c

-o는 gcc 설명서를 보면,

   -o file
       Place output in file file.  This applies to whatever sort of output
       is being produced, whether it be an executable file, an object
       file, an assembler file or preprocessed C code.
       If -o is not specified, the default is to put an executable file in
       a.out, the object file for source.suffix in source.o, its assembler
       file in source.s, a precompiled header file in source.suffix.gch,
       and all preprocessed C source on standard output.

-o 뒤에 나오는 파일에 결과물이 저장되고, -o를 설정하지 않았으면 결과물의 종류마다 미리 정해진 이름의 파일에 저장된다.

이렇게 생성된 prog는 executable object file이라고 부른다. ./prog를 통해 loader 가 작동하는데, prog 파일의 code와 data를 메모리로 복사해 가서 실행시키게 된다.

Tags:

Categories:

Updated: