티스토리 뷰
이번 글에서는 앞에서 설명한 링커 스크립트 파일과 crt0를 응용하여 startup 코드를 직접 만들어 보는 방법에 대해서 설명하도록 하겠다.
먼저 링커 스크립트 파일부터 만들어 보도록 하겠다.
이미 링커 스크립트 글에서 설명하였으므로 내용에 대해서는 다시 설명할 필요는 없을것 같다. 단지, 3번째 줄에 나오는 ENTRY()라는 명령어는, 프로그램이 ()안에 있는 심볼로부터 시작된다는것을 링커에게 알려주는 역할을 한다. 그렇다고 해서 반드시 CPU가 ENTRY()안에 있는 심볼로부터 시작된다는것을 의미하지는 않는다. 만약 _start 심볼의 주소가 0x100이라고 하더라도 AVR CPU는 0x100부터 프로그램이 시작되지는 않고, 무조건 0번지에서부터 시작된다.
그럼 왜, ENTRY()라는 것을 만들었을까?
그 이유는 링크 과정에서 한번도 함수콜이 되지 않는 함수들은 실행파일 코드에서 삭제되기 때문이다. 이러한 것을 garbage collection 기능이라고 한다.
_start함수는 가장 먼저 수행되는 함수이다. 따라서 어떤 함수에서도 불리워지지 않게 된다. 그럼, 링크는 필요없는 함수라고 간주하여 이 코드를 실행코드에서 삭제하게 되는 것이다. 그렇게 되면 자연적으로 그 다음에 나오는 코드들도 모두 삭제되기 때문에 결론적으로 실행 코드의 크기가 0이 되게 된다. 시험을 위해서 ENTRY()를 삭제하고 프로젝트를 빌드해 보기 바란다.
__data_start, __data_end, __data_load_start, __bss_start, __bss_end, __heap_start라고 되어 있는 것들은 링커에서 만들어주는 심볼명이다. 일종의 const 포인터 변수명이라고 생각하면 된다.
직접 만든 링커 스크립트를 프로젝트에서 사용하려면 다음과 같이 프로젝트 속성을 수정해 주어야 한다.
위의 그림과 같이 작성된 스크립트 파일의 이름이 startup.lds라면 "-Wl,-T../startup.lds"라는 옵션을 추가하여 링크 단계에서 프로그래머가 지정한 링커 스크립트를 사용하도록 하여야 한다.
링커 스크립트 파일을 작성하였으면 다음 단계로 startup 코드를 작성해 보도록 하겠다. Startup 코드는 어쩔수 없이 어셈블 코드로 작성할 수 밖에 없다. C코드로 작성하기 위해서는 반드시 스택 초기화가 먼저 전제되어야 하기 때문이다. 그 밖에도 CPU core 초기화 과정도 필요한 경우가 있는데 이 때에는 어셈블 코드로만 작성되어야 경우가 있다.
프로그래머가 startup코드를 따로 작성하지 않아도 컴파일러는 자동으로 CPU에 맞는 crt0코드를 넣어주도록 되어 있다. 그러나 프로그래머가 직접 startup 코드를 작성하고자 할 때에는 컴파일러에게 crt0 코드를 넣지 말라는 명령을 주어야 한다. 그 방법은 다음과 같다.
위의 그림처럼 linker 옵션에서 -nostartfiles를 선택해 주어야 한다.
어셈블 코드로 startup 코드를 작성하는 것은 그리 쉬운 작업이 아니다. AVR CPU의 경우는 그나마 비교적 간단하기 때문에 리버스 엔지니어링 과정으로도 충분히 어떤 절차를 수행하는지 알수 있긴 하지만, 좀 복잡한 CPU의 경우는 data 섹션 복사와 bss 섹션 초기와 이외에 많은 초기화 과정이 있을 수 있기 때문이다. 가장 좋은 방법은 CPU제조사에서 제공하는 startup 코드를 입수하는 것이겠지만 그렇지 못한 경우에는 다른 방법을 통해서 startup코드를 찾아보아야한다.
AVR의 경우 map 파일을 보면 startup 코드에 사용되었던 코드가 어떤 것들인지 대충 알수 있다.
Atmel Studio에서는 크게 두개의 개발툴을 기반으로 하는데, 하나는 당연히 GCC이고 다른 하나는 avr-libc 이다. 두개의 차이점은 다음과 같다.
libgcc는 컴파일러에서 제공하는 라이브러리이다. 이것은 CPU 의존적인 코드를 컴파일러에서 제공하는 것이다.
avr-libc는 C언어에서 기본적으로 사용하는 라이브러리들을 AVR에서 수행될 수 있도록 만들어 놓은 것들이다. 대표적으로 printf()라든가 memcpy() 등등 C에서 사용하는 라이브러리들인 것이다. 여기에는 math 라이브러리도 포함되어 있다.
.init2 0x00000068 0xc C:/Program Files (x86)/Atmel/Studio/7.0/Packs/atmel/ATmega_DFP/1.0.106/gcc/dev/atmega328p/avr5/crtatmega328p.o
.init2에 들어 있는 코드는 crtatmega328p.o에서 왔음을 알수 있다. 이 코드는 AVR libc 프로그램안에 있다는것을 의미한다.
.init4 0x00000074 0x16 c:/program files (x86)/atmel/studio/7.0/toolchain/avr8/avr8-gnu-toolchain/bin/../lib/gcc/avr/4.9.2/avr5\libgcc.a(_copy_data.o) 0x00000074 __do_copy_data .init4 0x0000008a 0x10 c:/program files (x86)/atmel/studio/7.0/toolchain/avr8/avr8-gnu-toolchain/bin/../lib/gcc/avr/4.9.2/avr5\libgcc.a(_clear_bss.o) 0x0000008a __do_clear_bss
위와 같이 .init4섹션에 들어가는 내용들은 libgcc에서 왔음을 알 수 있다. 즉, GCC 컴파일러 안에 관련된 내용이 있음을 추측할 수 있다.
그렇다면, 인터넷에서 avr-libc와 GCC 소스코드를 다운받아 필요한 파일을 찾아보면 소스코드를 볼 수 있을것이다.
avr-libc는 http://www.nongnu.org/avr-libc/ 에서 필요한 파일들을 받아볼 수 있다. Atmel Studio 7은 avr-libc 2.0을 기반하는것으로 보인다.
GCC는 https://gcc.gnu.org/ 에서 소스코드를 받을 수 있다. 위의 .init4에 대한 내용에서 볼수 있는것처럼 Atmel Studio 7에서는 GCC 4.9.2를 사용하고 있다는 것을 알수 있다.
.init2에 대한 소스는 avr-libc-2.0.0\crt1\gcrt1.S 에 있고, .init4에 대한 소스코드는 gcc-4.9.2\libgcc\config\avr\lib1funcs.S 에서 볼 수 있다.
먼저 .init2 섹션에 들어가는 코드부터 찾아보면 다음과 같이 되어 있다.
위의 내용을 참고하여 __init 코드를 만들어 보면 다음과 같이 된다.
4번줄에서처럼 .section이라는 키워드를 이용하여 input section 명을 지정해 줄 수 있다. 링커 스크립트 파일을 보면 .vectors input section이 .text 섹션에서 가장 먼저 위치되도록 되어 있다. 링커가 주소를 할당하는 순서는 링커 스크립트에 작성된 순서대로 할당하므로 반드시 그 순서에 주의를 하여야 한다.
6,7번줄을 보면 _start 심볼을 global로 지정해 주는 것을 볼 수 있다. 이렇게 함으로써 프로젝트내에서는 오직 하나의 _start 심볼만 존재하도록 한다. 링커 스크립트에서 _start를 프로그램의 진입 심볼로 지정해 주었으므로 링크는 여기에서부터 프로그램이 시작된다고 가정한다.
10,11번줄에는 lo8(), hi8()이 나오는데 ()안에 있는 심볼의 하위 1바이트와 상위 1바이트값을 각각 선택하는 매크로이다. 만약 심볼의 상위 3번째 바이트값을 알고 싶을때는 hh8()을 사용하면 된다.
.init2 섹션의 내용을 만들어 보았으니, 다음 단계로 .init4에 있는 __do_copy_data와 __do_clear_bss를 각각 구현해 보도록 하겠다.
GCC 소스코드에 있는 lib1funcs.S에서 위의 심볼들을 찾아본다.
위에서 필요한 내용만 찾아서 코드를 재작성하면 다음과 같이 만들수 있다.
이제 마지막으로 bss 섹션 초기화 코드를 찾아보면 다음과 같은 내용을 볼 수 있다.
위의 코드를 참조하여 코드를 재작성하면 다음과 같이 만들 수 있다.
.data 섹션 복사와 .bss 섹션 초기화 과정을 끝내면 드디어 main() 함수로 점프하면 된다.
프로젝트를 rebuild 하여 UART 터미널창에 알파벳 글자들이 제대로 출력되는지 확인해 본다.
만약 출력이 제대로 되지 않으면 앞의 글에서 설명한 crt0프로젝트의 map파일과 lss 파일과 비교하여 어디가 잘못되었는지 분석해본다.
이제, main() 함수를 c_entry()라고 변경하고, startup 코드에서도 call c_entry라고 바꾸어 보자.
에러없이 프로젝트가 빌드될테고 실제 보드에서 실행시켜 보아도 잘 동작할 것이다.
이 얘기는 임베디드 시스템 프로그램에서 startup 코드를 직접 만드는 경우에는 반드시 main()함수가 있을 필요는 없다는 뜻이 된다.
'심화' 카테고리의 다른 글
RTOS - Real Time Operating System (2) | 2016.10.15 |
---|---|
ISR(Interrupt Service Routine) (6) | 2016.10.02 |
crt0 (0) | 2016.10.01 |
Linker Script - 링커 스크립트 (3) | 2016.10.01 |
패킷 통신 - Request/Reply (0) | 2016.08.12 |