Hot/cold linking is a strategy used by PROS for splitting a program into two binary packages to reduce upload size.
- The cold image (cold.bin) contains libraries (libpros, liblvgl, libstdc++, etc...). The cold image is uploaded once, or whenever it is modified.
- The hot image (hot.bin) contains user functions and links against the cold image. The hot image is uploaded every time the program is built, and thus must be optimized for size.
Each image has its own address offset in memory:
- Cold memory begins at address
0x03800000
. This is the actual entrypoint of a user program's memory space and is the first thing that is executed by CPU0. - Hot memory begins at address
0x07800000
. - Source: https://github.com/purduesigbots/pros/blob/1e7513d4f110d2eac625b6300dbbb8c086ab6c0c/firmware/v5-hot.ld#L15
The hot_table
is a C struct stored in the cold image. It stores function pointers to user functions in the hot image. It looks roughly like this:
struct hot_table {
// Symbols containing compiler metadata.
char const* compile_timestamp;
char const* compile_directory;
// Symbols used by GCC for stack unwinding.
void* __exidx_start;
void* __exidx_end;
// Here's the important part:
struct {
// These just call their _cpp counterparts.
void (*autonomous)();
void (*initialize)();
void (*opcontrol)();
void (*disabled)();
void (*competition_initialize)();
// These are called by the system daemon after everything is initialized if a hot_table is available.
// https://github.com/purduesigbots/pros/blob/1e7513d4f110d2eac625b6300dbbb8c086ab6c0c/src/system/user_functions.c#L38
void (*cpp_autonomous)();
void (*cpp_initialize)();
void (*cpp_opcontrol)();
void (*cpp_disabled)();
void (*cpp_competition_initialize)();
} functions;
};
You can think of hot_table
as the "glue" between the hot and cold packages. Since the cold package contains kernel code and the kernel needs to call user functions as an entrypoint to actual user code, it needs hot_table
to access and call user functions from from the hot package.
Great. We have a struct that's supposed to store some function pointers to functions stored in the hot image's address space. How do we access these functions stored in the hot image to fill the struct on the cold image?
In the .hot_init
section of the hot package is a function called install_hot_table
. This function is what actually creates an instance of the hot_table
struct on program startup. The function signature looks like this:
__attribute__((section(".hot_init"))) void install_hot_table(struct hot_table* const tbl);
Removing all the macro garbage reveals a system roughly like this:
// Symbols added by the compiler
extern char const* _PROS_COMPILE_TIMESTAMP;
extern char const* _PROS_COMPILE_DIRECTORY;
extern const int _PROS_COMPILE_TIMESTAMP_INT;
extern unsigned __exidx_start;
extern unsigned __exidx_end;
// Forward declarations to the user functions in the acutal hot package
extern void autonomous();
extern void initialize();
extern void opcontrol();
extern void disabled();
extern void competition_initialize();
extern void cpp_autonomous();
extern void cpp_initialize();
extern void cpp_opcontrol();
extern void cpp_disabled();
extern void cpp_competition_initialize();
...
// Let's make a struct!
__attribute__((section(".hot_init"))) void install_hot_table(struct hot_table* const tbl) {
// Boring compiler stuff
tbl->compile_timestamp = _PROS_COMPILE_TIMESTAMP;
tbl->compile_directory = _PROS_COMPILE_DIRECTORY;
tbl->__exidx_start = &__exidx_start;
tbl->__exidx_end = &__exidx_end;
// There's our functions...
tbl->functions.autonomous = autonomous;
tbl->functions.initialize = initialize;
tbl->functions.opcontrol = opcontrol;
tbl->functions.disabled = disabled;
tbl->functions.competition_initialize = competition_initialize;
tbl->functions.cpp_autonomous = cpp_autonomous;
tbl->functions.cpp_initialize = cpp_initialize;
tbl->functions.cpp_opcontrol = cpp_opcontrol;
tbl->functions.cpp_disabled = cpp_disabled;
tbl->functions.cpp_competition_initialize = cpp_competition_initialize;
}
However, this function is only invoked by invoke_install_hot_table under the following condition:
void invoke_install_hot_table() {
// Check for.. something?
if (vexSystemLinkAddrGet() == (uint32_t)0x03800000 && MAGIC_ADDR[0] == MAGIC0 && MAGIC_ADDR[1] == MAGIC1) {
install_hot_table(HOT_TABLE);
} else {
// Can't install the hot table, zero it out instead.
memset(HOT_TABLE, 0, sizeof(*HOT_TABLE));
}
}
So, the requirements for initializing the hot table are:
vexSystemLinkAddrGet()
must be0x03800000
(whatever this entails)MAGIC_ADDR[0]
must beMAGIC0
andMAGIC_ADDR[1]
must beMAGIC1
Looking the top of the file reveals these two definitions for the magic numbers we saw previously:
#define MAGIC0 0x52616368
#define MAGIC1 0x8CEF7310
These two numbers are essentially "sanity checks" to ensure that the hot image exists in the address space of the current user program. They're set in the hot image's .hot_magic
section (located at 0x078
) like this:
__attribute__((section(".hot_magic"))) uint32_t MAGIC[] = {MAGIC0, MAGIC1};
So PROS creates this value pair containing the magic numbers in hot memory. In the cold image, it then checks that these numbers exist at a pointer to the address that MAGIC
is expected to be at.
uint32_t const volatile* const MAGIC_ADDR = MAGIC;
Assuming that the hot image exists, MAGIC_ADDR[0]
will point to MAGIC0
and MAGIC_ADDR[1]
will point to MAGIC1
This SDK call is pretty weird. PROS checks that its value is 0x038
, which is the start of a typical user program and cold memory. The leading theory is that is returns the starting address of whatever binary it's called from. It's essentially a check that invoke_install_hot_table
is being called from the cold package and nowhere else.