diff --git a/src/libcamera/base/backtrace.cpp b/src/libcamera/base/backtrace.cpp
index 913f7ba71b03..011f2e428d5d 100644
--- a/src/libcamera/base/backtrace.cpp
+++ b/src/libcamera/base/backtrace.cpp
@@ -12,9 +12,16 @@
 #include <stdlib.h>
 #endif
 
+#ifdef HAVE_DW
+#include <cxxabi.h>
+#include <elfutils/libdwfl.h>
+#include <unistd.h>
+#endif
+
 #include <sstream>
 
 #include <libcamera/base/span.h>
+#include <libcamera/base/utils.h>
 
 /**
  * \file backtrace.h
@@ -23,6 +30,101 @@
 
 namespace libcamera {
 
+namespace {
+
+#if HAVE_DW
+class DwflParser
+{
+public:
+	DwflParser();
+	~DwflParser();
+
+	bool isValid() const { return valid_; }
+	std::string stackEntry(const void *ip);
+
+private:
+	Dwfl_Callbacks callbacks_;
+	Dwfl *dwfl_;
+	bool valid_;
+};
+
+DwflParser::DwflParser()
+	: callbacks_({}), dwfl_(nullptr), valid_(false)
+{
+	callbacks_.find_elf = dwfl_linux_proc_find_elf;
+	callbacks_.find_debuginfo = dwfl_standard_find_debuginfo;
+
+	dwfl_ = dwfl_begin(&callbacks_);
+	if (!dwfl_)
+		return;
+
+	int ret = dwfl_linux_proc_report(dwfl_, getpid());
+	if (ret)
+		return;
+
+	ret = dwfl_report_end(dwfl_, nullptr, nullptr);
+	if (ret)
+		return;
+
+	valid_ = true;
+}
+
+DwflParser::~DwflParser()
+{
+	if (dwfl_)
+		dwfl_end(dwfl_);
+}
+
+std::string DwflParser::stackEntry(const void *ip)
+{
+	Dwarf_Addr addr = reinterpret_cast<Dwarf_Addr>(ip);
+
+	Dwfl_Module *module = dwfl_addrmodule(dwfl_, addr);
+	if (!module)
+		return std::string();
+
+	std::ostringstream entry;
+
+	GElf_Off offset;
+	GElf_Sym sym;
+	const char *symbol = dwfl_module_addrinfo(module, addr, &offset, &sym,
+						  nullptr, nullptr, nullptr);
+	if (symbol) {
+		char *name = abi::__cxa_demangle(symbol, nullptr, nullptr, nullptr);
+		entry << (name ? name : symbol) << "+0x" << std::hex << offset
+		      << std::dec;
+		free(name);
+	} else {
+		entry << "??? [" << utils::hex(addr) << "]";
+	}
+
+	entry << " (";
+
+	Dwfl_Line *line = dwfl_module_getsrc(module, addr);
+	if (line) {
+		const char *filename;
+		int lineNumber = 0;
+
+		filename = dwfl_lineinfo(line, &addr, &lineNumber, nullptr,
+					 nullptr, nullptr);
+
+		entry << (filename ? filename : "???") << ":" << lineNumber;
+	} else {
+		const char *filename = nullptr;
+
+		dwfl_module_info(module, nullptr, nullptr, nullptr, nullptr,
+				 nullptr, &filename, nullptr);
+
+		entry << (filename ? filename : "???") << " [" << utils::hex(addr) << "]";
+	}
+
+	entry << ")";
+	return entry.str();
+}
+#endif /* HAVE_DW */
+
+} /* namespace */
+
 /**
  * \class Backtrace
  * \brief Representation of a call stack backtrace
@@ -85,6 +187,24 @@ std::string Backtrace::toString(unsigned int skipLevels) const
 	if (backtrace_.size() <= skipLevels)
 		return std::string();
 
+#if HAVE_DW
+	DwflParser dwfl;
+
+	if (dwfl.isValid()) {
+		std::ostringstream msg;
+
+		Span<void *const> trace{ backtrace_ };
+		for (const void *ip : trace.subspan(skipLevels)) {
+			if (ip)
+				msg << dwfl.stackEntry(ip) << std::endl;
+			else
+				msg << "???" << std::endl;
+		}
+
+		return msg.str();
+	}
+#endif
+
 #if HAVE_BACKTRACE
 	Span<void *const> trace{ backtrace_ };
 	trace = trace.subspan(skipLevels);
diff --git a/src/libcamera/base/meson.build b/src/libcamera/base/meson.build
index 85af01a19365..1fa894cf1896 100644
--- a/src/libcamera/base/meson.build
+++ b/src/libcamera/base/meson.build
@@ -19,13 +19,20 @@ libcamera_base_sources = files([
     'utils.cpp',
 ])
 
+libdw = cc.find_library('libdw', required : false)
+
 if cc.has_header_symbol('execinfo.h', 'backtrace')
     config_h.set('HAVE_BACKTRACE', 1)
 endif
 
+if libdw.found()
+    config_h.set('HAVE_DW', 1)
+endif
+
 libcamera_base_deps = [
     dependency('threads'),
     libatomic,
+    libdw,
 ]
 
 # Internal components must use the libcamera_base_private dependency to enable
