ἅπαξ λεγόμενον
146.190.13.172

Static X server


December 2024

Introduction

When going through the current (as of writing this article) X.Org server documentation, one can notice one page mentioning a static server (The X.Org Foundation 2012) and wonder what it actually means, since the exact method of acquiring such a server is not really specified. The X.Org server can be successfully built by passing --disable-shared --enable-static to the configure script, but it is nearly unusable on a completely statically-linked operating system, since it uses dlopen(3) (IEEE and The Open Group 2024) to load driver modules (and their dependencies) at startup. While it would be possible to create a shared object linked against object archives containing position-independent code, a static system without a dlopen(3) interface would not be able to load it anyway.

When delving deeper into the documentation and sources, it becomes apparent that this was not always the case. Apparently, it was once possible to completely disable the dynamic loader and bulid in all the required modules by the means of an unset XFree86LOADER compile-time switch. Not only it disabled the loader (and the loadable parts in the drivers, which still follow the same loading convention), it also exposed parts of the server required by the linker to properly build a static executable.

The purpose of this article is to show how to build this fabled static server using modern X.Org server sources.

Prerequisites

Any reader attempting this endevour is required to obtain all of the sources for the X.Org server (version 21.1.14) and its dependencies. The X.Org server itself requires proper drivers to run. The author has managed to run this entire example on a Matrox G200 with the mga driver (available along with the other X.Org sources) and the evdev driver. The reader can try using their own distinct hardware, but it may require additional steps (like compiling a different driver which may not be so eager to be built as a static library). All of the packages used in this example can be built with a GCC 4.9.4 without any problems. Some packages require Python, but a compact static build without a dynamic module loader is sufficient (it is possible to build one by disabling the onboard package manager and manually removing some of the tests).

Unfortunately, some packages are built using the Meson build system. The author has had some problems with running it properly off a static Pythion installation, but nearly all of the required modules can be built with muon 0.4.0 and samurai 1.2 (which are C implementations of Meson and Ninja respectively). The only exception is the Mesa OpenGL library, but the reader can safely use the version 19.0.8, which was the last version using the autotools build system (unless they have a working Meson, but the newer Mesa versions may require a newer GCC). Mesa itself needs LLVM, but the old version 7.1.0 can be properly built with the GCC 4.9.4. The author has linked everything against the musl C library version 1.2.4, but any newer (and probably some older) versions should work as well.

Most of the X.Org packages properly call pkg-config (Nicholson 2010) to generate lists of include directories and linking information. Unfortunately (depending on the implementation of pkg-config and/or package), some of the .pc files may contain required libraries in Requires.private or Libs.private sections and the pkg-config may improperly handle static dependencies when generating flags for the linker. When building the libraries, the reader should pay attention to all files and either fix them or patch and/or configure their pkg-config implementation, so that generating --libs for any package also generates required flags for its dependencies. The author has thoroughly tested the pkgconf implementation and (if properly configured) it can even generate appropriate flags for more exotic file system hierarchy configurations like the versioned package directiories (Gregory 2024).

All of the packages in this example can actually be built on a system without a dynamic loader (and it would probably be easier to do so in an environment like this, otherwise the linker may try to sneak in references to various shared objects).

Drivers

This example uses the xf86-video-mga (for the Matrox G200) and xf86-input-evdev drivers (for mouse and keyboard). Nowadays they are loaded as shared objects at runtime (and the build defaults to that scheme), but both can be built as static libraries with proper configure flags (as they were fortunately not disabled).

While the evdev driver is quite modern (i.e., expected to be compiled only in a dynamic environment), the mga driver (version 2.1.0) still contains some echoes of the past. One can find the aforementioned XFree86LOADER switch in mga_driver.c. It enables the XF86ModuleVersionInfo and XF86ModuleData structures, which are used by the X server module loader (along with the mgaSetup function), but the only thing really needed in the static server build is the DriverRec structure, which the server is going to link against. If it is not possible to properly disable the compile-time switch, the #ifdef section should be manually removed before building.

The evdev driver does not respect the XFree86LOADER switch, but it should not be too hard to remove the problematic section. The file evdev.c contains the XF86ModuleVersionInfo and XF86ModuleData structures, which can be safely removed along with the EvdevPlug function and the EvdevUnplug procedure. This file also contains the InputDriverRec structure, which the X server is going to statically link against.

Both drivers and their dependencies should not pose any problems during build. The resulting object archives should be saved in known locations, as they are going to be needed during the X server linking process.

The X server

Most of the files in need of a change are located in ./hw/xfree86 directory of the source tree.

Whenever X needs to refer to a particular driver, it always looks for it in the same location: the pointer arrays defined in the common/xf86Globals.c file.

DriverPtr *xf86DriverList = NULL;
int xf86NumDrivers = 0;
InputDriverPtr *xf86InputDriverList = NULL;
int xf86NumInputDrivers = 0;

These arrays are empty when the server starts and are loaded dynamically depending on the system configuration by the means of filling them with pointers to the DriverRec and InputDriverRec structures contained within the shared objects. Obviously, such a setup is unacceptable in a static environment.

Looking at the ancient XFree86 sources, one can find this section was once also manipulated by the XFree86LOADER compile-time switch. It basically disabled the definitions, since the arrays were suposed to be defined in the files generated during static build. Delving a little deeper, one can find two shell scripts: drivers/confdrv.sh (which generates a drivers/drvConf.c file) and input/confdrv.sh (which generates a input/drvConf.c file). One could try to incorporate these files into the build process (and it should be done if the static build recipes are to be properly maintained), but for this simple example a change to the common/xf86Globals.c should suffice.

DriverPtr xf86DriverList[] = { &MGA };
int xf86NumDrivers = sizeof(xf86DriverList) / sizeof(xf86DriverList[0]);
InputDriverPtr *xf86InputDriverList = InputDriverPtr xf86InputDriverList[] = { &EVDEV };
int xf86NumInputDrivers = sizeof(xf86InputDriverList) / sizeof(xf86InputDriverList[0]);

The references to these drivers do not exist in the source tree, so they must be declared before using them (and it can be done in the same file at this point).

extern DriverRec MGA;
extern InputDriverRec EVDEV;

The types of the arrays have been changed, so their definitions in the internal headers also need to be changed. The first one appears in the common/xf86Priv.h file.

extern _X_EXPORT DriverPtr *xf86DriverList;

The reader should have no problems figuring it out.

extern _X_EXPORT DriverPtr xf86DriverList[];

The other one appears in common/xf86InPriv.h.

extern InputDriverPtr *xf86InputDriverList;

This change should also be obvious.

extern InputDriverPtr xf86InputDriverList[];

While the original XFree86 implementation had a rather sophisticated mechanism to disable the dynamic loader, this example will only focus on removing the unnecessary and harmful parts. The next file to work with is common/xf86Helper.c. The first four procedures make direct references to the dynamic loading mechanism.

/* Add a pointer to a new DriverRec to xf86DriverList */

void
xf86AddDriver(DriverPtr driver, void *module, int flags)
{
    /* Don't add null entries */
    if (!driver)
        return;

    if (xf86DriverList == NULL)
        xf86NumDrivers = 0;

    xf86NumDrivers++;
    xf86DriverList = xnfreallocarray(xf86DriverList,
                                     xf86NumDrivers, sizeof(DriverPtr));
    xf86DriverList[xf86NumDrivers - 1] = xnfalloc(sizeof(DriverRec));
    *xf86DriverList[xf86NumDrivers - 1] = *driver;
    xf86DriverList[xf86NumDrivers - 1]->module = module;
    xf86DriverList[xf86NumDrivers - 1]->refCount = 0;
}

void
xf86DeleteDriver(int drvIndex)
{
    if (xf86DriverList[drvIndex]
        && (!xf86DriverHasEntities(xf86DriverList[drvIndex]))) {
        if (xf86DriverList[drvIndex]->module)
            UnloadModule(xf86DriverList[drvIndex]->module);
        free(xf86DriverList[drvIndex]);
        xf86DriverList[drvIndex] = NULL;
    }
}

/* Add a pointer to a new InputDriverRec to xf86InputDriverList */

void
xf86AddInputDriver(InputDriverPtr driver, void *module, int flags)
{
    /* Don't add null entries */
    if (!driver)
        return;

    if (xf86InputDriverList == NULL)
        xf86NumInputDrivers = 0;

    xf86NumInputDrivers++;
    xf86InputDriverList = xnfreallocarray(xf86InputDriverList,
                                          xf86NumInputDrivers,
                                          sizeof(InputDriverPtr));
    xf86InputDriverList[xf86NumInputDrivers - 1] =
        xnfalloc(sizeof(InputDriverRec));
    *xf86InputDriverList[xf86NumInputDrivers - 1] = *driver;
    xf86InputDriverList[xf86NumInputDrivers - 1]->module = module;
}

void
xf86DeleteInputDriver(int drvIndex)
{
    if (xf86InputDriverList[drvIndex] && xf86InputDriverList[drvIndex]->module)
        UnloadModule(xf86InputDriverList[drvIndex]->module);
    free(xf86InputDriverList[drvIndex]);
    xf86InputDriverList[drvIndex] = NULL;
}

An alert reader will realize these procedures only make sense in a setup where we allow loading up drivers dynamically and not in the case where the driver array has already been allocated. The compile time switches that would apply the necessary changes are buried in ancient sources, but it is easy to mimic them by removing the problematic statements.

/* Add a pointer to a new DriverRec to xf86DriverList */

void
xf86AddDriver(DriverPtr driver, void *module, int flags)
{
}

void
xf86DeleteDriver(int drvIndex)
{
    if (xf86DriverList[drvIndex]
        && (!xf86DriverHasEntities(xf86DriverList[drvIndex]))) {
        xf86DriverList[drvIndex] = NULL;
    }
}

/* Add a pointer to a new InputDriverRec to xf86InputDriverList */

void
xf86AddInputDriver(InputDriverPtr driver, void *module, int flags)
{
}

void
xf86DeleteInputDriver(int drvIndex)
{
    xf86InputDriverList[drvIndex] = NULL;
}

All of these changes will make the drivers already loaded, but the server will try to load them anyway wheneven it needs them (e.g., after parsing the site configuration). Fortunately, the loader checks if the module has been compiled-in before trying (and failing in case of a static server) to load it. The list of these modules appears in loader/loadmod.c.

static const char *compiled_in_modules[] = {
    "ddc",
    "fb",
    "i2c",
    "ramdac",
    "dbe",
    "record",
    "extmod",
    "dri",
    "dri2",
#ifdef DRI3
    "dri3",
#endif
#ifdef PRESENT
    "present",
#endif
    NULL
};

This list should be complemented by the names of the required modules and their dependencies. The example below shows changes made by the author to properly start his server, but the reader’s mileage may vary.

static const char *compiled_in_modules[] = {
    "ddc",
    "fb",
    "i2c",
    "ramdac",
    "dbe",
    "record",
    "extmod",
    "dri",
    "dri2",
    "mga",
    "vgahw",
    "exa",
    "fbdevhw",
    "shadowfb",
    "int10",
    "evdev",
#ifdef DRI3
    "dri3",
#endif
#ifdef PRESENT
    "present",
#endif
    NULL
};

Other things to watch out for are the duplicate definitions shared between the server and its dependencies. An example the author encountered was the XkbFreeGeomOverlayKeys procedure in ./xkb/XKBGAlloc.c. If the linking fails because of the duplicate entries, the duplicates should be removed from the source tree.

The only thing left to do is adding the required references to the appropriate Makefiles. The files are generated by the configure script (and it shoud be run before this step with appropriate switches, e.g., disabling modules such as glx, dri, dri2, dri3, glamor, or others which may not be available at this point), so the author cannot guarantee the exact statement locations are going to be the same, but the operation basically boils down to adding object archive references for the compiled drivers. (In a more sophisticated static release, the references should be generated by the configure script itself.)

The first Makefile to change is ./hw/xfree86/Makefile.

	$(AM_V_CCLD)$(Xorg_LINK) $(Xorg_OBJECTS) $(Xorg_LDADD) $(LIBS)

The changes below should be tailored to suit the build system configuration. In an ideal world, all of the required switches would be gathered from querying pkg-config.

	$(AM_V_CCLD)$(Xorg_LINK) $(Xorg_OBJECTS) $(Xorg_LDADD) /app/xorg/xf86-video-mga-2.1.0/lib/xorg/modules/drivers/mga_drv.a /app/xorg/xf86-input-evdev-2.11.0/lib/xorg/modules/input/evdev_drv.a -L/app/xorg/xorg-server-21.1.14/lib/xorg/modules -L/app/freedesktop.org/libevdev-1.13.3/lib -L/app/bitmath/mtdev-1.1.7/lib -levdev -lmtdev -lvgahw -lexa -lfbdevhw -lshadowfb -lint10 -lxcvt $(LIBS)

The second Makefile to change is ./test/Makefile.

	$(AM_V_CCLD)$(tests_LINK) $(tests_OBJECTS) $(tests_LDADD) $(LIBS)

The changes are identical.

	$(AM_V_CCLD)$(tests_LINK) $(tests_OBJECTS) $(tests_LDADD) /app/xorg/xf86-video-mga-2.1.0/lib/xorg/modules/drivers/mga_drv.a /app/xorg/xf86-input-evdev-2.11.0/lib/xorg/modules/input/evdev_drv.a -L/app/xorg/xorg-server-21.1.14/lib/xorg/modules -L/app/freedesktop.org/libevdev-1.13.3/lib -L/app/bitmath/mtdev-1.1.7/lib -levdev -lmtdev -lvgahw -lexa -lfbdevhw -lshadowfb -lint10 -lxcvt $(LIBS)

The server should now build properly after running make. Installation, proper configuration, and running the server are beyond the scope of this article.

Conclusions

As of writing this article, the X.Org window server can still be built as a static server and the required modules can be statically linked during build. The required procedures should not change unless the X.Org developers rewrite the module loading mechanism, but given how it is tied to the entire architecture (including existing drivers, etc.), it is highly unlikely.

All of the above steps have been successfully run on a machine running a system where the musl C library is being used as the default (and the only) C standard library with no dynamic linking support whatsoever (not only the dynamic loader is missing, the dlopen(3) function indiscriminately fails). All of the required libaries, and the X.Org server itself, have been compiled using a regular GCC 4.9.4 compiler (with required patches for musl-compatibility and minor bug fixes).

The proper machine and system configuration is crucial for this solution to work and the reader should attempt it only if they already have experience with building large packages like the X.Org server and its dependencies on their machine. Knowing how the X window server works may also be helpful in case of some unusual configuration. The author has managed to run his first working build after initializing X, resetting the keyboard mode with a system request command, and changing the virtual terminal forth and back to actually see any output, so it is recommended to have an environment where debugging edge cases like that may not cause any problems.

For what it is worth, xclock(1) actually displayed the right time.

References

Gregory, David [hapax, pseud.]. 2024. “hier(7) revised,” Hapax Legomenon (July). http://146.190.13.172/articles/hier-7-revised/.

IEEE and The Open Group. 2024. The Open Group Base Specifications Issue 8: IEEE Std 1003.1™-2024 Edition. https://pubs.opengroup.org/onlinepubs/9799919799/.

Nicholson, Dan. 2010. Guide to pkg-config. https://people.freedesktop.org/~dbn/pkg-config-guide.html.

The X.Org Foundation. 2012. “XFree86 DDX Design,” Documentation for the X Window System. https://www.x.org/releases/current/doc/xorg-server/ddxDesign.html.