Build는
high-level code
를machine-readable and executable form
으로 바꾸는 과정이다.
Operation
을 Instruction
으로 바꾸기
variables와 fucntions의 symbols
을 memory addresses
로 바꾸기
현대의 컴퓨터시스템은 다양한 binding 메커니즘을 사용한다.
(여기서 binding
이란, symbol
을 memory addresses
로 바꾸는 과정이다.)
static binding
: code level에서 binding되는 것
dynamic binding
: execution time에서 binding 되는 것.
이 둘은 performance를 향상시킬 것인지 portability를 향상시킬 것인지의 trade-off이다.
일단 build는 컴퓨터 시스템이 modular development(modular compilation) 를 지원하기 때문에 굉장히 복잡해진다.
그럼 build의 각 단계에서 어떤일이 일어나는지 아주 자세하게 설명하겠다.
preprocessor는 그냥 source code 여러 개를 합치는 일을 한다. 원래는 사람이 애초에 하나의 파일로 합쳐서 만들어도 되지만 각 파일들로 conceptual separate(개념적으로 분리) 하기 위해서 여러 파일로 만든다.
compiler는 source code의 각 operation마다 그것에 맞는 틀? template? 을 가지고 있어서 그 template을 참고하여 모든 operation을 Assembly code (binary와 1대1대응되는 코드) 로 바꾼다.
방금 말한 template의 예를 들면 A = B 라는 source code operation이 있을 때 거기에 맞는 template으로 돌리면 (load reg1, [B] store reg1, [A]) 이런 식의 Machine code로 변형하는 것이다. (그러므로 c언어 컴파일러에는 c언어의 모든 operation(if, for, +, < 등등)에 대한 template을 당연히 다 가지고 있음.)
이렇게 나온 binary를 Object라고 한다. 그리고 Object file은 Machine readable이기 때문에 그 속의 instruction과 data 모두 Memory addresses이다. (CPU가 이해할 수 있는 address로 되어있다는 뜻.)
하지만 부족한 부분이 있을 경우 바로 실행할 수 있는 완전한 object (executable object)가 아닌 relocatable object라고 한다.
linker는 relocatable object의 부족한 부분을 매꿔서 실제 실행할 수 있는 object로 만들어 준다. 이때 부족한 부분이란, symbol이다. source code A에서 어떤 함수 f()를 불러서 쓰는데 이게 A에 정의되어 있지 않은 함수일 경우 compile time에 memory address로 바꾸지 못한다(당연히 현재 source code file 가지고는 f()가 어디에 정의되어있는지 모르기 때문). 이럴때는 compiler가 해당 f()를 symbol 에서 memory address 로 바꾸지 못하여 그대로 symbol 로 남아있게 된다.
이때! linker는 여러 object(혹은 library(=relocatable object들의 묶음))를 묶어주어, compile후에도 남아있는 symbol들을 실제 memory address로 바꾸어 주는 역할을 한다.
또한! 사진에도 보이다싶이 Dynamic linking을 하기위한 Dynamic linking driver도 같이 링크하게 된다.
linking time이 끝나면, 모든 symbol들이 완전히 본인만의 memory address가지게 되어 Executable object가 되고 사진에서 처럼 loader part까지 붙으면 진짜 곧바로 실행 할 수 있는 파일이 생성된다.
loader part(loading stuff, load해주는 loader와 헷갈리면 안 됨.) : 프로그램이 실행되기 우리는 main함수가 실행되기 이전에 무슨 셋팅을 하는지 정의하지 않았다. 프로그램이 해당 OS에서 실제로 실행(load)되기 위해서 더 붙는 파트(loader)가 있다.
loader는 OS로 하여금 해당 executable object을 메모리로 올려 실행시키게 한다.
원래 Executable object은 image of initial state of the memory of program(프로그램 초기상태의 메모리 이미지) 이지만 사실 그렇게 하면 파일이 너무 커져서 compressed form(압축된 형태) (압축된 object file format인 ELF에 대해서 배울 것임.)로 저장되어 있다가 Loader가 loading을 할 때 decompress(압축해제)를 해준 다음 메모리에 올린다.
Executable Object File 과 그의 extension(decomposition)
보다시피 loader가 확장한 것은 실제 메모리에 올라가는 것과 완전 같다.
모든게 다 잘되었지만 사실! binding(Symbol to address)해야할 것이 조금 더 남아있다. 아니 아직까지 binding을 미뤄놓은 symbol들이 있다고?? 이렇게 execution time(runtime)까지 binding을 미뤄놓는 것이 dynamic linking이다.
어떤 프로그램을 실행해서 그 결과 나왔을 때, 그 결과가 결정된 곳을 binding time 이라 한다.
Binding Time
예를 들어 사진의 맨 처음 예시 코드가 돌아가서 스크린에 “Hello, Application”이 찍혔을 경우, “It is bounded at coding time! “ 이라고 말할 수 있는 것이다.
그리고 linking의 예시를 보면 printf가 찍는 글자가 linking time에 어떤 object의 printf definition과 link되냐에 따라 달라지므로 “It is bounded at linking time!”이라고 할 수 있다.
왜 binding time이 다를까
여러 이유가 있다.
Modularity (이것 덕분에 사람들이 엄청나게 복잡한 프로그램도 만들 수 있게 되었다.)
Efficiency of time and space
time
: binding time이 linking time으로 미뤄지면 한 소스파일을 한 번 compile해서 계속 그 object를 쓸 수 있다.
space
: binding time이 runtime으로 미뤄지면 (dynamic linking) 여러 process가 한 shared module를 참조하게 되므로 redundent(중복된 부분)가 없어져서 space가 절약된다.
앞에서는 빌드의 각 단계에서 어떤 일들이 일어나는지와 binding(symbol to memory address) 개념을 알아보았다.
앞으로는 그 개념들이 실제로 어떻게 적용되는지 gcc 컴파일러를 이용해서 실습해보면 좋을 것 같다.