반응형
프로그램은 다른 프로그램에 의해 다른 형태로 번역된다.
'hello.c' 프로그램이 시스템에서 어떻게 실행되는지 과정을 알는것이 중요하다.
그 중에서 소스파일이 번역되는 과정을 알아보자
#inclue <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
뭘 번역한다는 거지?
- 인간이 이해할 수 있도록 고급 프로그램으로 사용한다. 그러나 'hello.c'를 시스템에서 실행시키려면 ,각 C 문장들은 다른 프로그램들에 의해 저급 기계어 인스트럭션들로 번역되어야 한다.
- 이 인스트럭션들은 실행가능 목적 프로그램( = 실행가능 목적 파일)이라고 하는 형태로 합쳐져서 바이너리 디스크 파일로 저장된다.
- 컴파일러 드라이버는 유닉스 시스템에서 아래와 같이 소스파일에서 오브젝트 파일로 번역한다.
👉gcc 컴파일러 드라이버는 소스파일 'hello.c' 를 읽어서 실행파일인 'hello' 로 번역한다.
linux> gcc -i hello hello.c
- 번역은 4개의 단계를 거쳐 실행되는데, 이 네 단계를 실행하는 프로그램들을 합쳐 컴파일 시스템이라고 한다.
(네 단계를 실행하는 프로그램에는 "전처리기", "컴파일러", "어셈블리", "링크"가 있다.)
컴파일 시스템
1. Pre-processor: 전처리 단계
과정
- 전처리기(cpp)은 본래의 C 프로그램을 # 문자로 시작하는 디렉티브(directive)에 따라 수정한다.
- 예를 들어, 'hello.c' 파일 첫 줄의 '#inclue<stdio.h>'는 전처리기에게 시스템 헤더파일인 'stdio.h'를 문장에 직접 삽입하라고 지시한다.
결과
- 일반적으로 '.i '로 끝나는 새로운 C 프로그램이 생성된다.
: '#'으로 시작하는 문장 처리 -> '~.i'로 끝나는 C프로그램 생성
: #include <stdio.h> - 시스템 헤더파일 stdio.h를 프로그램 문장에 직접 삽입해라!
2. 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' 함수는 이미 컴파일된 별도의 목적파일인 'print.o' 파일과 어떤 형태로든 결합되어야 한다.
- 링커 프로그램(Id)이 이 통합작업을 수행한다.
( 프로그래머가 새로 짠 부분과 기존의 기능들을 연결(link) 하는 기능을 수행한다. )
결과
- 실행가능 목적 파일( = 실행파일)로 메모리에 적재되어 시스템에 의해 실행된다.
→ (ex) 'print.o'의 통합 작업을 수행한다. 그 결과인 hello파일은 실행파일로 메모리에 적재되어 시스템에 의해 실행된다.
: '~.o'에 포함된 또다른 목적파일(가령 'printf.o')과 결합
: 그 결과 '~.o'는 실행파일로 메모리에 쌓이고 실행된다.
컴파일 시스템이 어떻게 동작하는지 이해하는 것은 중요하다.
'hello.c' 처럼 간단한 프로그램의 경우
컴파일 시스템이 정확하고 효율적인 기계어 코드를 만들어 줄 거라고 기대할 수 있다.
하지만, 프로그래머들이 어떻게 컴파일 시스템이 동작하는지 이해해야 하는 중요한 이유가 있다.
1. 프로그램 성능 최적화하기
- 최신 컴파일러들은 복잡한 도구로 대개 우수한 코드를 생성하므로,
프로그래머로서 효율적인 코드르 작성하기 위해서 컴파일러의 내부 동작을 알 필요는 없다. - 하지만 C 프로그램 작성 시 올바른 판단을 하기 위해서는 기계어 수준 코드에 대한 기본적인 이해를 할 필요가 있다.
- 컴파일러가 어떻게 C 문장들을 기계어 코드로 번역하는지 알 필요가 있다.
효율적인 코드 작성을 위한 판단 예시
- switch문은 if-else 문을 연속해서 사용하는 것보다 언제나 더 효율적일까?
- 함수 호출 시 발생하는 오버헤드는 얼마나 되는가?
- while 루프는 for 루프보다 더 효율적일까?
- 포인터 참조가 배열 인덱스보다 더 효율적일까?
- 합계를 지역 변수에 저장하면 참조형태로 넘겨받은 인자를 사용하는 것보다 ㄷ왜 루프가 더 빨리 실행되는가?
- 수식 연산시 괄호를 단순히 재배치하기만 해도 함수가 더 빨리 실행되는 이유가 무엇인가?
2. 링크 에러 이해하기
- 큰 규모의 소프트웨어 시스템을 빌드하려는 경우에 링커의 동작 에러가 발생하는 경우가 있다.
링크 관련 이슈
- 링커가 어떤 참조를 풀어낼 수 없다고 할 때는 무엇을 의미하는가?
- 정적변수와 전역변수의 차이는 무엇인가?
- 만일 각기 다른 파일에 동일한 이름의 두 개의 전역 변수를 정의한다면 무슨 의미가 일어나는가?
- 정적 라이브러리와 동적 라이브러리의 차이는 무엇인가?
- 컴파일 명령을 쉘에서 입력할 때 명령어 라인의 라이브러리들의 순서는 무슨 의미가 있는가?
- 왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는 걸까?
3. 보안 약점 피하기
- 오랫동안 버퍼 오버플로우(buffer overflow) 취약성이 네트워크상의 보안 약점의 주요 원인으로 설명되었다.
- 이 취약성은 프로그래머들이 신뢰할 수 없는 곳에서 획득한 데이터의 양과 형태를 주의 깊게 제한해야 할 필요를 거의 인식하지 못하기 때문에 생겨난다.
- 안전한 프로그래밍을 배우는 첫 단계는 프로그램 스택에 데이터와 제어 정보가 저장되는 방식 때문에 생겨나는 영향을 이해하는 것이다.
반응형
'CS > Computer System' 카테고리의 다른 글
[CS:APP] 3-4 정보 접근하기 (1) | 2024.09.04 |
---|---|
[CS:APP] 1-7 운영체제는 하드웨어를 관리한다 (1) | 2024.08.27 |
[CS:APP] 1-5~1-6) 캐시 메모리, 저장장치의 계층 구조 (2) | 2024.08.27 |
[CS:APP] 1-4 프로세서의 작동 원리 (3) | 2024.08.21 |
[CS:APP] 1-1 비트와 컨텍스트 (0) | 2024.08.12 |