[테크월드=정환용 기자] 펌웨어, 실시간 운영체제(Real Time Operation System, 이하 RTOS) 사용 등의 MCU 제어를 위한 소프트웨어 동작 중 데이터 영역의 메모리를 쓰고 지우면서 많은 활용을 한다. 이때 중요한 메모리 영역 중 하나로 스택(Stack) 영역이 있다. 일반적으로 스택은 후입 선출(Last In, First Out, LIFO) 특성을 가지며 함수의 로컬 변수, 함수 파라미터, 함수 호출을 위한 리턴값, 브랜치 주소 등 동작에 중요한 데이터들이 쓰이고 지워지는 영역이다. 소프트웨어 동작에 반드시 필요한 메모리 영역이기도 하다.

스택 메모리가 중요한 만큼 관리 또한 필요하다. 사이즈를 너무 작게 설정한 경우, 동작 중 스택 메모리 영역을 넘어 사용하게 되면(스택 오버플로우) 동작에 문제가 생기게 된다. 반대로 사이즈를 너무 크게 잡으면 스택 오버플로우 현상은 없지만 다른 데이터가 쓰이는 영역이 줄어들어 메모리 관리에 비효율적이다. 스택 영역의 정적, 동적인 방법으로 검사를 하고 모니터링해 적합한 메모리 크기 설정 등 효과적이고 효율적으로 스택을 사용해야 한다.

 

Stack 사용량 정적 분석
작성된 소프트웨어 빌드 과정 중 링킹 과정에서 정적 분석 방법으로 스택의 최대 사용량을 계산할 수 있다. 일반적으로 컴파일 과정에서 각각 함수들의 최대 스택 사용량 정보는 컴파일러가 만들어낼 수 있다. 이 정보에 각 함수별 콜 그래프와 최대 함수 호출 깊이 등 전체적인 스택 사용의 정보가 있으면, 정확한 스택 사용의 정보를 실제 코드 동작 없이 정적으로 확인할 수 있다. 참고로 콜그래프 정보는 별도 ‘stack usage control’을 작성해 정보를 추가해야 한다.

 

정적 스택 사용량 분석 사용하기

Project Option > Linker > Advenced > Enable stack usage analysis

▲[그림 1]

또한, 정적 분석된 스택 사용량 확인을 위해 링커 맵파일 생성이 반드시 필요하다. 기본적으로 링커 옵션 중 맵파일을 생성하도록 설정돼 있다.

Project Option > Linker > List > Generate linker map file

▲[그림 2]

[그림 2]와 같이 옵션 설정 후 빌드를 하면 링커 맵파일에 스택 분석항목이 추가돼 출력된다.
다음은 간단한 스택의 정적 분석 예시다. 일반적으로 프로그램 엔트리와 인터럽트 핸들러는 다른 함수에 의해 호출되지 않기 때문에, 콜 그래프 루트로 간주된다. [예제 1]에서 최대 스택 깊이는 프로그램 항목 호출 그래프 루트 (__iar_ program_start)의 경우 288바이트, 인터럽트 호출 그래프 루트(__interrupt_170 과 _default_handler)의 경우 전체 120바이트다.

▲[예제 1]
***********************************************************
*** STACK USAGE
***
***********************************************************
*** STACK USAGE
***
Call Graph Root Category      Max Use  Total Use
------------------------ ------- --------
interrupt 120 120
Program entry 288 288
Program entry
“__iar_program_start”: 0xffffb14c
Maximum call chain 288 bytes
“__iar_program_start” 4
“_main” 8
“_printf” 8
“__PrintfFullNoMb” 152
“__LdtobFullNoMb” in xprintffull_nomb.o [4] 80
“__GenldFullNoMb” in xprintffull_nomb.o [4] 36
interrupt
“__interrupt_170”: 0xffffaa22
Maximum call chain 52 bytes
“__interrupt_170” 52
interrupt
“_default_handler”: 0xffff98cb
Maximum call chain 68 bytes
“_default_handler” 52
“_abort” 4
“__exit” 12

 

간접 호출 지정
간접 호출은 함수 포인터를 통해 함수를 호출하는 것을 의미한다. 호출 대상의 함수는 링커에서 알 수 없기 때문에, 링커는 간접 호출에 대한 스택 사용 정보를 자동으로 검색할 수 없다. 다음의 예와 같이 경고 메시지가 링커에서 생성된다.

 

Warning[Lo009]: [stack usage analysis] the program contains at least one indirect call. Example: from “_BSP_IntHandler” in bsp_int.o [1]. A complete list of such functions is in the map file.

 

링커 맵 파일 내용 중 Stack Usage 항목에는 다음과 같이 메시지가 기록된다.

 

The following functions perform unknown indirect calls: “_BSP_IntHandler” in bsp_int.o [1]: 0xffffabd4

 

이 문제를 해결하려면 코드작성에서 #pragma calls 지시문을 사용해 명령문에서 간접적으로 호출할 수 있는 함수를 나열해야 한다. 이 지시문은 간접 호출문 바로 앞에 삽입하고 가능한 모든 호출 수신자 함수의 목록을 지정해야 한다.

예를 들어 [예시 2]의 코드는 함수 UartRxHandler ( ), UartTxHandler( ) 및 UartFaultHandler( )가 함수 포인터 isr ( )을 통해 간접 호출될 수 있음을 지정한다.

 

[예시 2]
void BSP_IntHandler (int int_id) {
void (*isr)(void);
……
if (int_id < BSP_INT_SRC_NBR) {
isr = BSP_IntVectTbl[int_id];
#pragma calls=UartRxHandler,UartTxHandler,UartFaultHandler
isr();
}
……
}

 

콜 그래프 루트 정보 제공
RTOS를 사용하는 멀티태스킹 환경에서, 각 태스크의 루트 기능은 호출 그래프 루트이기도 하다. 링커가 자동으로 식별할 수없는 경우도 있다. 링커는 다른 함수에 의해 호출되지 않았기 때문에, 다음과 같은 경고 메시지를 생성한다.

 

Warning[Lo008]: [stack usage analysis] at least one function appears to be uncalled. Example: “_App_TaskJoy” in app.o [1]. A complete list of uncalled functions is in the map file.

 

링커 맵 파일 내용 중 Stack Usage 항목에는 다음과 같이 메시지가 기록된다.

 

Uncalled function
“_App_TaskJoy” in app.o [1]: 0xffff992c
……
Uncalled function
“_App_TaskLCD” in app.o [1]: 0xffff9988
……
Uncalled function
“_App_TaskButton” in app.o [1]: 0xffff99f6
……

 

이 문제를 해결하려면 [예시 3]과 같이 개발자가 #pragma call_ graph_ root 지시문을 사용해 특정 함수를 호출 그래프 루트로 식별해야 한다.

 

▲[예시 3]
#pragma call_graph_root=”task” // task category
static void App_TaskJoy (void *p_arg)
{ …… }
#pragma call_graph_root=”task” // task category
static void App_TaskLCD (void *p_arg)
{ …… }
#pragma call_graph_root=”task” // task category
static void App_TaskButton (void *p_arg)
{ …… }
#pragma call_graph_root=”interrupt” // interrupt category
void OS_CPU_SysTickHandler (void)
{ …… }
#pragma call_graph_root=”task” // task category
static void App_TaskJoy (void *p_arg)
{ …… }
#pragma call_graph_root=”task” // task category
static void App_TaskLCD (void *p_arg)
{ …… }
#pragma call_graph_root=”task” // task category
static void App_TaskButton (void *p_arg)
{ …… }
#pragma call_graph_root=”interrupt” // interrupt category
void OS_CPU_SysTickHandler (void)
{ …… }

 

실제로, 콜 그래프나 인터럽트 이외의 문자열을 호출 그래프의 루트 카테고리의 이름으로 사용할 수 있다. 컴파일러는 인터럽트와 태스크 함수에 자동으로 콜 그래프 루트 카테고리를 지정한다.

이런 정적인 스택 분석 방법은 애플리케이션을 실행해 보기 전 최대의 스택 사용량을 확인할 수 있다. 때문에 스택의 최적화된 사이즈를 결정하는 데 많은 도움이 될 수 있다. 최적화된 스택의 사이즈 지정으로 메모리의 불필요한 낭비를 줄이고, 스택의 오버플로우 위험으로부터 벗어날 수 있다.

 

동적 스택 사용 확인
정적 스택 사용량 분석은 이론적으로 최대의 스택 사용량을 확인할 수 있다. 그러나 실제 애플리케이션의 동작에서는 실제 스택의 사용량은 다양하게 변경될 수 있다. IAR 임베디드 워크벤치(Embedded Workbench)의 C-SPY 디버거는 응용 프로그램이 실행되기 전에 스택 영역 전체에 특정패턴(예 : 0xCD)을 채울 수 있다. 전체의 스택 영역에서 특정 패턴이 얼마나 남아있는지를 사용해 스택 사용량을 표시한다.

일반적으로 실제 동작에서 확인되는 내용을 바탕으로 스택 사이즈를 정할 수 있다. 하지만 모든 런타임 시나리오를 정확하게 반영하지 못하는 경우를 대비해, 약간의 여유 공간을 확보하는 것이 현명할 수 있다.

Tools > Option 의 IDE Option 설정 창에서 Stack > Enable graphical stack display and stack usage tracking을 선택해, C-SPY 디버거 동작 중 동적 환경에서 스택의 사용량을 그래피컬하게 확인한다.

▲[그림 3]

 

스택 창은 C-SPY 디버거 동작 중 View 메뉴에서 활성화할 수 있다. 실행이 중지 될 때마다 C-SPY는 Stack 창에서 스택 사용에 대한 그래픽 표현을 업데이트한다.

▲[그림 4]

사용량 그래프 바에서 어두운 회색 영역은 사용된 스택 메모리를 나타내고, 밝은 회색 영역은 사용되지 않는 스택 메모리를 나타낸다. 스택 사용량이 IDE 옵션 대화 상자에서 설정할 수 있는 임계값을 초과하면, 그래픽 스택 막대가 빨간색으로 바뀐다. Stack 창을 이용해 실제의 애플리케이션 동작 중 스택의 사용량과 사용의 자세한 사항을 확인하며 디버깅한다.

 

Stack protection (스택 보호)
소프트웨어에서 스택 버퍼 오버플로우는 일반적으로 고정된 길이의 버퍼로 된 데이터 구조 외부 메모리 주소에 값을 쓸 때 발생한다. 그 결과 스택 내 의도하지 않은 데이터 값의 손상이 되고, 심지어 함수의 리턴 주소가 손상돼 프로그램이 잘못 동작하는 경우도 있다(참고로 고의적으로 스택의 데이터 값을 임의로 변경하는 것을 스택 스매싱이라고도 한다).

스택 버퍼의 오버플로우를 방지하는 방법 중 하나로 석탄 광산에 유독가스 발생을 대비해 카나리아를 데리고 들어가는 것과 유사하게, 스택 카나리아를 사용하는 방법이 있다. IAR 임베디드 워크벤치 버전 8.20부터는 이런 스택 보호 기능을 사용할 수 있다.

 

Stack protection(스택 보호) 기능 사용방법
IAR 임베디드 워크벤치 for ARM의 스택 보호 기능은 함수 상황에 따라 필요할 수도, 필요하지 않을 수도 있다. 정의된 지역 변수에 배열이나 배열의 멤버가 포함된 구조체가 있으면 함수에 스택 보호가 필요하다. 또한, 로컬 변수의 주소가 함수 외부에서 사용되는 경우, 해당 함수도 스택 보호가 필요하다.

함수가 스택 보호를 필요로 하는 경우, 배열 변수를 함수 스택 블록에서 가능한 한 높게 배치할 수 있도록 로컬 변수가 정렬된다. 이런 변수 다음에 카나리아 요소가 배치된다. 카나리아는 스택 보호 기능의 사용 시작 시 자동 초기화되며, 초기화 값은 전역 변수 __stack_chk_guard에서 가져온다. 함수 종료 시, 코드는 카나리아 요소가 여전히 초기화 값을 유지하고 있는지 확인한다. 만일 카나리아 값이 변경됐다면 스택에 문제가 생긴 것으로 판단해, __stack_chk_fail 함수를 호출한다. 스택 보호 기능을 사용하기 위해 —stack_protection 컴파일러 옵션을 사용한다.

▲[그림 5]

Application 프로젝트에서의 작업
스택 보호기능을 사용하기 위해 애플리케이션 프로젝트에서 다음과 같은 작업이 필요하다.

 

extern uint32_t __stack_chk_guard

 

전역 변수 __stack_chk_guard는 처음 사용하기 전에 초기화해야 한다. 초기화 값이 무작위로 지정되면 보다 안전하다.

 

__interwork __nounwind __noreturn void __stack_chk_fail(void)

 

__stack_chk_fail 함수의 목적은 스택에 발생한 문제에 대해 알리고 응용 프로그램을 종료하는 것이다. 이 함수의 반환 주소는 스택 문제 발생의 함수를 가리킨다. (프로그램 설치경로)armsrclibruntime 디렉토리의 stack_protection.c 파일을 __stack_chk_guard, __stack_chk_fail의 템플릿으로 사용할 수 있다.

 

맺음말
스택 메모리는 애플리케이션 동작에 매우 중요한 정보들이 저장되는 메모리다. 스택 오버플로우, 잘못된 메모리 접근으로 스택 메모리의 잘못된 수정 등 스택 메모리 사용의 문제가 생긴다면, 애플리케이션은 더 이상 정상적으로 동작할 수 없다. 정적, 동적인 방법 그리고 의도하지 않은 문제 검출을 위항 스택 보호 기능으로 스택을 분석, 모니터링해 최적화된 스택 사이즈 정의와 문제없는 애플리케이션을 만들기 바란다.

 

작성: IAR Systems 이현도 과장

이 기사를 공유합니다
저작권자 © 테크월드뉴스 무단전재 및 재배포 금지