diff --git a/core/sched.cc b/core/sched.cc
index 44a07cc948eb905bd54dd8017e19f8b3c08726f2..7e6e720b1cd8fc3f6b0ceeca1732f4b48e742a0b 100644
--- a/core/sched.cc
+++ b/core/sched.cc
@@ -908,6 +908,16 @@ unsigned long thread::id()
     return _id;
 }
 
+void thread::set_name(std::string name)
+{
+    _attr.name(name);
+}
+
+std::string thread::name() const
+{
+    return _attr._name.data();
+}
+
 void* thread::get_tls(ulong module)
 {
     if (module == elf::program::core_module_index) {
diff --git a/include/osv/sched.hh b/include/osv/sched.hh
index 643f8ef52c31b3a3337f5181ae75b7f7a40bab86..64c249b4136bc4ad8fbe27846fbda1e5148a76b8 100644
--- a/include/osv/sched.hh
+++ b/include/osv/sched.hh
@@ -296,6 +296,7 @@ public:
         stack_info _stack;
         cpu *_pinned_cpu;
         bool _detached;
+        std::array<char, 16> _name = {};
         attr() : _pinned_cpu(nullptr), _detached(false) { }
         attr &pin(cpu *c) {
             _pinned_cpu = c;
@@ -313,6 +314,10 @@ public:
             _detached = val;
             return *this;
         }
+        attr& name(std::string n) {
+            strncpy(_name.data(), n.data(), sizeof(_name) - 1);
+            return *this;
+        }
     };
 
 private:
@@ -386,6 +391,9 @@ public:
     void* get_tls(ulong module);
     void* setup_tls(ulong module, const void* tls_template,
             size_t init_size, size_t uninit_size);
+    void set_name(std::string name);
+    std::string name() const;
+    std::array<char, 16> name_raw() const { return _attr._name; }
     /**
      * Set thread's priority
      *
diff --git a/scripts/loader.py b/scripts/loader.py
index d135b3f4618adce1eec248d0ff77073b198ea9cf..7f6eeed53a8d6a39992ae553348009b30ae342e7 100644
--- a/scripts/loader.py
+++ b/scripts/loader.py
@@ -675,6 +675,7 @@ class osv_info_threads(gdb.Command):
             with thread_context(t, state):
                 cpu = thread_cpu(t)
                 tid = t['_id']
+                name = t['_attr']['_name']['_M_elems'].string()
                 newest_frame = gdb.selected_frame()
                 # Non-running threads have always, by definition, just called
                 # a reschedule, and the stack trace is filled with reschedule
@@ -700,8 +701,8 @@ class osv_info_threads(gdb.Command):
                 else:
                     location = '??'
 
-                gdb.write('%4d (0x%x) cpu%s %-10s %s vruntime %12g\n' %
-                          (tid, ulong(t),
+                gdb.write('%4d (0x%x) %-15s cpu%s %-10s %s vruntime %12g\n' %
+                          (tid, ulong(t), name,
                            cpu['arch']['acpi_id'],
                            thread_status(t),
                            location,