oh, I misunderstood the situation then.
I thought your main application code were running in the ram, only the init section were running directly from flash and it simply relocated the application to the ram, sort of like a bootloader.
but in reality, the majority of the code executes in-place from flash, only a small subset of functions (which I would refer to as the flash "driver" module) needs to be run in ram.
this is indeed a tricker situation, and my previous reply doesn't apply.
your application is essentially splitted into isolated execution domains, and the current toolchain doesn't directly support this use case. it's kind of like your application has two "address spaces", but not really.
I've no personal experience for such scenarios, but I would layout the approach how I would tackle the problem personally. just a disclaimer, this is 100% hypothetical, I don't know the details, or if it will work at all. you need to figure the details out yourself, for example, by asking a chatbot.
first, I would build a separate module for the flash driver that must runs in ram, this binary should be completely self-contained without external dependencies, and it uses a different linker script from the main application, so the symbols are placed at the ram address.
then for the main application, I would include the flash driver as a binary blob, and memcpy it to the correct ram address when initialization. I would probably strip the flash driver binary to reduce its size before embedding it into the main application.
alternatively, I can extract the loadable sections from the (fully linked, and self-contained) flash driver module, and include the sections instead of the the whole binary.
now here's the trick that makes this all work, i.e. how the symbols exposed by the flash driver are resolved? answer is to use the symbol table of the linked driver binary!
$ ld $(objects) --just-symbols=my_flash_dirver.elf -T my_app.ld -o my_app.elf
the --just-symbols command line option is available in the GNU linker (thus also the LLVM linker, because it is compatible with the GNU linker).
I would NOT use the #[link_section] approach like what you are doing currently. the problem is, there's no reliable way to guarantee your function is "self-contained" in the same section, i.e. it does not call external functions such as those from core, since core contains methods for many "built-in" types, and many lang items, which you cannot get rid of, otherwise the language could not work.
as an example, I would assume the write_volatile() function, as you mentioned, should normally be lowered into a single LLVM store primitive, but in debug builds, the "shim" function in core may not be inlined, and the ub_check assertions may not be elided, so you end up calling an external function instead, and it's completely out of your control.