RTOS 기반 설계에서 스택 오버플로 감지 - 2부
이 글은 RTOS 전문가 Jean J. Labrosse가 작성했습니다.
스택 오버플로를 감지하는 데 사용할 수 있는 많은 기술이 있습니다. 일부는 하드웨어를 사용하고 일부는 전적으로 소프트웨어에서 수행됩니다. 곧 살펴보겠지만, 하드웨어에 기능이 있는 것이 훨씬 더 나은데, 이는 스택 오버플로가 발생하는 즉시 감지할 수 있고 실제로 잘못된 액세스에 대한 쓰기가 하드웨어에 의해 사실상 방지되기 때문에 스택 오버플로를 피할 수 있기 때문입니다.
하드웨어 스택 오버플로 감지 메커니즘은 일반적으로 예외 처리기를 트리거합니다. 예외 처리기는 일반적으로 현재 PC(프로그램 카운터) 및 가능한 다른 CPU 레지스터를 현재 태스크의 스택에 저장합니다.
물론 스택 외부의 데이터에 액세스하려고 시도할 때 예외가 발생하기 때문에 처리기가 오버플로된 스택의 기준을 넘어서는 RAM이 있다고 가정하고 응용 프로그램의 일부 변수 또는 다른 스택을 덮어씁니다.
대부분의 경우 응용 프로그램 개발자는 스택 오버플로 조건에 대해 무엇을 해야 할지 결정해야 합니다. 예외 처리기가 임베디드 시스템을 기지의 안전한 상태로 두고 CPU를 재설정해야 할까요, 아니면 아무것도 하지 않아야 할까요?
CPU를 재설정하기로 결정한다면 오버플로가 발생했다는 사실을 저장하는 방법과 어떤 태스크가 오버플로를 유발했는지 알아낼 수 있으므로, 재설정 시 사용자에게 알릴 수 있습니다.
기술 1: 스택 한계 레지스터의 사용
일부 프로세서(유감스럽게도 극히 일부)에는 단순하지만 매우 효과적인 스택 포인터 오버플로 감지 레지스터가 있습니다. 이 기능은 ARMv8-M CPU 아키텍처 기반의 프로세서에서 사용할 수 있습니다. CPU의 스택 포인터가 이 레지스터(SP_LIMIT 레지스터라고 함)에 설정된 값 아래로 내려가면 (또는 스택 증가에 따라 위로 올라가면) 예외가 생성됩니다. 그림 3은 이것이 어떻게 작동하는지 보여줍니다.
그림 3 – 스택 한계 레지스터를 통한 스택 오버플로 감지
(1) SP_LIMIT 레지스터는 태스크가 전환될 때 RTOS의 컨텍스트 스위치 코드로 로드됩니다.
(2) SP_Limit이 가리키는 위치는 스택의 맨 아래이거나, 바람직하게는 예외 처리기가 예외를 처리하기 위해 문제가 되는 스택에 충분한 레지스터를 저장할 수 있는 충분한 공간을 가질 수 있는 위치일 수 있습니다.
(3) 스택이 증가함에 따라 SP 레지스터가 SP_Limit 아래로 내려가면 예외가 생성됩니다. SP 레지스터를 변경하려는 시도는 거부되어 응용 프로그램 코드를 유효한 스택 내에 유지합니다.
참고로 µC/OS-III 및 Cesium/OS3는 처음부터 스택 한계 레지스터가 있는 CPU를 지원하도록 설계되었지만 당시에는 그러한 기능을 지원하는 프로세서가 Infineon 80C166/167뿐이었습니다.
각 태스크에는 SP_Limit에 로드할 고유 값이 포함되어 있으며 이 값은 TCB(태스크 제어 블록)에 배치됩니다. 이 기능을 계획하지 않은 RTOS는 대부분 RTOS 개발자가 업그레이드해야 합니다.
CPU의 스택 오버플로 감지 하드웨어에서 사용되는 SP_Limit 레지스터의 값은 RTOS가 컨텍스트 스위치를 수행할 때마다 변경되어야 합니다. 이를 수행하기 위한 연속적인 이벤트는 다음 순서로 수행해야 합니다.
1) SP_Limit을 0으로 설정합니다. 이렇게 하면 SP 레지스터가 SP_Limit 아래로 내려가지 않습니다. 여기서는 스택이 높은 메모리에서 낮은 메모리로 증가하는 것으로 가정했지만 스택이 반대 방향으로 증가해도 개념이 비슷하게 작동합니다.
2) SP를 로드합니다.
3) TCB에서 새로운 태스크에 속하는 SP_LIMIT 값을 가져옵니다. SP_LIMIT 레지스터를 이 값으로 설정합니다.
기술 2: MPU의 사용
현재의 많은 프로세서에는 일반적으로 어드레스 버스를 모니터링하여 코드가 특정 메모리 위치 또는 I/O 포트에 액세스할 수 있는지 확인하는 MPU(메모리 보호 장치)가 장착되어 있습니다. MPU는 사용하기에 비교적 간단한 장치이지만 설정이 다소 복잡합니다.
그러나 스택 오버플로를 감지하기만 원한다면 많은 초기화 코드 없이 MPU를 잘 사용할 수 있습니다. MPU가 이미 칩에 있다면 추가 비용 없이 사용할 수 있으므로, 사용하지 않을 이유가 없습니다. 이어지는 논의에서는 MPU 영역을 설정할 것입니다. 즉, "만약 이 스택 영역(즉, 영역) 밖에 쓰기를 한다면 MPU가 예외를 생성할 것"입니다.
물론 다른 위치에 RAM이 있다면 해당 RAM에 액세스할 수 있는 권한을 제공하기 위해 다른 MPU 영역을 설정해야 합니다. 논의를 위해, ARMv7M 아키텍처의 Cortex-M이 사용되고 있으며 일반적으로 8-영역 MPU가 장착되어 있다고 가정합니다.
이 경우 전체 스택 주위에 MPU 영역을 배치할 수 있습니다. ARMv7M에서 스택 크기는 2의 거듭제곱(최소 32바이트)이어야 하고 동일한 2의 거듭제곱 기준 어드레스에 맞춰야 합니다. 즉, 스택은 크기가 32, 64, 128, 256, 512, ... 바이트여야 하고, 각각 0x??????E0, 0x??????C0, 0x??????80, 0x??????00, 0x?????E00 …에 맞춰야 합니다.
RMv8-M 아키텍처 기반 프로세서에서는 이러한 제한이 제거되었으며 MPU 영역 크기 단위는 32바이트이고 크기는 32바이트의 배수가 될 수 있으며 영역은 32바이트 경계에 맞춰야 합니다. 또한 일반적인 ARMv8M CPU에는 16-영역 MPU가 장착되어 있어 제어력이 향상됩니다.
스택을 설정하는 한 가지 방법은 그림 4와 같이 RAM의 베이스에서 스택을 시작하여 모든 스택을 인접한 메모리에 함께 위치시키는 것입니다.
이를 수행하는 방법은 이 문서의 범위를 벗어나지만 IAR 링커를 사용하면 쉽게 할 수 있습니다.
그림 4 – 지속적인 태스크 스택 찾기
RTOS 컨텍스트가 태스크 사이에서 전환함에 따라 그림 5와 같이 단일 MPU '보호 창'을 이동합니다. 즉, 태스크 전환이 발생하면 RTOS가 MPU를 다시 프로그래밍하여 해당 영역 중 하나를 새 태스크의 스택(빨간색 칸) 주위에 배치합니다.
ARMv7M(및 ARMv8M)에서는 이 영역이 다른 영역을 대체하기 때문에 (MPU의 다른 영역을 사용한다고 가정) 마지막 영역(즉, 8 영역 MPU의 영역 #7)을 사용하는 것이 좋습니다.
이 방법의 제한 사항 중 하나는 다른 태스크가 자체 영역 밖의 메모리에 액세스해야 하기 때문에 태스크 스택에 버퍼를 할당하고 해당 버퍼에 대한 포인터를 다른 태스크에 전달할 수 없다는 것입니다.
그러나 태스크의 스택에 버퍼를 할당하는 것은 어쨌든 좋은 방법이 아니며, MPU 위반으로 인한 고충은 그나마 나은 편입니다.
그림 5 – 컨텍스트 스위치 동안의 MPU 영역 이동
기술 3: 하드웨어 기반의 레드존
MPU를 사용하는 또 다른 방법은 실행 중인 태스크의 스택 영역 외부에 영역을 배치하는 것입니다. MPU 영역 내에서 쓰기가 발생하면 MPU가 예외를 트리거합니다.
이 경우 MPU가 이전 방법과 다르게 구성되어, 코드가 영역 외부에 작성할 때 예외를 생성하는 대신 그림 6과 같이 코드가 영역 내부에 작성할 때 위반이 발생합니다.
그림 6 – MPU 영역을 사용하여 스택 영역 외부의 쓰기 포착
이 방법은 태스크 스택에서 큰 배열이나 구조의 할당을 포착하기 위해 영역을 정말 크게 만들지 않는 한 이전 방법만큼 안정적이지 않습니다.
다른 문제는 태스크에 액세스해야 하는 메모리와 겹치도록 레드존을 배치하지 않아야 한다는 것입니다.
가장 큰 장점은 태스크의 스택 크기를 2의 거듭제곱으로 설정할 필요가 없다는 것입니다. 대부분의 경우 128~256바이트의 레드존으로 충분합니다.
기술 4: 소프트웨어 기반의 레드존
MPU가 없거나 MPU를 사용하고 싶지 않은 경우 소프트웨어 기반 레드존을 고려할 수 있습니다. 그러나 이 방법은 사용 중인 RTOS에서 지원해야 하며, 그렇지 않은 경우 직접 구현해야 할 수도 있습니다.
이 경우 레드존은 그림 7과 같이 스택의 맨 아래에서 스택 영역 내부에 배치됩니다.
레드존의 크기는 위험에 대한 선호도, 오버헤드, 이 레드존에 대한 각 스택에 할당하려는 RAM의 양에 따라 다릅니다. 태스크 초기화 동안 RTOS는 0xABCDEF01(다른 것일 수 있음)과 같이 알려진 패턴을 레드존에 배치합니다.
컨텍스트 스위치 동안 RTOS는 레드존 위치가 변경되었는지 확인하고 변경된 경우 수정 조치를 취하는 함수를 호출합니다.
그림 7 – 컨텍스트 스위치에서 RTOS에 의한 레드존 확인
상상할 수 있듯이 이것은 사실 확인 후의 일입니다. 다시 말해 피해는 이미 발생했을 것이고 예방할 수 없었을 것입니다.
태스크 스택의 사용 가능 영역이 512바이트이고 레드존이 모두 128바이트라면 640바이트를 할당해야 하므로 할당된 RAM의 효율성은 80%에 불과합니다!
레드존을 작게 만들면 이 방법을 사용하여 오버플로를 감지하기 위해 함수 입력 시 로컬 변수, 큰 배열 또는 데이터 구조를 초기화하는 것이 매우 중요합니다.
소프트웨어 레드존은 모든 CPU 아키텍처에서 이식 가능하기 때문에 좋습니다. 그러나 단점은 RAM뿐만 아니라 컨텍스트 스위치 중에 귀중한 CPU 사이클을 소모할 수 있다는 것입니다.
기술 5: 컨텍스트 스위치 동안의 SP 레지스터 검사
FreeRTOS는 컨텍스트 스위치 중에 스택 포인터의 값을 검사하는 기술을 사용합니다. 그 값이 스택의 기준 어드레스보다 작으면 RTOS는 태스크가 스택 외부에 무언가를 썼다는 것을 알게 됩니다.
이것은 확실히 가장 최악의 방법인데, 그것은 스택 포인터가 스택 외부에 있는 정확한 순간에 코드가 선점되어야 하기 때문입니다. 이것은 너무 늦을 수 있으며 예기치 않은 손상이 발생할 수 있습니다.
그러나 스택 기준 어드레스 대신 소프트웨어 기반 스택 한계를 사용하고 이에 대해 테스트한다면 태스크가 손상을 입히기 전에 스택 포인터를 잡을 수 있는 더 좋은 기회가 생깁니다.
더 자세히 알고 싶으신가요?
1부(RTOS 기반 설계에서 스택 오버플로)를 확인하거나 주문형 웨비나(RTOS 기반 응용 프로그램의 더 나은 디버깅을 위한 팁과 힌트)에 접속하십시오.
저자 소개
본 기사는 RTOS 개발 애플리케이션에 관한 시리즈의 일부입니다.
Jean Labrose는 높은 인지도를 가진 uC/OS-II와 uC/OS-III 커널의 저자이자 Micrium의 설립자로, 임베디드 소프트웨어의 uC/라인의 발전에 적극적으로 관여하고 있습니다.
Jean은 풍부한 경험과 임베디드 시스템 시장에 대한 깊은 이해를 바탕으로 Weston Embedded Solutions의 수석 조언자 및 컨설턴트로 재직하고 있으며, 현재 RTOS 제품의 향후 보다 발전된 제안을 마련하는데 기여하고 있습니다. Weston Embedded Solutions는 Micrium 코드베이스에서 파생된 매우 안정적인 Cesium RTOS 제품군의 지원 및 개발을 전문으로 합니다.
Jean.Labrosse@Weston-Embedded.com.