RTOS 기반 설계에서 스택 오버플로 - 1부
이 글은 RTOS 전문가 Jean J. Labrosse가 작성했습니다.
RTOS 기반 응용 프로그램의 각 태스크에는 자체 스택이 필요하며, 그 크기는 태스크 요구 사항(예: 함수 호출 중첩, 함수에 전달된 인수, 로컬 변수 등)에 따라 다릅니다.
스택 오버플로를 피하려면 개발자는 스택 공간을 많이 할당하되 RAM 낭비를 피하기 위해 과도하게 할당하지 않아야 합니다.
스택 오버플로란 무엇입니까?
아래는 스택 오버플로가 무엇인지에 대한 설명입니다. 논의를 위해 여기서는 스택이 높은 메모리에서 낮은 메모리로 증가한다고 가정합시다. 물론 스택이 다른 방향으로 증가할 때도 동일한 문제가 발생합니다. 그림 1을 참조하십시오.
그림 1 – 스택 오버플로
(1) CPU의 SP(스택 포인터) 레지스터는 태스크에 할당된 스택 공간 내부의 어딘가를 가리킵니다. 태스크는 아래와 같이 foo() 함수를 호출하려고 합니다.
void foo (void)
{
int i;
int array[10];
:
:
// Code
}
(2) foo()를 호출하면 CPU가 호출자의 반환 주소를 스택에 저장합니다. 물론 CPU와 컴파일러에 상당히 의존합니다.
(3) 그런 다음 컴파일러가 로컬 변수를 수용하도록 SP를 조정합니다. 불행히도 이 시점에서 스택이 오버플로 되었으며 (SP가 스택에 할당된 스토리지 영역 외부를 가리킴), foo()가 수행하는 모든 것들이 스택 베이스를 벗어난 데이터에 오류를 일으킵니다. 사실, 코드 흐름에 따라 배열이 사용되지 않을 수 있으며, 이 경우 문제가 바로 나타나지 않을 수 있습니다. 그러나 foo()가 다른 함수를 호출하면 스택 외부에 있는 무언가를 건드릴 가능성이 높습니다.
(4) 따라서 foo()가 코드를 실행하기 시작할 때 스택 포인터는 foo()를 호출하기 전의 위치에서 48바이트의 오프셋을 갖습니다 (스택 항목의 너비가 4바이트라고 가정).
(5) 보통은 여기에 무엇이 있는지 알 수 없습니다. 다른 태스크의 스택일 수 있으며 변수, 데이터 구조 혹은 응용 프로그램에 사용되는 배열일 수도 있습니다. 그것이 무엇이든 덮어쓰면 이상한 동작이 발생할 수 있습니다. 다른 태스크에서 계산된 값이 예상한 값과 다를 수 있으며 코드에서 잘못된 경로를 결정하도록 하거나 시스템이 정상적인 조건에서는 제대로 작동하지만 그후 실패할 수 있습니다. 이러한 것은 알 수 없으며 예측하기 매우 어렵습니다. 실제로 코드를 변경할 때마다 동작이 변경될 수 있습니다.
누군가 자신의 응용 프로그램이 "이상하게" 작동한다고 말할 때마다 스택 크기가 충분하지 않다는 것을 가장 먼저 떠올릴 수 있습니다.
태스크 스택의 크기는 어떻게 결정합니까?
태스크에 필요한 스택의 크기는 응용 프로그램에 따라 다르지만 다음을 추가하여 필요한 스택 공간을 수동으로 파악할 수 있습니다.
1) 모든 함수 호출 중첩에 필요한 메모리. 각 함수 호출 계층 수준에서,
- 함수 호출의 반환 주소에 대한 하나의 포인터 (CPU 아키텍처에 따라 다름). 일부 CPU는 실제로 해당 목적을 위해 지정된 특수 레지스터(링크 레지스터 (LR)라고도 함)에 반환 주소를 저장합니다. 그러나 함수가 또 다른 함수를 호출하는 경우 호출자가 LR을 저장해야 하므로 LR이 어쨌든 스택에 푸시된다고 가정하는 것이 현명할 수 있습니다.
- 해당 함수 호출에서 전달된 인수에 필요한 메모리. 인수는 흔히 CPU 레지스터에 전달되지만 함수가 다른 함수를 호출하면 레지스터 내용이 어쨌든 스택에 저장됩니다. 따라서 태스크 스택의 크기를 결정하기 위해서는 스택에 인수가 전달된다고 가정하는 것이 좋습니다.
- 해당 함수에 대한 로컬 변수 스토리지
- 함수 내부의 상태 저장 작업을 위한 추가 스택 공간
IAR 링커에는 각 태스크에 필요한 스택 공간의 양을 결정하는 데 도움이 되는 정돈된 함수가 있습니다.
그림 2와 같이 링커 구성에서 확인란을 선택하고 코드를 빌드하고 링크 맵(.MAP 파일)을 검사하여 응용 프로그램에서 각 함수에 대한 호출 스택 깊이(바이트 단위)를 표시하면 됩니다.
그런 다음 각 태스크에 대하여 태스크의 호출 스택 크기를 기록합니다. 최대 스택 크기를 결정하기 위해 몇 개의 숫자를 추가해야 하기 때문입니다.
그림 2 – IAR 링커 구성 옵션
2) 전체 CPU 컨텍스트(CPU에 따라 다름)와 필요시 FPU 레지스터를 위한 스토리지
3) 중첩된 각 ISR에 대하여 또 다른 전체 CPU 컨텍스트의 스토리지 (CPU에 ISR을 처리할 별도의 스택이 없는 경우)
4) 해당 ISR에서 사용되는 로컬 변수에 필요한 스택 공간.
아래의 간단한 방정식은 주어진 태스크에 필요한 총 태스크 스택 크기(바이트 단위)를 결정하는 데 사용할 수 있으며 약간의 여유 공간을 제공하기 위해 33%를 추가합니다.
실제로 대부분의 임베디드 응용 프로그램에서는 런타임 스택 사용량을 70% 표시 아래로 유지하는 것이 바람직합니다. 요구 사항과 필요에 확실히 더 보수적일 수 있습니다.
스택 사용량 계산은 코드의 정확한 경로가 항상 알려져 있다고 가정하지만 항상 가능하지는 않습니다.
특히, printf()와 같은 함수를 호출할 때 printf()에 필요한 스택 공간의 양을 추측하는 것조차 어렵거나 거의 불가능할 수 있습니다. 또한 함수 포인터 테이블을 통한 간접 함수 호출은 문제가 될 수 있습니다.
마지막으로, 이러한 유형의 코드에서는 스택 사용이 일반적으로 비결정적이기 (즉, 결정하기 어렵기) 때문에 재귀함수 코드 작성을 피해야 합니다.
일반적으로 말하자면 상당히 큰 스택 공간에서 시작하여 더 나쁜 상황에서 응용 프로그램을 실행하고 런타임에 스택 사용량을 모니터링합니다.
일부 RTOS(특히 uC/OS-III 및 Cesium/OS3)를 사용하면 런타임에 스택 사용량을 모니터링할 수 있으며 IAR의 C-SPY에 내장된 RTOS 인식 기능을 사용하여 이를 표시할 수 있습니다.
더 자세히 알고 싶습니까?
2부(RTOS 기반 설계에서 스택 오버플로 감지)를 확인하거나 주문형 웨비나(RTOS 기반 응용 프로그램의 더 나은 디버깅을 위한 팁과 힌트)에 접속하십시오.
저자 소개
본 기사는 RTOS 개발 애플리케이션에 관한 시리즈의 일부입니다.
Jean은 풍부한 경험과 임베디드 시스템 시장에 대한 깊은 이해를 바탕으로 Weston Embedded Solutions의 수석 조언자 및 컨설턴트로 재직하고 있으며, 현재 RTOS 제품의 향후 보다 발전된 제안을 마련하는데 기여하고 있습니다. Weston Embedded Solutions는 Micrium 코드베이스에서 파생된 매우 안정적인 Cesium RTOS 제품군의 지원 및 개발을 전문으로 합니다.
Jean.Labrosse@Weston-Embedded.com.