이번에는 Armv7 아키텍처 관점에서 AAPCS를 SP 레지스터와 LR 레지스터가 어떻게 바뀌는지 기준으로 설명합니다. 하지만 대부분의 시스템 소프트웨어 개발자는 SP와 LR 레지스터를 설정하는 어셈블리 명령어보다 C 언어로 프로그래밍합니다. 그래서 SP와 LR 레지스터를 보면 대부분 낯설게 느낍니다. 
 
그래서 이번 절에서는 함수를 호출하는 예제 코드를 분석하면서 함수를 호출했을 때 SP와 LR 레지스터가 어떻게 바뀌는지 알아보겠습니다.

함수가 호출될 때의 세부 동작 원리

다음과 같은 함수를 작성했다고 가정하겠습니다.

01 int add_func(int x, int y)
02 {
03    int result = x + y;
04    printf("x:%d, y:%d \n", x, y);
05   
06    return result;
07 }

add_func 함수를 작성하면 다음과 같이 동작할 것이라 예상합니다.

 다른 함수에서 add_func 함수를 호출할 수 있다.
 어떤 함수가 add_func 함수를 호출해도 add_func 함수를 호출한 코드로 복귀한다.

즉, add_func 함수는 다양한 상황에서 다른 함수로부터 호출될 수 있습니다. 예를 들면, for 문 내에서 반복적으로 호출되거나 다른 함수에서 여러 번에 걸쳐 호출될 수 있습니다. 또한 C 언어로 작성된 코드뿐만 아니라 어셈블리 명령어를 통해 함수가 호출될 수 있습니다. add_func 함수가 자신의 기능을 독립적으로 실행하려면 다음과 같은 요구사항을 만족해야 합니다.

 먼저 add_func 함수를 호출한 주소를 어딘가에 저장해 놓는다.
 add_func 함수를 호출한 주소로 복귀한다.

모듈 단위로 독립적인 기능을 수행하는 함수가 호출되려면 Arm 아키텍처의 도움이 필요합니다. 함수를 호출할 때 Arm 아키텍처는 다음과 같이 동작합니다.

함수를 호출하면 함수를 호출한 후 복귀할 주소를 R14 레지스터에 저장한다.

그렇다면 함수를 호출하면 복귀할 주소가 R14(링크 레지스터)에 어떤 방식으로 업데이트될까요? 서브루틴(함수, 레이블)이 호출될 때 'BL [함수 주소]'와 같은 명령어가 실행되는데, Arm 코어는 서브루틴을 호출한 후 복귀할 주소를 링크 레지스터인 LR 레지스터에 업데이트합니다. 그래서 서브루틴이 호출되면 가장 먼저 실행되는 명령어는 다음과 같습니다.

R14 레지스터(복귀할 주소가 저장됨)의 값을 프로세스의 스택 공간에 푸시(저장)

서브루틴에서 자신의 기능을 마무리한 후 프로세스의 스택에 저장된 링크 레지스터의 값을 다시 PC에 넣으면 서브루틴을 호출한 주소로 복귀합니다.

R14 레지스터의 동작 원리

이어서 다음 예제 코드를 보면서 R14 레지스터의 동작 원리를 알아봅시다.

01  0x1004 SOME_ROUTINE:  PUSH    {FP, LR}
02  0x1008                 ; // 명령어
03  0x100c                 BL SUB_ROUTINE
04  0x1010                 ; // 명령어
05  0x1014                 POP     {FP, PC}

01번째 줄에 있는 SOME_ROUTINE은 호출되는 함수나 레이블을 나타내는 심벌 이름입니다. 01번째 줄의 'PUSH {FP, LR}' 명령어가 실행되면 FP(R11)와 LR(R14) 레지스터의 값을 프로세스의 스택에 백업(푸시)합니다.

이어서 03번째 줄에서는 'BL SUB_ROUTINE' 명령어를 실행합니다. 여기서 03번째 줄의 주소는 0x100c입니다. 이 명령어를 실행하면 SUB_ROUTINE으로 분기한 다음에 복귀할 주소인 0x1010가 LR(R14) 레지스터에 업데이트됩니다. 0x1010은 3번째 줄 다음에 있는 주소이며, SUB_ROUTINE의 실행이 마무리된 다음에 바뀌는 프로그램 카운터입니다.

[정보] “//명령어” 주석의 의미

02 ~ 04번째 줄의 주석인 “//명령어”는 주소에 있는 명령어를 표기한 것입니다.


05번째 줄은 01번째 줄에서 프로세스의 스택에 백업한 FP(R11)와 LR(R14) 레지스터의 값을 다시 FP(R11)와 LR(R14) 레지스터에 로딩하는 동작입니다.

앞에서 든 예제와 같이 함수를 어느 코드(함수, 레이블)에서 호출하든 함수가 실행한 다음에 복귀할 주소를 LR(R14) 레지스터가 알고 있기 때문에 함수가 독립적인 하나의 기능으로 실행될 수 있습니다.

함수에 전달되는 인자는 어떻게 처리될까?

대부분의 함수는 인자를 받아서 이를 처리하는 구조입니다. 그럼 함수에 전달되는 인자는 어떤 방식으로 처리될까요? 바로 R0 ~ R3 레지스터에 실려서 전달됩니다. 다음 그림을 보면서 더 자세히 알아 봅시다.

 


그림 12.2 함수가 호출될 때 사용되는 R0 ~ R3와 R0 레지스터

그림 12.2의 왼쪽 부분을 보면 a, b, c, d 인자를 적용해 sub_rountine 함수를 호출합니다. 이처럼 sub_rountine 함수에 전달되는 인자는 R0 ~ R3 레지스터에 저장됩니다.

이번에는 그림 12.2의 오른쪽에 있는 sub_rountine 함수의 가장 아랫부분을 봅시다. 'return result;' 구문이 보이는데, 이는 result 값을 반환하는 동작입니다. 이때 R0 레지스터에 result 값이 실려서 sub_rountine 함수를 호출한 코드의 res 지역 변수에 전달됩니다.

[주의] 8바이트 단위 리턴 처리 방식

함수의 리턴 값 타입이 4바이트 단위면 R0 레지스터에 리턴 값을 저장합니다. 만약 리턴 값을 반환하는 변수의 타입이 8바이트 단위면 R0 레지스터로는 부족합니다. R0 레지스터는 4바이트 단위의 데이터를 저장하기 때문입니다. 그래서 64바이트 크기의 리턴 값은 R0 ~ R1 레지스터에 저장됩니다. sub_routine 함수의 아랫부분에 보이는 R0 ~ R1이 바로 이 부분을 나타냅니다.


함수에 전달되는 인자는 R0 ~ R3 레지스터에 저장되는데, 여기서 R0 ~ R3는 R0, R1, R2, R3 레지스터이니 레지스터의 개수가 4개입니다. 이 내용을 읽으면 "함수에 전달되는 인자는 무조건 R0 ~ R3 레지스터에 저장된다"라고 오해할 수 있습니다. 다음 표를 보면서 조금 더 자세히 설명하겠습니다.

인자의 개수 인자를 저장하는 레지스터

 

표 12.2 함수에 전달되는 인자의 개수별로 사용되는 레지스터

표 12.2를 보면 함수에 전달되는 인자의 개수에 따라 사용되는 레지스터가 다릅니다. 함수에 전달되는 인자의 개수가 1개이면 R0 레지스터에 인자의 값이 저장됩니다. 만약 인자의 개수가 2개면 R0와 R1 레지스터에 각각 인자의 값이 저장됩니다. 하지만 함수에 전달되는 인자가 언제나 R0 ~ R3 레지스터에 저장되지는 않습니다. 함수에 전달되는 인자의 개수가 5개 이상이면 1~4번째 인자까지는 R0 ~ R3 레지스터에 저장되고, 5번째 인자부터 프로세스의 스택 공간에 저장됩니다.  

예제 코드 분석: 함수에 전달되는 인자의 처리 방식

이번에는 다음 예제 코드를 보면서 함수에 전달되는 인자가 어떻게 처리되는지 알아봅시다. 

01 int add_func(int x, int y)
02 {
03    int result = x + y;
04    printf("x:%d, y:%d \n", x, y);
05   
06    return result;
07 }
    
add_func 함수는 2개의 인자를 받아서 두 인자의 값을 더한 결과를 반환합니다. 이때 add_func 함수의 인자인 x와 y는 각각 R0와 R1 레지스터에 실려서 전달됩니다. 또한 add_func 함수는 'return result;' 구문과 함께 result를 반환합니다. 함수가 반환하는 값인 result는 R0 레지스터에 저장됩니다.

이번에는 다음 그림을 보면서 함수에 전달되는 인자와 함수가 반환하는 값이 어느 레지스터에 전달되는지 정리해 봅시다.

 


그림 12.3 add_func 함수가 호출될 때 사용되는 R0 ~ R1과 R0 레지스터

그림 12.3은 main 함수에서 add_func 함수를 호출하는 동작을 나타냅니다. 그림의 왼쪽 부분을 보면 a와 b라는 인자를 사용해 add_func 함수를 호출합니다. 이때 함수에 전달되는 인자는 add_func 함수에서 이 인자를 사용할 수 있게 R0와 R1 레지스터에 저장됩니다.

이번에는 그림 오른쪽에 있는 add_func 함수의 가장 아랫부분을 봅시다. 'return result;' 구문이 실행되면 R0 레지스터에 result 값이 실려서 add_func 함수를 호출한 코드의 res 지역 변수에 전달됩니다.

정리

이제 AAPCS 관점에서 정리하면 함수가 호출될 때의 세부 동작은 다음과 같이 분류할 수 있습니다.

 


표 12.3 AAPCS 관점에서 본 함수가 호출될 때의 세부 동작
함수 호출 동작
 AAPCS 관점 동작
함수 호출과 관련된 세부 동작 Armv7 - AAPCS 동작
함수에 인자를 전달
R0 ~ R3 레지스터에 저장
함수가 기능을 수행한 결과를 반환 R0 레지스터에 저장
함수를 호출한 다음에 복귀할 주소 링크 레지스터(R14)에 업데이트


세부 구현 방식을 단계별로 살펴보니 함수 호출은 Armv7 아키텍처의 도움을 받아 실행된다는 사실을 알 수 있었습니다.

Armv7 아키텍처에서 정의된 범용 레지스터 중 SP(R13) 레지스터와 LR(R14) 레지스터는 AAPCS와 연관된 핵심 레지스터입니다. 먼저 범용 레지스터 가운데 SP, LR 레지스터의 역할을 알아봅시다.

SP와 LR 레지스터의 역할

먼저 다음 그림을 보면서 Armv7 아키텍처에서 정의된 레지스터 중 AAPCS와 연관된 레지스터를 알아봅시다.

 
그림 12.1 Armv7 아키텍처의 레지스터 중 AAPCS와 연관된 레지스터 

위 그림은 Armv7 아키텍처에서 동작 모드별로 정의된 레지스터 목록입니다. 그림의 아랫부분에 빗금으로 표시된 부분을 봅시다.

해당 부분의 윗부분에 있는 SP 레지스터를 보겠습니다. 그림의 가장 왼쪽에 SP가 있는데, 같은 행에 각 동작 모드별로 SP_usr, SP_svc, SP_abt 레지스터가 있습니다. 그중 SP_svc는 슈퍼바이저 모드에서만 사용되는 SP 레지스터, SP_irq는 IRQ 모드에서만 접근하는 SP 레지스터입니다.

SP의 아랫부분을 보면 LR이 있는데, 이는 링크 레지스터(Link Register)를 나타냅니다. 보다시피 LR 레지스터 또한 각 동작 모드별로 존재합니다. 예를 들어, LR_svc는 슈퍼바이저 모드에서만 사용되는 LR 레지스터, LR_irq는 IRQ 모드에서만 접근되는 LR 레지스터입니다. 

[중요] 동작 모드별 뱅크드 레지스터

Armv7 아키텍처에서 각 동작 모드별로 뱅크드 레지스터가 존재합니다. 뱅크드 레지스터는 특정 동작 모드에서 전용으로 사용되는 레지스터입니다. 예를 들어 슈퍼바이저 모드에서 R13 레지스터가 바뀌면 R13_svc 레지스터가 함께 변경됩니다. 이와 마찬가지로 슈퍼바이저 모드에서 R14 레지스터가 바뀌면 R14_svc 레지스터가 함께 변경됩니다.

뱅크드 레지스터가 동작 모드별로 존재하면 동작 모드가 변경됐을 때 레지스터를 백업할 필요가 없어 간결하게 소프트웨어 구조를 설계할 수 있습니다. 


Arm 스펙 문서에서 본 SP 레지스터

이번에는 Arm 스펙 문서를 보면서 SP와 LR 레지스터에 대해 알아봅시다. 먼저 SP 레지스터에 대한 내용을 소개합니다.   

출처: ARM® Cortex™-A Series Programmer's Guide https://developer.arm.com/documentation/den0013/latest 
3.1 Registers
R13 (in all modes) is the OS stack pointer, but it can be used as a general purpose register when not required for stack operations.

위 내용은 다음과 같이 해석할 수 있습니다.

 R13은 운영체제의 스택 포인터(모든 모드에서 사용)다.
 R13은 스택 연산을 수행하지 않을 때 범용 레지스터처럼 사용될 수 있다.

그런데 Arm 스펙 문서의 내용 중 'OS stack pointer'로 적힌 부분이 있는데, 조금 포괄적인 의미를 지니고 있어 의도를 정확히 파악하기 어렵습니다. 여기서 'OS stack pointer'는 "프로세스의 스택 주소를 가리킨다"라고 해석하면 됩니다. 대부분의 운영체제는 프로세스 중심으로 실행되고, 어떤 함수나 레이블을 실행하거나 호출하던 프로세스의 스택 공간 내에서 실행되기 때문입니다. 

이어서 SP 레지스터에 대해 추가로 설명된 내용을 보겠습니다.

출처: ARM® Cortex™-A Series Programmer's Guide https://developer.arm.com/documentation/den0013/latest 
3.1 Registers
For example, a program executing in User mode which specifies R13 will access R13_usr. A program executing in SVC mode which specifies R13 will access R13_svc.

위 내용은 다음과 같이 요약할 수 있습니다.

유저 모드에서 실행되는 프로그램에서 R13을 지정하면 R13_usr가, 슈퍼바이저 모드에서 실행되는 프로그램에서 R13을 지정하면 R13_svc가 함께 업데이트된다.

Armv7 아키텍처에서 어떤 어셈블리 코드를 실행하든 동작 모드(슈퍼바이저, 유저 등등)에서 실행됩니다. 위 내용은 각 동작 모드에서 R13에 접근하면 해당 모드별로 정의된 R13 레지스터도 함께 액세스된다는 의미입니다. 이해를 돕기 위해 다음과 같은 예를 들겠습니다.

 유저 모드인 경우: R13 레지스터에 접근하면 R13_usr 레지스터에도 액세스
 슈퍼바이저 모드인 경우: R13 레지스터에 접근하면 R13_svc 레지스터에도 액세스

[정보] AAPCS의 핵심 원리와 디버깅

사실 위 내용을 읽으면 잘 와 닿지 않는데, TRACE32 같은 하드웨어 디버깅 장비를 사용해 확인하면 바로 이해할 수 있습니다. 다음 동영상 링크를 보면 TRACE32로 각 모드별로 R13 레지스터에 접근하는 내용을 확인할 수 있습니다. 
- TRACE32로 프로세스의 cpu_context(컨텍스트) 확인해보기: - https://youtu.be/pObn1fb-1M0


Arm 스펙 문서에서 본 LR 레지스터

이어서 스펙 문서 분석을 통해 LR 레지스터에 대해 알아봅시다.

출처: ARM® Cortex™-A Series Programmer's Guide https://developer.arm.com/documentation/den0013/latest 
3.1 Registers 
R14 (the Link Register) holds the return address from a subroutine entered when you use the branch with link (BL) instruction. It too can be used as a general purpose register when it is not supporting returns from subroutines. 

LR 레지스터를 명쾌하게 설명했는데, 내용을 요약하면 다음과 같습니다.

 LR은 BL 명령어와 함께 서브루틴(함수, 레이블) 주소로 분기했을 때 서브루틴에서 복귀할 주소를 저장하는 레지스터다.
 서브루틴으로 복귀하는 상황이 아니면 범용 레지스터로 사용될 수 있다.


[정보] 서브루틴이란?

Arm 스펙 문서를 보면 서브루틴(subroutine)이란 용어가 보입니다.

출처: https://developer.arm.com/documentation/dui0473/c/writing-arm-assembly-language/subroutines-calls
Subroutines calls
A subroutine is a block of code that performs a task based on some arguments and optionally returns a result. By convention, registers R0 to R3 are used to pass arguments to subroutines, and R0 is used to pass a result back to the callers. A subroutine that needs more than 4 inputs uses the stack for the additional inputs.

위 스펙은 Armv7 기반으로 서브루틴 호출(subroutines call)을 설명합니다. 여기서 서브루틴은 함수 호출이나 레이블로 분기하는 동작을 포함하며, 일반적으로 함수 호출을 서브루틴으로 표현할 때가 많습니다. 


SP 레지스터와 마찬가지로 LR 레지스터도 각 모드별로 LR_<mode> 레지스터가 존재합니다. 예를 들면 다음과 같습니다.

 IRQ 모드인 경우: R14 레지스터에 접근하면 R14_irq 레지스터에도 액세스
 슈퍼바이저 모드인 경우: R14 레지스터에 접근하면 R14_svc 레지스터에도 액세스

[중요] 동작 모드별로 SP 레지스터와 LR 레지스터가 존재하는 이유

Arm 프로세서에 대해 깊게 분석하면 동작 모드별로 SP와 LR 레지스터가 존재하는 이유를 알게 됩니다. 그 이유는 다음과 같이 설명할 수 있습니다. 

SP 레지스터와 LR 레지스터는 동작 모드별로 실행되는 프로세스의 실행 흐름을 저장하는 중요한 역할을 수행한다.

동작 모드를 변경하면 이전 동작 모드에서 실행됐던 레지스터를 어딘가에 백업해야 합니다. 그런데 각 동작 모드별로 SP 레지스터나 LR 레지스터가 없으면 동작 모드별로 실행된 레지스터를 스택과 같은 메모리 공간에 백업하고 로딩하는 루틴이 실행돼야 합니다. 그 결과, 시스템의 복잡도가 늘어납니다. 그래서 프로세스의 실행 흐름 정보를 저장하는 SP와 LR 레지스터가 각 동작 모드별로 존재합니다.


이처럼 SP와 LR 레지스터는 Armv7 아키텍처에서 정의된 다른 범용 레지스터와는 달리 서브루틴을 호출하기 위해 사용됩니다. 단순히 어셈블리 명령어를 실행한 결과를 저장하는 임시 저장용 메모리 공간은 아닙니다.

SP와 LR 레지스터의 표기 방식과 주요 동작 정리

이번에는 SP와 LR 레지스터의 표기 방식과 주요 동작을 표로 정리해 보겠습니다.

표 12.1 SP와 LR 레지스터의 특징
레지스터 특징
SP(Stack Pointer)  - R13으로도 표기
- 사용 중인 (프로세스의) 스택 위치를 저장 
- 동작 모드별로 SP 레지스터가 존재

LR(Link Register) - R14로도 표기
- 서브루틴을 수행(함수 호출)한 후 복귀할 주소를 저장 
- 동작 모드별로 LR 레지스터가 존재


어셈블리 명령어 중에서 SP(R13)와 LR(R14) 레지스터를 보면 위 표의 내용이 머릿속에 떠올랐으면 좋겠습니다.

+ Recent posts