SW 사관학교 정글(Jungle)/컴퓨터 시스템(CSAPP)

[CSAPP]1-2,1-3 컴파일 시스템

jinsang-2 2024. 8. 27. 16:25
#inclue <stdio.h>

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

hello.c를 시스템에서 실행시키기 위해 저급 기계어 인스트럭션들로 번역되어야 한다!!

컴파일 시스템 = 전처리기 + 컴파일러 + 어셈블러 + 링커

컴파일 시스템

  1. Pre-processor: 전처리 단계
  • 전처리기(cpp)은 본래의 C 프로그램을 # 문자로 시작하는 디렉티브(directive)에 따라 수정
  • '~.i'로 끝나는 C프로그램 생성

#으로 시작하는 문장 처리 -> '~.i'로 끝나는 C프로그램 생성
#include <stdio.h> : 시스템 헤더파일 stdio.h를 프로그램 문장에 직접 삽입해라

  1. Compiler: 컴파일러

컴파일러(ccl)는 텍스트 파일 hello.i 를 텍스트 파일인 hello.s 로 번역하여, 이 파일에는 어셈블리어 프로그램이 저장된다.
이 프로그램은 다음과 같은 main 함수의 정의를 포함한다.👇 아래 코드의 2~7줄에서는 한 개의 저수준 기계어 명령어를 텍스트 형태로 나타내고 있다.

main:
    subq    $8, %rsp
    movl    $.LC0, %edi
    call    puts
    movl    $0, %eax
    addq    $8, %rsp
    ret

어셈블리어는 여러 상위 수준 언어의 컴파일러들을 위한 공통의 출력언어를 제공하기 때문에 유용하다.
(예를 들어, C와 Fortran 컴파일러는 둘 다 동일한 어셈블리어로 출력 파일을 생성한다)

결과

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

: 저수준 기계어 명령어들이 어셈블리어로 저장된다.
** 어셈블리어 : 여러 상위 언어의 컴파일러를 위한 공통의 출력 언어 제공
: '~.i' 파일 -> '~.s'파일로 번역

 

3. Assembler: 어셈블리 단계

과정

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

결과

  • 기계어 인스트럭션으로 번역한 바이너리 파일 hello.o (인스트럭션: 컴퓨터가 알아들을 수 있게 기계어로 이루어진 명령)

'~.s' -> 기계어 인스트럭션으로 번역 -> '~.o'(재배치 가능 목적프로그램 형태의 목적파일)로 저장
위의 목적파일은 함수의 인스트럭션을 인코딩하기 위한 17바이트 바이너리 파일이다

4. Linker: 링크 단계

과정

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

결과

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

→ (ex) printf.o 의 통합 작업을 수행한다. 그 결과인 hello파일은 실행파일로 메모리에 적재되어 시스템에 의해 실행된다.

: '~.o'에 포함된 또다른 목적파일(가령 'printf.o')과 결합
: 그 결과 '~.o'는 실행파일로 메모리에 쌓이고 실행된다.

컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다.

hello.c 처럼 간단한 프로그램의 경우

컴파일 시스템이 정확하고 효율적인 기계어 코드를 만들어 줄 거라고 기대할 수 있다.

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

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

  • 최신 컴파일러들은 복잡한 도구로 대개 우수한 코드를 생성하므로,
    프로그래머로서 효율적인 코드르 작성하기 위해서 컴파일러의 내부 동작을 알 필요는 없다.
  • 하지만 C 프로그램 작성 시 올바른 판단을 하기 위해서는 기계어 수준 코드에 대한 기본적인 이해를 할 필요가 있다.
  • 컴파일러가 어떻게 C 문장들을 기계어 코드로 번역하는지 알 필요가 있다.

효율적인 코드 작성을 위한 판단 예시
switch문은 if-else 문을 연속해서 사용하는 것보다 언제나 더 효율적일까?
함수 호출 시 발생하는 오버헤드는 얼마나 되는가?
while 루프는 for 루프보다 더 효율적일까?
포인터 참조가 배열 인덱스보다 더 효율적일까?
합계를 지역 변수에 저장하면 참조형태로 넘겨받은 인자를 사용하는 것보다 왜 루프가 더 빨리 실행되는가?
수식 연산시 괄호를 단순히 재배치하기만 해도 함수가 더 빨리 실행되는 이유가 무엇인가?

2. 링크 에러 이해하기

  • 큰 규모의 소프트웨어 시스템을 빌드하려는 경우에 링커의 동작 에러가 발생하는 경우가 있다.

링크 관련 이슈
링커가 어떤 참조를 풀어낼 수 없다고 할 때는 무엇을 의미하는가?
정적변수와 전역변수의 차이는 무엇인가?
만일 각기 다른 파일에 동일한 이름의 두 개의 전역 변수를 정의한다면 무슨 의미가 일어나는가?
정적 라이브러리와 동적 라이브러리의 차이는 무엇인가?
컴파일 명령을 쉘에서 입력할 때 명령어 라인의 라이브러리들의 순서는 무슨 의미가 있는가?
왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는 걸까?

3. 보안 약점 피하기

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