Debugging ARM without a Debugger 1: Use of Asserts
This is my first post in the series Debugging ARM without a Debugger.
This is an excerpt from my debugging techniques document for Real-time Programming. These techniques are written in the context of writing a QNX-like real-time microkernel and a model train controller on a ARMv4 (ARM920T, Technologic TS-7200). The source code is located here. My teammate (Pavel Bakhilau) and I are the authors of the code.
Failing fast is an extremely useful property when programming in C. For example, problems with pointers are much easier to debug if you know exactly when an invalid pointer value is passed into a function. Here are few tips for asserting effectively:
There is no such thing as putting too much asserts.
CPU power used for asserts will almost never cause a critical performance issue [in this course]. You can disable them when you know your code is perfect. Verify pointers every pointer dereference.
Assert pointers more aggressively.
Do not just check for NULLs. We know more about the pointer addresses. We know that the pointer address is limited by the size of the memory. As well, from the linker script, we can even deduce more information. For example, we know that normally, we would not want to dereference anything below the address 0x218000 because that is where the kernel is loaded. Similarly, we can figure out what memory region is text and data.
Remove all uncertainties.
Turn off interrupts as soon as possible in the assert macro. When things go wrong, you want to stop the program execution (and the trains) right away. If you do not turn off interrupts, a context switch might occur to other task and you might not be able to come back ever to stop and display what went wrong.
Print as much information as possible.
Make an assert macro that resembles printf and print as much contextual information as possible. When you have no debugger, rebooting and reproducing can be really time-consuming. 1.5 months is a very short time to build an operating system from scratch so use it wisely.
e.g. ASSERT(condition, “oops! var1:%d, var2:%x, var3:%s”, var1, var2, var3);
Example
Here’s a short snippet of ASSERT macro. It has evolved over 3 months and it looks really dirty but it works. (source)
typedef uint volatile * volatile vmemptr;
#define VMEM(x) (*(vmemptr)(x))
void bwprintf(int channel, char *fmt, ...);
#define READ_REGISTER(var) __asm volatile("mov %[" TOSTRING(var) "], " TOSTRING(var) "\n\t" : [var] "=r" (var))
#define READ_CPSR(var) __asm("mrs %[mode], cpsr" "\n\t" "and %[mode], %[mode], #0x1f" "\n\t" : [mode] "=r" (var))
void print_stack_trace(uint fp, int clearscreen);
void td_print_crash_dump();
int MyTid();
#if ASSERT_ENABLED
#define ASSERT(X, ...) { \
if (!(X)) { \
VMEM(VIC1 + INTENCLR_OFFSET) = ~0; /* turn off the vectored interrupt controllers */ \
VMEM(VIC2 + INTENCLR_OFFSET) = ~0; \
int cpsr; READ_CPSR(cpsr); \
int inusermode = ((cpsr & 0x1f) == 0x10); int tid = inusermode ? MyTid() : -1; \
bwprintf(0, "%c", 0x61); /* emergency shutdown of the train */ \
int fp, lr, pc; READ_REGISTER(fp); READ_REGISTER(lr); READ_REGISTER(pc); \
bwprintf(1, "\x1B[1;1H" "\x1B[1K"); \
bwprintf(1, "assertion failed in file " __FILE__ " line:" TOSTRING(__LINE__) " lr: %x pc: %x, tid: %d" CRLF, lr, pc, tid); \
bwprintf(1, "[%s] ", __func__); \
bwprintf(1, __VA_ARGS__); \
bwprintf(1, "\n"); /* if in usermode ask kernel for crashdump, otherwise print it directly */ \
if (inusermode) { __asm("swi 12\n\t");} else { td_print_crash_dump(); } \
bwprintf(1, "\x1B[1K"); \
print_stack_trace(fp, 0); \
die(); \
} \
}
#else
#define ASSERT(X, ...)
#endif
That’s it for today.