본문 바로가기
IT/임베디드 시스템

임베디드 소프트웨어 - 링커/로케이터1 (주소 재지정, 프로그램 컴포넌트 배치)

by 뽀짝뉴스 2020. 6. 19.

 

파일로부터 소스를 읽어서 링커에 적합한 오브젝트 파일을 생성하는 크로스 컴파일러의 작업은 네이티브 컴파일러와 큰 차이가 없지만, 임베디드 시스템을 위한 링커는 네이티브 링커와는 다른 많은 작업을 해야 합니다. 임베디드 시스템을 위한 링커는 종종 로케이터 또는 링커/로케이터라고 불릴 정도로 네이티브 링커와는 전혀 다른 프로그램입니다. 물론 크로스 링커라는 이름도 있지만요. 어쨌든 이번에는 로케이터와 네이티브 링커의 차이점과 로케이터 사용법에 대해서 다룰 겁니다.

 

 

[주소 재지정]

 

네이티브 링커와 로케이터의 차이점을 얘기해보면 출력 파일을 생성하는 방법에서 시작해야 합니다. 네이티브 링커는 사용자가 프로그램을 실행하길 원할 때마다 로더라고 불리는 운영체제의 일부분에 의해서 읽힐 수 있도록 호스트 시스템의 디스크 드라이버에 파일을 생성합니다. 로더는 프로그램이 올라갈 메모리를 찾고, 프로그램을 디스크에서 메모리로 복사하고 프로그램이 시작하기 전에 다른 다양한 처리를 수행합니다. 이와는 다르게 로케이터는 출력을 타깃 시스템에다가 복사하는 작업을 할 어떤 프로그램이 사용할 파일을 생성합니다. 나중에 로케이터의 출력은 스스로 실행될 겁니다. 임베디드 시스템에서는 별도의 분리된 운영체제가 없었던 것을 기억해봅시다. 로케이터는 응용 프로그램과 RTOS를 결합시키고, 이 결합된 것 모두를 한 번에 타깃 시스템에 올립니다. 이런 차이는 단순히 두 파일의 형식이 서로 다르다는 것 그 이상입니다. 파일들이 반드시 가져야 하는 정보에서의 차이가 있다는 것을 의미하죠.

 

네이티브 툴로 응용 소프트웨어를 작성할 때의 절차를 보여주는 예가 있습니다. 툴 체인이 반드시 해결해야 하는 문제는 많은 마이크로프로세서 명령어가 오퍼랜드(Operand)의 주소를 담고 있다는 겁니다. 가령 ABBOTT.C에 있는 MOVE 명령어는 idunno 변수의 값을 R1 레지스터에 이동시키기 때문에 idunno 변수의 주소를 반드시 갖고 있어야 합니다. 마찬가지로, shosonfirst 함수의 호출은 궁극적으로 whosonfirst의 주소를 가지고 있는 바이너리 CALL 명령어로 변환됩니다. 이런 문제를 해결하는 작업을 종종 주고 재지정(Address Resolution)이라고 부릅니다.

 

ABBOTT.C를 컴파일할 때, 컴파일러는 iddunno와 whosonfirst가 어떤 주소가 될지에 대해서 모릅니다. 그래서 링커를 위한 ABBOTT.OBJ 오브젝트 파일에 idunno의 주소를 MOVE 명령어에 넣어야 하고, whosonfirst의 주소를 CALL 명령어에 넣어야 한다는 것을 나타내는 플래그를 남겨둬야 합니다. COSTELLO.C를 컴파일할 때, 컴파일러는 COSTTELLO.OBJ 오브젝트 파일 안에 whosonfirst의 위치를 나타내는 플래그를 남겨둬야 합니다. 링커는 두 파일을 하나로 합칠 때, 실행 이미지의 시작점에서 상대적으로 idunno와 whosonfirst의 위치가 어딘지를 알아내서 실행 파일에 그런 정보를 넣어야 합니다.

 

로더가 프로그램을 메모리에 복사한 후에, 로더는 idunno와 whosonfirst가 메모리에 어디에 있는지 정확하게 알게 되고, ABBOTT.C로부터 온 CALL과 MOVE 명령어에 이들 주소 값을 반영합니다.

 

로더는 또한 운영체제의 함수를 호출하면서 프로그램을 끝내는 루틴에 들어 있는 CALL도 처리합니다. 대부분의 프로그램은 더 이상 수행할 것이 없을 때 프로그램을 끝내려고 운영체제의 함수를 호출합니다. 운영체제는 이미 응용 프로그램의 메모리 영역에 올라와 있기 때문에, 로더는 이러한 함수들이 어디에 있는지 알 수 있고 응용 프로그램에 있는 CALL 명령어를 수정합니다.

 

대부분의 임베디드 시스템에서는 로더가 없습니다. 로케이터가 작업을 마쳤을 때, 그 결과물은 타깃 시스템으로 복사됩니다. 그러므로 로케이터는 프로그램이 메모리 어디에 올라가는지 알아서 모든 주소를 변경해야 합니다. 로케이터는 프로그래머가 프로그램이 타깃의 어디로 올라갈지를 말해 줄 수 있도록 허용합니다.

 

로케이터는 다양한 수의 다른 출력 파일 형식을 사용하고, 이후 다룰 다양한 툴은 다양한 파일 형식을 받아들이는 차이가 있습니다. 분명 프로그램을 타깃 시스템에 올리는 툴은 로케이터가 어떤 파일 형식을 만들어 내는지 알고 있어야만 합니다. 이런 파일 형식의 세세한 부분까지 중요하진 않습니다. 일반적으로 이런 형식은 매우 간단한데 많이 쓰이는 파일 형식은 롬에 복사될 수 있는 단순한 바이너리 형태의 파일입니다.

 

 

[프로그램 컴포넌트를 올바르게 배치하기]

 

임베디드 환경에서 로케이터가 해결해야 하는 또 다른 문제는 프로그램의 일부분은 ROM에 있어야 하고, 또 다른 부분은 RAM에 있어야 할 필요가 있다는 겁니다. 임베디드 시스템을 위해 만들어진 한 형식 안에서, whosonfirst은 프로그램의 일부이고 전원이 꺼지더라도 기억되어야 하기 때문에 ROM에 올라가야 합니다. 다른 한편 idunno 변수는 데이터이고 값이 변경되어야 하기 때문에 RAM에 올라가야 합니다. 로더는 모든 프로그램을 RAM으로 올리기 때문에 이런 문제는 응용 프로그램의 세계에서는 발생하지 않습니다.

 

대부분의 툴 체인은 프로그램을 세그먼트라는 단위로 나눠서 이런 문제를 해결합니다. 각 세그먼트는 로케이터가 다른 세그먼트에 독립적으로 메모리에 위치시킬 수 있는 프로그램의 조각입니다. 가령 ROM에 올라갈 프로그램을 구성하는 명령어들은 한 종류의 세그먼트에 들어갈 수 있습니다. RAM에 올라갈 데이터들은 또 다른 세그먼트에 들어가고요.

 

세그먼트는 임베디드 시스템 프로그래머가 반드시 만나게 되는 다른 문제도 해결할 수 있습니다. 응용 프로그램 프로그래머는 일반적으로 명령어가 메모리에 어디에 위치하는지에 대해서 신경을 쓰지 않는 반면에, 모든 임베디드 시스템에서는 적어도 코드의 일부분에 대해서는 고려할 필요가 있습니다. 가령 마이크로프로세서가 전원이 들어왔을 때, 특정한 주소의 명령어가 실행됩니다. 임베디드 시스템 프로그래머는 프로그램의 첫 번째 명령어가 그 주소에 있는지를 확인해야 합니다.

 

이렇게 하기 위해서, 프로그래머는 보통 어셈블리 코드의 일부로 되어 있는 스타트업 코드를 독자적인 세그먼트에 넣어서 로케이터에게 그 세그먼트를 특별한 주소에 넣도록 해야 합니다.

 

어떻게 툴 체인이 x.c, y.c 그리고 z.asm 세 개의 모듈로 되어있는 가상의 시스템에서 동작하는지 보여주는 에도 있습니다. x.c에는 명령어들에 더해서 초기화되지 않은 데이터, 문자열 상수들이 선언되어 있다고 가정합시다. y.c에는 명령어들에 더해서 초기화되지 않은 데이터와 초기회 된 데이터가 있다고 가정합시다. z.asm에는 잡다한 어셈블리어 함수, 스타트업 코드, 그리고 약간의 초기화되지 않은 데이터가 있다고 생각해보고요.

 

크로스 컴파일러는 x.c를 세 개의 세그먼트로 가지고 있는 오브젝트 파일로 나눌 겁니다. 명령어를 담고 있는 세그먼트, 초기화되지 않은 데이터를 담고 있는 세그먼트, 문자열 상수를 담고 있는 세그먼트로 나눠집니다. 마찬가지로 크로스 컴파일러는 y.c를 명령어, 초기화되지 않은 데이터, 초기화된 데이터 세그먼트들로 나눌 겁니다.

 

프로그래머는 소스파일에 크로스 어셈블러가 무엇을 해야 할지를 정하는 명령어를 추가해서 z.asm을 세그먼트로 나눠야 합니다. 크로스 어셈블러는 그러한 명령어를 수행할 거고요.

 

링커와 로케이터는 이런 다양한 세그먼트들을 다시 섞습니다. z.asm으로부터 스타트업 코드를 프로세서가 처음 실행시키는 주소에 올리고, ROM에 있는 다른 주소에 각 모듈로부터의 코드를 올립니다. 데이터 세그먼트는 RAM에 올리고요. x.c의 문자열 상수를 담고 있는 세그먼트와 y.c로부터 초기화된 데이터를 담고 있는 세그먼트는 나중에 다루기 될 특별한 고려를 필요로 합니다.

 

대부분의 크로스 컴파일러는 자동적으로 각각의 모듈을 둘 또는 더 많은 세그먼트로 나눕니다.

 

- 명령어들 / 초기회 되지 않은 데이터 / 초기화된 데이터 / 문자열 상수

 

이들은 매우 합리적인 기본 행동 절차를 가지고 있습니다. 많은 크로스 컴파일러들이 커맨드 라인 옵션이나 C 코드 안에 특별한 #pragmas 전처리 명령을 사용해서 이런 절차를 변경할 수 있도록 합니다.

 

크로스 어셈블러는 또한 세그먼트들을 정의할 수 있거나 아니면 어셈블러 출력의 어디에 위치되어야 하는지를 정할 수 있도록 해줍니다. 그러나 크로스 컴파일러와는 다르게 대부분의 크로스 어셈블러는 기본적인 행동 절차가 없습니다. 프로그래머가 직접 코드가 담길 세그먼트를 정의해야만 하는 것이죠.

댓글