반응형

제목: 펌웨어 결함 찾기(Finding Firmware Defects)

저자: Sean Beatty, High Impact Services, Inc.

문서유형: 기술문서( 5페이지), 2002


임베디드 시스템 소프트웨어(펌웨어라고도 불림)의 특성이 어떻게 소프트웨어 테스팅 프로세스에 영향을 미치는지 설명하고, 세 가지의 유형의 펌웨어 결함 발견 활동을 기술한 자료



임베디드 시스템의 특징

임베디드 시스템은 타 소프트웨어 기반 애플리케이션과는 다른 많은 특징을 가진다.

 

임베디드 시스템은 범용 컴퓨팅과 대조적으로 매우 특정한 계산을 수행하는 경향이 있다.

예를 들어, 데스크탑 애플리케이션이 부동 소수점 계산(floating-point math)을 사용하는 반면 많은 임베디드 시스템이 고정 소수점 계산(fixed-point math)을 사용함. 이게 계산 속도를 높이고 임베디드 마이크로프로세서의 고정 메모리 리소스를 적게 요구하는 반면, 계산 프로그래밍을 훨씬 더 복잡하게 만들어서 에러 가능성을 높이는 결과로 이어질 수 있음

 

임베디드 시스템은 에러를 처리하는데 있어서 제약을 가진다.

에러가 발생했습니다…” 같은 메시지가 써진 다이얼로그 박스를 디스플레이 할 수 있는 경우가 드물다. 임베디드 시스템이 지속적으로 필요로 되는 경향이 있기 때문에, 중요 에러를 마주쳤을 때 최선책이 종종 마이크로프로세서를 단순 리셋하는 것인데, 이게 많은 부분에 큰 영향을 끼침. 에러에 대응하고 시정 조치를 취할 수 있는 능력이 제한되기 때문에 임베디드 시스템에서는 에러가 더 심각해짐

 

임베디드 시스템은 하드웨어와 밀접하게 연관되는 특징을 가진다.

시스템이 운영되고 있는 환경에 대한 데이터를 얻는데 센서가 사용됨. 센서로부터 오는 데이터가 환경에 있는 노이즈에 의해 오염될 수도 있으며, 이렇게 오염된 데이터가 알고리즘으로 입력되면 그로 인한 문제가 야기될 수 있음. 따라서 센서 데이터가 노이즈를 제거해야 한다는 조건이 종종 붙게 됨

 

PC 애플리케이션 프로그램은 컴퓨터가 부팅된 후에야 비로소 시작이 되는 반면 임베디드 시스템 코드는 파워가 시스템에 들어온 순간부터 그것이 차단된 순간까지 모든걸 처리해야만 한다.

게다가 많은 전지식(battery powered) 시스템이 배터리 수명을 연장하기 위해서 저전력 모드에 들어가게 되는데, 이렇게 다양한 파워 모드에 들어가고 나가는 전이(transitions)가 펌웨어에 의해 관리됨. 이로 인해 추가된 복잡도는 잠재 에러의 또 다른 원천지가 됨(특히, 드물게 실행되는 모드 변경에서 에러 가능성이 높음)

 

치명적 에러로부터 복구하기 위해 대부분의 임베디드 시스템이 워치도그 타이머를 사용한다.

만약 이 타이머가 지정된 시간 내에 액세스되지 않으면 마이크로프로세서가 리셋됨(, 시스템의 모든 올바른 운영 모드에서는 워치도그 타이머가 그 시간이 소진되기 전에 반드시 액세스 되어야 함을 의미). 만약 완료하는데 예상 보다 오래 걸리는 흔치 않은 이벤트 시퀀스가 발생하면 워치도그가 타임아웃되고 시스템이 강제로 리셋되게 되는데, 이 경우 시스템 신뢰성을 개선하고자 의도했던 기기가 오히려 심각한 에러의 원천지가 되어 버림

 

대부분의 임베디드 시스템이 가상 메모리(virtual memory)를 가지지 않는다.

만약 중요 메모리 리소스가 고갈되면 시스템이 크래시되거나, 리셋되거나, 또는 예상 못한 방식으로 실패하게 될 수도 있음. 특히 가능한 최악의 상황을 대비한 충분한 스택 공간이 반드시 할당되어야 함. 어떤 코드 경로가 스택 공간을 가장 많이 소비하는지가 보통 분명하지 않으며, 3자 라이브러리가 사용되는 경우라면 그것들이 얼마나 스택을 필요로 하는지도 반드시 고려해야 함. 최악 경우 스택 깊이를 정확하게 결정하고 이를 위한 충분한 메모리를 할당하지 못하면 특정 조건 하에서 시스템이 실패하는 결과를 낳음(게다가 실패를 야기하는 이 조건을 재현하기도 종종 어려움)



임베디드 시스템의 결함 타입

모든 종류의 컴퓨터 소프트웨어에서 공통적으로 볼 수 있는 소프트웨어 결함 타입으로 아래와 같은 것들이 있다.

  • 요구사항과 설계에서의 에러, 누락, 모호함
  • 로직, 수학적 프로세싱, 알고리즘에 있어서의 에러
  • 소프트웨어 제어 흐름(브랜치, 루프 등)에서의 문제
  • 부정확한 데이터, 잘못된 데이터 상에서 운용, 데이터에 민감한 에러
  • 초기화와 모드 변경 문제
  • 프로그램의 다른 부분(서브루틴, 글로벌 데이터 등)과의 인터페이스에서의 문제

 

실시간 임베디드 시스템은 위의 모든 결함 타입에 더하여 아래와 같은 잠재적인 문제들을 가진다.

  • 필요한 오퍼레이션을 적시에 수행하는 것이 마이크로프로세서의 능력을 넘어섬
  • 워치도그 타이머에 의해 야기된 불필요한 리셋
  • 파워 모드에서의 이상
  • 시스템의 하드웨어 주변장치로의 부정확한 인터페이싱
  • 제한된 리소스(스택, 힙 등)의 용량을 초과
  • 이벤트 반응 마감시간(deadlines)을 맞추지 못함 


소프트웨어 품질 보증 활동

종종 테스팅으로 총칭되는 소프트웨어 품질 보증 활동이 아래의 세 개 카테고리로 나뉜다.

  • 체킹(checking): 리뷰, 인스펙션, 워크쓰루
  • 동작 확인(demonstrating): 기능적 테스팅, 구조적 테스팅, 통합 테스팅, 회귀 테스팅
  • 증명(proving): 타이밍 분석, 기타 분석

 

위 세 가지 액티비티 모두 잠재적 소프트웨어 결함을 찾아내려는 의도를 가짐. Checking은 바람직한 특성들을 나열한 목록을 기준으로 소프트웨어를 체크하고, demonstrating은 정의된 조건 하에서 소프트웨어가 정확하게 동작하는지를 확인하며, proving은 모든 가능한 조건 하에서 소프트웨어가 특정 실패 타입을 보이지 않는다는 것을 증명한다.


코드 리뷰(Code reviews)

  • 코드 리뷰는 개발 프로세스의 초기에 잠재적 소프트웨어 결함을 식별하는 수단으로써 인기가 많아짐
  • 논리적 문제, 수학적 문제, 신택스 에러, 타이핑 에러, 잘라내기-붙여넣기(cut-and-paste) 관련 에러를 발견하는데 능숙함
  • 테스트가 불가능한 요구사항/코드 경로를 검증하기 위해 엄격한 인스펙션이 때때로 사용됨
  • 리뷰나 인스펙션은 그 세밀한 성격 때문에 일반적으로 코드의 개별 유닛(, 단일 기능, 관련된 기능들의 작은 그룹) 상에 수행됨
  • 프로그램의 큰 범위와 관련된 문제(, 이벤트 타이밍, 워치도그 사용, 기타 시스템 이슈)를 식별하는데는 이 방법이 효과적이지 않음


기능적 테스팅(Functional testing)

  • 기능적 테스팅은 시스템이 예상대로 동작하는지를 확인하기 위해 수행됨
  • 누락된 요구사항 또는 모호한 요구사항을 식별하는데 효과적이며, 시스템 컴포넌트들의 다양한 인터페이스 간의 문제도 종종 찾아냄
  • 기능적 테스팅은 시스템의 가장 중요한 기능이나 가장 흔히 사용되는 기능에 집중하는 경향이 있으며, 코드의 모든 경로를 이 기법을 사용해 실행시키는 것은 거의 불가능함


구조적 테스팅(structural testing)

  • 위의 기능적 테스팅과 대조적으로 구조적 테스팅(화이트박스 테스팅으로도 불려짐)은 모든 가능한 코드 경로를 강제로 실행시키는데 효과적임
  • 구조적 테스팅에서는 소프트웨어의 정밀 조사가 요구되기 때문에 인스펙션이 발견하는 것과 같은 타입의 에러(, 수학적 문제, 로직 문제, 구조와 데이터 문제)를 많이 찾아냄
  • 인스펙션과 마찬가지로 독립된 개별 기능(또는 소프트웨어 유닛)에 집중함. 따라서 타이밍, 인터페이스, 시스템 레벨 요구사항에 관련된 에러는 놓칠 수 있음


분석(Analysis)

  • 분석은 테스팅과 인스펙션이 놓친 문제를 찾아냄
  • 분석은 매우 지루한 일이 될 수 있으므로, 일반적으로 다른 방식으로 검증될 수 없는 부분들에 집중됨
  • 코드의 상세한 분석을 통해 스택 깊이(stack depth), 워치도그 타이머 사용, 공유 리소스(shared resources), 타이밍에 있어서의 잠재적 문제들이 드러나게 됨



스택 깊이 분석(stack depth analysis)

최대 스택 깊이(the maximum stack depth)는 어떤 특정 기능 또는 코드 부분의 속성이 아니라 전체 시스템의 속성(property)이지만, 쉽게 테스트 될 수 있는 요구사항이 아니다. 함수 호출과 패러미터, 임시 변수(temporary variables), 인터럽트(interrupts), 태스크(tasks) 등이 운영을 위해서 스택 공간을 필요로 하지만, 코드가 완성되기 전까지는 최대 스택 깊이가 결정될 수 없다. 또한 어떤 경로가 특정 시스템 운영 인스턴스에서 가장 많은 스택을 필요로 할지도 일반적으로 분명하지 않다. 이런 이유로 코드가 아래 처럼 철저하게 분석되어야 한다.

 

  • 각 태스크의 콜 트리(a call tree) 구축
  • 각 함수(function)가 사용하는 스택 결정
  • 각 태스크 콜 트리의 최악 경우 스택 깊이(the worst case stack depth)를 결정
  • 각 태스크의 스택을 합계
  • 각 레벨의 인터럽트에 필요한 스택을 추가
  • 초기화 또는 RTOS가 사용하는 스택이 있으면 추가

 

이런 분석을 통해 최대 스택 깊이가 결정되면 그 사용을 위한 적절한 양의 메모리가 할당될 수 있다.


공유 데이터 분석(shared data analysis)

분석을 통해 찾아낼 수 있는 또 다른 잠재 결함으로 데이터 공유 위반(data sharing violations)이 있다. 임베디드 시스템이 종종 글로벌 데이터를 사용하며, 거의 모든 임베디드 시스템이 인터럽트를 사용한다. 인터럽트가 값을 리턴할 수 없으므로 인터럽트 서비스 루틴은 보통 그 결과를 글로벌 데이터 저장소에 쓰며, 따라서 인터럽트 서비스 루틴과 나머지 메모리 간의 데이터 공유가 신중하게 관리되어야 한다. 또한 인터럽트와 시스템의 다른 부분들 간에 공유되는 데이터 뿐만 아니라 멀티태스킹 환경에서 우선 순위가 다른 태스크들 간에 공유되는 데이터도 신중하게 관리되어야 하며, 이에 실패하면 데이터가 덮어쓰기 되거나 또는 훼손되게 된다.


Extern int result;

void Update_result(int offset, int scale)

{

int temp;

temp = read_port_F()

temp *= scale;

temp += offset;

disable_interrupts();

result = temp; /* line 9 */

enable_interrupts();

} 

[글로벌 데이터를 안전하게 변경하기]


  • 위 예에서 외부 포트 F가 읽혀지고, 그 값에 scale offset이 추가되며, 최종 결과가 글로벌 변수 result에 쓰여지게 됨(이 글로벌 변수는 나중에 인터럽트 서비스 루틴에 의해 사용됨)
  • 이 데이터를 사용하는 인터럽트가 언제든 발생할 수 있기 때문에 모든 예비 계산을 임시 변수에 집어 넣는 것이 중요(임시 변수 temp가 해당 함수 내부 범위에서만 유효)
  • 만약 라인 9에서 result로 쓰기가 단일 마이크로프로세스 인스트럭션에서 수행되면, 이게 인터럽트될 수 없으며 인터럽트 서비스 루틴이 result로부터 항상 정확한 값을 읽게 됨
  • 하지만 이 코드가 8-비트 마이크로프로세서 상에서 실행되면(, 메모리로 한번에 8 비트 만을 쓰기함), 쓰기가 2개의 마이크로프로세스 인스트럭션에 의해 수행된다. 이 두 인스트럭션의 중간에 인터럽트가 발생할 수 있고, 이 경우 인터럽트 서비스 루틴이 1 바이트는 오래되고 다른 1 바이트는 새 것인 데이터를 사용하게 되는데, 이게 심각한 문제를 야기할 수 있음
  • 위 코드에서처럼 쓰기(write) 주변에서 인터럽트를 비활성화 시키면 이런 에러를 예방하게 됨


기타 권고되는 임베디드 시스템 결함 발견 활동

아래와 같은 항목들을 체킹하며 소프트웨어의 시스템 레벨 인스펙션을 수행한다.

  • 모든 주변 장치 레지스터(peripheral registers)가 정확하게 이해되고 액세스 되는지 확인한다.
  • 워치도그 타이머가 활성화되었는지 확인한다(많은 경우에 코드가 개발되는 동안 디버깅이 용이하도록 엔지니어가 이걸 비활성화 시켜 놓음).
  • 모든 디지털 입력이 적절하게 디바운싱(de-bounced) 되었는지 확인한다.
  • 아날로그-디지털 변환기(그리고 디지털-아날로그 변환기)의 턴 온 지연 시간(turn-on delay)이 적절하게 처리되는지 확인한다.
  • 파워 업 동작과 파워 다운 동작, 저전력 모드로부터의 진입/진출을 면밀하게 조사한다.
  • 단순히 예상되는 인터럽트 뿐만이 아닌 모든 인터럽트에 대한 적절한 인터럽트 벡터(interrupt vector)가 정의되었는지 확인한다.


구조적(화이트 박스) 단위 테스팅 동안 아래와 같은 것들을 확인한다.

  • 각 함수의 최악 경우 스택 사용(the worst-case stack usage)을 결정한다.
  • 함수에서 코드 경로의 최장 실행 시간(호출에서부터 리턴까지)을 결정한다.
  • 재귀적 루틴(recursive routines), 하드웨어 신호 또는 메시지 기다리기 같은 비결정적 타이밍 구조(non-deterministic timing structures)가 있는지 찾는다. 이것들이 차후 수행되는 타이밍 분석을 부정확하게 만들 수 있음


아래와 같은 소프트웨어 상의 중요 분석을 수행한다.

  • 스택 깊이(Stack Depth): 최악 경우 스택 깊이를 결정하고, 적절한 스택이 할당되었는지 확인한다.
  • 워치도그 사용(Watchdog Usage): 단위 테스트에서 확보한 타이밍 데이터를 사용하여 워치도그 타이머가 리셋되는 모든 장소를 찾고, 모든 올바른 시스템 운영 모드에서 워치도그 타이머가 타임아웃 되기 전에 항상 리셋되는지 확인한다.
  • 공유 데이터(Shared Data): 인터럽트 서비스 루틴과 코드의 기타 다른 부분에 의해 액세스 되는 모든 데이터를 찾는다. 멀티 태스킹 설계가 사용되는 경우라면, 우선 순위가 다른 태스크들에 의해 공유되는 모든 데이터도 찾는다. 데이터 훼손이 생기지 않도록 데이터 상에 적절한 보호책(protection measures)이 사용되는지 검증한다.
  • 데드락(Deadlock): 데드락을 야기할 수 있는 여러 다른 태스크 간에 공유되는 모든 데이터의 할당 그래프(an allocation graph)를 구축한다. 시스템의 설계와 공유 데이터 항목이 잠기는(locked) 순서를 통해 데드락이 예방되는지 확인한다.
  • 가동률(Utilization): 단위 테스트에서 확보한 타이밍 정보를 사용해서 시스템에 있는 모든 태스크의 최악 경우 실행 시간(the worst-case execution times)을 결정한다. 마이크로프로세서가 최악 상황 하에서, 그리고 최고의 발생률에서, 모든 태스크를 마감시간까지 완성할 수 있는지 검증한다.
  • 스케쥴링 가능성 분석(Schedulability): 앞서 확보한 최악 경우 태스크 실행 시간을 사용하여 모든 태스크의 일정이 잡힐 수 있고 모든 상황에서 그 마감시간을 맞출 수 있는지 결정한다. 시스템의 설계를 따르는 스케쥴링 알고리즘을 사용한다. , 비율 단조 스케줄링(RMA)
  • 타이밍(Timing): 각 태스크의 기타 타이밍 요구사항들이 최악 상황에서 충족되는지 확인한다. , 릴리즈 타임 지터(release-time jitter), --단 완료(end-to-end completion)


반응형

+ Recent posts