Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

8179
1390
307630


[Arm프로세서] AAPCS: Armv8: 함수를 호출하기 위한 디자인 Armv7: 함수 호출 규약

이번 절에서는 AAPCS64 기준으로, 실제 함수를 호출하는 예시 코드와 함께, 함수를 호출하면 SP와 X30 레지스터가 어떻게 바뀌는지 알아보겠습니다.

함수가 호출될 때의 세부 동작 원리 파악하기

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

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 }

[정보]
위에서 든 예시 코드는 6.2 절에서 봤던 코드와 같은데, Armv8 아키텍처 기반의 Arm 코어에서 이 코드를 실행하면 같은 기능을 수행합니다. 

Armv7 아키텍처 다룬 함수 디자인 규칙은 Armv8 아키텍처에 거의 그대로 적용됩니다. 공통으로 적용될 수 있는 원칙은 다음과 같습니다.

   * 먼저 add_func() 함수가 실행된 후 복귀할 주소를 어딘가에 저장해 놓는다.
   * add_func() 함수의 실행을 마무리한 후 복귀한다.

위와 같이 add_func() 함수가 호출되려면, 다음과 같은 요구사항을 만족해야 합니다. 

    “함수를 호출하면 함수를 호출한 후 복귀할 주소를 링크 레지스터인
      X30 레지스터에 업데이트한다.” 

'bl [함수 주소]' 명령어가 실행되면 지정된 주소로 분기하는데, 이 때 Arm 코어는 서브 루틴을 호출한 후 복귀할 주소를 링크 레지스터인 X30 레지스터에 업데이트합니다. 그래서 서브 루틴이 호출된 후 가장 먼저 실행되는 명령어는 다음과 같습니다.

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

서브 루틴은 자신의 기능을 마무리한 후 스택에 저장된 X30 레지스터의 값을 다시 X30 레지스터에 로딩합니다. 이어서 ret 명령어를 실행하면, X30 레지스터의 값을 PC에 넣어주게 되는데 이를 통해 서브 루틴을 호출한 주소로 복귀합니다. 

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

01  0x1004 some_routine:  stp x29, x30, [sp, #-0x10]! 
02  0x1008                 ; // code
03  0x100c                 bl 0x2000 <sub_routine>
04  0x1010                 ; // code
05  0x1014                 ldp x29, x30, [sp], #0x10
06  0x1018                 ret

[정보]
01번째 줄은 some_routine은 호출되는 함수나 레이블을 나타내는 심벌 이름을 나타냅니다.

01번째 줄이 실행되면 x29, x30 레지스터의 값을 프로세스의 스택에 백업(푸시)합니다. 

이어서 03번째 줄을 보겠습니다. 
'bl 0x2000 <sub_routine>' 명령어를 실행하면 sub_routine 심벌이 위치한 0x2000 주소로 분기합니다. 그런데 03번째 줄의 주소는 0x100c이고 04번째 줄의 주소는 0x1010입니다. 03번째 줄을 실행해 'sub_routine'이란 심벌의 서브 루틴으로 분기한 다음에 어느 주소로 복귀해야 할까요? 당연히 04번째 줄인 0x1010 주소입니다.

그래서 03번째 줄 명령어를 실행하면 Arm 코어는 하드웨어적으로 다음과 같은 동작을 수행합니다.

   "X30 레지스터에 복귀할 주소인 0x1010를 업데이트해준다."

02~04번째 줄은 어느 함수에서도 실행될 수 있는 일반적인 루틴을 표기한 것입니다.

05번째 줄은 01번째 줄에서 프로세스의 스택에 백업한 x29와 x30 레지스터의 값을 다시 x29, x30 레지스터에 로딩하는 동작입니다.

'bl [주소]' 패턴의 명령어를 실행하면 Arm 프로세서는 하드웨어적으로 서브 루틴을 호출한 다음에 복귀할 주소를 X30 레지스터에 업데이트해주므로, 함수가 독립적인 하나의 기능으로 실행하게 됩니다. 

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

Armv7 아키텍처에서 함수에 전달되는 인자는 R0 ~ R3 레지스터에 저장됩니다. 그렇다면 Armv8 아키텍처에서 함수에 전달되는 인자는 어느 레지스터에 저장될까요? 바로 X0-X7 레지스터에 저장됩니다. Armv7 아키텍처에 비해 더 많은 갯수의 인자가 레지스터에 저장됩니다. 

다음 그림을 보면서 Armv8 아키텍처에서는 함수에 전달되는 인자를 처리하는 방식을 알아봅시다.

 

그림 7.2 함수가 호출될 때 사용되는 X0-X7와 X0 레지스터

그림 7.2 왼쪽 부분을 보면 a, b, c, d 인자를 적용해 sub_rountine() 함수를 호출합니다. 각각 인자는 다음과 같은 레지스터에 저장됩니다.

   * X0: a 
   * X1: b 
   * X2: c
   * X3: d

Armv7 아키텍처에서는 4개의 함수 인자까지만 R0-R3 레지스터에 저장됩니다. 하지만, Armv8 아키텍처에서는 8개의 함수에 전달되는 인자까지 X0-X7 레지스터에 저장됩니다.

여기서 X0~X7은 X0부터 X7까지 레지스터이니 레지스터의 갯수가 8개입니다.  이 내용을 읽으면 "함수에 전달되는 인자는 언제나 X0-X7 레지스터에 저장된다"라고 간주할 수 있는데, 다음 표를 보면 함수에 전달되는 인자가 늘어남에 따라, 어떤 방식으로 처리되는지 알 수 있습니다.


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

위 표를 보면 함수에 전달되는 인자의 갯수에 따라 사용되는 레지스터가 다르다는 사실을 알 수 있습니다.
 
함수에 전달되는 인자의 갯수가 8개 이하면, X0-X7 레지스터에 인자의 값이 저장됩니다. 만약 인자의 갯수가 9개 이상으로 늘어나면, 8개까지의 인자까지는 X0-X7 레지스터에 저장되고, 9번째 인자부터는 프로세스의 스택 공간에 저장됩니다.

이어서 그림의 오른쪽에 보이는 sub_rountine() 함수의 가장 아랫 부분을 봅시다. 'return result;' 구문이 확인되는데, 이는 함수를 종료하면서 result 값을 반환하는 동작입니다. 이 때 result의 값은 어느 레지스터에 저장될까요? X0 레지스터에 result값이 저장돼 sub_rountine() 함수를 호출할 코드의 res 이란 지역 변수에 전달됩니다.

[주의]
이처럼 Arm 아키텍처에 따라 서브 루틴을 호출할 때 세부 동작이 약간 다릅니다.

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

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

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는, X0와 X1 레지스터에 실려서 전달됩니다. 또한 add_func() 함수는 'return result;' 구문과 함께 result를 반환합니다. 여기서 함수가 반환하는 값인 result는 X0 레지스터에 저장됩니다.

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

 
그림 7.3 add_func() 함수가 호출될 때 사용되는 X0-X1와 X0 레지스터

그림 7.3를 보면 main() 함수에서 add_func() 함수를 호출하는 동작을 알 수 있습니다.

그림의 왼쪽 부분을 보면 a와 b라는 인자와 함께 add_func() 함수를 호출합니다. 함수에 전달되는 인자는 X0와 X1 레지스터에 저장됩니다. 이는 Armv7 아키텍처의 동작 방식(R0-R1 레지스터에 저장)과 거의 유사합니다.
 
이번에 그림에서 add_func() 함수의 가장 아랫 부분을 봅시다. 'return result;' 구문인데, 이 구문이 실행되면 함수의 실행을 종료하면서 result의 값을 X0 레지스터에 저장합니다. X0 레지스터에 저장된 값이 result은 add_func() 함수를 호출한 코드의 res 지역 변수에 전달됩니다.

여기까지 함수가 호출될 때의 다자인에 대해 알아봤습니다. Armv8 아키텍처에서 서브 루틴(함수)를 호출하기 위한 세세한 처리 방식은 Armv7 아키텍처와 거의 유사합니다. 유사점과 차이점은 다음 표에서 확인할 수 있습니다.


표 7.3 Armv8와 Armv7 아키텍처 별 AAPCS를 처리하는 방식

이처럼 CPU 아키텍처에 따라 '함수 호출'을 가능하게 하는, 세부 구현 방식에 차이가 있습니다.

Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자



덧글

댓글 입력 영역