diff --git a/dlls/ntdll/unix/signal_arm.c b/dlls/ntdll/unix/signal_arm.c
index add01687c4ba524ce8503f80adb66f3fcc8770c1..9300ea2adc3dfc7f3f79da3c49088afa4ec2409b 100644
--- a/dlls/ntdll/unix/signal_arm.c
+++ b/dlls/ntdll/unix/signal_arm.c
@@ -55,6 +55,9 @@
 # define UNW_LOCAL_ONLY
 # include <libunwind.h>
 #endif
+#ifdef HAVE_LINK_H
+# include <link.h>
+#endif
 
 #define NONAMELESSUNION
 #define NONAMELESSSTRUCT
@@ -223,13 +226,421 @@ static BOOL is_inside_syscall( ucontext_t *sigcontext )
 
 extern void raise_func_trampoline( EXCEPTION_RECORD *rec, CONTEXT *context, void *dispatcher );
 
+struct exidx_entry
+{
+    uint32_t addr;
+    uint32_t data;
+};
+
+static uint32_t prel31_to_abs(const uint32_t *ptr)
+{
+    uint32_t prel31 = *ptr;
+    uint32_t rel = prel31 | ((prel31 << 1) & 0x80000000);
+    return (uintptr_t)ptr + rel;
+}
+
+static uint8_t get_byte(const uint32_t *ptr, int offset, int bytes)
+{
+    int word = offset >> 2;
+    int byte = offset & 0x3;
+    if (offset >= bytes)
+        return 0xb0; /* finish opcode */
+    return (ptr[word] >> (24 - 8*byte)) & 0xff;
+}
+
+static uint32_t get_uleb128(const uint32_t *ptr, int *offset, int bytes)
+{
+    int shift = 0;
+    uint32_t val = 0;
+    while (1)
+    {
+        uint8_t byte = get_byte(ptr, (*offset)++, bytes);
+        val |= (byte & 0x7f) << shift;
+        if ((byte & 0x80) == 0)
+            break;
+        shift += 7;
+    }
+    return val;
+}
+
+static void pop_regs(CONTEXT *context, uint32_t regs)
+{
+    int i;
+    DWORD new_sp = 0;
+    for (i = 0; i < 16; i++)
+    {
+        if (regs & (1U << i))
+        {
+            DWORD val = *(DWORD *)context->Sp;
+            if (i != 13)
+                (&context->R0)[i] = val;
+            else
+                new_sp = val;
+            context->Sp += 4;
+        }
+    }
+    if (regs & (1 << 13))
+        context->Sp = new_sp;
+}
+
+static void pop_vfp(CONTEXT *context, int first, int last)
+{
+    int i;
+    for (i = first; i <= last; i++)
+    {
+        context->u.D[i] = *(ULONGLONG *)context->Sp;
+        context->Sp += 8;
+    }
+}
+
+static uint32_t regmask(int first_bit, int n_bits)
+{
+    return ((1U << (n_bits + 1)) - 1) << first_bit;
+}
+
 /***********************************************************************
- *           unwind_builtin_dll
+ *           ehabi_virtual_unwind
  */
-NTSTATUS CDECL unwind_builtin_dll( ULONG type, struct _DISPATCHER_CONTEXT *dispatch, CONTEXT *context )
+static NTSTATUS ehabi_virtual_unwind( DWORD ip, DWORD *frame, CONTEXT *context,
+                                      const struct exidx_entry *entry,
+                                      PEXCEPTION_ROUTINE *handler, void **handler_data )
+{
+    const uint32_t *ptr;
+    const void *lsda = NULL;
+    int compact_inline = 0;
+    int offset = 0;
+    int bytes = 0;
+    int personality;
+    int extra_words;
+    int finish = 0;
+    int set_pc = 0;
+    DWORD func_begin = prel31_to_abs(&entry->addr);
+
+    *frame = context->Sp;
+
+    TRACE( "ip %#x function %#lx\n",
+           ip, (unsigned long)func_begin );
+
+    if (entry->data == 1)
+    {
+        ERR("EXIDX_CANTUNWIND\n");
+        return STATUS_UNSUCCESSFUL;
+    }
+    else if (entry->data & 0x80000000)
+    {
+        if ((entry->data & 0x7f000000) != 0)
+        {
+            ERR("compact inline EXIDX must have personality 0\n");
+            return STATUS_UNSUCCESSFUL;
+        }
+        ptr = &entry->data;
+        compact_inline = 1;
+    }
+    else
+    {
+        ptr = (uint32_t *)prel31_to_abs(&entry->data);
+    }
+
+    if ((*ptr & 0x80000000) == 0)
+    {
+        /* Generic */
+        void *personality_func = (void *)prel31_to_abs(ptr);
+        int words = (ptr[1] >> 24) & 0xff;
+        lsda = ptr + 1 + words + 1;
+
+        ERR("generic EHABI unwinding not supported\n");
+        (void)personality_func;
+        return STATUS_UNSUCCESSFUL;
+    }
+
+    /* Compact */
+
+    personality = (*ptr >> 24) & 0x0f;
+    switch (personality)
+    {
+    case 0:
+        if (!compact_inline)
+            lsda = ptr + 1;
+        extra_words = 0;
+        offset = 1;
+        break;
+    case 1:
+        extra_words = (*ptr >> 16) & 0xff;
+        lsda = ptr + extra_words + 1;
+        offset = 2;
+        break;
+    case 2:
+        extra_words = (*ptr >> 16) & 0xff;
+        lsda = ptr + extra_words + 1;
+        offset = 2;
+        break;
+    default:
+        ERR("unsupported compact EXIDX personality %d\n", personality);
+        return STATUS_UNSUCCESSFUL;
+    }
+
+    /* Not inspecting the descriptors */
+    (void)lsda;
+
+    bytes = 4 + 4*extra_words;
+    while (offset < bytes && !finish)
+    {
+        uint8_t byte = get_byte(ptr, offset++, bytes);
+        if ((byte & 0xc0) == 0x00)
+        {
+            /* Increment Sp */
+            context->Sp += (byte & 0x3f) * 4 + 4;
+        }
+        else if ((byte & 0xc0) == 0x40)
+        {
+            /* Decrement Sp */
+            context->Sp -= (byte & 0x3f) * 4 + 4;
+        }
+        else if ((byte & 0xf0) == 0x80)
+        {
+            /* Pop {r4-r15} based on register mask */
+            int regs = ((byte & 0x0f) << 8) | get_byte(ptr, offset++, bytes);
+            if (!regs)
+            {
+                ERR("refuse to unwind\n");
+                return STATUS_UNSUCCESSFUL;
+            }
+            regs <<= 4;
+            pop_regs(context, regs);
+            if (regs & (1 << 15))
+                set_pc = 1;
+        }
+        else if ((byte & 0xf0) == 0x90)
+        {
+            /* Restore Sp from other register */
+            int reg = byte & 0x0f;
+            if (reg == 13 || reg == 15)
+            {
+                ERR("reserved opcode\n");
+                return STATUS_UNSUCCESSFUL;
+            }
+            context->Sp = (&context->R0)[reg];
+        }
+        else if ((byte & 0xf0) == 0xa0)
+        {
+            /* Pop r4-r(4+n) (+lr) */
+            int n = byte & 0x07;
+            int regs = regmask(4, n);
+            if (byte & 0x08)
+                regs |= 1 << 14;
+            pop_regs(context, regs);
+        }
+        else if (byte == 0xb0)
+        {
+            finish = 1;
+        }
+        else if (byte == 0xb1)
+        {
+            /* Pop {r0-r3} based on register mask */
+            int regs = get_byte(ptr, offset++, bytes);
+            if (regs == 0 || (regs & 0xf0) != 0)
+            {
+                ERR("spare opcode\n");
+                return STATUS_UNSUCCESSFUL;
+            }
+            pop_regs(context, regs);
+        }
+        else if (byte == 0xb2)
+        {
+            /* Increment Sp by a larger amount */
+            int imm = get_uleb128(ptr, &offset, bytes);
+            context->Sp += 0x204 + imm * 4;
+        }
+        else if (byte == 0xb3)
+        {
+            /* Pop VFP registers as if saved by FSTMFDX; this opcode
+             * is deprecated. */
+            ERR("FSTMFDX unsupported\n");
+            return STATUS_UNSUCCESSFUL;
+        }
+        else if ((byte & 0xfc) == 0xb4)
+        {
+            ERR("spare opcode\n");
+            return STATUS_UNSUCCESSFUL;
+        }
+        else if ((byte & 0xf8) == 0xb8)
+        {
+            /* Pop VFP registers as if saved by FSTMFDX; this opcode
+             * is deprecated. */
+            ERR("FSTMFDX unsupported\n");
+            return STATUS_UNSUCCESSFUL;
+        }
+        else if ((byte & 0xf8) == 0xc0)
+        {
+            ERR("spare opcode / iWMMX\n");
+            return STATUS_UNSUCCESSFUL;
+        }
+        else if ((byte & 0xfe) == 0xc8)
+        {
+            /* Pop VFP registers d(16+ssss)-d(16+ssss+cccc), or
+             * d(0+ssss)-d(0+ssss+cccc) as if saved by VPUSH */
+            int first, last;
+            if ((byte & 0x01) == 0)
+                first = 16;
+            else
+                first = 0;
+            byte = get_byte(ptr, offset++, bytes);
+            first += (byte & 0xf0) >> 4;
+            last = first + (byte & 0x0f);
+            if (last >= 32)
+            {
+                ERR("reserved opcode\n");
+                return STATUS_UNSUCCESSFUL;
+            }
+            pop_vfp(context, first, last);
+        }
+        else if ((byte & 0xf8) == 0xc8)
+        {
+            ERR("spare opcode\n");
+            return STATUS_UNSUCCESSFUL;
+        }
+        else if ((byte & 0xf8) == 0xd0)
+        {
+            /* Pop VFP registers d8-d(8+n) as if saved by VPUSH */
+            int n = byte & 0x07;
+            pop_vfp(context, 8, 8 + n);
+        }
+        else
+        {
+            ERR("spare opcode\n");
+            return STATUS_UNSUCCESSFUL;
+        }
+    }
+    if (offset > bytes)
+    {
+        ERR("truncated opcodes\n");
+        return STATUS_UNSUCCESSFUL;
+    }
+
+    *handler      = NULL; /* personality */
+    *handler_data = NULL; /* lsda */
+
+    context->ContextFlags |= CONTEXT_UNWOUND_TO_CALL;
+    if (!set_pc)
+        context->Pc = context->Lr;
+
+    /* There's no need to check for raise_func_trampoline and manually restore
+     * Lr separately from Pc like with libunwind; the EHABI unwind info
+     * describes how both of them are restored separately, and as long as
+     * the unwind info restored Pc, it doesn't have to be set from Lr. */
+
+    TRACE( "next function pc=%08x\n", context->Pc );
+    TRACE("  r0=%08x  r1=%08x  r2=%08x  r3=%08x\n",
+          context->R0, context->R1, context->R2, context->R3 );
+    TRACE("  r4=%08x  r5=%08x  r6=%08x  r7=%08x\n",
+          context->R4, context->R5, context->R6, context->R7 );
+    TRACE("  r8=%08x  r9=%08x r10=%08x r11=%08x\n",
+          context->R8, context->R9, context->R10, context->R11 );
+    TRACE(" r12=%08x  sp=%08x  lr=%08x  pc=%08x\n",
+          context->R12, context->Sp, context->Lr, context->Pc );
+
+    return STATUS_SUCCESS;
+}
+
+#ifdef linux
+struct iterate_data
+{
+    ULONG_PTR ip;
+    int failed;
+    struct exidx_entry *entry;
+};
+
+static int contains_addr(struct dl_phdr_info *info, const ElfW(Phdr) *phdr, struct iterate_data *data)
+{
+    if (phdr->p_type != PT_LOAD)
+        return 0;
+    return data->ip >= info->dlpi_addr + phdr->p_vaddr && data->ip < info->dlpi_addr + phdr->p_vaddr + phdr->p_memsz;
+}
+
+static int check_exidx(struct dl_phdr_info *info, size_t info_size, void *arg)
 {
+    struct iterate_data *data = arg;
+    int i;
+    int found_addr;
+    const ElfW(Phdr) *exidx = NULL;
+    struct exidx_entry *begin, *end;
+
+    if (info->dlpi_phnum == 0 || data->ip < info->dlpi_addr || data->failed)
+        return 0;
+
+    found_addr = 0;
+    for (i = 0; i < info->dlpi_phnum; i++)
+    {
+        const ElfW(Phdr) *phdr = &info->dlpi_phdr[i];
+        if (contains_addr(info, phdr, data))
+            found_addr = 1;
+        if (phdr->p_type == PT_ARM_EXIDX)
+            exidx = phdr;
+    }
+
+    if (!found_addr || !exidx)
+    {
+        if (found_addr)
+        {
+            TRACE("found matching address in %s, but no EXIDX\n", info->dlpi_name);
+            data->failed = 1;
+        }
+        return 0;
+    }
+
+    begin = (struct exidx_entry *)(info->dlpi_addr + exidx->p_vaddr);
+    end = (struct exidx_entry *)(info->dlpi_addr + exidx->p_vaddr + exidx->p_memsz);
+    if (data->ip < prel31_to_abs(&begin->addr))
+    {
+        TRACE("%lx before EXIDX start at %x\n", data->ip, prel31_to_abs(&begin->addr));
+        data->failed = 1;
+        return 0;
+    }
+
+    while (begin + 1 < end)
+    {
+        struct exidx_entry *mid = begin + (end - begin)/2;
+        uint32_t abs_addr = prel31_to_abs(&mid->addr);
+        if (abs_addr > data->ip)
+        {
+            end = mid;
+        }
+        else if (abs_addr < data->ip)
+        {
+            begin = mid;
+        }
+        else
+        {
+            begin = mid;
+            end = mid + 1;
+        }
+    }
+
+    data->entry = begin;
+    TRACE("found %lx in %s, base %x, entry %p with addr %x (rel %x) data %x\n",
+          data->ip, info->dlpi_name, info->dlpi_addr, begin,
+          prel31_to_abs(&begin->addr),
+          prel31_to_abs(&begin->addr) - info->dlpi_addr, begin->data);
+    return 1;
+}
+
+static const struct exidx_entry *find_exidx_entry( void *ip )
+{
+    struct iterate_data data = {};
+
+    data.ip = (ULONG_PTR)ip;
+    data.failed = 0;
+    data.entry = NULL;
+    dl_iterate_phdr(check_exidx, &data);
+
+    return data.entry;
+}
+#endif
+
 #ifdef HAVE_LIBUNWIND
-    DWORD ip = context->Pc - (dispatch->ControlPcIsUnwound ? 2 : 0);
+static NTSTATUS libunwind_virtual_unwind( DWORD ip, DWORD *frame, CONTEXT *context,
+                                          PEXCEPTION_ROUTINE *handler, void **handler_data )
+{
     unw_context_t unw_context;
     unw_cursor_t cursor;
     unw_proc_info_t info;
@@ -261,8 +672,8 @@ NTSTATUS CDECL unwind_builtin_dll( ULONG type, struct _DISPATCHER_CONTEXT *dispa
         TRACE( "no info found for %x ip %x-%x, %s\n",
                ip, info.start_ip, info.end_ip, status == STATUS_SUCCESS ?
                "assuming leaf function" : "error, stuck" );
-        dispatch->LanguageHandler = NULL;
-        dispatch->EstablisherFrame = context->Sp;
+        *handler = NULL;
+        *frame = context->Sp;
         context->Pc = context->Lr;
         context->ContextFlags |= CONTEXT_UNWOUND_TO_CALL;
         return status;
@@ -279,9 +690,9 @@ NTSTATUS CDECL unwind_builtin_dll( ULONG type, struct _DISPATCHER_CONTEXT *dispa
         return STATUS_INVALID_DISPOSITION;
     }
 
-    dispatch->LanguageHandler  = (void *)info.handler;
-    dispatch->HandlerData      = (void *)info.lsda;
-    dispatch->EstablisherFrame = context->Sp;
+    *handler      = (void *)info.handler;
+    *handler_data = (void *)info.lsda;
+    *frame        = context->Sp;
 
     for (i = 0; i <= 12; i++)
         unw_get_reg( &cursor, UNW_ARM_R0 + i, (unw_word_t *)&(&context->R0)[i] );
@@ -297,7 +708,7 @@ NTSTATUS CDECL unwind_builtin_dll( ULONG type, struct _DISPATCHER_CONTEXT *dispa
          * individual values, thus do that manually here.
          * (The function we unwind to might be a leaf function that hasn't
          * backed up its own original Lr value on the stack.) */
-        const DWORD *orig_lr = (const DWORD *) dispatch->EstablisherFrame;
+        const DWORD *orig_lr = (const DWORD *) *frame;
         context->Lr = *orig_lr;
     }
 
@@ -311,6 +722,25 @@ NTSTATUS CDECL unwind_builtin_dll( ULONG type, struct _DISPATCHER_CONTEXT *dispa
     TRACE(" r12=%08x  sp=%08x  lr=%08x  pc=%08x\n",
           context->R12, context->Sp, context->Lr, context->Pc );
     return STATUS_SUCCESS;
+}
+#endif
+
+/***********************************************************************
+ *           unwind_builtin_dll
+ */
+NTSTATUS CDECL unwind_builtin_dll( ULONG type, struct _DISPATCHER_CONTEXT *dispatch, CONTEXT *context )
+{
+    DWORD ip = context->Pc - (dispatch->ControlPcIsUnwound ? 2 : 0);
+#ifdef linux
+    const struct exidx_entry *entry = find_exidx_entry( (void *)ip );
+
+    if (entry)
+        return ehabi_virtual_unwind( ip, &dispatch->EstablisherFrame, context, entry,
+                                     &dispatch->LanguageHandler, &dispatch->HandlerData );
+#endif
+#ifdef HAVE_LIBUNWIND
+    return libunwind_virtual_unwind( ip, &dispatch->EstablisherFrame, context,
+                                     &dispatch->LanguageHandler, &dispatch->HandlerData );
 #else
     ERR("libunwind not available, unable to unwind\n");
     return STATUS_INVALID_DISPOSITION;