오브젝트 파일에 대한 레이아웃을 정의하는 링커 스크립트(linker script) 에 대한 모든 것에 대해 알아본다.
일반 애플리케이션 프로그래밍을 하는 경우에는 링커 스크립트에 대해 모르더라도 크게 문제가 되지 않는다.
컴파일 시에 자동으로 링커에 의해서 링킹되어 오브젝트 파일이 생성되고, 실행시에 로더에 의해서 가상 메모리의 특정 영역에 로딩되기 때문이다.
하지만, 부트로더나 커널의 경우 링커와 로더가 제공되지 않기 때문에, 스스로 링킹과 로딩 정보를 명시해주어야 한다.
여기서는 이해를 돕기 위해, 간단한 예제를 들어 설명하겠다.
링커 스크립트 사용 예제
기존의 vpos 의 경우, 부트로더에서 커널의 정확한 크기를 몰라서 SDRAM 으로 복사할 크기를 적당한 값으로 정의했었다.
만일, 커널의 크기가 정의된 값보다 클 경우 제대로 부팅되지 못하는 문제가 있었다.
이런 문제를 해결하고자, 커널의 정확한 크기를 알 수 있는 방법이 있다. 바로 링커 스크립트를 활용하는 방법이다.
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) ENTRY(reset) SECTIONS { . = 0x00000000; . = ALIGN(4); .text : {./obj/bootloader/start.o(.text) *(.text)} . = ALIGN(4); .rodata : {*(.rodata)} . = ALIGN(4); .data : {*(EXCLUDE_FILE(./obj/kernel/scv_kernel.bin.o).data)} . = ALIGN(4); __bss_start = .; .bss : {*(.bss)} _end = .; . = ALIGN(4); _os_start = . ; .cuteOS : {./obj/kernel/scv_kernel.bin.o} . = ALIGN(4); _os_end = . ; }
링커 스크립트 설명
OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")
arm-linux-ld 가 만들어 낼 최종 결과 파일의 포맷을 나타낸다. 즉, little endian 포맷의 파일을 생성할 것인지, big endian 포맷의 파일을 생성할 것인지를 결정하는 역할을 한다.
링커 스크립트 내의 <OUTPUT_FORMAT> 키워드는 arm-linux-ld 명령어의 ←EB> 또는 ←EL> 옵션과 같이 사용할 수 있다.
만약 arm-linux-ld 명령어를 ←EB> 옵션과 함께 사용하였다면 두 번째 항목에 해당하는 포맷의 파일을 생성해 내며(elf32-bigarm), arm-linux-ld 명령어를 ←EL> 옵션과 함께 사용하였다면 세 번째 항목에 해당하는 파일 포맷을 생성해낸다(elf32-littlearm).
이 두 옵션을 모두 사용하지 않을 디폴트로 첫 번째 항목에 해당하는 파일 포맷을 생성해 낸다. 여기서는 elf32-littlearm 포맷의 파일을 생성한다.
OUTPUT_ARCH(arm)
이 부분은 최종 결과 파일이 동작할 CPU 의 아키텍처를 나타낸다. 즉, 이 파일은 ARM CPU 상에서 동작한다는 의미이다.
ENTRY(reset)
최종 결과 파일의 시작 지점을 나타낸다. 즉, 여기서 파일의 시작 지점은 reset 이 된다.
SECTIONS { ... }
이 부분은 링커(arm-linux-ld)가 입력 파일들의 세션들을 결과 파일의 어떤 세션들로 위치시킬지를 결정하는 역할을 한다.
.text : {./obj/bootloader/start.o(.text) *(.text)}
이 문장은 start.o 입력파일의 .text 세션과 그 외의 입력 파일들(*)의 .text 세션을 결과 파일의 .text 세션에 위치시키는 역할을 한다.
참고로 C 파일을 컴파일하여 오브젝트 파일을 생성할 경우, 기본적으로 함수는 .text 세션에, 전역변수는 .data 세션에, 초기화 되지 않은 전역변수는 .bss 세션에 놓인다.
그 외에도 여러가지 세션들이 있으며, 또한 새로운 세션을 정의해서 쓸 수도 있다.
. = 0x00000000;
이 부분은 현재의 위치는 0x00000000 번지라는 의미이다.
. = ALIGN(4);
이 부분은 현재의 위치를 4 바이트 경계에 놓겠다는 의미이다.
.rodata : {*(.rodata)}
이 부분은 모든 입력 파일의 .rodata 세션을 결과 파일의 .rodata 세션에 놓겠다는 의미이다.
.data : {*(EXCLUDE_FILE(./obj/kernel/scv_kernel.bin.o).data)}
이 부분은 커널 오브젝트 파일(scv_kernel.bin.o)을 제외한 모든 입력 파일의 .data 세션을 결과 파일의 .data 세션에 놓겠다는 의미이다.
커널 오브젝트 파일(scv_kernel.bin.o) 은 순수 바이너리 이미지이다.
__bss_start = .;
이 부분은 __bss_start 심벌의 주소 값은 현재 위치와 같다는 의미이다. arm-linux-nm 명령어를 이용하여 이 부분을 확인해보기 바란다.
.bss : {*(.bss)}
이 부분은 모든 입력 파일의 .bss 세션을 결과 파일의 .bss 세션에 놓겠다는 의미이다.
_os_start = . ; .cuteOS : {./obj/kernel/scv_kernel.bin.o} . = ALIGN(4); _os_end = . ;
이 부분은 결과 파일의 .cuteOS 세션에 scv_kernel.bin.o 파일을 놓는 역할을 한다.
_os_start 와 _os_end 심벌이 .cuteOS 세션의 앞뒤에 오는 걸 확인해 볼 수 있다.
즉, _os_start 심벌은 scv_kernel.bin.o 이미지의 시작위치를 나타내며, _os_end 심벌은 scv_kernel.bin.o 이미지의 끝 위치를 나타낸다.
Makefile 에서의 활용
링커 스크립트를 알고 있으면 어떤 일을 할 수 있을까? 컴파일되어 나온 오브젝트 파일을 자기 마음대로 레이아웃을 지정할 수 있다.
필요없는 부분을 생략함으로서 파일의 크기를 줄일 수 있고, 또한 특정한 영역을 지정할 수 있다. 이는 컴파일 시에 지정할 수 있으며, 보통 makefile 에서 선언해준다.
# BOOTLOADER OBJ = start.o led.o clksetup.o memsetup.o cuteOS.bin.o cute-boot: $(OBJ) arm-linux-ld $(OBJ) -o cute-boot -Ttext 0x00000000 -N -T cute-boot.lds arm-linux-objcopy cute-boot cute-boot.bin -O binary start.o: start.S arm-linux-gcc start.S -c -DOS_RAM_BASE = 0x30100000 led.o: led.S arm-linux-gcc led.S -c clksetup.o: clksetup.S arm-linux-gcc clksetup.S -c memsetup.o: memsetup.S arm-linux-gcc memsetup.S -c # KERNEL cuteOS.bin.o: cuteOS arm-linux-ld -r -o cuteOS.bin.o -b binary cuteOS.bin cuteOS: head.o main.o arm-linux-ld head.o main.o -o cuteOS -Ttext 0x30100000 -N arm-linux-objcopy cuteOS cuteOS.bin -O binary head.o: head.S arm-linux-gcc head.S -c main.o: main.c arm-linux-gcc main.c -c clean: rm -f *.o rm -f cute-boot rm -f cute-boot.bin rm -f cuteOS rm -f cuteOS.bin
위의 makefile 은 크게 부트로더를 만드는 부분과 커널을 만드는 부분으로 나뉜다.
make 명령을 수행할 경우 다음과 같이 수행된다.
#make arm-linux-gcc start.S -c -DOS_RAM_BASE=0x30100000 arm-linux-gcc led.S -c arm-linux-gcc clksetup.S -c arm-linux-gcc memsetup.S -c arm-linux-gcc head.S -c arm-linux-gcc main.c -c arm-linux-ld head.o main.o -o cuteOS -Ttext 0x30100000 -N arm-linux-objcopy cuteOS cuteOS.bin -O binary arm-linux-ld -r -o cuteOS.bin.o -b binary cuteOS.bin arm-linux-ld start.o led.o clksetup.o memsetup.o cuteOS.bin.o -o cute-boot -Ttext 0x00000000 -N -T cute-boot.lds arm-linux-objcopy cute-boot cute-boot.bin -O binary
여기서는 arm-linux-gcc를 이용하여 start.S, led.S, clksetup.S, memsetup.S, head.S, main.c 파일을 각각 start.o, led.o, clksetup.o, memsetup.o, head.o, main.o 파일로 만든다.
그리고 arm-linux-ld 명령어를 이용하여 head.o, main.o 파일로 cuteOS 결과 파일을 만들어낸다(head.S 파일과 main.c 파일은 뒤에서 살펴보기로 하자).
cuteOS가 동작할 메모리 위치는 0x30100000 번지가 된다. -N 옵션은 .data 세션을 page 경계에 놓지 말고 .text 세션의 바로 뒤에 붙이는 역할을 한다.
파일의 크기를 줄이기 위해 이 옵션을 사용했다.
그리고 arm-linux-objcopy 명령어를 이용하여 cuteOS 파일로 순수 바이너리 이미지인 cuteOS.bin 파일을 만들어낸다.
이 파일이 실제 RAM 상에서 동작할 커널의 이미지이다. 다음은 cuteOS.bin 입력 파일을 cuteOS.bin.o 결과 파일로 만드는 과정이다.
여기서 ←b binary> 옵션은 입력 파일 cuteOS.bin 파일이 바이너리 파일이란 의미이며, -o는 output을 의미한다.
또한 -r 옵션은 arm-linux-ld의 입력파일로 다시 사용할 수 있도록 결과 파일을 만들라는 의미이며, relocateable의 약자이다.
이 파일은 다음에 오는 armlinux-ld 명령어를 이용하여 cute-boot 결과 파일을 만드는 과정에서 입력 파일로 사용하게 된다.
즉, arm-linux-ld 명령어를 이용하여 start.o led.o clksetup.o memsetup.o cuteOS.bin.o 입력 파일로 cute-boot 결과 파일을 만든다.
←T cute-boot.lds> 옵션은 arm-linux-ld가 사용할 링커 스크립트 파일로 cuteboot.lds 파일을 사용하라는 의미이다.
마지막으로 arm-linux-objcopy 명령어를 이용하여 cute-boot.bin 파일을 만든다. 이 파일을 JTAG를 이용하여 ROM 상에 다운 받아 실행하면 된다.