diff --git a/Makefile.am b/Makefile.am
index e7fd030f..f7b59a47 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -725,7 +725,8 @@ src_ofonod_SOURCES = $(builtin_sources) $(gatchat_sources) src/ofono.ver \
 			src/handsfree-audio.c src/bluetooth.h \
 			src/hfp.h src/siri.c \
 			src/netmon.c src/lte.c src/ims.c \
-			src/netmonagent.c src/netmonagent.h
+			src/netmonagent.c src/netmonagent.h \
+			src/module.c
 
 src_ofonod_LDADD = gdbus/libgdbus-internal.la $(builtin_libadd) $(ell_ldadd) \
 			@GLIB_LIBS@ @DBUS_LIBS@ -ldl
diff --git a/src/main.c b/src/main.c
index 4529cde1..82157700 100644
--- a/src/main.c
+++ b/src/main.c
@@ -274,12 +274,10 @@ int main(int argc, char **argv)
 
 	__ofono_dbus_init(conn);
 
-	__ofono_modemwatch_init();
-
-	__ofono_manager_init();
+	if (__ofono_modules_init() < 0)
+		goto fail_module_init;
 
 	__ofono_plugin_init(option_plugin, option_noplugin);
-
 	g_free(option_plugin);
 	g_free(option_noplugin);
 
@@ -287,10 +285,9 @@ int main(int argc, char **argv)
 
 	__ofono_plugin_cleanup();
 
-	__ofono_manager_cleanup();
-
-	__ofono_modemwatch_cleanup();
+	__ofono_modules_cleanup();
 
+fail_module_init:
 	__ofono_dbus_cleanup();
 	dbus_connection_unref(conn);
 
diff --git a/src/manager.c b/src/manager.c
index 404f2cad..e3307adb 100644
--- a/src/manager.c
+++ b/src/manager.c
@@ -95,7 +95,7 @@ static const GDBusSignalTable manager_signals[] = {
 	{ }
 };
 
-int __ofono_manager_init(void)
+static int manager_init(void)
 {
 	DBusConnection *conn = ofono_dbus_get_connection();
 	gboolean ret;
@@ -111,10 +111,12 @@ int __ofono_manager_init(void)
 	return 0;
 }
 
-void __ofono_manager_cleanup(void)
+static void manager_cleanup(void)
 {
 	DBusConnection *conn = ofono_dbus_get_connection();
 
 	g_dbus_unregister_interface(conn, OFONO_MANAGER_PATH,
 					OFONO_MANAGER_INTERFACE);
 }
+
+OFONO_MODULE(manager, manager_init, manager_cleanup)
diff --git a/src/modem.c b/src/modem.c
index 60717717..354d2b86 100644
--- a/src/modem.c
+++ b/src/modem.c
@@ -1918,12 +1918,13 @@ static void sim_watch(struct ofono_atom *atom,
 							modem, NULL);
 }
 
-void __ofono_modemwatch_init(void)
+static int modemwatch_init(void)
 {
 	g_modemwatches = __ofono_watchlist_new(g_free);
+	return 0;
 }
 
-void __ofono_modemwatch_cleanup(void)
+static void modemwatch_cleanup(void)
 {
 	__ofono_watchlist_free(g_modemwatches);
 }
@@ -2319,3 +2320,5 @@ void __ofono_modem_dec_emergency_mode(struct ofono_modem *modem)
 out:
 	modem->emergency--;
 }
+
+OFONO_MODULE(modemwatch, modemwatch_init, modemwatch_cleanup)
diff --git a/src/module.c b/src/module.c
new file mode 100644
index 00000000..8730f2e1
--- /dev/null
+++ b/src/module.c
@@ -0,0 +1,55 @@
+/*
+ *  oFono - Open Source Telephony
+ *  Copyright (C) 2023  Cruise, LLC
+ *
+ * SPDX-License-Identifier: GPL-2.0-only
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <errno.h>
+#include <ell/ell.h>
+
+#include "ofono.h"
+
+extern struct ofono_module_desc __start___ofono_module[];
+extern struct ofono_module_desc __stop___ofono_module[];
+
+int __ofono_modules_init(void)
+{
+	struct ofono_module_desc *desc;
+	size_t i;
+	int r;
+	size_t n_modules = __stop___ofono_module - __start___ofono_module;
+
+	DBG("");
+
+	for (i = 0; i < n_modules; i++) {
+		desc = __start___ofono_module + i;
+		r = desc->init();
+
+		if (r < 0) {
+			ofono_error("Module %s failed to start: %s(%d)",
+					desc->name, strerror(-r), -r);
+			return r;
+		}
+	}
+
+	return 0;
+}
+
+void __ofono_modules_cleanup(void)
+{
+	struct ofono_module_desc *desc;
+	size_t i;
+	size_t n_modules = __stop___ofono_module - __start___ofono_module;
+
+	l_debug("");
+
+	for (i = n_modules; i > 0; i--) {
+		desc = __start___ofono_module + i - 1;
+		desc->exit();
+	}
+}
diff --git a/src/ofono.h b/src/ofono.h
index 2e51776b..e289ff08 100644
--- a/src/ofono.h
+++ b/src/ofono.h
@@ -28,9 +28,6 @@
 
 void __ofono_exit(void);
 
-int __ofono_manager_init(void);
-void __ofono_manager_cleanup(void);
-
 int __ofono_handsfree_audio_manager_init(void);
 void __ofono_handsfree_audio_manager_cleanup(void);
 
@@ -97,6 +94,25 @@ gboolean __ofono_watchlist_remove_item(struct ofono_watchlist *watchlist,
 					unsigned int id);
 void __ofono_watchlist_free(struct ofono_watchlist *watchlist);
 
+struct ofono_module_desc {
+	const char *name;
+	int (*init)(void);
+	void (*exit)(void);
+} __attribute__((aligned(8)));
+
+#define OFONO_MODULE(name, init, exit)					\
+	_Pragma("GCC diagnostic push")					\
+	_Pragma("GCC diagnostic ignored \"-Wattributes\"")		\
+	static struct ofono_module_desc __ofono_module_ ## name		\
+		__attribute__((used, retain, section("__ofono_module"),	\
+			       aligned(8))) = {				\
+			#name, init, exit				\
+		};							\
+	_Pragma("GCC diagnostic pop")
+
+int __ofono_modules_init(void);
+void __ofono_modules_cleanup(void);
+
 #include <ofono/plugin.h>
 
 int __ofono_plugin_init(const char *pattern, const char *exclude);
