우리가 C코드를 작성해서 실행 가능한 파일을 만들고 실행을 하는 것에 대한 개괄적인 모습이다.
먼저 C로 작성한 코드는 컴파일러
를 통해서 어셈블리어로 번역되고 어셈블러
는 오브젝트 파일(머신 언어로 작성됨)을 만든다. 그리고 필요한 라이브러리들을 연결해주는 Linker
가 참조하고 있는 라이브러리에서 필요한 데이터 및 명령어들을 가져온다. 그리고 실행 가능한 파일을 만들어내고 마지막으로 Loader
가 메인 메모리에 프로그램을 올려서 프로그램을 실행한다.
Linker는 위 그림처럼 Object File에서 다른 라이브러리를 참조하고 있을 경우에 해당 라이브러리에서 필요한 명령어들을 가져와서 실행가능한 파일을 만들어낸다.
위 사진은 하나의 프로젝트를 컴파일을 하는데 여러 소스파일들로 구성되어 있을 경우이다. 소스 파일은 각각 컴파일링이 되어서 Object File을 만들고 Linker
는 각 Object File들이 참조하는 라이브러리를 가져와서 실행가능한 파일을 만든다. 그래서 위 구조에서 알 수 있듯이 소스 코드를 분할하면은 나중에 소스코드를 변경해도 해당 소스 코드만 다시 컴파일링을 하면 되기에 컴파일 시간을 아낄 수 있다.
어셈블러
는 어셈블리 언어 프로그램을 Object File로 변환한다. Object File은 명령어, 데이터 등을 포함한다. 그리고 어셈블러는 모든 명령어를 2진 명령어로 바꾸기 위해서 모든 Label에 대응하는 실제 주소를 알아야 한다. 그 떄 Symbol table
에 있는 값들을 통해서 분기를 하거나 명령어를 실행한다.
그리고 어셈블러는 기계어로 직접적으로 번역되지 않는 의사 명령어도 실행한다. 가령 MOV 명령어나 CMP(비교 연산) 명령어를 각각 ORR 연산자나 SUBS 연산자로 바꿔준다.
UNIX 시스템에서 오브젝트 파일은 6개의 distinct pieces를 갖고 있다.
Header
: 오브젝트 파일의 크기나 다른 pieces의 위치를 알려준다.Text segment
: 실제 machine language code를 갖고 있다.Static data segment
: 정적인 데이터가 할당된 부분이다. section은 data section과 bss section으로 나뉘는데 각 전역변수가 초기화가 되었는지 안되었는지로 나뉜다. 초기화가 되었으면 data section에 저장된다.Relocation info
: 프로그램이 메모리 로드될 때 절대 주소에 의존하는 명령어와 데이터를 식별한다.Symbol table
: 외부 참조에 대한 내용Debug info
: 디버깅과 관련된 내용, 배포될 때는 제거된다.
위처럼, 링커는 xxxx, yyyy같이 심볼을을 실제 주소값으로 변경해서 하나의 Object File들을 만들어내고, 실제 그 주소로 가서 필요한 instruction, data를 가져와서 하나의 실행가능한 파일을 만들어내는 것을 두고 Static Linking이라고 한다.
다음은 링킹의 한 예제이다.
Text segment 부분에서부터 순차적으로 명령어들은 실행이 될 것이다. 제일먼저 40 0000번지에 있는 명령어를 보면, x27에서 0만큼 떨어진 곳의 데이터를 x0에 로드하라고 하고 있다. 화살표를 보아하니 x27에는 1000 0000번지가 저장되어 있었을 것이다. 그래서 X를 로드한 후 2번째 명령어인 40 0004번지에 잇는 데이터를 실행하려고 했더니 000 00FC번지로 분기하라고 하고 있다. 그러면 아마도 LR에는 40 0008번지가 저장이 될 것이다. 그런데 왜 분기를 000 00FC로 분기할까? Text segment는 40 0000번지부터 시작하는데 말이다. 그 이유는 현재 시스템에서는 상대 주소를 채택하고 있고 또, byte단위로 명령어를 구분하는 것이 아니라 word단위로 명령어들을 구분하고 있기 떄문이다.
프로그램 로딩 과정은 위와 같다. 먼저 Header를 읽어서 각 segment의 사이즈를 파악한다. 그 다음 프로그램을 올릴 주소 공간을 확보한다. 그리고 최초로 코드 영역과 static 영역을 메모리에 로드한다. 이때 우리가 알아야할 사실을 최초로 로드된 코드와 static 영역의 메모리 주소는 변하지 않는다는 것이다. 그리고 프로그램을 실행하기 위한 인자들을 스택에 올린다. SP, FP를 포함한 레지스터를 초기화한 이후에 실행을 하는 시점으로 분기한다. 스택에 올려져 있는 x0, x1 ... 등 arguments를 레지스터에 올리고 main 프로시저(함수)를 호출한다. 그리고 마지막에 main이 리턴하면 exit라는 시스템콜을 호출한다.
실행가능한 파일을 만들 때 참조하고 있는 모든 라이브러리에서 명령어들을 가져와서 만드는 것이 아니라 필요한 시점에만 명령어 및 라이브러리를 가져와서 사용하는 것을 두고 Dynamic Linking 이라고 한다.
Dynamic Linking은
이 있다.
말 그대로 게으른 링킹이다. 링킹을 최대한 마지막, 마지막으로 미뤄서 링킹을 하므로 Dynamic Linking을 Lazy Linking 이라고도 부른다.
Linking은 처음 링킹할 때와 처음이 아닐 때로 구분할 수 있다.
처음 링킹을 할 때는 Data 영억에 있는 Linker의 주소로 분기한 이후에 Linker에서 DLL(Dynamic Linking Library)에 가서 필요한 라이브러리의 주소를 찾은 후에 분기하고 DLL루틴을 실행하고 다시 그 다음 명령(맨 처음 라이브러리를 호출한 명령어의 다음 명령어)을 실행한다.
두번째 링킹을 할 때는 한 번 Linker를 통해서 DLL파일의 위치를 알고 있기 때문에 Data 영역에 있는(첫 실행 떄 가져옴) DLL파일의 위치로 가서 참조한다. 그래서 첫 링킹은 시간이 좀 걸리지만 두번째 링킹은 그에 비해 좀 덜 걸린다.(Linker로 DLL파일을 찾는 과정이 줄기에)