ARM GCC에서 전역 레지스터 변수 활용하기

U-Boot 소스에서 register volatile gd_t *gd asm ("r8")와 같은 구문을 마주쳤을 때, 단순한 변수 선언으로 보이지만 실제로는 CPU 레지스터를 직접 활용하는 고급 기법이다. 이 글에서는 ARM 아키텍처에서 레지스터 변수를 정의하고 활용하는 방법을 살펴본다.

레지스터 변수의 개념

일반적인 변수는 메모리에 저장되어 접근 시마다 메모리 버스를 통한 트랜잭션이 발생한다. 반면 레지스터 변수는 CPU 내부 레지스터에 직접 배치되어 접근 속도가 극적으로 향상된다. register 키워드는 컴파일러에게 해당 변수를 레지스터에 배치할 것을 요청하며, volatile은 컴파일러의 최적화를 방지하여 예상치 못한 코드 변환을 막는다. asm("r8")은 구체적인 레지스터를 명시적으로 지정한다.

이 기법의 장점은 명확하다: 메모리 접근 없이 즉시 데이터를 조작할 수 있다. 하지만 ARM 아키텍처는 범용 레지스터가 16개(R0-R15)로 제한되어 있어, 레지스터 하나를 전역 변수로 할당하는 것은 상당한 비용이다.

APCS와 레지스터 규약

ARM Procedure Call Standard(APCS)는 함수 호출 시 레지스터 사용 규칙을 정의한다. 이 규약을 이해해야 적절한 레지스터를 선택할 수 있다.

레지스터APCS 별칭용도
R0-R3a1-a4인자 전달, 임시값 (호출자 보존 불필요)
R4-R11v1-v8지역 변수, callee가 저장/복원 필요
R12ip함수 내 임시 사용
R13sp스택 포인터
R14lr링크 레지스터 (복귀 주소)
R15pc프로그램 카운터

R0-R3는 함수 호출 시 자유롭 덮어쓰기가 허용되므로 전역 변수 저장에 부적합하다. R4-R11은 함수에서 사용 시 반드시 저장하고 복원해야 하므로, 값이 유지될 것이 보장된다. 따라서 전역 레지스터 변수에는 R4-R11 범위 내에서 미사용 레지스터를 선택해야 한다.

실전 예제: 부트로드에서 전역 레지스터 변수

다음은 임베디드 부트로드 환경에서 전역 레지스터 변수를 활용하는 완전한 예제다. 여러 소스 파일에서 동일한 레지스터를 공유하는 패턴을 보여준다.

시작 코드 (startup.S)

.section .text, "ax"
.global _entry
_entry:
    ldr     sp, =0x4000         @ 스택 초기화 (16KB)
    bl      board_init          @ 하드웨어 초기화
    bl      kernel_main         @ 메인 진입점
_hang:
    b       _hang               @ 무한 루프

하드웨어 초기화 (board.c)

/* R9를 전역 컨텍스트 포인터로 할당 */
register volatile struct sys_ctx *ctx_ptr asm("r9");

void board_init(void)
{
    static struct sys_ctx boot_context;
    
    ctx_ptr = &boot_context;
    ctx_ptr->magic = 0xDEADBEEF;
    ctx_ptr->status = 0;
}

메인 로직 (main.c)

/* 동일한 레지스터, 동일한 타입으로 재선언 */
register volatile struct sys_ctx *ctx_ptr asm("r9");

extern void timer_setup(void);

int kernel_main(void)
{
    /* board.c에서 설정된 ctx_ptr 직접 사용 */
    if (ctx_ptr->magic != 0xDEADBEEF)
        return -1;
    
    ctx_ptr->status |= 1;
    timer_setup();
    
    while (!(ctx_ptr->status & 0x80))
        ;  /* 하드웨어 이벤트 대기 */
    
    return 0;
}

빌드 스크립트 (GNU Makefile)

CROSS   := arm-linux-
CC      := $(CROSS)gcc
LD      := $(CROSS)ld
OBJCOPY := $(CROSS)objcopy
OBJDUMP := $(CROSS)objdump

CFLAGS  := -Wall -Wextra -O2 -march=armv7-a -ffreestanding
LDFLAGS := -Ttext=0x8000

OBJS    := startup.o board.o main.o

all: firmware.bin

firmware.bin: $(OBJS)
	$(LD) $(LDFLAGS) -o firmware.elf $(OBJS)
	$(OBJCOPY) -O binary -S firmware.elf $@
	$(OBJDUMP) -d -m arm firmware.elf > firmware.lst

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

%.o: %.S
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm -f *.o *.elf *.bin *.lst

.PHONY: all clean

컴파일 결과 분석

위 코드를 빌드하고 firmware.lst를 확인하면 다음과 같은 패턴을 확인할 수 있다:

00008000 <_entry>:
    8000:   e3a0da01    mov     sp, #4096       @ 스택 설정
    8004:   eb000003    bl      8018 <board_init>
    8008:   eb000008    bl      8028 <kernel_main>

0000800c <_hang>:
    800c:   eafffffe    b       800c <_hang>

00008018 <board_init>:
    8018:   e59f3010    ldr     r3, [pc, #16]   @ &boot_context
    801c:   e3a02eef    mov     r2, #15646720   @ 0xDEADBEEF 상위
    8020:   e3822ebe    orr     r2, r2, #48879  @ 0xBEEF 하위
    8024:   e5893000    str     r3, [r9]        @ ctx_ptr = 주소 (R9!)
    8028:   e5892004    str     r2, [r9, #4]    @ magic 설정
    ...

핵심은 str r3, [r9]와 같은 명령에서 R9가 직접 사용된다는 점이다. 컴파일러는 ctx_ptr을 메모리 참조 없이 R9로 치환했으며, 이는 일반 포인터 대비 상당한 성능 이점을 제공한다.

전역 레지스터 변수의 핵심 특성

일반적인 extern 전역 변수와 달리, 레지스터 변수는 각 사용 파일에서 반드시 재선언되어야 한다. 이는 다음과 같은 이유 때문이다:

  • 일반 전역 변수: 컴파일러가 데이터 섹션에 기호(symbol)를 생성, 링커가 모든 참조를 단일 주소로 해석
  • 레지스터 변수: 저장 공간이 이미 하드웨어에 고정, 컴파일러가 기호 없이 레지스터 번호로 직접 치환

따라서 링커는 레지스터 변수에 대해 어 처리도 수행하지 않으며, 이는 서로 다른 파일에서 동일한 이름으로 다른 레지스터를 지정해도 오류 없이 링크됨을 의미한다. 하지만 이는 치명적인 버그로 이어질 수 있으므로, 반드시 동일한 레지스터를 사용해야 한다.

주의사항 및 디버깅 팁

인터럽트 컨텍스트 보존: ISR에서 R9를 사용한다면 진입 시 push {r9}로 저장하고 퇴출 전 pop {r9}로 복원해야 한다. 그렇지 않으면 ISR 반환 후 전역 컨텍스트가 손상된다.

디버깅 난이도: 레지스터 변수는 메모리에 존재하지 않아 GDB 등에서 print ctx_ptr가 제대로 동작하지 않을 수 있다. info registers r9로 직접 확인해야 한다.

컴파일러 제약: 최신 GCC에서는 -ffixed-r9 옵션을 추가하여 R9를 일반 레지스터 할당에서 제외할 수 있다. 이는 전역 레지스터 변수와 파일러의 자동 레지스터 할당 간 충돌을 방지한다.

CFLAGS += -ffixed-r9 -ffixed-r8  # R8, R9를 컴파일러 자동 할당에서 제외

휴대성 고려: 이 기법은 ARM 특화이며, 다른 아키텍처로의 이식 시 조건부 컴파일이 필요하다:

#ifdef __arm__
register volatile struct sys_ctx *ctx_ptr asm("r9");
#else
extern struct sys_ctx *ctx_ptr;  /* 일반 전역 변수로 대체 */
#endif

태그: ARM APCS register-variable embedded-systems U-Boot

5월 23일 04:37에 게시됨