20001060: 0000534d .word 0x0000534d @ memset 20001064: 0000434d .word 0x0000434d @ memcpy 20001068: 00003453 .word 0x00003453 @ memset4 2000106c: 00003443 .word 0x00003443 @ memcpy44What I think happens is that it performs the lookups for each of these during initialization, replacing each with a ROM address that will work just fine when placed in a register for the "bx" instruction.
10001d98 <__wrap___aeabi_memcpy>: 10001d98: 4b01 ldr r3, [pc, #4] @ (10001da0 <__wrap___aeabi_memcpy+0x8>) 10001d9a: 685b ldr r3, [r3, #4] 10001d9c: 4718 bx r3 10001d9e: 0000 .short 0x0000 10001da0: 20001060 .word 0x20001060How exactly does the [pc, #4] address work? Note that the disassembler tells us that this yields 0x10001da0. For this to work, the PC must be 0x1001d9c, which seems surprising.
Some digging reveals the statement that in thumb state (T32) (and the RP2040 always and only runs in thumb state), the value of the PC is the address of the current instruction plus 4. This is because of the ARM processor pipelining. This explaims how the above code works and why the 2 byte short needs to be there as padding.
If we were in A32 state using PC relative addressing, the PC value would be the address of the current instruction plus 8!
10001d7c <__aeabi_mem_init>: 10001d7c: 4801 ldr r0, [pc, #4] @ (10001d84 <__aeabi_mem_init+0x8>) 10001d7e: 2104 movs r1, #4 10001d80: 4b01 ldr r3, [pc, #4] @ (10001d88 <__aeabi_mem_init+0xc>) 10001d82: 4718 bx r3 10001d84: 20001060 .word 0x20001060 10001d88: 100018b5 .word 0x100018b5This loads the address of the table into r0, a count of 4 into r1, then makes a call to 0x100018b5, which is rom_funcs_lookup() called in thumb mode. I am willing to take this as the code I predicted.
The next game would be to find out just how and where this gets called during initialization. Sorting this out using the disassembly is not satisfying. It would be better to find the SDK sources and study them.
Looking for "aeabi_mem_init", I find this in src/rp2_common/pico_mem_ops/mem_ops_aeabi.S
If you are this far into all this, the file is fairly short and worth a peek. The relevant parts follow. Note that it always sets up these 4 functions and the wrappers for them.
.equ MEM_FUNC_COUNT, 4
.align 2
aeabi_mem_funcs:
.word ROM_FUNC_MEMSET
.word ROM_FUNC_MEMCPY
.word ROM_FUNC_MEMSET4
.word ROM_FUNC_MEMCPY44
aeabi_mem_funcs_end:
.section .text
regular_func __aeabi_mem_init
ldr r0, =aeabi_mem_funcs
movs r1, #MEM_FUNC_COUNT
ldr r3, =rom_funcs_lookup
bx r3
Along with this, we have the following definitions in
src/rp2_common/pico_bootrom/include/pico/bootrom.h
#define ROM_FUNC_MEMSET ROM_TABLE_CODE('M', 'S')
#define ROM_FUNC_MEMSET4 ROM_TABLE_CODE('S', '4')
#define ROM_FUNC_MEMCPY ROM_TABLE_CODE('M', 'C')
#define ROM_FUNC_MEMCPY44 ROM_TABLE_CODE('C', '4')
Looking for "rom_funcs_lookup", I find this in src/rp2_common/pico_bootrom/bootrom.c
This is a very short file. And in C! We saw this in our disassembly, but it is all but trivial to understand in C:
bool rom_funcs_lookup(uint32_t *table, unsigned int count) {
bool ok = true;
for (unsigned int i = 0; i < count; i++) {
table[i] = (uintptr_t) rom_func_lookup(table[i]);
if (!table[i]) ok = false;
}
return ok;
}
Indeed, it does as we thought and replaces each table entry with the lookup value!
The all important lookup is done by this function:
void *rom_func_lookup(uint32_t code) {
return rom_func_lookup_inline(code);
}
Along with this definition in src/rp2_common/pico_bootrom/include/pico/bootrom.h
// Lookup a bootrom function by code. This method is forcibly inlined into the caller for FLASH/RAM sensitive code usage
// return a pointer to the function, or NULL if the code does not match any bootrom function
static __force_inline void *rom_func_lookup_inline(uint32_t code) {
rom_table_lookup_fn rom_table_lookup = (rom_table_lookup_fn) rom_hword_as_ptr(0x18);
uint16_t *func_table = (uint16_t *) rom_hword_as_ptr(0x14);
return rom_table_lookup(func_table, code);
}
Notice some interesting C trickery here to use the halfword (2 byte) pointer at
the offset 0x18 in the ROM as a function pointer.
Similar tricker is used with the halfword pointer at 0x14 which has the pointer
to the lookup table in the ROM.
__pre_init __aeabi_mem_init, 00001I have seen this sort of thing before. My bet is that the linker is being used to collect a variety of things that are tagged like this with __pre_init. Indeed, grep finds four things for us that are tagged in this way:
./src/rp2_common/pico_bit_ops/bit_ops_aeabi.S:__pre_init __aeabi_bits_init, 00010 ./src/rp2_common/pico_float/float_aeabi.S:__pre_init __aeabi_float_init, 00020 ./src/rp2_common/pico_mem_ops/mem_ops_aeabi.S:__pre_init __aeabi_mem_init, 00001 ./src/rp2_common/pico_double/double_aeabi.S:__pre_init __aeabi_double_init, 00020For the macro itself, we need to look at src/rp2_common/pico_platform/include/pico/asm_helper.S
.macro __pre_init func, priority_string .section .preinit_array.\priority_string .align 2 .word \func .endm
So a 2 byte pointer is generated and placed in a section called ".preinit_array.PRI" where "PRI" is the priority string. The idea with the priority string is to cause these things to get ordered so that certain things get called first as the list is processed. There is related stuff in the linker scripts, but the support code is in src/rp2_common/pico_runtime/runtime.c ---
// Start and end points of the constructor list,
// defined by the linker script.
extern void (*__preinit_array_start)(void);
extern void (*__preinit_array_end)(void);
// Call each function in the list.
// We have to take the address of the symbols, as __preinit_array_start *is*
// the first function pointer, not the address of it.
for (void (**p)(void) = &__preinit_array_start; p < &__preinit_array_end; ++p) {
(*p)();
}
The above code is in the function runtime_init() and is simple enough.
It runs through the list calling each function.
The functions neither take arguments nor return values.
Have any comments? Questions? Drop me a line!
Tom's software pages / tom@mmto.org