The big problem is the loadable modules, when being built, won’t know where the various pieces of code will be, so it is hard to ‘load’ them. And if the ‘core’ module didn’t need to use them, they might not even be included in the core image.
If I was to build such a system, I would add an API module to the core code that generates a jump table to the routines I want the loadable modules to be able to use, and a matching module that the loadable modules link with that provide the addresses of the entries in the table. You also need to leave ram space open in the linker definitions to have room to load into.
Be aware that unloading a module may tricky, as you need to make sure that nothing is still referencing the module, and it has freed all the resources that it has acquired.
This will NOT give you a shell like interface, just a run time system configuration ability.