ntdll: Implement ARM EHABI unwinding.

This avoids relying on libunwind, which isn't always available,
and which can be brittle (e.g. current git master of libunwind fails, see
https://github.com/libunwind/libunwind/pull/203#issuecomment-984126066).

This allows unwinding with the EXIDX/EXTBL info which is used
normally for C++ exception handling/unwinding. This avoids needing
to keep the .so files unstripped and avoids needing libunwind to
load .debug_frame from disk instead of the already mapped
EXIDX/EXTBL.

This patch uses the dl_iterate_phdr function for finding the EXIDX
section; keeping this call within #ifdef linux to avoid breaking
someone's build, even though it probably is available on most unix
(or ELF) platforms.

Alternatively, we could add configure checks for this function.

This passes all my unwinding tests, for full ELF builds of Wine,
built with both GCC and Clang. (It also works for PE builds, where
only very few ELF bits need to be unwound.)

Signed-off-by: Martin Storsjö <martin@martin.st>
This commit is contained in:
Martin Storsjö 2022-10-18 22:55:47 +03:00 committed by Alexandre Julliard
parent f760976803
commit a27b202a4d

View file

@ -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 );
/***********************************************************************
* unwind_builtin_dll
*/
NTSTATUS CDECL unwind_builtin_dll( ULONG type, struct _DISPATCHER_CONTEXT *dispatch, CONTEXT *context )
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;
}
/***********************************************************************
* ehabi_virtual_unwind
*/
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;