Part1) CH2. 소스 코드에서 이진 파일로

songtofu·2022년 11월 13일
0

전문가를 위한 C

목록 보기
2/10

앞서.

  • 실제 소스 코드는 수많은 텍스트 파일로 이루어짐 (= 코드베이스)
  • 소스 코드 안의 각각의 텍스트 파일은 프로그래밍 언어로 쓰인, 텍스트로 된 명령어를 포함한다.
  • CPU는 텍스트로 된 명령어를 실행할 수 없고, 이를 실행하기 위해 기계 수준의 명령어로 컴파일(번역)해야하고 그 결과 프로그램이 실행된다.
  • 이 장의 주제 5개
    1. 표준 컴파일 파이프라인
    2. 전처리기
    3. 컴파일러
    4. 어셈블러
    5. 링커

2.1) 표준 컴파일 파이프라인

  • C 파일을 컴파일 할 때, 소스 코드는 네 가지 요소로 구성된 파이프라인으로 진입해, 특정 작업을 수행한다.
    1. 전처리기
    2. 컴파일러
    3. 어셈블러
    4. 링커
  • 컴포넌트 중 어느 작은 한 단계라도 실패한다면 이는 컴파일 실패 발생.
  • 재배치 가능한 목적 파일과 같은 어떤 중간 결과물은 소스 파일 하나가 앞의 1,2,3만 성공적으로 통과해도 만들어진다.
  • 실행 가능한 목적 파일은 이미 준비된, 재배치할 수 있는 목적 파일 몇가지를 합쳐서 만듬.
  • 플랫폼 : 특정 하드웨어(또는 아키텍처)에서 실행되는 운영체제의 결합
  • 운영체제는 플랫폼의 소프트웨어 컴포넌트, 아키텍처는 하드웨어

    크로스 플랫폼 vs 이식 가능
    - 크로스 플랫폼: 각 플랫폼에 다라 다른 이진 파일과 인스톨러가 있음
    - 이식 가능: 모든 플랫폼에서 같은 이진 파일과 인스톨러를 사용
    만약, C/C++ 코드가 이식 가능하다고 한다면, 소스 코드에 어떠한 변경이나 수정을 거치지 않고 서로 다른 플랫폼에서 컴파일 할 수 있다는 말. 그러나, 최종 목적 파일에 이식성이 있다는 의미는 아님.

2.1.1. 프로젝트 빌드

  • C 언어의 코드베이스는 헤더 파일(.h)와 소스 파일(.c)파일이 있음.
  • 함수 선언은 반환형과 함수 시그니처로 구성된다.
double average(int *, int);
반환형   함수 시그니처
  • 함수 선언은 함수의 사용 방법에 관한 것, 함수 정의는 함수가 실행되는 방식
    -하나의 선언에 대한 두 가지 정의는 허용되지 않는다.
  • 헤더 파일은 2개의 소스 파일 사이를 잇는 다리, 빌드는 함께
  • main 함수는 컴파일러가 프로그램의 시작점으로 인식

예제 빌드하기

  • C/C++ 프로젝트를 빌드한다 = 1. 코드베이스 내의 모든 소스 파일을 컴파일해(중간목적파일) 재배치 가능한 목적 파일을 만들고, 2. 재배치 가능한 목적파일을 결합해 정적 라이브러리 또는 실행 이진 파일과 같은 최종 결과물을 만들겠다.
  • 규칙 1: 소스 파일만 컴파일
  • 규칙 2: 각 소스 파일을 따로따로 컴파일

2.1.2. 1단계: 전처리

  • 전처리된 코드 = 변환 단위 = 컴파일 단위
  • 변환 단위 = 전처리기가 생성한 C 언어 코드의 하나의 논리 단위, 컴파일될 준비를 마친 것
  • gcc에 경우 -E 옵션을 사용하면 변환 단위로 덤프하라고 요청할 수 있음.

2.1.3. 2단계: 컴파일

  • 변환 단위를 입력하고, 그에 해당하는 어셈블리 코드를 출력하는 단계
  • 컴파일 결과로 얻은 어셈블리 코드에 -S 옵션을 써서 덤프할 수 있음.
  • 컴파일러는 변환 단위를 구문 분석하고 이를 대상 아키텍처에 맞는 어셈블리 코드로 변환
  • 대상 아키텍처(=호스트 아키텍처) = 프로그램이 컴파일되어 실행될 하드웨어나 CPU를 뜻 함.

2.1.4. 3단계: 어셈블리

  • 목적 : 실질적인 기계 수준 명령어(또는 기계어 코드)를 만드는 것
  • 각 아키텍처는 고유의 어셈블러를 가짐. 이 어셈블러는 아키텍처의 고유한 어셈블리 코드를 해당 아키텍처의 기계어로 변환할 수 있다.
  • 재배치 가능한 목적 파일은 실행할 수 없다. 이 파일은 변환 단위에서 생성된 기계 수준의 명령어만 포함
  • 재배치 가능한 목적 파일은 소스 파일의 함수에 해당하는 기계 수준의 명령어 및 전역 변수를 위해 미리 할당된 항목만 포함
  • 목적 파일은 일반적으로 이진으로 된 내용물을 갖는다.
  • 어셈블리는 하나의 소스 파일을 컴파일하는 마지막 단계이다.

2.1.5. 4단계: 링크

  • 재배치 가능한 목적 파일을 결합하는 일을 하는 단계

새 아키텍처 지원

  • 새로운 아키텍처를 위한 프로그램은 조건 2개를 충족해야함.
    1. 어셈블리어가 알려져 있습니다.
    2. 제조사에 의해 개발된 필수 어셈블러 도구(혹은 프로그램)이 있어야함. 이를 사용해 어셈블리 코드를 그에 해당하는 기계어 수준 명령어로 변환할 수 있음.
  • 위 2개 조건을 충족하면, 소스 코드에서 기계 수준의 명령어 생성 가능.
  • 새로운 아키텍처를 위해 필요한 두 가지 도구
    1. C 컴파일러
    2. 링커
  • 운영체제에서 이러한 도구가 있는 하드웨어는 새로운 플랫폼을 만들 수 있다.
  • 몇 개의 기초 모듈을 만들 수 있다면, 그것으로 다른 모듈도 만들 수 있음, 전체 시스템이 새로운 아키텍처에서 작동할 수 있음.

단계 세부 사항

  • 어셈블러와 링커는 컴파일러와 별도로 실행할 수 있다.

2.2) 전처리기

  • 전처리기는 컴파일러로 소스 코드를 보내기 전에 소스 코드를 수정할 수 있도록 한다. 동시에, 소스 코드를 헤더 파일로 나누어서 나중에 여러 개의 다른 소스 파일에 포함할 수 있도록 하며, 선언을 재사용할 수 있도록 함.
  • 전처리기는 C 언어 문법을 전혀 모르기 때문에 오류를 찾지 못함.
  • 전처리기는 파일의 내용을 복제하거나 텍스트 치환을 통해 매크로를 확장함으로써 포함과 같은 단순한 일만 수행.
  • 전처리기가 다른 일을 추가로 수행하려면 입력 파일을 구문 분석하는 파서가 필요하다. 즉, 전처리기는 파서를 이용해서 입력 코드에서 지시자를 찾는다.
  • 전처리기 파서는 C 컴파일러가 사용하는 파서와 다르다.
  • .i 확장자를 가진 파일은 이미 전처리되었으므로 곧바로 컴파일 단계로 보내야함.

2.3) 컴파일러

  • 컴파일러 = 전처리기가 준비한 변환 단위를 받고 그에 해당하는 어셈블리 명령어 생성
  • 컴파일할 때 어려운 점은 대상 아키텍처가 허용하는 올바른 어셈블리 명령어를 생성하는 일.
  • gcc(또는 다른 C 컴파일러)가 이러한 어려움을 극복하는 방식은 미션을 2단계로 나누는 것이다.
    1. 컴파일러 프론트엔드: 변환 단위를 구문 분석한 뒤, 이를 재배치 가능하며 C에 독립적인 데이터 구조(= 추상 구문 트리 = AST)로 변환하는 것 -> 아키텍처에 독립적, 대상 며령어 집합에 상관없이 수행 가능
    2. 컴파일러 백엔드: AST를 사용해 대상 아키텍처에 맞는 어셈블리 명령어를 생성 -> 아키텍처에 의존적, 컴파일러가 대상 명령어 집합에 관해 알아야함.

2.3.1. 추상 구문 트리(AST)

  • 아키텍처에 의존적이지 않은, 트리 모양의 자료 구조에 결괏값을 저장, 이 최종 자료구조 = AST
  • 컴파일러 프런트엔드가 다른 언어를 지원할 수 있도록 충분히 변경할 수 있다.
  • AST가 만들어지면 1. 컴파일러 백엔드가 AST를 최적화 한다. 2. 최적화된 AST를 기반으로 대상 아키텍처에 관한 어셈블리 코드를 생성.
  • 모든 컴파일러에 자체적은 AST 구현이 있는 것은 아님.
  • 이점: 명령어의 순서를 재배열 할 수 있음, 사용하지 않은 브랜치를 줄일 수 있음, 브랜치를 대체해 성능을 더 높이면서도 프로그램의 목적은 보존할 수 있음. == 최적화

2.4) 어셈블러

  • 어셈블러가 목적 파일에 무엇을 넣을 수 있는지!!
    ex) 같은 아키텍처에서 서로 다른 유닉스 계열 운영체제를 2가지 설치, 설치된 어셈블러는 같지 않을 수 있음 = 같은 하드웨어에 있기 때문에 기계 수준의 명령어가 같더라도, 생성된 목적 파일은 다를 수 있음.
  • 다른 운영체제에서 동일한 프로그램을 컴파일했을 때 결과가 달라질 수 있음.
  • 목적 파일이 같을 수는 없지만, 같은 기계 수준의 명령어를 포함한다는 의미.
  • 나아가 목적 파일이 다양한 운영체제에서 별도의 포맷을 가질 수 있음을 증명.
  • 기계 수준 명령어를 목적 파일에 저장할 때, 각 운영체제는 고유한 특정 이진 파일 포맷 또는 목적 파일 포맷을 정의
  • 목적 파일의 내용을 특정하는 두가지 요소(= 플랫폼)
    1. 아키텍처(또는 하드웨어)
    2. 운영체제

2.5) 링커

  • C/C++ 프로젝트는 실행 파일, 정적 라이브러리, 동적 라이브러리 or 공유 목적 파일을 만들 수 있다.
  • 링커 = 주어진 재배치 가능한 목적 파일로부터 이 목록에 있는 결과물을 만드는 역할을 함.
  • 목적 파일은 변환 단위에 해당하는 기계 수준의 명령어를 포함한다. 그러나 이 명령어는 무작위로 파일에 들어가지 않고 심벌이라고 하는 섹션 아래로 묶인다.
  • 심벌은 링커가 작동하는 방식 및 링커가 목적 파일 몇 개를 하나로 모아 더 큰 파일로 만드는 방식을 설명하는 컴포넌트이다.
  • 심벌 테이블은 목적 파일에서 정의된 모든 심벌을 포함, 심벌에 관한 더 많은 정보를 제공할 수 있다.

2.5.2. 링커는 속을 수 있다.

  • add_1.c
int add(int a, int b, int c, int d) {
	return a + b + c + d;
}
  • add_2.c
int add(int a, int b){
	return a + b;
}

와 같은 상황 발생.
이러한 현상은 입력 자료형에 따라 함수 심벌 이름을 변경해 막을 수 있음 = 네임 맹글링

profile
읽으면 머리에 안들어와서 직접 쓰는 중. 잘못된 부분 지적 대환영

0개의 댓글