Study

컴파일러에 대해서...

슈라。 2010. 1. 25. 21:40

  컴파일러는 보통 front end(프론트엔드), optimizer(최적화기), back end(백 엔드) 세 개의 기본적인 요소로 구성된다.

▶ 프론트 엔드(front end)
  front end는 high level language로 작성된 소스코드를 분석하는 몇 가지 단계를 수행한다. 먼저 lexical analysis(어휘 분석)이나 scanning의 단계를 수행하는데, 소스 파일의 토큰을 검사하는 과정이다. 여기서 토큰은 소스코드를 구성하는 텍스트 심볼을 의미한다.

 if (var != 0){}

  위의 코드 라인에서 심볼은 if, (, var, != 등이며 모두 토큰이 된다. 토큰을 스캔하는 동안 어휘 분석기는 해당 토큰이 프로그래밍 언어의 규약에 맞는지 검사하고 문맥상 잘못된 토큰이 발견되면 컴파일러는 에러를 보고한다.
  front end는 프로그램 코드의 유효성을 검사하고 코드를 컴파일러의 intermediate representation(중간 표현)으로 변환하는 역할만을 수행할 뿐 프로그램의 의미를 변경시키지 않는다.

- intermediate representation(중간 표현)

  컴파일러의 주요 역할은 코드를 다른 형태의 표현으로 변환하는 것이다. 이를 위해서 컴파일러는 intermediate representation(중간 표현, 또는 내부 표현)이라고 하는 코드에 대한 자신의 표현 형태를 가진다. 이를 이용해서 코드의 에러를 탐지하고 코드 성능을 향상시키며, 결국에는 기계어 코드를 만들어낸다.
  컴파일러가 어떤 형태의 코드 중간 표현을 사용할 것인지는 입력으로 받아들이는 것이 어떤 종류의 소스이고 어떤 종류의 목적코드를 생상해야 하는지에 따라서 달라진다. 어떤 중간 표현은 하이레벨 언어와 매우 유사한 형태를 가지며 어떤 중간 표현은 중간 표현이 어셈블리 코드와 유사한 경우도 있다. 중요한 것은 컴파일러는 동시에 여러 가지 형태의 중간 표현을 가지지 않는 것이다.


 최적화기(optimizer)
  컴파일러의 최적화기는 코드의 효율성을 높이기 위해서 다양한 종류의 기술을 이용한다. 최적화기의 중요한 두 가지 역할은 코드 성능을 가능한 한 최고로 향상시키고 최대한 프로그램 바이너리의 크기를 줄이는 것이다. 대부분의 컴파일러는 이 두 가지 목적을 달성하기 위해 최적화를 수행한다. 최적화기는 특정 프로세서에 국한된 최적화 작업이 아닌 일반적인 프로그램 코드 향상 작업을 수행한다. 
  가독성에 직접 영향을 미치는 최적화 작업은 최적화기에 의해서 수행되는 것이 아니라 백엔드에서 수행되는 프로세서 관련 최적화 작업에 의해서 이뤄진다.

- 코드 구조 최적화
  최적화기는 코드의 의미를 그대로 유지하면서 동시에 효율성을 높이기 위해 코드의 구조를 변경시키곤 한다. 예를 들어 반복문은 부분적으로나 전체적으로 해제되기도 한다. 반복문의 해제는 점프 명령을 이용해서 같은 코드 부분은 반복 실행하는 대신 단순히 반복할 코드를 복제해서 연속적으로 수행하는 것을 의미한다. 이렇게 하면 바이너리의 크기는 커지지만 카운터를 관리할 필요가 없어지고 조건에 따라서 실행을 분기시킬 필요가 없다. 또한 반복문은 부분적으로 해제해서 반복 실행을 감소시킬 수도 있다.

- 잉여 제거 최적화(redundancy elimination)
  잉여 제거는 코드 최적화 분야에서 상당히 중요한 요소다. 프로그래머는 똑같은 계산을 한 번 이상 반복하거나 전혀 사용하지 않는 변수에 값을 할당하는 일과 같은 쓸데 없는 코드를 만들어 내곤 한다. 최적화기는 이런 제거 가능한 코드를 찾아내서 그것을 실제로 제거하기 위한 알고리즘을 가지고 있다. 


 백엔드(back end)
  back end는 code generator라고도 불리며 앞 단계의 컴파일 과정에서 처리되어 생성된 intermediate representation(중간 표현)로부터 특정 대상 코드를 생성한다. 
  코드 생성기는 실질적으로 어셈블리 언어 명령을 선택하는 것이므로 특정 플랫폼에 대한 최적화 수행을 위한 충분한 정보를 가지고 있는 유일한 컴파일러 요소라고 할 수 있다. 

  다음은 코드 생성 과정에서 수행되는 가장 중요한 세 가지 단계다.


- 명령 선택 : 이 단계에서 중간 표현 코드가 플랫폼에 맞는 명령으로 변환된다. 명령 선택은 프로그램 전체의 성능에 매우 중요하므로 컴파일러는 각 명령의 다양한 특성을 잘아야 한다.

- 레지스터 할당 : 중간 표현에서는 수없이 많은 레지스터를 이용할 수 있으므로 모든 지역 변수를 레지스터에 할당한다. 하지만 프로세서에는 한정된 수의 레지스터만이 존재하고 그 레지스터만을 이용하는 코드를 생성해야 한다. 따라서 컴파일러는 어느 변수를 레지스터에 할당하고 어느 변수를 스택에 할당할 것인지 결정해야 한다.

- 명령 스케줄링 : 요즘 프로세서 대부분은 동시에 여러 개의 명령을 처리할 수 있으므로 각 명령 간의 데이터 동기화가 새로운 이슈가 된다. 어떤 명령이 연산을 수행해서 결과 값을 레지스터에 저장하고 곧바로 다음 명령이 그 레지스터에서 값을 읽으려면 시간 지연이 필요하다. 첫 번째 연산이 완료될 때까지 시간이 필요하기 때문이다. 이런 이유로 코드 생성기는 병렬 처리를 위해서 명령의 순서를 바꿔주는 명령 스케줄링 알고리즘을 이용한다. 그렇게 해서 생성된 코드를 인터리브드 코드(interleaved code)라고 한다. 즉, 두개의 개별적인 데이터를 처리하는 두 개의 명령을 연속적으로 수행되게 재배치 하는 것이다.


출처 - 리버싱: 리버스 엔지니어링 비밀을 파헤치다