For my answer, I am going to assume you are running rocket-chip in a Verilator simulation as described by the README (cd emulator; make run). In this scenario, you are running rocket-chip tethered to the front-end server (riscv-fesvr). Any syscalls such as a printf are proxied and executed on your host machine.
1) Rocket-chip "boots up" by executing code out of the boot rom. There's a single line of code that says "spin here forever." It does so faithfully.
2) riscv-fesvr uses the Debug Transport Module (DTM), described by the External Debug Specification, to interrupt the rocket core and inject small snippets of code to execute on rocket in "debug" mode, a higher privileged mode than machine mode. The test program is loaded one word at a time over the DTM. Once the test program has been loaded into rocket's memory space, the DTM then tells rocket to jump into the beginning of the program and we can finally begin executing the actual test program.
3) when rocket is finished running a test program, it writes an exit code to the "tohost" memory location that resides in rocket's memory space. The exit code is stored in memory as (exitcode << 1 | 1). The riscv-fesvr periodically polls the DTM to read the "tohost" memory location. If it sees a 1 in the lsb, it ends execution of the emulation and reports the exit code. Notice that this polling of the DTM is a destructive, in-band operation. So for example, you shouldn't use the default Verilator rocket-chip emulator to benchmark rocket!!
4) If the riscv-fesvr polls the "tohost" memory location and see a non-zero value, but a 0 in the lsb, it recognizes that as a memory location that is holding a proxied syscall (the cleverness here is that memory locations are always half-word aligned, so we can do double duty here and share the same communication mechanism for exiting simulations and proxying syscalls). A printf can be handled by the riscv-fesvr reading rocket's memory as specified by syscall stored to the tohost memory location.
5) At Berkeley, we execute rocket-chip in a tethered mode on Zynq FPGAs, but we've added a Tethered Serial Interface (TSI) to make loading programs and handling proxied syscalls higher performance (so rocket can use the out-of-band TSI instead of the in-band DTM). Some users of rocket-chip execute rocket in a self-hosting mode on FPGAs, in which case I guess you plug in a monitor directly and see what happens?
Apologies if any of this is incorrect; stuff changes really quickly around here. =)
-Chris