Arm Linux Kernel Hacks

rousalome.egloos.com

포토로그 Kernel Crash


통계 위젯 (화이트)

8179
1390
307630


[Arm프로세서] AAPCS: Armv7: SP(스택 포인터) 레지스터의 세부 동작 Armv7: 함수 호출 규약

SP는 스택 포인터(Stack Pointer) 레지스터라고 하며 약어로 R13으로 표기합니다. 일반적으로 여러분이 작성한 코드는 프로세스의 스택 공간에서 동작하므로, 어떤 코드가 실행하던 스택 주소의 위치를 알 수 있습니다. 스택 주소의 위치를 추적하는 레지스터가 SP 혹은 R13입니다. 

SP 레지스터와 프로세스의 스택과의 관계

그렇다면 SP 레지스터는 어떤 값을 저장할까요? SP(R13) 레지스터는 프로세스 스택 포인터의 주소를 저장합니다. 그러면 여기서 말하는, 프로세스의 스택 포인터 주소는 무엇일까요? 바로 프로세스에게 할당된 스택 주소의 위치를 나타냅니다. 

프로세스는 처음 생성될 때 운영체제로부터 스택 공간을 할당받고, 자신에게 할당된 스택 공간 내에서 함수를 호출합니다. 달리 말하면 SP 레지스터는 프로세스가 할당된 스택 메모리 주소의 범위 내에서 바뀝니다.

다음 그림을 보면서 SP 레지스터의 범위를 알아봅시다.

 

그림 6.10 프로세스의 스택 주소와 SP 레지스터의 관계

위 그림은 0xC000_0000~0xC000_2000 구간인 프로세스의 스택 공간에서 main() 함수에서 __printf() 함수까지 호출되는 흐름을 나타냅니다. 가장 마지막에 실행된 함수가 __printf() 함수이므로, SP 레지스터는 __printf() 함수가 호출된 프로세스의 스택 주소를 가리키고 있습니다.

반복해 설명드리지만, 프로세스는 자신의 스택 공간에서만 실행하므로, 현재 실행 중인 코드의 스택 주소를 저장하는 SP 레지스터는 0xC000_0000 ~ 0xC000_2000 구간의 값을 저장합니다. 

이처럼 부처님 손바닥에 있는 손오공과 같이, SP 레지스터는 프로세스의 스택 메모리 주소 공간 범위 내에서 업데이트됩니다. 만약 SP 레지스터가 프로세스의 스택 주소 공간을 넘게 되면, 이를 스택 오버플로우라고 하며, 대부분의 경우 크래시가 발생해 시스템은 오동작합니다. 

소프트웨어 개발자는 스택 오버플로우가 발생하지 않게 주의해 코드를 입력해야 합니다. 스택 오버플로우가 발생하면 치명적인 오류를 유발하므로, 만약 예기치 않게 스택 오버플로우가 유발되면 반드시 해결해야 합니다.
push 명령어를 실행하면 SP 레지스터와 프로세스의 스택은 어떻게 바뀔까?

'push' 명령어를 분석할 때는 레지스터와 스택 메모리 공간과 함께 조금 입체적으로 시야를 넓히면 이해가 빠릅니다. 'push' 명령어가 실행되면 푸시하려는 레지스터의 갯수에 따라 SP 레지스터의 값이 업데이트되면서, 프로세스의 스택 공간에 지정된 레지스터의 값이 저장되기 때문입니다.

이점을 유념하면서 다음 어셈블리 코드를 분석해 봅시다.

01 00010434 <add_func>:
02
03 int add_func(int x, int y)
04 {
05   10434:       e92d4800        push    {fp, lr}
06   10438:       e28db004        add     fp, sp, #4
07   1043c:       e24dd010        sub     sp, sp, #16
08   10440:       e50b0010        str     r0, [fp, #-16]
09   10444:       e50b1014        str     r1, [fp, #-20]  ; 0xffffffec
10        int result = x + y;
11   10448:       e51b2010        ldr     r2, [fp, #-16]
12   1044c:       e51b3014        ldr     r3, [fp, #-20]  ; 0xffffffec
13   10450:       e0823003        add     r3, r2, r3
14   10454:       e50b3008        str     r3, [fp, #-8]
15        printf("x:%d, y:%d \n", x, y);
16   10458:       e51b2014        ldr     r2, [fp, #-20]  ; 0xffffffec
17   1045c:       e51b1010        ldr     r1, [fp, #-16]
18   10460:       e59f0010        ldr     r0, [pc, #16]   ; 10478 <add_func+0x44>
19   10464:       ebffff9f        bl      102e8 <printf@plt>
20
21        return result;
22   10468:       e51b3008        ldr     r3, [fp, #-8]
23 }
24   1046c:       e1a00003        mov     r0, r3
25   10470:       e24bd004        sub     sp, fp, #4
26   10474:       e8bd8800        pop     {fp, pc}
27   10478:       00010554        .word   0x00010554

add_func() 함수의 주소로 분기하면 가장 먼저 05번째 줄이 실행됩니다.

05번째 줄을 보면 'push {fp, lr}' 명령어가 보이는데, 이는 fp(r11)과 lr(R14) 레지스터를 스택에 푸시하는 동작입니다. 이 명령어를 실행하면 두 가지 동작을 동시에 수행합니다.

   * fp(r11)과 lr(R14) 레지스터의 값이 스택에 푸시된다.
   * SP 레지스터가 0x08 만큼 감소된다.

이번에는 다음 그림을 보면서, 'push {fp, lr}' 명령어를 실행하면 업데이트되는 SP 레지스터의 값과 스택 주소와 데이터를 확인해 봅시다. 

 
그림 6.11 스택 주소와 스택 공간에 있는 데이터

'push {fp, lr}' 명령어를 실행하기 직전에, Arm 코어는 프로세스의 스택 주소를 SP 레지스터에 저장하고 있습니다. 이는 어떤 코드가 실행돼도 Arm 코어는 프로세스의 스택 주소를 알고 있다고 볼 수 있습니다.

이해를 돕기 위해 한 가지 예를 들겠습니다.
'push {fp, lr}' 명령어를 실행하기 전에, SP 레지스터의 값이 0xbefff5f8이라고 가정합시다. 이 때 'push {fp, lr}' 명령어를 실행하면 SP 레지스터의 값은 0xbefff5f0로 변경됩니다. push 명령어를 실행하면 지정된 레지스터를 프로세스의 스택 공간에 푸시하면서 SP 레지스터는 업데이트됩니다. 

이처럼 SP 레지스터는 프로세스의 스택 메모리 공간을 같이 떠올리면서 분석해야, 세부 동작 원리를 정확히 이해할 수 있습니다.

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



덧글

댓글 입력 영역