Armv7 아키텍처에서 push 명령어를 실행한 후 함수가 종료되기 직전에 반드시 실행되는 명령어가 pop입니다. 마찬가지로 Armv8 아키텍처에서 stp 명령어를 사용해 스택에 레지스터를 푸시한 후, 함수를 종료하기 직전에 실행되는 명령어는 ldp입니다.
Arm 스팩 문서에서 ldp 명령어 분석하기
이번에도 Arm 스팩 문서를 보면서 ldp 명령어에 대해 살펴봅시다.
C6.2.129 LDP
Load Pair of Registers calculates an address from a base register value and an immediate offset, loads two 32-bit words or two 64-bit doublewords from memory, and writes them to two registers.
[출처] DDI0487Fc_armv8_arm.pdf
스팩 문서에서 설명된 ldp 명령어의 동작은 다음과 같이 요약할 수 있습니다.
* 베이스 레지스터가 가리키는 값을 기준으로 저장된 레지스터를 지정된 레지스터에 로딩함
* 베이스 레지스터의 값에는 오프셋이 적용될 수 있음
그런데 ldp 명령어에 대한 설명를 읽어 보면 SP 레지스터에 대한 내용이 없어서, "ldp 명령어가 AAPCS와 어떤 연관이 있는가"라는 의문이 듭니다.
ldp 명령어의 세부 문법 알아보기
그런데 ldp 명령어는 SP 레지스터와 함께 사용되며, 표기법은 아래 그림과 같습니다.

그림 7.8 ldp 명령어의 포맷1: 레지스터를 로딩
먼저 1번째 명령어는 SP 레지스터가 가리키는 프로세스의 스택 주소에 저장된 값을 <Xt1>, <Xt2> 레지스터에 로딩하는 동작을 수행합니다. 여기서 주목해야 할 구문은 '[<SP>, #<imm>]'입니다. SP 레지스터에 #<imm>으로 지정된 오프셋을 적용한 주소에 있는 값을 Xt1와 Xt2 레지스터에 로딩하는 동작입니다. .
이 명령어에서 유심히 봐야 할 구문은 가장 오른쪽에 보이는 '[]' 괄호입니다.
'[]'와 같은 괄호 안에 <SP>와 #<imm>가 함께 있습니다. 이 경우 SP 레지스터의 값에 #<imm> 오프셋만을 적용한 주소에 있는 데이터를 Xt1와 Xt2 레지스터에 로딩합니다.
이어서 다음 그림을 보면서 LDP 명령어의 다른 포멧을 확인해봅시다.

그림 7.9 ldp 명령어의 포맷2: 레지스터를 로딩하면서 SP 레지스터 업데이트
현재 SP 레지스터가 가리키는 프로세스의 스택에 위치한 데이터를 Xt1과 Xt2 레지스터에 로딩하면서 SP 레지스터의 값을 업데이트하는 동작입니다.
이 명령어에서 유심히 봐야 할 구문은 가장 오른쪽에 있는 '[]' 괄호입니다.
'[]'와 같은 괄호 안에 <SP>가 있고 #<imm>는 '[]' 괄호 밖에 있습니다. 이 경우 SP 레지스터는 #<imm>으로 지정된 오프셋만큼 업데이트됩니다.
ldp 명령어를 예제 어셈블리 명령어로 함께 분석하기
ldp 명령어의 포멧을 알아봤으니 다음과 같은 예제 코드를 보면서 ldp 명령어가 어떻게
사용되는지 알아봅시다.
01 <__fork>:
02 stp x29, x30, [sp, #-0x20]!
03 stp x28, x27, [sp, #+0x10]
...
04 ldp x28, x27, [sp, #+0x10]
05 ldp x29, x30, [sp] #+0x20
위 예제 코드에서 분석할 코드를 04~05번째 줄이며, 다음과 같은 04번째 줄을 실행하기 전에,
SP 레지스터와 스택에 저장된 값이 다음 그림과 같은 조건이라고 가정하겠습니다.

그림 7.10 ldp 명령어를 실행하기 전의 스택의 상태
위 그림을 보면 SP 레지스터는 0x1ffe0이고 0x1ffe0 주소 아랫 부분에 푸시된 데이터를 확인할 수 있습니다.
먼저 04번째 줄을 분석하겠습니다.

그림 7.11 ldp 명령어를 실행해 스택의 데이터를 레지스터에 로딩
ldp 명령어를 분석할 때는 가장 오른쪽에 있는 구문을 봐야 합니다.
'[sp, #+0x10]' 구문은 SP 레지스터의 값인 0x1ffe0에 0x10을 더한 주소를 의미합니다.
이를 다음 공식에 따라 분석할 수 있습니다.
"[0x1fff0]= [0x1ffe0+0x10] = [sp, #+0x10]"
이어서 0x1fff0 주소에 있는 x28의 값을 x28, 0x1fff8 주소에 있는 x27의 값을 0x27에 로딩합니다. 이 동작은 그림의 화살표로 표기된 부분에 해당됩니다. 참고로 0x1fff0와 0x1fff8 주소에 저장된 데이터는 함수의 03번째 줄에서 이미 푸시된 x28, x27 레지스터의 값입니다.
01 <__fork>:
02 stp x29, x30, [sp, #-0x20]!
03 stp x28, x27, [sp, #+0x10]
이어서 다음 그림을 보면서 05번째 줄을 분석하겠습니다.

그림 7.12 ldp 명령어를 실행해 SP 레지스터를 업데이트
역시 ldp 명령어를 분석할 때는 가장 오른쪽에 있는 구문을 봐야 합니다. 괄호 '[]' 안에 sp만 표시돼 있고, 오프셋은 괄호 '[]' 밖에 있습니다. 이 명령어는 스택에 저장된 데이터를 x29와 x30 레지스터에 로딩하면서 sp 레지스터의 값을 변경하는 동작입니다. 이를 조금 더 세분화하면 다음과 같이 설명할 수 있습니다.
* 0x1ffe0 주소에 푸시된 x29 레지스터의 값을 x29 레지스터에 로딩
* 0x1ffe8 주소에 푸시된 x30 레지스터의 값을 x30 레지스터에 로딩
* sp 레지스터는 0x1ffe0에서 0x20000으로 변경
이처럼 ldp 명령어를 분석할 때는 명령어의 오른쪽 부분에 오프셋이 괄호 안에 있는지를 먼저 확인하는 것이 중요합니다.
Armv8 아키텍처: AAPCS(함수호출 규약)
❑ 스택과 관련된 명령어
❑ 브랜치와 복귀 명령어
AAPCS와 C 코드 최적화
Written by <디버깅을 통해 배우는 리눅스 커널의 구조와 원리> 저자
최근 덧글