One would usually learn about this kind of stuff when they study an architecture course in college and dive into the sections of an executable. To keep this short, there are multiple sections inside of an executable binary where your compiler and linker will place needed variables depending on content. The linker uses a memory map to define these spaces in the executable. An example of such a space is the block starting symbol address, also called .bss. This is an area of the executable that gets mapped to memory that contains statically allocated variables, so, for example, when you declare a static variable, this location in the executable is where those variables reside when placed in memory. There are many such sections inside of an executable for other kinds of variables, like constants and such, but that's not important.
What is important is that we can use a loader script with our linker to define our own areas of the executable, map them to specific memory addresses, and then put variables into them. You can do this by simply adding a few options to your linker settings in your makefile, and defining variables in your source code accordingly.
This is the line I use in my makefiles to call the linker when linking my dreamcast .o objects into my elf after compilation:
Code: Select all
$(KOS_CCPLUS) $(KOS_LDFLAGS) -o ./dreamcast-debug.elf $^ -T link.ld $(CXXFLAGS) $(LIBS) $(KOS_LIBS)
Code: Select all
SECTIONS
{
.mySegment 0x8C200000 : {KEEP(*(.Section1))}
.mySegment 0x8C204000 : {KEEP(*(.Section2))}
}
We define where our variables are allocated in our source code using gcc reserved keywords, like so:
Code: Select all
int Variable1 __attribute__((section(".Section1"))) = 0x9ABCDEF0;
int Variable2 __attribute__((section(".Section2"))) = 0x01010101;
Code: Select all
printf("adr %p\n", (void*)&Variable1);
printf("val 0x%x\n", Variable1);
Variable1 = 0;
printf("val 0x%x\n", Variable1);
printf("adr %p\n", (void*)&Variable2);
printf("val 0x%x\n", Variable2);
Variable2 = 0;
printf("val 0x%x\n", Variable2);
And when we run the program, it confirms our variables are in the right location:
You can actually take this a bit further to ensure cache cohesion: if you define a variable in the range 0xA0000000-0xBFFFFFFF instead of 0x80000000-0x9FFFFFFF, you'll get a variable which is not cached when accessed. The Dreamcast memory map defines this area as "Privileged mode only, no cache." Just like in the previous area, the range of 0x0C000000 to 0x0FFFFFFF is the location of system ram. Keep in mind that this is the same system ram as before, just mirrored, so, for example, 0x8C200000 and 0xAC200000 point to the same area of memory. The only difference is accessing the variable through 0xAC200000 will skip putting it into ram. A variable must still be defined at 0xAC200000. We do this by creating a segment in our link.ld:
Code: Select all
.mySegment3 0xAC206000 : {KEEP(*(.NonCachedSection))}
Code: Select all
int NonCachedVariable __attribute__((section(".NonCachedSection"))) = 0xDEADBEEF;
Hopefully this helps someone out there more properly manage their cache cohesion!