10

用户空间程序如何在 64 位 Windows(当前为 XP-64)下配置“GS:”?
(通过配置,将 GS:0 设置为任意 64 位线性地址)。

我正在尝试将“JIT”环境移植到最初为 Win32 开发的 X86-64。

一个不幸的设计方面是相同的代码需要在多个用户空间线程(例如,“光纤”)上运行。Win32 版本的代码为此使用 GS 选择器,并生成正确的前缀来访问本地数据——“mov eax,GS:[offset]”指向当前任务的正确数据。Win32 版本的代码会将一个值加载到 GS 中,只要它有一个可以工作的值。

目前发现64位windows不支持LDT,所以在Win32下用的方法不行。然而,X86-64 指令集包括“SWAPGS”,以及一种在不使用传统分段的情况下加载 GS 的方法——但这只适用于内核空间。

根据 X64 手册,即使 Win64 允许访问描述符——它不允许——也无法设置段基的高 32 位。设置这些的唯一方法是通过 GS_BASE_MSR(和相应的 FS_BASE_MSR - 其他段基在 64 位模式下被忽略)。WRMSR指令是Ring0,所以不能直接使用。

我希望有一个 Zw* 函数,它允许我在用户空间或 Windows API 的其他一些黑暗角落中更改“GS:”。我相信 Windows 仍然使用 FS: 作为自己的 TLS,所以必须有一些机制可用?


此示例代码说明了该问题。我提前为使用字节码道歉 - VS 不会为 64 位编译进行内联汇编,我试图将其保留为一个文件以用于说明目的。

该程序在 XP-32 上显示“PASS”,而在 XP-x64 上不显示。


#include <windows.h>
#include <string.h>
#include <stdio.h>


unsigned char GetDS32[] = 
            {0x8C,0xD8,     // mov eax, ds
             0xC3};         // ret

unsigned char SetGS32[] =
            {0x8E,0x6C,0x24,0x04,   // mov gs, ss:[sp+4] 
             0xC3 };                // ret

unsigned char UseGS32[] = 
           { 0x8B,0x44,0x24,0x04,   // mov eax, ss:[sp+4] 
             0x65,0x8B,0x00,        // mov eax, gs:[eax] 
             0xc3 };                // ret

unsigned char SetGS64[] =
            {0x8E,0xe9,             // mov gs, rcx
             0xC3 };                // ret

unsigned char UseGS64[] =       
           { 0x65,0x8B,0x01,         // mov eax, gs:[rcx]
             0xc3 };

typedef WORD(*fcnGetDS)(void);
typedef void(*fcnSetGS)(WORD);
typedef DWORD(*fcnUseGS)(LPVOID);
int (*NtSetLdtEntries)(DWORD, DWORD, DWORD, DWORD, DWORD, DWORD);

int main( void )
{
    SYSTEM_INFO si;
    GetSystemInfo(&si);
    LPVOID p = VirtualAlloc(NULL, 1024, MEM_COMMIT|MEM_TOP_DOWN,PAGE_EXECUTE_READWRITE);
    fcnGetDS GetDS = (fcnGetDS)((LPBYTE)p+16);
    fcnUseGS UseGS = (fcnUseGS)((LPBYTE)p+32);
    fcnSetGS SetGS = (fcnSetGS)((LPBYTE)p+48);
    *(DWORD *)p = 0x12345678;

    if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) 
    {
        memcpy( GetDS, &GetDS32, sizeof(GetDS32));
        memcpy( UseGS, &UseGS64, sizeof(UseGS64));
        memcpy( SetGS, &SetGS64, sizeof(SetGS64));
    }
    else
    {
        memcpy( GetDS, &GetDS32, sizeof(GetDS32));
        memcpy( UseGS, &UseGS32, sizeof(UseGS32));
        memcpy( SetGS, &SetGS32, sizeof(SetGS32));
    }

    SetGS(GetDS());
    if (UseGS(p) != 0x12345678) exit(-1);

    if (si.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64) 
    {
        // The gist of the question - What is the 64-bit equivalent of the following code
    }
    else
    {
        DWORD base = (DWORD)p;
        LDT_ENTRY ll;
        int ret;
        *(FARPROC*)(&NtSetLdtEntries) = GetProcAddress(LoadLibrary("ntdll.dll"), "NtSetLdtEntries");
        ll.BaseLow = base & 0xFFFF;
        ll.HighWord.Bytes.BaseMid = base >> 16;
        ll.HighWord.Bytes.BaseHi = base >> 24;
        ll.LimitLow = 400;     
        ll.HighWord.Bits.LimitHi = 0;
        ll.HighWord.Bits.Granularity = 0;
        ll.HighWord.Bits.Default_Big = 1; 
        ll.HighWord.Bits.Reserved_0 = 0;
        ll.HighWord.Bits.Sys = 0; 
        ll.HighWord.Bits.Pres = 1;
        ll.HighWord.Bits.Dpl = 3; 
        ll.HighWord.Bits.Type = 0x13; 
        ret = NtSetLdtEntries(0x80, *(DWORD*)&ll, *((DWORD*)(&ll)+1),0,0,0);
        if (ret < 0) { exit(-1);}
        SetGS(0x84);
    }
    if (UseGS(0) != 0x12345678) exit(-1);
    printf("PASS\n");
}
4

7 回答 7

4

您可以直接通过SetThreadcontext API修改线程上下文。但是,您需要确保在更改上下文时线程没有运行。要么挂起它并从另一个线程修改上下文,要么触发一个虚假的 SEH 异常并修改 SEH 处理程序中的线程上下文。然后操作系统将为您更改线程上下文并重新调度线程。

更新:

第二种方法的示例代码:

__try
{
    __asm int 3 // trigger fake exception
}
__except(filter(GetExceptionCode(), GetExceptionInformation()))
{
}

int filter(unsigned int code, struct _EXCEPTION_POINTERS *ep)
{
    ep->ContextRecord->SegGs = 23;
    ep->ContextRecord->Eip++;
    return EXCEPTION_CONTINUE_EXECUTION;
}

try 块中的指令基本上会引发软件异常。然后操作系统将控制转移到修改线程上下文的过滤程序,有效地告诉操作系统跳过 int3 指令并继续执行。
这是一种 hack,但它的所有记录功能:)

于 2009-07-26T19:19:33.407 回答
2

为什么需要设置GS寄存器?Windows 为您设置 if 指向 TLS 空间。

虽然我没有为 X64 编码,但我已经构建了一个编译器,它使用 FS 生成管理线程的 X32 位代码。在 X64 下,GS 取代了 FS,其他一切都一样。因此,GS 指向线程本地存储。如果您分配了一个线程局部变量块(在 Win32 上,我们在偏移量 0 处分配了 64 个中的 32 个),您的线程现在可以直接访问 32 个存储位置以进行任何它希望使用的操作。您不需要分配工作线程特定的空间;Windows 已经为您完成了。

当然,您可能希望将您认为特定线程数据复制到您预留的这个空间中,在您设置的任何调度程序中运行您的语言特定线程。

于 2009-07-28T04:12:32.573 回答
1

如果您只是转移到操作系统线程会发生什么?性能有那么差吗?

您可以使用单个指针大小的 TLS 插槽来存储轻量级线程存储区域的基础。您只需要在上下文切换期间换出一个指针。每当您需要该值时,从那里加载一个新的临时寄存器,您不必担心使用跨函数调用保留的少数几个之一。

另一个受支持的解决方案是使用Fiber API来安排轻量级线程。然后,您将更改 JIT 以正确调用FlsGet/SetValue.

抱歉,听起来旧代码是为依赖段前缀进行寻址而编写的,而现在 LDT 不适用于这种事情。您将不得不稍微修复代码生成。

现有代码大量使用缩放索引 + 基址寻址,其中 GS 术语作为第三术语。我想我可以使用 'lea' 后跟两个注册表单

听起来是个不错的计划。

像“mov eax, mem”这样的情况,它接受前缀但需要完全替换才能使用基于寄存器的寻址

也许您可以将它们移至地址+偏移地址。偏移寄存器可以是保存 TLS 块基址的寄存器。

于 2009-07-31T00:11:48.240 回答
1

Haven't ever modified GS in x64 code, so I may be wrong, but shouldn't you be able to modify GS by PUSH/POP or by LGS?

Update: Intel manuals say also mov SegReg, Reg is permissible in 64-bit mode.

于 2009-07-26T19:13:45.400 回答
1

为什么不使用 GetFiberData 或者你想避免这两个额外的指令?

于 2009-07-31T15:56:52.180 回答
1

由于 x86_64 比 x86 有更多的寄存器,如果不能使用 GS,您可能需要考虑的一个选项就是使用通用寄存器之一(例如 EBP)作为基指针,并弥补与新的 R8-R15 寄存器的区别。

于 2009-07-29T02:46:15.903 回答
0

x86-64 没有在寻址中添加新术语 - 现有代码大量使用缩放索引 + 基址寻址,其中 GS 术语作为第三术语。

我对您的问题感到相当困惑,但希望这个汇编程序有所帮​​助。我还没有将它移植到 C 代码中,但很快就会这样做:

读取__declspec(thread)数据

    mov     ecx, cs:TlsIndex ; TlsIndex is a memory location 
                             ; containing a DWORD with the value 0
    mov     rax, gs:58h
    mov     edx, 830h
    mov     rax, [rax+rcx*8]
    mov     rax, [rdx+rax]
    retn

不好意思,我没有写数据的例子,以上摘自我逆向工程的一些反汇编代码。

更新:这是等效的。上面的C代码,虽然不是我写的。我相信它是由 NTAuthority 和/或citizenmp 编写的。

rage::scrThread* GetActiveThread()
{
    char* moduleTls = *(char**)__readgsqword(88);

    return *reinterpret_cast<rage::scrThread**>(moduleTls + 2096);
}

这是被写入的相同内容:

void SetActiveThread(rage::scrThread* thread)
{
    char* moduleTls = *(char**)__readgsqword(88);
    *reinterpret_cast<rage::scrThread**>(moduleTls + 2096) = thread;
}
于 2017-02-01T09:46:57.970 回答