diff --git a/examples/ulib/Makefile b/examples/ulib/Makefile
index f70333a3b9b..81885cf8726 100644
--- a/examples/ulib/Makefile
+++ b/examples/ulib/Makefile
@@ -35,36 +35,60 @@ LIB_STATIC_LDS ?= static.lds
 # The main Makefile passes in Q=@ for quiet output
 Q ?=
 
+# Common compiler flags for programs using system headers first
+SYSTEM_CFLAGS := -I$(UBOOT_BUILD)/include -idirafter$(srctree)/include \
+		-include $(srctree)/include/linux/compiler_attributes.h
+
+# Common compiler flags for programs using U-Boot headers first (like U-Boot
+# internal build)
+UBOOT_CFLAGS := -nostdinc -isystem $(shell $(CC) -print-file-name=include) \
+	-I$(UBOOT_BUILD)/include \
+	-I$(srctree)/include \
+	-I$(srctree)/arch/sandbox/include \
+	-include $(UBOOT_BUILD)/include/config.h \
+	-include $(srctree)/include/linux/kconfig.h \
+	-I$(srctree)/dts/upstream/include \
+	-D__KERNEL__ -DCONFIG_SYS_TEXT_BASE=0
+
+SHARED_LDFLAGS := -L$(UBOOT_BUILD) -lu-boot -Wl,-rpath,$(UBOOT_BUILD)
+
+STATIC_LDFLAGS := -Wl,-T,$(LIB_STATIC_LDS) \
+	-Wl,--whole-archive $(UBOOT_BUILD)/libu-boot.a -Wl,--no-whole-archive \
+	-lpthread -ldl $(PLATFORM_LIBS) -Wl,-z,noexecstack
+
+# Program definitions - can be single file or multi-object
 DEMO_SRC := $(EXAMPLE_DIR)/demo.c
+demo-objs := demo.o demo_helper.o
 DEMO_BINS := $(OUTDIR)/demo $(OUTDIR)/demo_static
 
+ALL_BINS := $(DEMO_BINS)
+
 # Default target builds both programs
-all: $(DEMO_BINS)
+all: $(ALL_BINS)
 
 # Create the output directory if it doesn't exist
 $(OUTDIR):
 	@mkdir -p $@
 
-# The U-Boot library must be built before we can link against it. This is
-# signalled by the presence of the $(UBOOT_BUILD)/examples/ulib directory.
-# This is an order-only prerequisite, so it does not trigger a rebuild if the
-# timestamp of the directory changes.
-$(DEMO_BINS): | $(UBOOT_BUILD)/examples/ulib $(OUTDIR)
+# Pattern rule for building object files with system headers first (default)
+$(OUTDIR)/%.o: $(EXAMPLE_DIR)/%.c | $(OUTDIR)
+	$(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -c -o $@ $<
+
+# Pattern rule for building object files with U-Boot headers first
+$(OUTDIR)/%_uboot.o: $(EXAMPLE_DIR)/%.c | $(OUTDIR)
+	$(CC) $(CFLAGS) $(UBOOT_CFLAGS) -c -o $@ $<
+
+# The U-Boot library must be built before we can link against it
+# Order-only prerequisites ensure libraries exist before linking
+$(ALL_BINS): | $(UBOOT_BUILD)/libu-boot.a $(UBOOT_BUILD)/libu-boot.so $(OUTDIR)
 
 # Build demo (dynamically linked with libu-boot.so)
-$(OUTDIR)/demo: $(DEMO_SRC)
-	$(CC) $(CFLAGS) \
-		-idirafter$(srctree)/include -o $@ $< \
-		-L$(UBOOT_BUILD) -lu-boot \
-		-Wl,-rpath,$(UBOOT_BUILD)
+$(OUTDIR)/demo: $(if $(demo-objs),$(addprefix $(OUTDIR)/,$(demo-objs)),$(DEMO_SRC))
+	$(if $(demo-objs),$(CC) $(CFLAGS) -o $@ $^ $(SHARED_LDFLAGS),$(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -o $@ $< $(SHARED_LDFLAGS))
 
 # Build demo_static (statically linked with libu-boot.a)
-$(OUTDIR)/demo_static: $(DEMO_SRC)
-	$(CC) $(CFLAGS) \
-		-idirafter$(srctree)/include -o $@ $< \
-		-Wl,-T,$(LIB_STATIC_LDS) \
-		-Wl,--whole-archive $(UBOOT_BUILD)/libu-boot.a -Wl,--no-whole-archive \
-		-lpthread -ldl $(PLATFORM_LIBS) -Wl,-z,noexecstack
+$(OUTDIR)/demo_static: $(if $(demo-objs),$(addprefix $(OUTDIR)/,$(demo-objs)),$(DEMO_SRC))
+	$(if $(demo-objs),$(CC) $(CFLAGS) -o $@ $^ $(STATIC_LDFLAGS),$(CC) $(CFLAGS) $(SYSTEM_CFLAGS) -o $@ $< $(STATIC_LDFLAGS))
 
 clean:
 	$(Q)rm -f $(DEMO_BINS)
diff --git a/examples/ulib/demo.c b/examples/ulib/demo.c
index 2a9f7720beb..9d916d3878a 100644
--- a/examples/ulib/demo.c
+++ b/examples/ulib/demo.c
@@ -17,10 +17,11 @@
 #include <os.h>
 #include <u-boot-lib.h>
 #include <version_string.h>
+#include "demo_helper.h"
 
 int main(int argc, char *argv[])
 {
-	int fd, lines = 0;
+	int fd, result, lines = 0;
 	char line[256];
 
 	/* Init U-Boot library */
@@ -29,8 +30,7 @@ int main(int argc, char *argv[])
 		return 1;
 	}
 
-	printf("1U-Boot Library Demo\n");
-	printf("================================\n");
+	demo_show_banner();
 	printf("U-Boot version: %s\n", version_string);
 	printf("\n");
 
@@ -54,6 +54,12 @@ int main(int argc, char *argv[])
 
 	printf("\nRead %d line(s) using U-Boot library functions.\n", lines);
 
+	/* Test the helper function */
+	result = demo_add_numbers(42, 13);
+	printf("Helper function result: %d\n", result);
+
+	demo_show_footer();
+
 	/* Clean up */
 	ulib_uninit();
 
diff --git a/examples/ulib/demo_helper.c b/examples/ulib/demo_helper.c
new file mode 100644
index 00000000000..935443657bd
--- /dev/null
+++ b/examples/ulib/demo_helper.c
@@ -0,0 +1,30 @@
+// SPDX-License-Identifier: GPL-2.0+
+/*
+ * Helper functions for U-Boot library demo
+ *
+ * Copyright 2025 Canonical Ltd.
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#include <u-boot-api.h>
+
+void demo_show_banner(void)
+{
+	ub_printf("=================================\n");
+	ub_printf("    U-Boot Library Demo Helper\n");
+	ub_printf("=================================\n");
+}
+
+void demo_show_footer(void)
+{
+	ub_printf("=================================\n");
+	ub_printf("      Demo Complete!\n");
+	ub_printf("=================================\n");
+}
+
+int demo_add_numbers(int a, int b)
+{
+	ub_printf("Helper: Adding %d + %d = %d\n", a, b, a + b);
+
+	return a + b;
+}
diff --git a/examples/ulib/demo_helper.h b/examples/ulib/demo_helper.h
new file mode 100644
index 00000000000..b4ab862941b
--- /dev/null
+++ b/examples/ulib/demo_helper.h
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: GPL-2.0+ */
+/*
+ * Helper functions for U-Boot library demo
+ *
+ * Copyright 2025 Canonical Ltd.
+ * Written by Simon Glass <simon.glass@canonical.com>
+ */
+
+#ifndef __DEMO_HELPER_H
+#define __DEMO_HELPER_H
+
+/**
+ * demo_show_banner() - Show the demo banner
+ */
+void demo_show_banner(void);
+
+/**
+ * demo_show_footer() - Show the demo footer
+ */
+void demo_show_footer(void);
+
+/**
+ * demo_add_numbers() - Add two numbers and print the result
+ *
+ * @a: First number
+ * @b: Second number
+ * Return: Sum of the two numbers
+ */
+int demo_add_numbers(int a, int b);
+
+#endif /* __DEMO_HELPER_H */
