Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

8179
1390
307630


[Arm프로세서] AAPCS: Armv8: 스택과 관련된 명령어 - stp Armv7: 함수 호출 규약

SP 레지스터가 변경될 때 실행되는 명령어는 stp와 ltp입니다. 먼저 명령어의 정의를 소개하고, 예제 코드를 분석하면서 명령어의 동작 원리에 대해 알아봅시다.

Armv7 아키텍처에서 SP 레지스터가 변경되면서 프로세스의 스택 공간에 레지스터를 푸시하는 명령어는 'push'입니다. Armv8 아키텍처에서는 push 대신에 stp 명령어를 사용해 push 명령어와 같은 동작을 수행합니다.

Arm 스팩 문서에서 stp 명령어 알아보기

먼저 Arm 스팩 문서를 보면서 stp 명령어에 대해 알아봅시다.

C6.2.273 STP

Store Pair of Registers calculates an address from a base register value and an immediate offset, and stores two 32-bit words or two 64-bit doublewords to the calculated address, from two registers. 
[출처] DDI0487Fc_armv8_arm.pdf

스팩 문서에서 설명된 stp 명령어의 동작은 다음과 같이 요약할 수 있습니다.

   1. 베이스 레지스터가 가리키는 값을 기준으로 지정된 레지스터를 저장함
   2. 베이스 레지스터의 값에 오프셋이 적용될 수 있음

stp 명령어에 대한 설명에는 SP 레지스터에 대한 내용이 없어서, "stp 명령어가 AAPCS와 어떤 연관이 있는가"라는 의문이 듭니다. 

stp 명령어의 세부 문법 알아보기

그런데 stp 명령어는 SP 레지스터와 함께 사용되며, 표기법은 아래 그림과 같습니다.

 
그림 7.4 stp 명령어의 포멧1

위 그림에서 명시된 STP 명령어의 표기법을 설명하기 전에, 표기법에서 보이는 각 구문에 대해 설명하겠습니다.

왼쪽에 보이는 <Xt1>, <Xt2>는 저장하려는 레지스터를 의미합니다. 오른쪽 부분에 있는 <SP>는 SP 레지스터를 의미하고 <imm>은 오프셋을 뜻합니다.

위 명령어에서 눈여겨 볼 부분은 STP 명령어의 가장 오른쪽 부분에 있는 !기호입니다. 이 명령어는 SP 레지스터를 <imm> 만큼 업데이트한 후, 업데이트된 스택 주소에 Xt1과 Xt2 레지스터를 푸시하는 동작입니다.  
이번에는 STP 명령어의 동작을 나타낸 다른 그림을 보겠습니다.

 
그림 7.5 stp 명령어의 포멧2

위 명령어는 SP 레지스터의 값에서 #<imm> 오프셋을 적용한 주소에 Xt1과 Xt2 레지스터를 저장하는 명령어입니다. 

stp 명령어에서 유심히 봐야할 부분이 명령어의 가장 오른쪽 부분입니다. 그림 7.4에서 소개된 명령어와는 달리, STP 명령어의 가장 오른쪽 부분에 !이 없으므로 SP 레지스터의 값이 업데이트되지 않습니다.

stp 명령어를 예제 어셈블리 명령어로 함께 분석하기

stp 명령어의 포멧을 알아봤으니 다음과 같은 예제 코드를 보면서 stp 명령어가 어떻게 
사용되는지 알아봅시다.

01 <__fork>:
02   stp x29, x30, [sp, #-0x20]!
03   stp x28, x27, [sp, #+0x10]

02번째 줄이 실행되기 전에 SP 레지스터는 0x20000이라고 가정하겠습니다.

아래 그림을 보면서 02번째 줄이 실행될 때 내부 동작 방식을 알아봅시다.

 

그림 7.6 stp 명령어를 실행할 때 업데이트되는 SP 레지스터  

아래 공식에 따라 SP 레지스터의 값이 업데이트되면서 스택 공간에 x29와 x30의 값을 저장합니다.

    "SP = 0x1ffe0 = 0x20000 - 0x20"

또한 0x1ffe0와 0x1ffe0+0x8 주소에 x29와 x30의 값을 저장합니다. 

02번째 어셈블리 명령어가 실행되면 SP 레지스터를 업데이트하면서 프로세스의 스택 공간에 지정된 레지스터를 푸시하는 동작을 수행합니다. 

'stp x29, x30, [sp, #-0x20]!' 명령어를 Armv7 아키텍처에서 사용된 명령어를 활용해 어떻게 표기할 수 있을까요? 유사하게 다음과 같이 표기할 수 있습니다.

01 sub sp, sp, 0x10
02 push {x29, x30}

01번째 줄은 sp 레지스터의 값을 0x10만큼 빼서 업데이트합니다. 02번째 줄은 x29와 x30 레지스터를 sp 레지스터에 푸시하면서, sp 레지스터는 0x10(x29, x30 크기)만큼 업데이트됩니다. 

이번에는 03번째 줄의 명령어를 분석하겠습니다.

01 <__fork>:
02   stp x29, x30, [sp, #-0x20]!
03   stp x28, x27, [sp, #+0x10]

03번째 줄이 실행되면서, 다음 그림과 같이 스택 공간에 데이터가 업데이트됩니다.

 

그림 7.7 stp 명령어를 실행해 스택에 레지스터를 푸시

먼저 x27과 x28는 푸시할 레지스터의 값을 의미합니다. 만약 x27 레지스터의 값이 0x27, x28 레지스터의 값이 0x28이면 0x27, 0x28이 프로세스의 스택 공간에 저장됩니다.

이어서 '[sp, #0x10]' 구문의 의미를 알아봅시다. 이 구문은 sp 레지스터의 값을 기준으로 +0x10 오프셋 주소에 지정된 레지스터(x27, x28)를 저장하는 동작으로 해석할 수 있습니다. 그림과 같이 sp 레지스터에서 +0x10만큼 떨어진 주소에 x27, x28 레지스터의 값이 저장됩니다.

이 명령어가 실행될 때는 SP 레지스터가 업데이트되지는 않습니다. stp 명령어의 가장 오른쪽 부분에 !가 없기 때문입니다.
[중요]
6장에서 Armv7 아키텍처 기반의 Arm 코어는 스타트업 코드가 실행될 때 SP 레지스터의 값을 처음 알게 된다고 말씀드렸습니다. 그렇다면 Armv8 아키텍처 기반의 Arm 코어는 언제 SP 레지스터의 값을 알게 될까요? 스타트업 코드가 실행될 때 업데이트됩니다. 

이번에도 실전 프로젝트에서 가장 많이 사용되는 부트로더인 LK(Little Kernel)의 스타트업 코드를 보면서 이 내용을 확인해 봅시다.

https://github.com/littlekernel/lk/blob/master/arch/arm64/start.S
...
01    /* Set up the stack */
02    ldr     tmp, =__stack_end
03    mov     tmp2, #ARCH_DEFAULT_STACK_SIZE
04    mul     tmp2, tmp2, cpuid
05    sub     sp, tmp, tmp2

02~04번째 줄은 스택 사이즈를 계산하는 명령어이고, 05번째 줄에서 SP 레지스터에 tmps를 저장해 SP 레지스터를 설정합니다. 이후 Arm 코어는 계속 SP 레지스터의 값을 계속 알게 됩니다.

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




덧글

댓글 입력 영역