If wine dlls are built with frame pointers enabled, the frame pointer will be used during unwinding.
If we don't restore frame pointer before calling the user mode callback, then later when the unwinder encounters the user mode callback frame, it will set the frame pointer to something unexpected (depends on what it was during `call_user_mode_callback`). Then for the subsequent frame it adjusts the stack pointer based on the frame pointer, thus derailing the unwinding process.
-- v5: ntdll: Also restore rbp before calling user mode callback.
From: Yuxuan Shui yshui@codeweavers.com
If wine dlls are built with frame pointers enabled, the frame pointer will be used during unwinding.
If we don't restore frame pointer before calling the user mode callback, then later when the unwinder encounters the user mode callback frame, it will set the frame pointer to something unexpected (depends on what it was during `call_user_mode_callback`). Then for the subsequent frame it adjusts the stack pointer based on the frame pointer, thus derailing the unwinding process. --- dlls/ntdll/unix/signal_x86_64.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/dlls/ntdll/unix/signal_x86_64.c b/dlls/ntdll/unix/signal_x86_64.c index 6ec662b9850..3bed0c3fada 100644 --- a/dlls/ntdll/unix/signal_x86_64.c +++ b/dlls/ntdll/unix/signal_x86_64.c @@ -1642,8 +1642,8 @@ __ASM_GLOBAL_FUNC( call_user_mode_callback, "andq $~63,%rsp\n\t" "leaq 0x10(%rbp),%rax\n\t" "movq %rax,0xa8(%rsp)\n\t" /* frame->syscall_cfa */ - "movq 0x378(%r13),%r10\n\t" /* thread_data->syscall_frame */ - "movq %r10,0xa0(%rsp)\n\t" /* frame->prev_frame */ + "movq 0x378(%r13),%r14\n\t" /* thread_data->syscall_frame */ + "movq %r14,0xa0(%rsp)\n\t" /* frame->prev_frame */ "movq %rsp,0x378(%r13)\n\t" /* thread_data->syscall_frame */ "testl $1,0x380(%r13)\n\t" /* thread_data->syscall_trace */ "jz 1f\n\t" @@ -1658,6 +1658,7 @@ __ASM_GLOBAL_FUNC( call_user_mode_callback, "movq %r15,%rcx\n" /* func */ /* switch to user stack */ "1:\tmovq %rdi,%rsp\n\t" /* user_rsp */ + "movq 0x98(%r14),%rbp\n\t" /* user_frame->Rbp */ #ifdef __linux__ "movw 0x338(%r13),%ax\n" /* amd64_thread_data()->fs */ "testw %ax,%ax\n\t"
On Sat Jul 5 01:15:57 2025 +0000, Yuxuan Shui wrote:
then the test can randomly fail. is there a way to test this that's no probabilistic?
If you set that to 0xdeadbeef before going through callback dispatcher, then modify to something else before unwind to initial frame where they are supposed to be 0xdeadbeef if restoed, such chance is very low. The test I see in snippet doesn't look right for other reasons. You can't be modifying non-volatile registers in the middle of C code, that unfortunately has to be binary function which will modify those, call CreateWindow and restore the registers back before returning. If the same compiled code is running on Windows that of course hints that it might indeed not be restored on Windows but it is quite hard to follow guessing which exact way such code can be broken and which it can't.
// unwinding out of a syscall seems to break some internal state?`
More likely those registers (which hold the code's variables) being voluntary altered break something (and thus hard to judge if the test results are correct in general or this breakage triggers some unrelated difference).
You can't be modifying non-volatile registers in the middle of C code
You just need to put them in the [output list](https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#OutputOperands) and the compiler knows you are modifying them, so that should be fine. (albeit i mixed up output and input operands).
On Sat Jul 5 01:35:22 2025 +0000, Yuxuan Shui wrote:
You can't be modifying non-volatile registers in the middle of C code
You just need to put them in the [output list](https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html#OutputOperands) and the compiler knows you are modifying them, so that should be fine. (albeit i mixed up output and input operands).
but what is going to prevent compiler from using those immdeiately after you modified them? The only way to interpret such test is to compile, run, and carefully study its disassembly.
On Sat Jul 5 01:39:17 2025 +0000, Paul Gofman wrote:
but what is going to prevent compiler from using those immdeiately after you modified them? The only way to interpret such test is to compile, run, and carefully study its disassembly.
And breakage on function exit clearly indicates problems around that (while maybe exactly that part can be fixed by properly indicating registers as output). Also, even for temporary ad-hoc testing, why not use explicit register names as in %r12 and mark those in output as clobbered also by name? I don't know if maybe everyone knows what are those 'j', 'k' correspond at once, but I find it difficult to even find anywhere what those are :)
compiler from using those immdeiately after you modified them?
that's fair. this is meant to be a quick test, and many registers did retain 0xdeadbeef.
And breakage on function exit.
that's an old comment i forgot to delete. that happened when i was using RtlUnwindEx and was actually unwinding out of the syscall.
On Sat Jul 5 01:54:30 2025 +0000, Yuxuan Shui wrote:
compiler from using those immdeiately after you modified them?
that's fair. this is meant to be a quick test, and many registers did retain 0xdeadbeef.
And breakage on function exit.
that's an old comment i forgot to delete. that happened when i was using RtlUnwindEx and was actually unwinding out of the syscall.
Actually, [looks like](https://testbot.winehq.org/JobDetails.pl?Key=159099) all non-volatile registers are actually all _zeroed_ before calling user callback.
On Sat Jul 5 01:55:26 2025 +0000, Yuxuan Shui wrote:
Actually, [looks like](https://testbot.winehq.org/JobDetails.pl?Key=159099) all non-volatile registers are actually all _zeroed_ before calling user callback.
But your test shows (Win11): ``` rip=00007ff750e81a12 rsp=00000010617ff900 rbp=00000010617ffa70 eflags=00000202 rax=00007ffd89112a00 rbx=0000000000000000 rcx=00000010617ff0f0 rdx=0000000000000082 rsi=0000000000000000 rdi=0000000000000000 r8=0000000000000000 r9=0000000000000000 r10=cf18061710d6ea70 r11=0000000000000246 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 mxcsr=00001f80 rip=00007ff750e81a25 rsp=00000010617ff900 rbp=00000010617ffa70 eflags=00000246 rax=00007ffd89112a00 rbx=00000000deadbeaf rcx=00007ff750e87040 rdx=0000000000000000 rsi=00000000deadbeaf rdi=00000000deadbeaf r8=00000010617ff8c8 r9=0000000000000000 r10=0000000000000000 r11=0000000000000246 r12=00000000deadbeaf r13=00000000deadbeaf r14=00000000deadbeaf r15=0000000000000000 mxcsr=00001f80
```
I see many non-volatile registers actually restored to 0xdeadbeef. For others, I am not immediately fully sure if they were actually deadbeef'ed ("i", "j", "k" thing is hard to follow), or werent' altered after before getting to the unwind target place.
On Sat Jul 5 01:59:45 2025 +0000, Paul Gofman wrote:
But your test shows (Win11):
rip=00007ff750e81a12 rsp=00000010617ff900 rbp=00000010617ffa70 eflags=00000202 rax=00007ffd89112a00 rbx=0000000000000000 rcx=00000010617ff0f0 rdx=0000000000000082 rsi=0000000000000000 rdi=0000000000000000 r8=0000000000000000 r9=0000000000000000 r10=cf18061710d6ea70 r11=0000000000000246 r12=0000000000000000 r13=0000000000000000 r14=0000000000000000 r15=0000000000000000 mxcsr=00001f80 rip=00007ff750e81a25 rsp=00000010617ff900 rbp=00000010617ffa70 eflags=00000246 rax=00007ffd89112a00 rbx=00000000deadbeaf rcx=00007ff750e87040 rdx=0000000000000000 rsi=00000000deadbeaf rdi=00000000deadbeaf r8=00000010617ff8c8 r9=0000000000000000 r10=0000000000000000 r11=0000000000000246 r12=00000000deadbeaf r13=00000000deadbeaf r14=00000000deadbeaf r15=0000000000000000 mxcsr=00001f80
I see many non-volatile registers actually restored to 0xdeadbeef. For others, I am not immediately fully sure if they were actually deadbeef'ed ("i", "j", "k" thing is hard to follow), or werent' altered after before getting to the unwind target place.
Also, you'd need to "spoil" the current register values before doing unwind, or it may err on this side if those registers are just preseved as deadbeef throughout the call chain.
I see many non-volatile registers actually restored to 0xdeadbeef.
er, sorry, the print out might be confusing. the upper half are registers returned by RtlVirtualUnwind, and I don't see any deadbeefs there?
the lower half and registers before calling destroywindow.
On Sat Jul 5 08:03:26 2025 +0000, Yuxuan Shui wrote:
I see many non-volatile registers actually restored to 0xdeadbeef.
er, sorry, the print out might be confusing. the upper half are registers returned by RtlVirtualUnwind, and I don't see any deadbeefs there? the lower half and registers before calling destroywindow.
I updated the test slightly:
```diff diff --git c/unwind test.c w/unwind test.c index 9fc6c12..0c2eced 100644 --- c/unwind test.c +++ w/unwind test.c @@ -136,13 +136,12 @@ int WINAPI mainCRTStartup(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR lp "mov $0xdeadbeaf, %10\n\t" "mov $0xdeadbeaf, %11\n\t" "mov $0xdeadbeaf, %12\n\t" - : : "r"(a), "r"(b), "r"(c), "r"(d), "r"(e), "r"(f), "r"(g), "r"(h), "r"(i), "r"(j), "r"(k), - "r"(l), "r"(m): ); - RtlCaptureContext(&prev_context); + : "=r"(a), "=r"(b), "=r"(c), "=r"(d), "=r"(e), "=r"(f), "=r"(g), "=r"(h), "=r"(i), "=r"(j), "=r"(k), + "=r"(l), "=r"(m): : ); destroy_window_launch_pad(hwnd, __builtin_frame_address(0)); + RtlCaptureContext(&prev_context); TRACE_CONTEXT(&prev_context);
- // unwinding out of a syscall seems to break some internal state? TerminateProcess(GetCurrentProcess(), 0); __builtin_unreachable();
```