본문 바로가기

Hacked Brain/embeddedland.net

보드를 살려보자-5

저 자 : 유영창
출판일 : 2003년11월호

== start.S 어셈블러
<리스트 1>에서 가장 먼저 보이는 것은 (1)인데, 이렇게 선언되면 이후 코드의 어드레스 영역이나 기타 조건은 링커 스크립트에 선언된 .text라는 선언에 영향을 받는다. 일반적으로는 프로그램 코드 영역을 의미한다. 하드웨어 리셋이 발생하면 ARM은 0x00000000번지의 명령을 수행한다. 이 번지는 링커 스크립트에 의해서 _start 라벨에 맵핑된다. 좀더 정확히 말하면 링커 스크립트에서 .text에 대해 0x00000000번지에 할당되어 있기 때문이고 _start 라벨이 가장 먼저 기술되었기 때문이다. 이 _start가 0x00000000번지이기 때문에 이 라벨 이후에 인터럽트 벡터 테이블이 들어간다. 그래서 각 벡터 테이블이 수행할 명령을 기술한다.

사용자 삽입 이미지

<리스트 1> start.S

<리스트 1>에서 (2)의 첫 문장(b reset)은 실제로 수행되는 것이다. 그외 인터럽트가 발생하면 <리스트 1>의 마지막에 기술되어 있는 (3)을 통해 error_loop문을 수행한다. 즉 reset을 제외한 모든 인터럽트는 error_loop로 분기하라는 의미이다(ARM의 명령에서 b는 branch의 약자).
이 error_loop는 무한 루프를 돈다. 즉 정상적인 reset 이외에는 그냥 무한 루프를 돌게 만들었다. 하드웨어 리셋에 의해서 reset 라벨로 분기하면 가장 처음 하는 것은 LED와 관계된 GPIO 레지스터의 초기화이다. 즉 가장 먼저 GPIO의 입출력 방향 선택 레지스터에 출력으로 만든다.
우선 ldr이라는 명령을 알아보자(<리스트 1>의 (4)). 이 명령은 메모리의 내용을 레지스터로 읽어 오는 명령이다. 이 수행문은 정확히 말하면 두 가지 의미를 가지고 있다. 하나는 PXA_REG_GP_BASE라는 값을 특정한 데이터 영역에 컴파일러가 알아서 기술하고, 그 위치에 해당하는 어드레스를 이용해 r0 레지스터에 값을 가져오는 것이다. 일반적으로 ARM의 상수 값은 12비트 이상을 지정하기 힘들기 때문에 12비트 이상의 상수 값은 이런 식으로 표현한다. 이 문장은 r0 레지스터에 GPIO 레지스터의 가장 선두 주소를 넣는다.
다음에 수행되는 (5)는 r1 레지스터에 DEBUG_LED1의 상수 값을 대입하는 문장이다. 즉 mov는 레지스터의 내용을 다루는데 사용되는 데이터의 이동 명령이다. 이렇게 해서 GPIO 베이스 레지스터 주소를 r0 레지스터에 디버그 LED의 GPIO에 해당하는 비트를 r1에 넣고 (6) 명령을 이용해 GPIO 입출력 레지스터에 출력으로 설정한다. str은 ldr의 반대 명령으로 레지스터의 내용을 메모리에 전송하는 명령이다. 이 문장을 수행한 결과를 풀어 쓰면 다음과 같은 표현이 된다.
[ PXA_REG_GP_BASE + PXA_REG_OFFSET_GPDR0 ] ← DEBUG_LED1

이렇게 GPIO의 입출력 방향을 설정한 이후에 (7) 명령을 수행하여 GPIO의 출력을 LOW로 만든다. 이 문장은 GPCR0의 레지스터 중 GPIO를 클리어하는 레지스터에 DEBUG_LED1의 비트를 대입함으로써 LED에 연결된 GPIO의 출력을 LOW로 만드는 것이다. 즉 처음에는 LED를 켠다(일반적으로 하드웨어 회로에서 출력이 LOW가 되면 LED가 켜지게 만든다).
이제 LED를 점멸시키기 위해서는 일정 시간을 대기하고 LED에 연결된 GPIO의 출력을 HIGH로 놓고 다시 일정 시간을 대기한 다음 LED에 연결된 GPIO의 출력을 LOW로 놓는다. 그리고 이것을 계속 반복한다.
이렇게 처리되면 R0에는 GPIO 제어 레지스터의 선두 주소가, R1에는 LED에 해당하는 GPIO의 비트 값이 자리잡게 된다. ARM은 총 15개의 레지스터가 있는데, R13은 주로 스택 포인터로, R14는 분기 후 복귀할 때 복귀될 주소로 이용된다. R15는 현재 수행중인 주소를 관리한다. 따라서 일반적인 목적으로 사용될 수 있는 레지스터는 R0에서 R12까지이다.
이 프로그램에서 이미 R0와 R1을 사용하고 있으므로 대기를 수행하기 위해서 R4 레지스터를 사용하고 있다. 대기를 하기 위한 첫 번째 루틴이 (8)인데 이것은 r4에 WAIT_TIME_NOCACHE 대기 루프 값을 넣고 계속 1씩 감소시킨다. 그 값이 0이 되기 전까지는 이 과정을 반복하여 지연을 만들어내는 것이다. nop라는 명령은 아무런 수행은 하지 않고 단순하게 시간만 소모하는 명령이다. 또 r4 값을 1씩 감소하기 위해서 subs라는 명령을 사용하고 있는데 원래 명령은 sub이고 그 뒤에 붙는 s 문자는 연산 결과가 플래그에 영향을 미치는가를 표시한다. 없으면 연산 결과가 플래그에 영향을 미치지 않기 때문에 그 뒤의 조건문에 영향을 미치지 않는다. subs는 연산 결과가 플래그에 영향을 미치므로 그 뒤에 나오는 조건 분기 명령인 bne에 영향을 미친다. 이 bne의 가장 처음 글자인 b는 앞서 설명했듯이 분기 명령이고, 뒤의 ne는 Not Equal로 ‘0이 아니면’이라는 뜻이다. 즉 연산 결과가 0이 아니면 B20으로 분기하라는 명령이다.
이렇게 일정 시간을 대기한 후에 LED를 끄기 위해서 (9) 문장을 수행한다. 결국 이와 같은 명령을 통해서 LED를 점멸시키는 것이다. 지금까지 LED를 점멸시키는 프로그램 소스를 살펴보았으므로 이를 컴파일하기 위한 내용을 살펴보자.

== 링커 스크립트
start.S 어셈블러 소스를 보면 알겠지만 여기에는 컴파일되어 실행 파일이 생성된 이후 이 실행 파일의 실제 시작 주소에 대한 정보가 없다. 이것은 컴파일러가 오브젝트 파일만을 만들기 때문이다. 이 오브젝트 파일은 링커(armv5l-linux-ld)에 의해서 실제 수행 가능한 코드로 만들어진다. 그래서 링커는 각 프로그램의 실제 배치와 관련된 주소 정보와 소스의 섹션들을 정렬하는 방법에 대해 기술한 ‘링커 스크립트’라는 파일을 필요로 한다.
start.S는 C 소스가 아니기 때문에 그리 복잡한 정의까지는 필요치 않다. 하지만 <리스트 2>의 start-ld-script는 부트로더의 전형적인 구조이므로 그냥 사용하면 될 것이다.

사용자 삽입 이미지

<리스트 2> start-ld-script

<리스트 2>에서 첫 행의 OUTPUT_FORMAT이라는 명령은 링커에 의해서 생성되는 실행 파일 포맷이 elf 포맷이고 32비트 리틀 엔디언 형식의 정수 표현을 갖는다는 의미이다. 두 번째 줄의 OUTPUT_ ARCH란 명령은 출력 아키텍처가 arm이라는 의미이다.
링커는 실행 파일의 시작점에 해당하는 라벨 심볼 이름을 기술하게 되어 있는데, 이것을 지정하는 것이 ENTRY이다. 실제로 이 프로그램에서는 리셋에서 시작된다는 것이 정해져 있다(C가 아니기에 무의미하지만 컴파일 경고를 방지하기 위해서 사용하고 있다).
다음은 SECTIONS라는 명령인데, 이 명령을 상세하게 설명하려면 너무 복잡하므로 단순하게 코드나 데이터의 주소와 배열 순서에 대한 정보라고만 기억하자. 이 SECTIONS에서 가장 처음 보이는 (1) 문장은 시작 주소를 지정하는 문장이다. 좀더 정확히 설명하면 .은 현재 위치를, 그 뒤에는 현재 위치에 대입하는 주소를 지정한다. 이 문장이 맨 처음 나왔기 때문에 SECTIONS에 선언된 각 섹션의 가장 선두를 지정하게 되는 것이다.
(2) 문장은 각 코드나 데이터의 섹션간 정렬은 4바이트 단위로 만들라는 것이다. 32비트 코드들은 대개 4바이트 단위로 패치되기 때문에 섹션의 정렬에 따라 4바이트로 끊어지지 않더라도 강제로 맞추라는 의미가 된다. <리스트 2>에서 (3) 문장이 계속 보이는데, 바로 이것이 섹션을 나누는 문장이다. 이 문장은 일반적으로 이렇게 쓴다는 정도만 기억해 두자. 간단히 설명하면 나중에 사용될 이지부트가 C 함수와 혼합되는 링커 스크립트를 만들기 위한 것이다.
이 리눅스 커널에서 사용되는 섹션 구조는 이것보다 상당히 복잡한데 기회가 되면 직접 한번 살펴보기 바란다. 이 링크 스크립트에 대한 것이 궁금하면 역시 kldp.or.kr에서 매우 상세하게 기술된 문서가 있으니 컴파일러 관련 문서를 찾아보기 바란다.