멘토 그래픽스



치료보다 예방:  컴파일러 런타임 오류 어떻게 잡나

컴파일러는 일반적으로 상위 레벨 언어를 어셈블러나 기계어로 변환시켜주는 도구로 간주된다. 물론 컴파일러가 그러한 기능을 수행하나, 그것들은 더 많은 잠재력들을 가지고 있다. 코드들은 컴파일러에 의해 생성되거나, 사용자가 작성한 코드의 런-타임(run-time) 오류를 발견하는 성능을 추가해주는 런-타임 라이브러리로부터 추출할 수 있다. 이것은 여러 소프트웨어 개발에서 유용하나, 종종 오류-성공 에러를 조절하는 시스템을 필요로 하는 임베디드 시스템에서는 핵심적인 기능이다. 이 글은 현재 사용되고 있는 많은 오류 검출 및 추적기술을 다루고 이런 기술들이 현재의 컴파일러에 어떻게 적용될 것인지를 서술한다.

글: Ville-Veikko Helppi, 멘토 그래픽스.
www.mentor.com


에러는 피할 수 없는 것

소프트웨어는 당연히 오류들을 가지고 있으며, 오류는 중요한 소프트웨어를 작성하는데 불가피하게 나타난다.
프로그램이 크면 클수록 에러들은 더욱더 많아 질 것이고, 더욱더 다루기 힘들게 될 것이다.
이런 불가피함을 인식하고 개발과정에서 제작자에게 알려주는 것이 목적에 적합한 소프트웨어 개발에 있어 핵심 기술이다. 첫 번째 종류의 에러는 문법이다.
흔한 실수가 문법에 맞지 않는 코드를 작성하는 것인데 예를 들면

X = Y Z;

이것은 사소한 것으로 컴파일러는 에러를 발견하여 쉽게 해결할 수 있다.
코드가 문법적으로 옳지만 프로그램 작성자의 의도를 실제로 표현하지 못할 때 더욱 미묘해지는데 예를 들면,
if ( X = Y )
Z = 99;

이것은 전체적으로 문제가 없다. Y 의 값이 X에 하달되고, 만약 수가 0(zero)가 아니라면 Z값은 99가 설정될 것이다. 프로그램 작성자의 의도는 아마도 X 와 Y 값을 평가하고, 그 값이 같을 경우에 Z의 값을 설정하는 것이다. 에러는 비교연산자 (==) 대신에 할당 연산자 (=)를 사용한 것이다.
아마 이러한 에러를 겪어 보지 않은 C프로그램 작성자는 없을 것이다. 다행히 지금의 많은 컴파일러들은 경고를 보여주고 아래와 같이 수정하거나 재 작성할 것을 권해준다.

X = Y
if (X !=0)
Z = 99;

이것은 명확하고 모호하지 않으며, 초기 버전으로써 가장 이상적인 코드를 작성해준다. 다른 에러들은 소프트웨어의 논리적 문제점들을 보여 줄 것이다.
즉, 프로그램이 의도한 데로 동작하지 않는 것인데 일반적으로 이러한 것들이 디버깅과 검사가 소프트웨어 개발에 들이는 노력 중 큰 비중을 차지하는 이유들이다.많은 논리적 에러들이 특정 응용 프로그램에 있기에, 그것을 찾기 위한 컴파일러로부터 도움을 기대하는 것은 비현실적인 것이다. 그러나, 논리적인 에러가 인식할 수 있을 정도로 거의 치명적일 때, 컴파일러가 도울 수 있으며, RTC(run-time-checking)는 가장 유용한 활용 방법이다.

프로그래밍 언어와 임베디드 코드

피상적으로, 프로그램이 내장된 시스템들과 데스크톱이 개발된 방법 사이에 아주 큰 유사점이 있다. 예를 들면, 거의 비슷한 프로그래밍 언어인 C언어와 C 언어가 일반적일 것이다. 버그를 처리하는 태도는 다음의 두 가지가 다를 수 있다. 데스크톱 소프트웨어는 일반적으로 알려진 버그들을 가진 채로 판매된다.
이러한 것들은 기능적인 제한 또는 성능의 저하를 초래할 수도 있다. 종종 이러한 버그들은 소프트웨어의 오류가 중요한 장애를 유발하지 않을 것 같아서 수용 가능한 것으로 간주되기도 한다.
제품의 초기 버전은 제품의 업데이트 및 패치로 효과적으로 쉽게 배포되어 용납되기도 한다. 하지만 임베디드 소프트웨어에서, 버그를 대하는 태도는 세 가지 핵심 측면에서 다르다.

* 많은 임베디드 응용장치들은 위험하거나 비용이 많이 들 수 있는 오작동이 걱정거리다.
* 임베디드 응용장치들을 위한 소프트웨어 업데이트 배포는 매우 편리하거나 경제적이다.
* 임베디드 시스템은 재설정 없이 오랜 기간 동안 실행되어야만 하기에 오랜 실행 시간동안 쌍인 중요성들이 치명적이 될 가능성이 있다.

서로 다른 프로그래밍 언어들은 여러 다른 방법으로 버그들을 처리하도록 설계되었다. C 언어와 C은 임베디드 애플리케이션을 위해 고안되지 않았기에 전통적인 시행과정에서 런타임 오류를 다루는 것을 불필요한 노력으로 나타내며 완전히 무시하는 경향이 있다.
반면에, Ada 언어는 정확하게 런타임 오류 처리를 필요로 하는 목적으로 고안 되었다. 그러나 이것은 값이 비싸다. C은 에러들을 예외적으로 처리할 수 있는 유용한 에러 처리 기능을 가지고 있다. 이것은 유용하게 사용될 수 있고, 실제 응용장치에 근거한 에러 체크를 위해 기반 되었다. 실행 세부 사항을 보기 전에 RTC의 적합성과 추가되는 노력의 중요성이 인식되어야 한다.

RTC의 능력들

런타임 검사의 핵심은 컴파일러에 의해 추가 되거나 런타임 라이브러리로부터 추출될 수 있는 약간의 특별한 코드이다. 이러한 부가적인 코드는 즉각적으로 눈에 보이지 않고, 소스 코드를 변경시킬 수 없다. 필요할 경우 여러 가지 검사를 수행하기 위해 추가된 단순한 부가적인 이진수의 지시 사항들이 있다.
여분의 코드 실행은 더 많은 시간이 소요되고, 응용장치의 성능은 저하될 것이다. 추가적인 지침은 또한 응용장치의 전반적인 코드의 양을 증가시킨다.
여기에 추가되는 노력이 RTC의 매력을 떨어뜨리고 내장된 응용장치들은 거의 항상 모든 자원을 소모한다. 그러나 그것들은 선택 사항이다. 하나 이상의 컴파일러 스위치들/옵션들의 활성화와/비활성하게 하는 RTC의 기능이 있어서 RTC가 필요로 할 때, 선택적으로 추가하거나 뺄 수 있다.

RTC의 기능들

다른 임베디드 컴파일러 툴킷은 다양한 런타임 검사 기능을 제공한다. 약간의 이러한 RTC 기능은 다음 사항들을 포함하고 있다.

* 0으로 나누기
수학적으로, 0으로 나누는 것은 의미가 없다. 그래서 컴퓨터에서, 그러한 시도의 결과는 불확실하다. 만약 0으로 나누는 것을 수행하는 하드웨어가 있다면 일반적으로 예외로 처리될 것이다. 하드웨어의 지원 없이 라이브러리 작업에 의한 수행된 나누기는 일반적으로 사용자 에러 함수를 호출한다. 이 실험은 최소한의 추가 노력이 필요하며, 거의 선택되지 않을 것이다.

* 널 포인터 디레퍼런스(NULL pointer dereference)
주소 0의 메모리 위치가 일반적으로 "특별"하기 때문에, 관례적으로 이 값은 사용되지 않는 포인터를 명시하기 위해 사용된다.
만약 코드가 NULL 포인터의 값의 조회를 시도하면, 명확하게 논리적 에러로 발생하고 이 상황은 검출될 필요가 있다. 일부 CPU와 메모리 처리장치(MMU)들이 메모리 주소 0을 조회하기 위해 사용된다.
하지만 대부분의 장비에서 각각의 포인터 값을 조회하기 전에 에러 루틴 즉 포인터가 NULL일 경우를 테스트하는 런타임 체킹이 필요하다.

* 배열 범위 확인
C언어와 C 언어에서, 크기 n의 배열은 0부터 n-1까지의 값으로 색인된다. 인덱스의 어떤 다른 값은 배열 범위 밖에서 메모리를 참조할 것이며 이 값을 찾는 것은 단순해 보인다. 예를 들면

int x, n, arr[10];

n = 10;
x = arr[n];

최고 상위레벨 인덱스 arr은 9가 되기 때문에 x에 이런 할당은 당연히 문제를 초래할 것이다. 인덱스로 사용하기 전에 n값을 검사하는 여분의 코드를 포함시키면 이런 것들은 컴파일러에게 단순한 문제로 될 것이다.
그러나 C언어는 매우 융통성 있고 항상 배열 크기는 항상 컴파일러에게 알려지지 않을 것이다. (모듈에 전달되는 변수 혹은 기능 인자로 전달되어질 경우). 이러한 제한은 이런 종류의 테스트를 불가능하게 한다. 게다가 더욱이, C언어에 있어 배열 이름은 단순히 연속된 포인터이다. 다음과 같은 코드로 표현될 수는 있다.

int x, n, *ptr, arr[10];

ptr = arr;
n= 10;
x = *(ptr n) ;

이 상황에서 런타임 검사 코드를 작성하는 것은 불가능할 것이다.

* Stack Overflow (및 Underflow)
스택을 위한 정확한 메모리의 할당은 정말로 어렵다. 왜냐하면 코드의 실행 없이, 필요한 스택의 양을  예상한다는 것은 불가능하기 때문이다.
스택 사용의 모니터링에 대한 일반적인 접근은 스택에 할당된 공간의 양 끝에 "단어 보호"를 배정하는 것이다. 이러한 단어들은 정기적으로 모니터링 하는 고유한 값으로 초기화 된다. 값의 변화는 오버플로 또는 언더플러 스택을 나타낸다. 이러한 접근은 대부분의 경우, 운영 체제의 사용과 정기적인 검사가 주기적으로 각 작업에 대한 스택의 완전성 검사를 실행할 때 적용되어진다.
컴파일러는 또한 스택을 검사하는 기능을 추가할 수 있다. 이 코드는 그 포인터가 입력 범위 내에서 스택을 검사하기 위한 함수가 들어올 때마다 호출된다.

* 스위치 스테이트먼트(switch Statement)
C언어와 C에서, 스위치 스테이트먼트는 아주 복잡하고 논리적 결정을 해야 하는 것들을 다양하고 효과적인 방법으로 구현할 수 있다.
조건문이 특정 조건과 맞지 않는다면 default가 선택되도록 기술하는 것이 종종 좋은 경우가 된다.
어떤 경우에는 일치되는 값이 없어 어떤 동작도 필요 없을 때 동작이 없는 default 값이 아마도 최선의 방법이 될 것이다.
그러나 몇몇 프로그램 작성자들은 이 접근방법을 선택하지 않는데 이런 이류로 많은 컴파일러들이 일치되는 값이 없을 때 런타임 오류를 검사하는 기능을 제공한다.

* 초기화되지 않은 변수 사용
비록 컴파일러는 지속적으로 사용되지 않는 변수 선언들을 쉽게 검색할 수 있지만, 이것은 아래 예와 같이 초기화 되지 않은 변수 사용에 있어서 잠재적인 문제점이 있다.

int x, y ;

. . .
y = f() ;
if ( y == 0 )
x = 0 ;
else
x ;

x가 할당되기 전에 특정 지점에서 수의 증가가 발생될 수 있다. 런타임 검사는 이것을 수용하기 위해 포함될 수 있다. 변수에 값이 할당되거나 변수의 값이 이용될 때 각 변수와 관련된 신호가 설정될 수 있다.
만일 변수에 대한 접근이 이루어 져도 신호가 설정되지 않는다면 오류를 표시한다. 분명히 이러한 검사는 메모리 오버헤드로 나타날 것이다.

* 할당 범위
모든 정수 변수 형태들은 그들의 용량에 따라 여러 종류의 제한이 있다. char, int 또는 long의 실제 크기는 컴파일러 마다 다양하다. 또한 임의의 비트 필드 변수 크기는 내부 구조에 의해 정해질 것이다. 런타임 검사는 허용 범위 내에서 값이 변수에 할당되는 것을 검사할 수 있다.

* Heap 확인
Heap은 C언어에서 malloc()과 free() 그리고 C에서 new와 delete 명령어의 사용으로 동적 할당을 위한 메모리 사용 영역이다.
가능한 런타임 검사는 메모리 부족 감지와 메모리 블록 일관성 검사를 포함한 HEAP 검사를 할 수 있다.

* Dangling 포인터들
동적 메모리 사용 검사 시에, 보다 정교한 기능은 메모리 블록을 할당하는 포인터를 추적하는 기능이다. 만약 블록 할당이 해제된 후에 그런 포인터가 이용된다면 결과는 에러이다.

* 메모리 부족 검사
또 다른 동적 메모리 검사는 효과적인 메모리 부족을 알리는 것을 목표로 한다. 다시 할당된 블록의 포인터들이 추적된다. 만약 그들의 값들이 변한다면 메모리 블록이 현재 "고립" 에러를 야기할 수 있는 마지막으로 유지되던 것이 바뀔 때까지 문제는 발생하지 않을 것이다.

* 부동 소수점 연산 오류
수 많은 에러들은 부동 소수점 연산중에 발생할 수 있다. 이것들은 일반적으로 무한대 또는 수가 아닌(NaN) 것으로 표현 되는 값의 사용 시도와 관련이 있다.
만약 하드웨어를 사용하여 부동 소수점을 사용한 경우, CPU의 예외가 에러 검출의 결과가 발생할 것이다. 소프트웨어 라이브러리는 일상적인 에러처리 작업에 들어간다.

RTC를 사용할 때

런타임 에러 검사는 광범위한 범위의 오류를 검출할 수 있는 잠재성이 있기 때문에, 개발 단계에 사용할만한 가치가 있다. 여러 미묘한 버그들 중에 코딩의 초기 단계에 나타날 수 있고, 초기에 버그를 찾는 것은 나중에 시간을 절약할 수 있다. 메모리 부족은 좋은 예이다.
확실한 확인 없이 그런 에러들은 예고 없이 나타나고 자원의(메모리) 부족이 발생하기 까지는 다른 부작용들은 분명하지 않다. 런타임 검사의 모든 장점을 이용하여 기능과 모듈의 정밀한 단위 검사를 수행하는 것이 좋은 전략이다.
이것은 시간이 걸리나 소프트웨어를 공정에 통합할 때 큰 효율을 구할 수 있다.


표 1. 이용 가능한 RTC 기능의 비교


실제 컴파일러

지금까지 우리는 임베디드 컴파일러에 적용되는 run-time 검사 기능의 범위를 보았다. 각 제품마다 제공되는 정확한 세부 기능은 공급업체로부터 직접 구할 수 있다.
실제 컴파일러의 몇 가지 예를 보는 것과 사용 가능한 RTC 기능을 비교하는 것은 유용하다. <표 1>의 비교에서, 4 가지 컴파일러를 조사했다.

* A는 Mentor Graphics EDGE C/C compiler for ARM
* B는 널리 사용되는 공개 소스 컴파일러이다.
* C와 D는 다른 상업적으로 이용할 수 있는 임베디드 컴파일러

결론

이 문서는 임베디드 컴파일러를 구매한 구매자의 지침서가 되는 것을 의도하지 않았고, 당신의 다음 임베디드 프로젝트를 위해 소프트웨어를 살펴볼 때 요구되는 질문들에 대해 당신이 준비할 수 있도록 의도하였다.
당신의 임베디드 소프트웨어의 최대 품질과 안정성을 위해 전체 개발 과정에서 런타임 검사의 사용을 주의 깊게 고려할 필요가 있다.


>>> 저자소개
Vile-Veikko Helppi 는 멘토 그래픽사에서 임베디드 부문의 제품 마케팅 매니저이다. 그는 임베디드 컴파일러 제품 라인과 시뮬레이션/프로토타이핑을 책임지고 있다. Ville-Veikko는 핀란드의 Oulu 대학의 전기 공학(임베디드 시스템)석사와 경제 및 경영학 석사를 마쳤고 임베디드 소프트웨어 산업에서 10년의 경험을 가지고 있다.
이 기사를 공유합니다
저작권자 © 테크월드뉴스 무단전재 및 재배포 금지