아래의 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
다음으로, 컴파일 시스템을 알아봅시다~!
전처리기(cpp)는 본래의 C 프로그램을 #문자로 시작하는 디렉티브(directive)에 따라 수정한다.
예를 들어 hello.c
파일 첫 줄의 #include<stdio.h>
는 전처리기에게 시스템 헤더파일인 stdio.h
를 프로그램 문장에 직접 삽입하라고 지시한다.
.i
로 끝나는 새로운 C 프로그램이 생성된다.컴파일러(ccl)는 텍스트 파일 hello.i
를 텍스트 파일인 hello.s
로 번역하며, 이 파일에는 어셈블리어 프로그램이 저장된다.
이 프로그램은 다음과 같은 main 함수의 정의를 포함한다.
아래 코드의 2~7줄에서는 한 개의 저수준 기계어 명령어를 텍스트 형태로 나타내고 있다.
main: subq $8, %rsp movl $.LCO, %edi call puts movl $0, %eax addq $8, %rsp ret
hello.s
어셈블러(as)가 hello.s
를 기계어 인스트럭션으로 번역하고,
이들을 재배치가능 목적프로그램의 형태로 묶어서 hello.o
라는 목적파일에 그 결과를 저장한다.
이 파일은 main 함수의 인스트럭션들을 인코딩하기 위한 17바이트를 포함하는 바이너리 파일이다.
hello.o
위에서 작성한 hello 프로그램은 C 컴파일러에서 제공하는 표준 C 라이브러리에 들어있는 printf
함수를 호출하고 있다.
printf
함수는 이미 컴파일된 별도의 목적파일인 printf.o
에 들어있다.
printf.o
파일은 hello.o
파일과 어떤 형태로든 결합되어야 한다.
링커 프로그램(ld)이 이 통합작업을 수행한다.
hello
파일hello.c
처럼 간단한 프로그램은,
컴파일 시스템이 정확하고 효율적인 기계어 코드를 만들어 줄 거라고 기대할 수 있다.
하지만, 프로그래머들이 어떻게 컴파일 시스템이 동작하는지 이해해야 하는 중요한 이유가 있다!
최신 컴파일러들은 복잡한 도구로 대개 우수한 코드를 생성하므로,
프로그래머로서 효율적인 코드 작성을 위해 컴파일러의 내부 동작을 알 필요는 없다.
그렇지만, C 프로그램 작성 시 올바른 판단을 하기 위해서는
기계어 수준 코드에 대한 기본적인 이해를 할 필요가 있다.
컴파일러가 어떻게 C 문장들을 기계어 코드로 번역하는지 알 필요가 있다.
# 효율적인 코드 작성을 위한 판단 예시
* switch 문은 if-else 문을 연속해서 사용하는 것보다 언제나 더 효율적일까?
* 함수 호출 시 발생하는 오버헤드는 얼마나 되는가?
* while 루프는 for 루프보다 더 효율적일까?
* 포인터 참조가 배열 인덱스보다 더 효율적인가?
* 합계를 지역 변수에 저장하면 참조형태로 넘겨받은 인자를 사용하는 것보다 왜 루프가 더 빨리 실행되는가?
* 수식 연산시 괄호를 단순히 재배치하기만 해도 함수가 더 빨리 실행되는 이유는 무엇인가?
# 링크 관련 이슈들
* 예를 들어 링커가 어떤 참조를 풀어낼 수 없다고 할 때, 무엇을 의미하는지?
* 정적변수와 전역변수의 차이는 무엇인가?
* 만일 각기 다른 파일에 동일한 이름이 두 개의 전역변수를 정의한다면 무슨 일이 일어나는가?
* 정적 라이브러리와 동적 라이브러리의 차이는 무엇인가?
* 컴파일 명령을 쉘에서 입력할 때 명령어 라인의 라이브러리들의 순서는 무슨 의미가 있는가?
* 왜 링커와 관련된 에러들은 실행하기 전까지는 나타나지 않는 걸까?
오랫동안 버퍼 오버플로우(buffer overflow) 취약성이 인터넷과 네트워크상의 보안 약점의 주요 원인으로 설명되었다.
이 취약성은 프로그래머들이 신뢰할 수 없는 곳에서 획득한 데이터의 양과 형태를 주의 깊게 제한해야 할 필요를 거의 인식하지 못하기 때문에 생겨난다.
안전한 프로그래밍을 배우는 첫 단계는 프로그램 스택에 데이터와 제어 정보가 저장되는 방식 때문에 생겨나는 영향을 이해하는 것이다.