Bypassing the Bootloader in Embedded Development in Visual Studio Code
You’re being a good engineer and have this super awesome system that allows you to do remote code updates with code signing, etc. on your embedded processor. That’s awesome. Here’s the problem. You’re trying to do development on this system. Guess what? When you directly flash the code, the bootloader refuses to acknowledge it because it doesn’t have the CRC or code signing. Now you think you’ve got a build problem or worse! Options?
- Take the time to push every build through the preparation process for the bootloader, load it onto the device, then attach to the running code. How long is that going to take you? You may start to feel like a hardware engineer instead of a software engineer.
- Just flash it directly with no bootloader. That changes things which means you may not be able to replicate that world-ending bug that was just reported. You’ve also got to do a separate compile now for the production code since it will live in a different memory location. Oh, you’ve got to put the correct version of the bootloader back on for testing at some future point.
If only you could just start running the code at the right address… There is a way and we’ve found how to do it in VS Code when using a J-Link! You should be able to port this to your particular case. This is good for most ARM Cortex-M processors.
What We’re Doing (IDE Independent)
There’s three things that need to be done to start executing you code:
- Set the stack pointer (SP)
- Set the program counter (PC)
- Set the Vector Table Offset Register (VTOR)
Once these three things are done we’re off to the races. When we do this, we need to have the MCU halted so that we are not executing while setting these registers.
How to Find It
Arm is kind enough to mandate the location of the SP and PC on boot. They are the first two words in the vector table. Here’s an example from an STM32 which is in assembly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /****************************************************************************** * * The minimal vector table for a Cortex M. Note that the proper constructs * must be placed on this to ensure that it ends up at physical address * 0x0000.0000. * *******************************************************************************/ .section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word MemManage_Handler /**** The rest of the Vector table is cut since it is not important to this discussion ****/ |
For many other vendors, this is a C structure. Below is an example for an NXP Cortex-M processor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | //***************************************************************************** // The vector table. // This relies on the linker script to place at correct location in memory. //***************************************************************************** extern void (* const g_pfnVectors[])(void); extern void * __Vectors __attribute__ ((alias ("g_pfnVectors"))); __attribute__ ((used, section(".isr_vector"))) void (* const g_pfnVectors[])(void) = { // Core Level - CM33 &_vStackTop, // The initial stack pointer ResetISR, // The reset handler NMI_Handler, // The NMI handler HardFault_Handler, // The hard fault handler /**** The rest of the Vector table is cut since it is not important to this discussion ****/ |
In both code snippets the highlighted rows are the stack pointer and reset function, which is the first thing called when the device reboots. That takes care of two things, but what about the Vector Table Offset Register? The device by default reboots to 0x0000 0000. We have a bootloader so our software has to have this placed elsewhere. One more thing, where the VTOR is in memory is not guaranteed so you must find it in the processor documentation. Once we have all of this, we can set our registers:
- VTOR = <Address vector table is at in Flash RAM>
- SP = *VTOR // We are storing the first word of the VTOR here
- PC = *(VTOR + 4 bytes) //We are storing the address of the reset handler/ISR here
How to Implement in GDB
There are five six things we need to do in GDB to make this work:
monitor halt
Stop the MCUset <em>(void</em>*)0xE000ED08 = *(void *)0x8020000
VTOR Register to provide vector table offset to.set $sp = **(void **)0x8020000
Setting stack pointer to pointer at addressset $pc =**(void **)0x8020004
Setting program counter to pointer at addresstbreak main
Set breakpoint to stop on main, this is bonus pointscontinue
Run to breakpoint
Visual Studio Code Configuration
To implement the GDB commands in VS Code requires a few steps. First you need to make sure you have the Cortex-Debug plugin installed. In the launch.json file your configuration needs to be altered to have the entries in the highlighted rows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | "configurations": [ { "name": "Some Launch Configuration", "executable": "${workspaceFolder}/path/to/binary.elf", "cwd": "${workspaceFolder}", "request": "launch", "type": "cortex-debug", "serverpath": "C:/Program Files/SEGGER/JLink/JLinkGDBServerCL.exe", "servertype": "jlink", "interface": "swd", "armToolchainPath": "C:/arm/bin", "device": "STM32H753II", "svdFile":"${workspaceFolder}/STM32H7x3.svd", "rtos": "FreeRTOS", "showDevDebugOutput":"parsed", "postStartSessionCommands": [ //"runToEntryPoint" cannot be set or it will take precedence over this "monitor halt", "set *(void**)0xE000ED08 = *(void *)0x8020000", //VTOR Register to provide vector table offset to. "set $sp = **(void **)0x8020000", //Setting stack pointer to pointer at address "set $pc = **(void **)0x8020004", //Setting program counter to pointer at address "tbreak main", //Set breakpoint to stop on main "continue" //Run to breakpoint ], "postRestartCommands": [ "monitor halt", "set *(void**)0xE000ED08 = *(void *)0x8020000", //VTOR Register to provide vector table offset to. "set $sp = **(void **)0x8020000", //Setting stack pointer to pointer at address "set $pc = **(void **)0x8020004", //Setting program counter to pointer at address "tbreak main", //Set breakpoint to stop on main ], "breakAfterReset": false, }, |
You will need to change the values for your particular MCU and where you have your vector table stored. In this example the vector table is stored at 0x8020000 and the VTOR is at 0xE000ED08. **Note** Check your documentation to make sure the VTOR is referenced to 0x0000 0000. If it’s not you have to take that into account for the value you give it.
You also need to make sure that runToEntryPoint is commented out of your configuration or it will supersede postStartSessionCommands.
We hope this is useful to you in the future!