ginge works well not because more is known about the platforms it's emulating but because it's starting from Linux. This gives you a lot of flexibility that you don't get starting with any old code, and is a big factor as to why user mode emulation in qemu is so much faster than system mode emulation. For instance Linux apps generally don't care that much about where their code, data, stack, heap, etc are allocated, they just care that the memory is valid and there's enough of it. So you're walled off a lot by the high level information present in the executable and the syscalls to the OS itself (and shared libraries, potentially). The emulated code is always in user mode and is fairly safe.
ginge does do some hardware emulation but it's mostly pretty constrained; GP2X and Wiz stuff tends to write some hardware registers to setup video modes then gets a pointer to a framebuffer. Nothing like giving big command lists to graphics accelerators. And even then it's still gated by access to /dev/mem, so you only have to trap memory supplied by /dev/mem instead of trying to trap any old memory access. And the programs it has to worry about are more constrained.. there are just a few dozen at most emulators worth supporting, and the games tend to be ported from other platforms.
Let's look at something like GBA, since it's fairly simple, and since this came up before a lot (remember GP Advance on GP32?). I've made big posts about this before, listing problems/limitations but I don't mind taking a fresh stab at it:
1) You need to default your emulated memory to no-execute in order to determine what's code so you can write protect it. Otherwise you get cache coherency problems with code that's created or modified in RAM. This is a problem for older ARMs that don't have execute protect in their MMU; this is something that was only recently added.
2) You need to mark pages conservatively as write protect if they contain code, and detect modification w/SIGSEGV so you can clear caches. But you end up with false positives where data is modified in the same page as code. On systems with a lot of RAM this might not come up a lot but with something like GBA where there's 32KB of fast RAM it probably happens all the time (with 4KB pages). Even if you do use SIGSEGVs to catch SMC in a dynarec you're still better off there than w/virtualization, because the dynarec can track down to single instruction granularity what's code and what's data. Therefore it can determine in the handler if you really modified code, while a VM has to assume it did and therefore must flush that part of icache/dcache/etc (requires a syscall on ARM = relatively expensive).. on the dynarec, if a write is accessing code pages a lot you can patch it, but on the VM you have more limited ability to do this (more on this later)
3) You can run into problems providing some of the emulated address space. At least some of the emulated code will be working with physical addresses that have to be at fixed locations (and if the emulated machine has/uses virtual addresses they could be anywhere as well, depending on how the OS sets them up). Some addresses are going to simply be unavailable in your emulated space because the OS needs to put kernel, stack, heap, code, shared libraries, etc there. You can mitigate this a bit by messing with the program's link script but there are some things you can't do, or could require using a different OS kernel/kernel options.
4) You have to put the VM code somewhere, preferably where the emulated game CAN'T see it. Pulling off the latter is tricky unless you leverage hardware virtualization (which not everything does, does Medfield even have it?), because if the VM is running in user mode you can't simultaneously protect it and make it runnable.
5) You could have problems patching code to try to work around performance issues.. for instance on ARM branches have a +/-32MB limit. So let's say you did find a good hole in the emulated address space to put your VM code in - if you want to put stubs there that patched code calls this hole needs to be +/-32MB away from the code you're patching. That's why you have less ability to patch code than you do with recompiled code, where you have full control over where you can put that code and the stubs.
6) You need enough hardware virtualization to trap things like returns from interrupts so you can emulate interrupt handlers, and the game needs to think it's in system mode when it isn't really (so you need to trap access to various registers).
7) You have to emulate the game real-time which has several of its own limitations, risks, and implementation headaches that I won't get into just now.