[CS:APP] 1-2~1-3) 컴파일 시스템

2023. 3. 6. 22:15Computer Science

728x90
반응형

프로그램은 다른 프로그램에 의해 다른 형태로 번역된다

아래의 hello.c 프로그램이 시스템에서 실행되는 과정을 알아보자!
그 중에서 소스 파일이 번역되는 과정을 알아보자!

hello.c

#include <stdio.h>

int main()
{
    printf("hello, world\n");
    return 0;
}

 

뭘 번역한다는 거야?

  • hello.c를 시스템에서 실행시키려면,
    각 C 문장들은 다른 프로그램들에 의해 저급 기계어 인스트럭션들로 번역되어야 한다.
  • 이 인스트럭션들은 실행 가능 목적 프로그램( = 실행가능 목적 파일) 이라는 형태로 합쳐져서 바이너리 디스크 파일로 저장된다.
  • 컴파일러 드라이버는 유닉스 시스템에서 아래와 같이 소스파일에서 오브젝트 파일로 번역한다.

👇🏻 GCC 컴파일러 드라이버는 소스파일 hello.c 를 읽어서 실행파일인 hello 로 번역한다.

linux> gcc -o hello hello.c
  • 번역은 4개의 단계를 거쳐서 실행되는데,
    이 네 단계를 실행하는 프로그램들을 합쳐서 컴파일 시스템이라고 부른다.

 

다음으로, 컴파일 시스템을 알아봅시다~!

 

컴파일 시스템

1. Pre-processor : 전처리 단계

과정

  • 전처리기(cpp)는 본래의 C 프로그램을 #문자로 시작하는 디렉티브(directive)에 따라 수정한다.
  • 예를 들어 hello.c 파일 첫 줄의 #include<stdio.h> 는 전처리기에게 시스템 헤더파일인 stdio.h를 프로그램 문장에 직접 삽입하라고 지시한다.

결과

  • 그 결과 일반적으로 .i로 끝나는 새로운 C 프로그램이 생성된다.

 

2. Compiler : 컴파일러

과정

  • 컴파일러(ccl)는 텍스트 파일 hello.i를 텍스트 파일인 hello.s로 번역하며, 이 파일에는 어셈블리어 프로그램이 저장된다.
  • 이 프로그램은 다음과 같은 main 함수의 정의를 포함한다.

아래 코드의 2~7줄에서는 한 개의 저수준 기계어 명령어를 텍스트 형태로 나타내고 있다.

main:
    subq    $8, %rsp
    movl    $.LCO, %edi
    call    puts
    movl    $0, %eax
    addq    $8, %rsp
    ret
  • 어셈블리어는 여러 상위수준 언어의 컴파일러들을 위한 공통의 출력언어를 제공하기 때문에 유용하다.
    (예를 들어, C와 Fortran 컴파일러는 둘 다 동일한 어셈블리어로 출력 파일을 생성한다.)

결과

  • 어셈블리어 프로그램이 저장된 텍스트 파일 hello.s

 

3. Assembler : 어셈블리 단계

과정

  • 어셈블러(as)가 hello.s기계어 인스트럭션으로 번역하고,
    이들을 재배치가능 목적프로그램의 형태로 묶어서 hello.o라는 목적파일에 그 결과를 저장한다.
  • 이 파일은 main 함수의 인스트럭션들을 인코딩하기 위한 17바이트를 포함하는 바이너리 파일이다.

결과

  • 기계어 인스트럭션으로 번역한 바이너리 파일 hello.o

 

4. Linker : 링크 단계

과정

  • 위에서 작성한 hello 프로그램은 C 컴파일러에서 제공하는 표준 C 라이브러리에 들어있는 printf 함수를 호출하고 있다.
  • printf 함수는 이미 컴파일된 별도의 목적파일인 printf.o에 들어있다.
  • printf.o 파일은 hello.o 파일과 어떤 형태로든 결합되어야 한다.
  • 링커 프로그램(ld)이 이 통합작업을 수행한다.

결과

  • 실행가능 목적파일( = 실행파일)로 메모리에 적재되어 시스템에 의해 실행될 hello 파일

 

컴파일 시스템의 동작을 이해해야 하는 이유

hello.c처럼 간단한 프로그램은,
컴파일 시스템이 정확하고 효율적인 기계어 코드를 만들어 줄 거라고 기대할 수 있다.

하지만, 프로그래머들이 어떻게 컴파일 시스템이 동작하는지 이해해야 하는 중요한 이유가 있다!

 

1. 프로그램 성능 최적화하기

  • 최신 컴파일러들은 복잡한 도구로 대개 우수한 코드를 생성하므로,
    프로그래머로서 효율적인 코드 작성을 위해 컴파일러의 내부 동작을 알 필요는 없다.
  • 그렇지만, C 프로그램 작성 시 올바른 판단을 하기 위해서는
    기계어 수준 코드에 대한 기본적인 이해를 할 필요가 있다.
  • 컴파일러가 어떻게 C 문장들을 기계어 코드로 번역하는지 알 필요가 있다.
# 효율적인 코드 작성을 위한 판단 예시
* switch 문은 if-else 문을 연속해서 사용하는 것보다 언제나 더 효율적일까?
* 함수 호출 시 발생하는 오버헤드는 얼마나 되는가?
* while 루프는 for 루프보다 더 효율적일까?
* 포인터 참조가 배열 인덱스보다 더 효율적인가?
* 합계를 지역 변수에 저장하면 참조형태로 넘겨받은 인자를 사용하는 것보다 왜 루프가 더 빨리 실행되는가?
* 수식 연산시 괄호를 단순히 재배치하기만 해도 함수가 더 빨리 실행되는 이유는 무엇인가?

 

2. 링크 에러 이해하기

  • 프로그래밍 에러 중, 시스템을 빌드하려는데 링커의 동작 에러가 발생하는 경우가 있다.
# 링크 관련 이슈들
* 예를 들어 링커가 어떤 참조를 풀어낼 수 없다고 할 때, 무엇을 의미하는지?
* 정적변수와 전역변수의 차이는 무엇인가?
* 만일 각기 다른 파일에 동일한 이름이 두 개의 전역변수를 정의한다면 무슨 일이 일어나는가?
* 정적 라이브러리와 동적 라이브러리의 차이는 무엇인가?
* 컴파일 명령을 쉘에서 입력할 때 명령어 라인의 라이브러리들의 순서는 무슨 의미가 있는가?
* 왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는 걸까?

 

3. 보안 약점 피하기 (security hole)

  • 오랫동안 버퍼 오버플로우(buffer overflow) 취약성이 인터넷과 네트워크상의 보안 약점의 주요 원인으로 설명되었다.
  • 이 취약성은 프로그래머들이 신뢰할 수 없는 곳에서 획득한 데이터의 양과 형태를 주의 깊게 제한해야 할 필요를 거의 인식하지 못하기 때문에 생겨난다.
  • 안전한 프로그래밍을 배우는 첫 단계는 프로그램 스택에 데이터와 제어 정보가 저장되는 방식 때문에 생겨나는 영향을 이해하는 것이다.
728x90
반응형