On 7/8/2026 6:19 PM, Ziyang Zhang wrote: > Document the dlcall plugin under Example Plugins: what it does, the trusted- > guests and guest_base == 0 constraints, how to load it, and a pointer to > Lorelei, one end-to-end userspace implementation, for the toolchain and a > runnable example. > > Co-authored-by: Kailiang Xu <[email protected]> > Co-authored-by: Mingyuan Xia <[email protected]> > Signed-off-by: Ziyang Zhang <[email protected]> > --- > docs/about/emulation.rst | 202 +++++++++++++++++++++++++++++++++++++++ > 1 file changed, 202 insertions(+) > > diff --git a/docs/about/emulation.rst b/docs/about/emulation.rst > index 3b4c365933..b8fdc88a45 100644 > --- a/docs/about/emulation.rst > +++ b/docs/about/emulation.rst > @@ -1046,6 +1046,208 @@ Count traps > This plugin counts the number of interrupts (asynchronous events), exceptions > (synchronous events) and host calls (e.g. semihosting) per cpu. > > +Dynamic Linking Call > +.................... > + > +``contrib/plugins/dlcall.c`` > + > +This plugin provides a dynamic linking function call interception mechanism > +for linux-user guests: the guest hands a call off to the host, where the > plugin > +runs native code in its place instead of the guest emulating it. Interception > +alone enables several uses, for instance tracing or auditing guest calls. > +One use is acceleration by leveraging the host's native shared libraries. For > +example, a thunk layer can run the stock zlib ``minizip`` utility under > +emulation while forwarding its ``deflate`` calls to the host's native zlib > +library (libz). This avoids emulating those selected library calls > instruction > +by instruction. > + > +The guest issues a reserved "magic" system call (4096 by default, > configurable > +with ``syscall_num=N``) whose first argument selects a pass-through > operation: > +dlopen/dlclose a host library, dlsym a symbol, and invoke a resolved host > +function. The plugin performs the operation on the host and consumes the > +syscall, so the real kernel never sees it. > + > +.. warning:: > + > + Trusted guests only. The guest can load arbitrary host libraries and run > + arbitrary code in the QEMU host process. The plugin is not a sandbox and > + provides no isolation. It also requires ``guest_base == 0`` (qemu-user's > + default), as guest pointers are dereferenced as host addresses with no > + translation. > + > +The plugin intentionally keeps the QEMU side lightweight and knows nothing > +about any particular library or its calling convention. Turning a real > library > +into working thunks, including argument marshalling, callbacks and variadic > +functions, is done entirely in userspace, and any toolchain can implement the > +interface. > + > +Loading the plugin is all that is required from QEMU's side: > + > +.. code-block:: shell > + > + qemu-x86_64 -plugin contrib/plugins/libdlcall.so <guest-program> ... > + > +`Lorelei <https://github.com/rover2024/lorelei>`_ is one end-to-end userspace > +implementation of this: it provides the guest and host runtimes and an > +automated toolchain that generates the thunks from a library's headers, so > guest > +library calls run on the host's native libraries. It supports an x86_64 guest > +running on an x86_64, aarch64 or riscv64 host. > + > +A minimal end-to-end example uses a one-function library, ``libhello.so``, > built > +two ways: the guest build tags its output ``from guest`` and the host build > tags > +it ``from host``. An unmodified guest program ``main`` calls ``hello("world", > +7)``, and the thunk makes that same binary reach the host build in place of > its > +own. The sources live under ``src/``: > + > +.. code-block:: c > + > + /* src/hello.h */ > + #ifdef __cplusplus > + extern "C" { > + #endif > + void hello(const char *name, int lucky); > + #ifdef __cplusplus > + } > + #endif > + > +.. code-block:: c > + > + /* src/hello_guest.c */ > + #include "hello.h" > + #include <stdio.h> > + > + void hello(const char *name, int lucky) > + { > + printf("hello from guest: %s, lucky %d\n", name, lucky); > + fflush(stdout); > + } > + > +.. code-block:: c > + > + /* src/hello_host.c */ > + #include "hello.h" > + #include <stdio.h> > + > + void hello(const char *name, int lucky) > + { > + printf("hello from host: %s, lucky %d\n", name, lucky); > + fflush(stdout); > + } > + > +.. code-block:: c > + > + /* src/main.c */ > + #include "hello.h" > + > + int main(void) > + { > + hello("world", 7); > + return 0; > + } > + > +Lorelei ships a prebuilt toolchain (a "devkit") in its releases. Download the > +one for your host and unpack it: > + > +.. code-block:: shell > + > + # <arch> is your host architecture: x86_64, aarch64 or riscv64 > + wget > https://github.com/rover2024/lorelei/releases/download/v<version>/lorelei-devkit-<arch>-<version>.tar.xz > + tar -xf lorelei-devkit-<arch>-<version>.tar.xz > + DEVKIT=lorelei-devkit-<arch> > + > +Build the guest ``libhello.so`` (x86_64) and the host ``libhello.so`` (this > +host's own architecture), then the guest program. ``main`` links the guest > +library with an rpath, so by default it loads that one: > + > +.. code-block:: shell > + > + mkdir -p build/guest build/host > + $DEVKIT/bin/x86_64-linux-gnu-clang -shared -fPIC -o > build/guest/libhello.so src/hello_guest.c > + cc -shared -fPIC -o build/host/libhello.so src/hello_host.c > + $DEVKIT/bin/x86_64-linux-gnu-clang src/main.c -Isrc -Lbuild/guest -lhello > \ > + -Wl,-rpath,'$ORIGIN' -o build/guest/main > +
Why is rpath needed? We don't want necessarily to find the library relative to where the binary is located. In practice, we'll have: - the binary - folder A containing host library - folder B containing thunks A and B are not related to binary location. > +Run it under qemu. It loads its own guest library: > + > +.. code-block:: shell > + > + qemu-x86_64 build/guest/main > + > +which prints:: > + > + hello from guest: world, lucky 7 > + > +Now generate the thunk from the host ``libhello.so``. This produces a > guest-side > +``libhello.so`` that stands in for the guest build, and a host-side thunk > library > +that dispatches to the host build: > + > +.. code-block:: shell > + > + $DEVKIT/bin/LoreMakeThunk.py --name hello --lib build/host/libhello.so \ > + --header hello.h -o thunks -- -Isrc > + > +Run the same ``main`` under the plugin, with the generated guest > ``libhello.so`` > +ahead of its own on ``LD_LIBRARY_PATH``. The call reaches the host build now, > +from the same unmodified binary: > + > +.. code-block:: shell > + > + LORELEI_THUNK_PATH=thunks \ > + LD_LIBRARY_PATH=$DEVKIT/lib:build/host \ > + qemu-x86_64 -plugin contrib/plugins/libdlcall.so \ > + -E > LD_LIBRARY_PATH=thunks/x86_64/lib/x86_64-LoreGTL:$DEVKIT/x86_64/lib \ > + build/guest/main > + > +which prints:: > + > + hello from host: world, lucky 7 > + > +The guest ``LD_LIBRARY_PATH``, passed with ``-E``, is searched before the > +program's rpath, so its generated ``libhello.so`` replaces the guest build: > + > +* ``thunks/x86_64/lib/x86_64-LoreGTL`` holds the generated guest > ``libhello.so``. > +* ``$DEVKIT/x86_64/lib`` holds the guest runtime support shipped with the > devkit. > + > +The host ``LD_LIBRARY_PATH`` is QEMU's own search path: > + > +* ``$DEVKIT/lib`` holds the host runtime support shipped with the devkit. > +* ``build/host`` holds the host ``libhello.so`` the host thunk dispatches to. > + > +The finished tree:: > + > + src/ > + hello.h > + hello_guest.c > + hello_host.c > + main.c > + build/ > + guest/ > + libhello.so (guest build of the library) > + main (the guest program) > + host/ > + libhello.so (host build of the library) > + thunks/ > + lib/x86_64-LoreHTL/libhello_HTL.so (host thunk) > + x86_64/lib/x86_64-LoreGTL/libhello.so (guest thunk, the drop-in) > + > +See the runnable > +`hello <https://github.com/rover2024/lorelei/tree/main/examples/hello>`_ > +(minimal) and > +`demo <https://github.com/rover2024/lorelei/tree/main/examples/demo>`_ > +(variadic functions and a callback that reenters the guest) examples, and > +`Lorelei <https://github.com/rover2024/lorelei>`_ for the toolchain and the > +runtime environment they expect. > + > +.. list-table:: Dynamic Linking Call arguments > + :widths: 20 80 > + :header-rows: 1 > + > + * - Option > + - Description > + * - syscall_num=N > + - The magic syscall number the guest issues (default 4096). Must be high > + enough not to clash with a real syscall. > + > Other emulation features > ------------------------ >
