Chapter 5의 아래 부분은 요약 및 정리에서 생략하도록 합니다.
[※] Context와 AAPCS (Chapter 2에서 이미 설명했습니다.)
[※] Pointer와 배열은 ~ Stack과 heap에 대한 소고 (p.365~385) (기초 CS 내용입니다.)
[※] 함수가 불렸을 때 일어나는 일(p.395) (기초 CS 내용이고 본문에 설명이 잘 돼있습니다.)
임베디드 시스템에서 stack도 heap도 모두 초기화되지 않은 메모리상 (.bss 영역) 연속된 전역변수 배열이라 생각합시다. 그리고 스택은 높은주소에서 낮은주소로 아래를 향해 자라나는 full descending stack입니다. 예를 들어, stack[2000]이 있다고 하면, stack[1999] -> stack[1998] ... -> stack[0] 순으로 저장됩니다.
Stack backtracking
스택은 프로그램 실행흐름의 과거가 기록돼있습니다. 스택을 거슬러 올라가면 특정시점에 실행하고 있던 서브루틴의 상태를 짐작할 수 있습니다. 따라서 exception이 발생했을 때 스택을 거슬러 올라가면서 문제원인을 찾는 것이 ‘stack backtracing’ 디버깅입니다.
위 그림은 어떤 프로그램을 실행 중 중간에 멈췄을 때의 context입니다.
- 우선 CPSR을 확인해보니 T-bit가 활성화 돼있으니 Thumb mode 인 것을 확인할 수 있습니다.
- 다음으로 PC를 보니 `0x1E6C_19BC`네요. `mov r4, #0x0` 명령어를 실행할 때 프로그램이 멈춰있습니다.
- 현재 실행중인 함수는 `word b_funct()`입니다.
- 이 함수에 진입할 때 R4, R5, R6, R14 4개 레지스터를 스택에 백업했네요.
- 컴파일러가 이 함수 내에서 R4, R5, R6를 사용한다고 판단했나 봅니다.
- 추가로 스택포인터를 0xC8만큼 더 이동했습니다. 왜그럴까요? `word array[100]` 때문입니다.
- Thumb 모드이므로 1-WORD는 2-byte, 100 * 2 = 200-byte = 0xC8 byte입니다. 이 로컬 배열을 스택에 저장하기 위해 스택포인터를 미리 이동시킨 것임을 짐작할 수 있습니다.
- 누가 이 함수를 호출했는지 확인하기 위해 LR을 보니 `0x1E6C_1A1D`입니다.
- 현재 Thumb 모드라 그런지 LR이 홀수네요. 1을 빼서 짝수로 만들어줍시다. 그러면 원래 LR은 `0x1E6C_1A1C`입니다.
- 이 함수에 진입할 때 R4, R5, R6, R14 4개 레지스터를 스택에 백업했네요.
- LR을 확인하지 않고도 스택을 들여다보면 복귀주소를 찾을 수 있습니다.
- 현재 스택포인터는 `0x1F6E_92C8`입니다. 그리고 방금 스택포인터를 `0xC8`만큼 이동시킨 이유를 다뤘습니다.
- 즉, 함수 `b_funct()`가 호출된 시점의 스택포인터는 `0x1F6E_92C8 + 0xC8 = 0x1F6E_9390`입니다.
- 위에서 함수 `b_funct()`가 호출됐을 때 레지스터 R4, R5, R6, LR을 스택에 넣은 것을 확인했습니다.
- 따라서 계산한 스택포인터로부터 4 * 4-byte를 살펴보면, R4 = 0, R5 = 0, R6 = `0x0000_FFFF`, LR = `0x1E6C_1A1D` 인 것을 알 수 있습니다.
- Thumb 모드이므로 방금처럼 홀수에서 짝수로 만들어주기 위해 1을 빼주면 복귀주소는 `0x1E6C_1A1C`입니다.
- 도출해 낸 복귀주소로 가보니 `b_funct()`를 호출했던 건 `a_funct()`였다는 것을 알게 됐습니다.
- 함수 `a_funct()`는 진입하면서 R4, LR을 스택에 push 했습니다. 이번에는 누가 `a_funct()`를 호출했는지 확인해 보겠습니다.
- 저희는 이미 `b_funct()`를 호출한 직후, 아직 context를 백업하기 전의 스택포인터가 ` 0x1F6E_9390`임을 구했습니다.
- 그 영역을 확인해 보니 R4, LR이 순서대로 스택에 저장된 것을 알 수 있습니다.
- Thumb mode 주소 보정을 해주면, `a_funct()`에 대한 복귀주소가 `0x1E6C_1A58`임을 알 수 있습니다.
이런 방식으로 스택을 거슬러 올라가면 프로그램의 과거에 대한 여러 가지 정보를 얻을 수 있습니다.
Q. 스택의 크기는 어떻게 정할까요?
스택을 사용하는 주체 (전체 애플리케이션 or 서브루틴(함수))에 대해 여러가지 시뮬레이션을 돌립니다. 분기가 있다면 각 분기마다 모두 들어가 보도록 되도록 많은 경우의 수를 탐색합니다. 그중에서 가장 스택을 많이 사용한 경우가 나올 겁니다. 그 스택 최대 사용량의 125%를 스택의 크기로 잡습니다.
예를 들어 `0x1000`에서 시작한 스택포인터가 있고, 스택을 최대한 많이 사용했을 때의 스택포인터가 `0x400`이라고 한다면, `0x0C00`만큼 사용한 것이니 여기의 125%인 `0x1000`을 스택크기로 잡습니다.
'Embedded' 카테고리의 다른 글
임베디드 레시피 Chapter 7. Device control (0) | 2025.02.01 |
---|---|
임베디드 레시피 Chapter 6. RTOS & Kernel (0) | 2025.01.31 |
임베디드 레시피 Chapter 4. ARM ② Assembly와 Bootloader (1) | 2025.01.29 |
임베디드 레시피 Chapter 3. SW ① 컴파일부터 로드 (0) | 2025.01.25 |
임베디드 레시피 Chapter 2. ARM (0) | 2025.01.20 |