March 7, 2026

Adventures with memcpy and the RP2040 - the answer

Thinking this over, I got the idea that the code is not actually using this table verbatim:
20001060:   0000534d    .word   0x0000534d	@ memset
20001064:   0000434d    .word   0x0000434d	@ memcpy
20001068:   00003453    .word   0x00003453	@ memset4
2000106c:   00003443    .word   0x00003443	@ memcpy44
What 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.
Let's see if we can find the code that does this in our disassembly.

A short note on PC relative addressing

Consider this disassembled code:
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   0x20001060
How 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!

Back to the original topic

I search on the string "20001060" -- it is referenced in __aeabi_mem_init() which is suggestive. Here is the code:
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   0x100018b5
This 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.

Look at the SDK

I have something called "pico-sdk" on my system from 2023. I use grep to search in it and find:

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.

So where does aeabi_mem_init get called

With the SDK sources, we don't need to go looking for references to an address, we can just grep for this string. The only thing we find is the following line in mem_ops_aeabi.S where the function is defined.
__pre_init __aeabi_mem_init, 00001
I 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, 00020
For 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