이번에는 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 아키텍처의 도움을 받아 실행된다는 사실을 알 수 있었습니다.

+ Recent posts