'임베디드 스케치'의 4번째 요약 및 정리는 exception 및 디버깅과 연관된 내용을 담았으며 원본 교제의 다음 부분을 포함합니다.
- 책의 Chapter 3 : 307 ‘소스 레벨 디버깅이 뭐죠?’
- 책의 Chapter 5
- 506 ‘모드 비트와 Exception 처리’
- 507 ‘ARM 프로세서와 하드웨어 디버거?’
- 508 ‘하드웨어 디버거와 Breakpoint’
- 509 ‘하드웨어 디버거와 array, 그리고 pointer?’
- 책의 Chapter 7
- 700 ‘디버깅의 달인’
- 701 ‘고급 디버깅의 기술’
[※] 표시와 함께 존댓말로 적힌 문장은 책에는 없으나 부가설명이 필요한 경우 구글링을 통해 제가 추가 학습한 내용입니다!
1. 소스 레벨 디버깅

- C source Debugging window
- 개발자가 만든 C 레벨에서 한 라인씩 실행하면서 프로그램 수행 순서를 모두 추적하도록 돕는다.
- IDE에서 하듯 특정 라인에 breakpoint를 걸어 halt 시킬 수 있다.
- Flash memory dump window
- 현재 플래시 메모리에 들어 있는 바이너리를 보거나 peripheral register 값을 볼 수 있다.
- Function call history window
- 프로그램 수행 순서가 어떻게 돼 왔는지를 한 눈에 알아 볼 수가 있다.
- 특정 함수에 있는 지역변수의 값도 모니터링 할 수 있다.
- ARM Core register window
- 현재 ARM Core 내부의 레지스터들의 값을 확인 할 수 있다.
- ARM 파이프라인 과정을 확인할 수 있다.
- 옛날에는 숙련된 개발자의 노하우와 감, 추측이 디버깅에 중요했지만, 이제는 HW 디버깅툴을 잘 사용하는 것 또한 중요하다.
2. HW 디버거의 사용
2.1. HW 디버거의 대표 기능, Breakpoint
- HW 디버거의 최대 장점은 개발자가 core를 자유자재로 halt/ running 시키면서 실행흐름, 변수, 레지스터값, 메모리값을 확인할 수 있다는 점이다.
- 이 기능의 핵심은 ‘Breakpoint’다. Breakpoint는 개발자가 원하는 조건에서 core를 멈춘다.
- Core 제조회사는 breakpoint를 지원하기 위해 core 내부에 ICE(In-Circuit Emulator) Breakpoint Register를 만든다.
- 예를 들어, ARM core의 이름에 ‘I’가 포함된다면, ICE Breakpoint 레지스터가 포함된 모델이다.
- 이 레지스터를 활용해서 2가지 방법으로 breakpoint를 사용할 수 있다.
2.1.1. Soft Breakpoint
- 동작 과정은 다음과 같다.
- 개발자가 멈추고 싶은 주소에 ICE breakpoint 레지스터를 통해 breakpoint를 설정한다.
- 해당 주소의 원본 코드를 백업해둔다.
- 해당 주소에
0xBEBEBEBE라는 값을 덮어씌운다.
- ARM core를 running 한다.
- 해당 주소에 PC값이 도달해 fetch를 할 때 data bus로 load되는 값이
0xBEBEBEBE라는 사실을 ICE Breakpoint 레지스터가 인식한다.
- ICE Breakpoint 레지스터는 core에게 멈추라는 명령을 보내 core가 멈춘다.
- 백업해둔 원본 코드를 해당 주소에 덮어씌워 복구한다.
- ([※] 어디에 백업하는건지, 백업과 복원은 누가 담당하는지는 모르겠네요…)
- Soft breakpoint는 개수 제한 없이 무한정으로 사용할 수 있다.
- 하지만, soft breakpoint가 지정하는 주소는 반드시 메모리 영역만 가능하다. 왜냐하면, 특정 주소에 코드값을 쓰고 복원하는 과정이 포함되기 때문이다. 따라서 NOR 플래시를 사용하는 시스템이라면 soft breakpoint를 사용할 수 없다.
- [※] 사실 이 부분이 조금 이해가 안 됐습니다. Breakpoint는 instruction이 수행되는 주소에 걸테니 메모리 영역의 주소이므로 NOR 플래시든 NAND 플래시든 상관없이 soft breakpoint를 사용할 수 있다고 생각하기 때문입니다. NOR/ NAND 플래시 모두 특정 주소만 핀포인트로 overwrite 할 수 없기 때문에 (NOR 플래시는 chip/sector erase 한 뒤 write 해야 하고, NAND도 block단위로 erase하고 다시 write 합니다.) soft breakpoint랑 플래시 메모리랑은 상관 없지 않나라는 의문이 들었습니다.
- [※] 구글링 해보니 soft breakpoint는 SW Breakpoint라고 부르는 것 같습니다.
2.1.2. On-chip Breakpoint (HW Breakpoint)
- On-chip breakpoint는 이름 그대로 ICE breakpoint 레지스터에 개발자가 직접 breakpoint를 걸고 싶은 주소를 등록한다.
- ICE breakpoint 레지스터는 현재 PC값과 등록된 주소를 실시간으로 비교해서 일치할 때 ARM core를 멈춘다.
- On-chip breakpoint는 3가지 기능을 제공한다.
- Instruction breakpoint: 개발자가 원하는 소스 라인에 breakpoint를 설정하는 기능.
- Memory Read/ Write breakpoint: Global 변수, 특정 메모리 주소, peripheral 주소에 write/ read 할 때 멈추는 기능. 주로 LDR/ STR 명령어를 수행할 때 멈춘다.
- Data value breakpoint: Global 변수, 특정 메모리 주소, peripheral 주소에 개발자가 원하는 값이 write 될 때 멈추는 기능.
2.1.3. ICE Breakpoint Register
- ARM core마다 제공되는 ICE breakpoint 레지스터 개수가 다르다.
- ARM7, ARM9는 2개 watchpoint를 제공한다. 따라서 on-chip breakpoint를 2개 밖에 사용 못 한다.
- ARM11, Cortex-A8, Cortex-R4는 6개 watchpoint를 제공한다.
- Cortex-M3는 4개 watchpoint를 제공한다.
2.1.4. Breakpoint 사용법
HW 디버거를 사용해서 breakpoint를 설정하는 방법은 다음과 같다.
- Soft breakpoint & On-chip breakpoint 설정하기
Break.Set 0x3003D7E8
Break.Set 0x3003D7E8 /onchip
Break.Set 0x3003D7E8 /write
Break.Set 0x3003D7E8 /read
Break.Set 0x3003D7E8 /readwrite
Break.Set 0x3003D7E8 /data 0x10
- Global 변수에 breakpoint 설정하기
Var.Break.Set 변수이름 /write
Var.Break.Set 변수이름 /read
- 특정 주소 구간에 breakpoint 설정하기
Break.Set 0x0--0x1000
Break.Set 0x0--0x1000 /write
Break.Set 0x0--0x1000 /read
2.2. HW 디버거의 기타 기능들
([※] 2.1.4절에서 HW 디버거의 사용법에 대해서 다뤄봤습니다. 이번 절에서는 HW 디버거가 할 수 있는 기능에 대해 조금 더 자세히 배워보도록 하겠습니다.)
2.2.1. ARM core 제어
- 바이너리가 메모리에 없어도 HW 디버거를 사용하면 ARM core를 직접 제어할 수 있다.
- 예를 들어, 두 정수 a, b를 덧셈하고 싶을 때 C프로그램으로 짠다면 아래와 같다.
#include <stdio.h>
int main() {
int a = 5, b = 5, add= 0;
add = a + b;
return 0;
}
- 원래는 위 C프로그램을 컴파일하고 바이너리로 만들어 SDRAM에 올려 CPU가 어셈블리 명령어를 수행하도록 해야하지만, HW 디버거를 사용한다면 직접 ARM core의 레지스터를 조작해서 덧셈을 할 수 있다.
; exam.cmm
Register.Set R2 0x5 ; a = 5
Register.Set R3 0x5 ; b = 5
Data.Set 0x3003D7EC %long R(R2) ; 메모리 주소에 a값 저장
Data.Set 0x3003D7E8 %long R(R3) ; -0x4 주소에 b값 저장
Data.Set 0x3003D7E4 %long R(R2)+R(R3) ; -0x4 주소에 a+b값 저장
2.2.2. LED 제어 (Peripheral 제어)
- 타겟 보드에 연결돼있는 LED를 C프로그램 작성 없이 제어해보자.
- 현재 LED는 3.3V VCC에 연결돼있으므로 LED가 연결된 핀에
0을 set해서 GND로 만들면 LED가 켜질 것이다.


- MCU의 데이터시트를 보자. LED는 포트 B의 2, 3, 4번 핀에 연결돼있다고 가정하자.
- [※] 데이터시트에서 Port B control 레지스터 부분을 확인해봅시다.
- [※] 포트 B의 입출력 방향을 제어하는 레지스터를 GPBCON이라 부르고
0x5600_0010번지에 있습니다.
- [※] GPBCON의 [9:4] bits를
010101로 설정하면, 2, 3, 4번 핀의 입출력 방향이 ‘OUTPUT’이 됩니다.
- [※] 포트 B의 입출력 값을 제어하는 레지스터는 GPBDAT이라 부르고
0x5600_0014번지에 있습니다.
- [※] GPBDAT의 원하는 bit에
0을 설정하면 GND로 처리 돼 해당 핀과 연결된 LED가 점등합니다.
- [※] 위 과정을 수행하는 C프로그램과 HW 디버거 명령어를 살펴보고 서로 비교해봅시다.
#define rGBPCON (*(volatile unsigned *) 0x56000010)
#define rGBPDAT (*(volatile unsigned *) 0x56000014)
unsigned int ledData;
void Delay(unsigned short time);
void testLED() {
rGBPCON = (rGBPCON & ~(0x3F<<4) | 0x15<<4);
ledData = 0xFFFFFFFB;
rGBPDAT = ledData;
Delay(100);
ledData = 0xFFFFFFF7;
rGBPDAT = ledData;
Delay(100);
ledData = 0xFFFFFFEF;
rGBPDAT = ledData;
Delay(100);
ledData = 0xFFFFFFFF;
rGBPDAT = ledData;
Delay(100);
}
; LED_test.cmm
B::
PER.Set AD:0x56000010 %LONG 0x150 ; GPBCON에 0x150 write
PER.Set AD:0x56000014 %L 0xFB ; GPBDAT의 GPB2에 GND
wait 100.ms
PER.Set AD:0x56000014 %L 0xF7 ; GPBDAT의 GPB3에 GND
wait 100.ms
PER.Set AD:0x56000014 %L 0xEF ; GPBDAT의 GPB4에 GND
wait 100.ms
PER.Set AD:0x56000014 %L 0xFF ; GPBDAT clear
wait 100.ms
ENDDO
- [※] C언어 프로그램에 대한 부가설명 하겠습니다.
- [※]
rGBPCON = (rGBPCON & ~(0x3F<<4) | 0x15<<4);은 GBPCON의 9, 7, 5번 bit를 0으로 만듭니다.
- [※]
~(0x3F<<4)는 0011_1111_0000이고 0x15<<4는 0001_0101_0000입니다.
- [※] 두 수를 OR연산하면
1101_0101_1111이 되겠지요? 얘를 모든 bit가 1인 GBPCON과 & 연산 한다는 건 특정 bit를 0으로 만들겠다는 의미와 같습니다.
- [※] 따라서 9, 7, 5번 자리 bit가
0이 돼 GBP 2, 3, 4번 pin이 OUTPUT으로 설정되겠네요.
- [※] 근데 왜
0x3F와 0x15를 사용했는지는 잘 모르겠습니다. 바로 1101_0101_1111 = 0xD5F랑 & 연산 했으면 된다고 생각하거든요.
- [※] GBPDAT에 순서대로
0xFFFF_FFFB, 0xFFFF_FFF7, 0xFFFF_FFEF를 쓰는걸 볼 수 있습니다.
- [※]
0xFFFB = 1111_1111_1111_1011이니 2번 bit가 0이 되서 GBP2가 GND 되겠네요.
- [※]
0xFFF7 = 1111_1111_1111_0111이니 3번 bit가 0이 되서 GBP3가 GND 되겠네요.
- [※]
0xFFEF = 1111_1111_1110_1111이니 4번 bit가 0이 되서 GBP4가 GND 되겠네요.
- [※] 위 과정을 HW 디버거로 하면 위 코드와 같습니다.
- [※] 한 가지 의문은
PER.Set AD:0x56000010 %LONG 0x150에서 왜 0x150을 write 하는지 모르겠습니다. GBPCON은 모든 bit가 1로 초기화 된 상태라 0x150=0001_0101_0000을 write 하면 안되고 원하는 bit 이외에는 1로 set 된 1101_0101_1111 = 0xD5F를 write 해야 한다고 생각하기 때문입니다.
2.2.3. 특정 변수값 변경
- 만일 특정 변수값을 변경하고 싶다면, 소스코드를 수정하고, 재컴파일 하고, 플래시메모리에 바이너리를 재다운로드 하는 귀찮은 과정을 거쳐야 한다.
- 하지만, 반드시 소스코드와 어셈블리 명령어가 같아야만 시스템이 동작할까?
- 아니다! 소스코드는 개발자를 위한 것이지 결코 ARM core를 위한 것이 아니다!
- ARM core는 오직 어셈블리 명령어만 관심이 있기 때문에 소스코드를 고치지 않고 현재 플래시메모리 또는 메모리에 올라간 바이너리 및 어셈블리 명령어만 수정하면 된다.
DATA.ASSEMBLE 0x00000480 MOV R3, #0x7
DATA.ASSEMBLE 0x00000488 MOV R3, #0x9
- HW 디버거로 위 명령어를 입력하면, 지정한 주소의 명령어가 위와 같이 바뀌게 된다.
- 즉, 저 주소를 실행할 때 R3 레지스터에 들어있는 값이 바뀌게 되는 것이다.
2.2.4. 특정 라인 주석 처리
- 소스코드의 특정 라인을 수행하고 싶지 않을 때가 있다면, 주석 처리를 한 후 재컴파일 해야 할 것이다.
- 하지만, ARM 명령어 중 ‘NOP’을 사용한다면 주석 처리를 한 것과 같은 효과를 낼 수 있다.
DATA.ASSEMBLE 0x00000480 NOP명령어로 특정 주소의 명령어를 NOP으로 덮어씌워 해당 라인의 명령어를 수행하지 않도록 할 수 있다.
2.2.5. 메모리에 있는 데이터를 복사
0x0~0xC번지 데이터를 0x2000으로 복사하고 싶다면 HW 디버거로 다음 명령어를 사용하면 된다.
DATA.COPY 0x0--0xC 0x2000
- 또한, 덤프 관련 명령어도 제공된다.
DATA.SAVE.BINARY 0x0--0x1000 C:\DUMP.bin라고 입력하면 C드라이브에 DUMP.bin이라는 바이너리 파일이 덤프될 것이다.
2.3. 변수는 어떻게 저장되는가
- 개발자는 배열과 포인터 변수들이 메모리에 어떻게 할당되는지를 알고, 메모리 구조도 잘 파악하며 프로그램을 만들 수 있는 인재가 돼야 한다.
- 평소에 알고도 얼떨결에 지나쳤던 배열과 포인터에 대해 HW 디버거로 분석해보자.
2.3.1. 배열
- 배열에 저장할 값의 범위를 알고 있다면, 메모리를 단축할 수 있다.
char arr[2][2][2]와 int arr[2][2][2]는 둘 다 8개의 데이터를 저장할 수 있지만, HW 디버거로 검사하면, 전자는 0x3002_0000부터 16B만 할당되지만, 후자는 32B가 할당된다.
- 따라서 만일 배열에
0x00~0xFF값이 들어간다면 char형으로 선언하는 게 메모리를 적게 쓸 수 있다.
2.3.2. 포인터
int* p와 int *p는 같은 표현이고 개발자 개인의 취향이다.
char str[] = "Embedded_C";
char* pChar = str;
char c1 = *(++pChar);
char c2 = *(++pChar);
char c3 = *(pChar+1);
char c4 = *(pChar+2);
- 위 프로그램이 실행되면 각 변수에는 어떤 값들이 저장될지 HW 디버거로 알아보자.
pChar는 문자열 str의 시작주소를 가리키고 있다.
- 변수
c1은 pChar이 char형의 크기만큼 주소가 증가한 곳의 문자(m)가 저장된다.
- 변수
c2는 pChar이 char형의 크기만큼 주소가 증가한 곳의 문자(b)가 저장된다.
- 변수
c3는 pChar이 가리키는 주소로부터 1×(char형 크기)만큼 떨어진 곳의 문자(e)가 저장된다.
- 변수
c4는 pChar이 가리키는 주소로부터 2×(char형 크기)만큼 떨어진 곳의 문자(d)가 저장된다.
- 이 예제를 통해
*(++pChar)와 *(pChar+1)의 차이를 알자.
2.3.3. 포인터의 포인터
unsigned char *pChar;
unsigned char **ppChar;
unsigned char ***pppChar;
const char cstr[] = "Embedded";
void foo() {
pChar = (unsigned char *)&cstr[0];
ppChar = (unsigned char **)&pChar;
pppChar = (unsigned char ***)&ppChar;
}
- 위 함수가 실행된 뒤 HW 디버거로 변수와 주소를 살펴보면 다음과 같다.
pChar은 0x3002_0124번지 변수이며 내부에 cstr의 시작주소인 0x0001_00D0를 갖고 있다.
ppChar은 0x3002_0120번지 변수이며 내부에 pChar의 주소인 0x3002_0124를 갖고 있다.
pppChar은 0x3002_011C번지 변수이며 내부에 ppChar의 주소인 0x3002_0120을 갖고 있다.
- 결국 세 변수 모두
0x0001_00D0번지의 E를 가리키고 있다.
- 변경하지 않을 문자열은 꼭 const 선언을 통해 불필요한 삽질을 줄이자.
3. 디버깅 기술
3.1. 디버깅을 잘하기 위한 팁
- 현재 개발 중인 MCU의 특징에 대해 잘 알고 있어야 한다.
- 최근 MCU는 멀티프로세서 또는 SMP가 내장된 ARM 프로세서를 사용한다.
- 멀티프로세서: 서로 다른 ARM core끼리 조합한 것 (i.e. ARM9 + ARM11)
- SMP: 같은 ARM core끼리 조합해 듀얼코어/ 쿼드코어처럼 사용하는 것
- RTOS의 경우 멀티프로세서 환경은 각 프로세서마다 따로 포팅하고, SMP는 하나만 포팅하면 된다.
- 멀티프로세서는 각 RTOS에서 task를 처리하므로 속도가 빠르지만, 각 RTOS마다 어떤 한 task가 프로세서를 사용 중이면 다른 task는 waiting 해야 한다는 단점이 있다.
- SMP는 한 task가 프로세서를 사용해도 다른 task는 놀고있는 다른 프로세서가 처리하면 되므로 waiting time이 상대적으로 짧다. 같은 ARM core를 사용하기 때문에 사용하는 어셈블리 명령어도 같기 때문에 다른 task도 실행시킬 수 있다. 이때 RTOS는 SMP2(SMP 스케줄링)을 지원해야 한다.
- 이러한 MCU의 특징을 알고 있어야 개발 및 디버깅 시 큰 도움이 된다.
- 현재 개발하는 프로세서의 특징과 어셈블리 명령어를 잘 알고 있어야 한다.
- 예를 들어, ARM11부터 도입된 ‘trustzone’이라는 기능의 특징을 알고 있어야만 개발 도중에 문제가 생기더라도 디버깅을 할 수 있다.
- 버전의 발전에 따라 다양해지는 어셈블리 명령어도 많이 알고 있어야 모르는 어셈블리 코드가 나왔을 때 당황하지 않게 된다.
- 크로스컴파일러가 어떤 원리로 바이너리를 생성하는지 알아야 한다.
.text와 .data영역이 어느 주소에서 동작하는지 파악해두면 디버깅할 때 문제 접근이 쉬워진다.
- R13은
.data영역의 주소를, R14, R15(PC)는 .text영역의 주소를 가리키고 있어야 함을 알고 있어야 한다.
- 개발 중인 RTOS와 기타 RTOS와의 차이점을 알아야 한다.
- RTOS의 기본 지식인 task 단위 프로그래밍, 세마포어, 메시지큐 등을 잘 알고 있어야 한다.
- 각 기능은 RTOS마다 조금씩 다르게 구현 돼 있으므로 차이점도 알고 있어야 한다.
- HW 디버거 및 디버깅툴을 잘 사용할 줄 알아야 한다.
- 툴에서 제공하는 명령어를 많이 알고 응용력을 발휘할 수 있어야 한다.
- 리셋 같은 문제가 발생했을 때 1~4를 기반으로 HW디버거를 사용해 해당 상황을 재현하고 당시 어셈블리 명령어와 레지스터 상태를 분석할 수 있어야 한다.
- 평소 다양한 부서 사람들과 친하게 지내야 한다.
- 개발자 한 사람이 모든 것을 할 수는 없다.
- 프로젝트는 모듈별로, 각 모듈은 여러 개발 부서로 나뉘기 때문에 어떤 문제가 발생했다면 우리 부서만 문제를 분석하는 것이 아니라 인근 부서와 함께 문제점을 찾아야 할 일이 종종 생긴다.
- 워낙 전문가 그룹으로 나누어져 있기 때문에 어려운 문제일수록 접근하기도 힘들고, 또 쉽게 포기하게 된다.
- 따라서 평소에 여러 부서의 개발자와 친하게 지내야 큰 도움을 받을 수 있다.
3.2. 대표적인 디버깅 상황
3.2.1. Sleep mode 진입 후 일정시간 지나서 wake-up 할 때 리셋 발생
- Sleep mode에 진입할 때 peripheral의 인터럽트와 파워를 비활성화 한다.
- 이때 PMIC(Power Management IC) 관련 레지스터를 설정해 사용하지 않는 peripheral에 전력을 공급하지 않는 방법으로 베터리 사용량을 최소화 하게 된다.
- 하지만, 항상 동작해야 하는 peripheral(모뎀 등) 관련 인터럽트 & 파워는 놔둔다.
- Wake-up 할 때는 sleep mode에 진입하기 전의 설정값을 그대로 복원해줘야 한다. 이 부분이 제대로 되지 않을 때 상기한 리셋 문제가 발생할 수 있다.
3.2.2. 부팅 시 LCD에 로그 화면이 나오고 무한 재부팅 발생
- 부팅 시 device driver들이 초기화 될 때 순간적으로 전력소모량이 높아지는 현상이 있다.
- 초기화 할 peripheral과 device driver의 개수가 늘수록 clock이 불안정해져 무한 리셋 현상이 발생한다.
- 새로운 peripheral 또는 device를 연결할 때는 반드시 startup.s의 PLL 설정을 새롭게 해주자.
- 특히 APB([※] peripheral들 연결된 bus를 말해요)관련 clock값들을 새로워진 환경에 맞게 설정해주자.
- 거의 전압 threshold 관련 문제다.
- HW 설계가 잘못 돼 threshold 근방의 애매모호한 신호가 발생하는 경우 이런 문제가 생긴다.
- 캐패시터를 연결하는 등 HW적으로 해결하거나 연속해서 신호를 여러 번 체크해서 SW적으로 해결할 수 있다.
3.2.4. 애플리케이션 동작 때 시스템이 lock-up 되는 경우
- RTOS를 사용하는 경우, 각 task는 TCB와 stack을 할당받는데, 보통 연속된 주소로 할당된다.
- 개발자의 코드 실수로 인해 stack 영역을 overflow해서 TCB 영역에 read/ write 하는 경우 TCB 구조체의 데이터가 손상돼 어떤 상황이 발생될지 예측할 수 없는 상황이 발생한다.
3.2.5. printf() 사용 여부에 따라 타겟이 리셋되는 경우
printf()는 보통 SW 디버깅을 위해 사용한다.
- 사용하고, 확인하고, 지우면서 개발 중에 사용하는 건 괜찮다. 하지만, 개발 일정에 쫓기다보면 실수로 놔두고 제품 출시 직전에야 지우는 경우가 있다.
- 이럴 경우, 이미 제품의 delay는 printf()가 있는 상황에 맞춰져 개발됐기 때문에 printf()가 사라짐으로써 각 task 사이에 균형이 깨져 reset이 언제 발생될지 모르는 예측 불허 상태에 놓인다.
- Task A와 B가 번갈아가면서 10ms 동안 실행되는데 A에 printf()를 넣고 잊었다고 생각해보자.
- Task A는 10ms보다 아주 약간 조금 더 긴 시간이 소요될 것이고,
Task B는 그 시간만큼 A가 끝나기를 아주 조금씩 기다릴 것이다.
- 시간이 한참 흐르게 되면 이 아주 약간의 시간이 점점 쌓여 커져 나중에는 task B가 task A가 끝나기를 한참을 기다려야 수행되게 된다.
- Watchdog timer는 이를 타겟이 lock-up된 상황이라 판단해 자동으로 시스템을 리셋하게 된다.
3.3. 고급 디버깅 기술 : Exception Debugging
3.3.1. Mode bit와 Exception 처리
- ARM core에는 주로 사용하는 6개 mode가 있으며 CPSR, SPSR 레지스터에서 모드 정보를 알 수 있다고 배웠다.
- 6개 mode에는 임베디드 시스템이 오동작 할 경우, 친절하게 어디서 문제가 발생했는지를 알려 주는 Abort 모드와 Undefined 모드가 포함된다.

- 만일, ARM core가 exception이 발생했다고 판다할 경우 위 표의 미리 정의된 주소로 무조건 점프하고 CPSR과 SPSR을 수정하도록 설계 돼있다.
- Low address, high address 여부는 CP15의 CR 레지스터의 ‘V’ bit에 표현돼 있다.
- Data Abort Exception
- 존재하지 않는 주소/ 접근할 수 없는 주소에 데이터를 read/ write 하려고 시도할 때 발생한다.
- 예를 들어, 32MB SDRAM의
0x0400_0000번지 주소에 접근하는 경우 abort 된다.
- Data abort가 발생했을 때 다음과 같은 동작을 수행하게 된다.
- SPSR_abt 레지스터에 CPSR 값을 저장(백업)한다.
- CPSR 레지스터 값을 변경한다.
- CPSR[5] =
0 : ARM 명령어 체제로 전환
(∵THUMB는 CPSR 제어 불가해 exception 처리 불가능하므로)
- CPSR[7] =
1 : IRQ를 비활성화 한다.
- CPSR[4:0] =
10111 : Abort mode로 전환한다.
- R14(LR_abt) 레지스터에 PC값을 저장한다.
- PC를 지정된 주소(
0x0000_0010)으로 바꿔 점프한다.
- 여기서 링크 레지스터에 저장하는 PC값은 abort가 발생한 주소 +
0x8이라는 사실이 중요하다.
- 왜냐하면, Fetch → Decode → Execute 파이프라인에서 Execute 단계에서 abort가 발생한 시점의 PC값은 2개 명령어 아래의 instruction을 fetch하는 단계에 있기 때문이다. 따라서 이때의 PC값은 abort를 유발한 execute 단계의 명령어보다
0x8위에 있다.
- 따라서, HW 디버거를 사용할 때 data abort가 발생했다면,
- 우선, 링크 레지스터를 확인하고 해당 주소를 덤프해서 살펴본다.
- 해당 주소로부터
0x8을 뺀 주소의 명령어가 data abort를 발생시킨 범인이다.
- Prefetch Abort Exception
- 존재하지 않는 주소/ 접근할 수 없는 주소에 대해 fetch 하는 경우 발생한다.
- Data abort와 거의 비슷한 과정을 수행한다.
- 이때, data abort 때 링크 레지스터에 저장되는 PC값은 abort 발생 지점 +
0x8번지지만, prefetch abort 때 저장되는 값은 +0x4번지다.
- 왜냐하면, abort가 fetch와 decode 사이에서 발생하기 때문에 PC값은 1개 명령어 아래에 있기 때문이다.
- Undefined Exception
- Decode 단계에서 instruction을 이해할 수 없어 기계어로 번역할 수 없을 때 발생한다.
- 컴파일 에러 또는 개발자의 포인터 사용 미숙으로 일부 코드 영역이 깨지는 것이 원인이다.
- Undefined exception이 발생하면 다음 과정을 수행한다.
- SPSR_undef 레지스터에 CPSR 값을 저장(백업)한다.
- CPSR 레지스터 값을 변경한다.
- CPSR[5] =
0 : ARM 명령어 체제로 전환
(∵THUMB는 CPSR 제어 불가해 exception 처리 불가능하므로)
- CPSR[7] =
1 : IRQ를 비활성화 한다.
- CPSR[4:0] =
11011 : Undef mode로 전환한다.
- R14(LR_undef) 레지스터에 PC값을 저장한다.
- PC를 지정된 주소(
0x0000_0004)으로 바꿔 점프한다.
- Undefined exception 또한 decode 단계에서 발생하므로 PC값은 exception이 발생한 곳의
0x4번지 아래를 가리키고 있다.
3.3.2. Exception Debugging
이번 절에서는 위에서 다룬 3가지 exception의 실제 사례를 통해 디버깅 하는 방법을 살펴보자.
- Data Abort exception
- 예제 1. Alignment fault
- HW 디버거를 통해 레지스터 윈도우를 확인하니 abort mode로 진입했음을 알 수 있다.
- 링크 레지스터(R14) 값 -
0x8을 해보니 STR R3, [R2]명령어를 확인할 수 있다.
- R2가 가리키는 주소에 R3 값을 write 하는 명령어이고, R2값은
0x3000_0002이므로 xxxx_xxxx나 DEAD_DEAD가 아니니 존재하는 주소인데도 불구하고 왜 data abort가 발생한걸까?
- 소스코드 라인을 보니
*one = 0x0304를 수행하다가 abort가 발생했다.
- 변수
one은 unsigned int *형이므로 ARM 컴파일러는 4-byte 단위(0x0, 0x4, 0x8, 0xC)로 할당할 것이다.
- 그런데 소스코드를 보니
one의 선언 및 초기화 때 unsigned int* one = (unsigned int *) 0x30000002로 끝이 2로 끝나는 주소를 할당했음을 알 수 있다. 즉, 주소의 alignment가 맞지 않아서 ‘alignment fault’가 발생한 것이다.
- 이런 경우 컴파일 할 때 에러는 발생하지 않지만 warning 메시지는 나온다.
- ARM9 이후로 형 선언과 주소 할당이 맞지 않을 때 exception 처리를 하는 ‘Alignment Fault Checking Register’ 레지스터(CP15의 C가 있다. 개발자는 이 레지스터를 활성화/ 비활성화 해서 alignment fault를 무시할지 안 할지 설정 할 수 있다.
- 예제 2. Permission fault
- HW 디버거를 통해 레지스터 윈도우를 확인하니 abort mode로 진입했음을 알 수 있다.
- 링크 레지스터(R14) 값 -
0x8을 해보니 이번에도 STR R3, [R2]명령어 때문에 abort가 발생했음을 알 수 있다.
- 그러나, 이번에는 변수
star를 0x3000_0A60으로 alignment도 잘 맞춰 선언했고 중간에 *star = 0x2008로 데이터값을 넣는것도 문제가 없는데 왜 이때 abort가 발생했을까?
- SPSR을 확인해보자. SPSR이
0x6000_00D0으로 mode bit 부분인 SPSR[4:0]이 10000이므로 0x10인 USR(유저)모드임을 알 수 있다.
- USR에서 abort가 난 경우 data abort를 의심해볼 수 있는데, 일반적으로 MMU에 의한 권한과 밀접관 관계가 있다.
- 권한을 확인해보기 위해 MMU page table을 덤프해보자.
C: 3000_0000 — 300F_FFFF | A: 3000_0000 — 300F_FFFF | 00 | 0010_0000 | P: readwrite, U: readonly문장을 확인해보니, 변수 star이 저장된 주소 영역이 USR모드에서는 읽기전용으로 권한 설정이 된 것을 알 수 있다.
- ARM core가 write 할 수 없는 주소에 write하려고 시도했으니 data abort가 발생한 것이다.
MMU_SetMPT()함수를 사용해서 권한을 readwrite(RW_CNB)로 바꾸던지, 변수 star를 읽고쓰기가 모두 가능한 주소로 선언해서 해결할 수 있다.
- Undefined Exception
- 잘못된 포인터 사용으로 인한 일부 코드 영역 깨짐
- ([※] 원본 책 기준 이 부분 코드에 오타가 많습니다. 주의하세요!)
- HW 디버거를 통해 레지스터 윈도우를 확인하니 UND mode로 진입했음을 알 수 있다.
- 링크 레지스터(R14) 값 -
0x4를 하니 STC P0, C0, [R0]명령어 때문에 exception이 발생했음을 알 수 있다.
- STC 명령어는 Co-processor의 레지스터 값을 SDRAM에 저장하는 명령어이고, 단독으로 사용할 수 없는 명령어이므로 undefined exception이 발생한 것이다.
- [※] STC(Store Co-processor Register to SDRAM) 명령어는
STC Pn, Cn, address꼴로 사용됩니다. n번 co-processor의 n번 레지스터 값을 SDRAM의 지정된 주소에 저장하는 명령어입니다.
- [※] 하지만, ARM은 Co-processor 관련해서 MCR, MRC 명령어 2개만 표준 명령어로 지원합니다. 따라서 디버깅 용도를 제외하고 STC 명령어를 사용하면 undefined exception이 발생합니다.
- 따라서, 어떤 이유로
0x3000_0168번지에 잘못된 값이 write 돼 코드가 깨져 STC 명령어로 바뀐 것이라고 유추할 수 있다.
- HW 디버거를 이용해서
0x3000_0168번지에 write on-chip breakpoint를 걸어보자.
- Write on-chip breakpoint로 시스템이 멈췄을 때 PC값 -
0x8을 한 주소로 가보니 STR R3, [R2]명령어를 확인할 수 있다. R3는 0xED00_0000이, R2는 0x3000_0168을 갖고 있으므로 R2가 가리키는 주소에 잘못된 값인 R3를 저장하면서 해당 주소의 명령어가 오염된 것이다.
0x3000_0168번지로 가보니 원래 명령어는 STR R3, [R11, #-0x14]임을 확인할 수 있다.
- ([※] 예제2도 예제1과 거의 똑같은 시나리오라 스킵하겠습니다.)
- Reset exception
- 순간적으로 MCU에 충분한 전압이 인가되지 않는 경우의 리셋
- 보통 PMIC 관련된 코드에서 오류가 있을 가능성이 높다.
- 본인이 작성한 코드가 아니라면, BSP 제공 업체에 문의해서 해결한다.
- 해결하기 까다로운 exception이다. 왜냐하면 이미 MCU가 리셋돼 core의 레지스터 값이 남은게 없기 때문이다. 따라서 HW 디버거도 무용지물이 되기 때문에 디버깅하기 어렵다.
- Watchdog Timer에 의한 리셋
- Watchdog timer란, 일정시간마다 주기적으로 watchdog 레지스터에 일정한 값을 설정하지 않을 때 문제가 발생했다고 판단해서 타겟을 자동으로 리셋시키는 timer를 말한다.
- 따라서 리셋되기 전에 일정시간마다 watchdog timer가 초기화되도록 미리 코드를 만들어야 한다.
- Watchdog timer에 의한 리셋이 발생하면, 시스템은 무조건
0x0000_0000 또는 0xFFFF_0000으로 점프한다. ([※]High address, low address에 대해서는 위에 설명드렸습니다.)
- SPSR_rst에 CPSR 값을 저장한다.
- CPSR[5] =
0, CPSR[7:6] = 1 (IRQ/ FIQ 비활성화)
- R14에 PC 값 저장한다.
- PC를
0x0000_0000으로 설정 후 점프한다.
- 사람 손이 잘 닿지 않는 곳에 있는 임베디드 시스템의 경우 watichdog timer가 큰 도움이 된다.