Как использовать "GS:" в 64-битной сборке Windows (например, портирование TLS-кода)

Как программа пользовательского пространства может настроить "GS:" под 64-разрядную Windows (в настоящее время XP-64)?
(С помощью configure установите GS: 0 на произвольный 64-битный линейный адрес).

Я пытаюсь перенести среду JIT на X86-64, которая была первоначально разработана для Win32.

Одним из неудачных аспектов дизайна является то, что идентичный код должен выполняться на нескольких потоках пользовательского пространства (например, "волокна" ). В версии кода Win32 для этого используется GS-селектор и создается правильный префикс для доступа к локальным данным - "mov eax, GS: [offset]" указывает на правильные данные для текущей задачи. Код из версии Win32 будет загружать значение в GS, если бы оно имело значение, которое могло бы работать.

До сих пор мне удалось найти, что 64-битные окна не поддерживают LDT, поэтому метод, используемый в Win32, не будет работать. Однако набор команд X86-64 включает в себя "SWAPGS", а также метод загрузки GS без использования унаследованной сегментации, но который работает только в пространстве ядра.

В соответствии с руководствами X64, даже если Win64 разрешил доступ к дескрипторам - чего у него нет - не существует способа установить высокие 32-бит базы сегментов. Единственный способ установить это через GS_BASE_MSR (и соответствующий FS_BASE_MSR - остальные сегментные базы игнорируются в 64-битном режиме). Инструкция WRMSR - Ring0, поэтому я не могу использовать ее напрямую.

Я надеюсь на функцию Zw *, которая позволяет мне изменять "GS:" в пользовательском пространстве или в каком-либо другом темном углу Windows API. Я считаю, что Windows по-прежнему использует FS: для своего собственного TLS, поэтому какой-то механизм должен быть доступен?


Этот пример кода иллюстрирует проблему. Я заранее извиняюсь за использование байтового кода - VS не будет делать встроенную сборку для 64-битного компиляции, и я пытался сохранить это как один файл для иллюстративных целей.

Программа отображает "PASS" на XP-32 и не работает на 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");
}

Ответ 1

Вы можете напрямую изменить контекст потока через 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 и продолжить выполнение.
Это своего рода взлом, но все его документально подтвержденные функции:)

Ответ 2

Зачем вам нужно установить регистр GS? Windows устанавливает, если для вас, чтобы указать на пространство TLS.

Пока я не кодировал X64, я создал компилятор, который генерирует X32-бит-код, который управляет потоками, используя FS. В разделе X64 GS заменяет FS, а все остальное работает одинаково. Таким образом, GS указывает на локальное хранилище потоков. Если вы выделили блок локальных переменных потока (на Win32 мы выделяем 32 из 64 при смещении 0), ваш поток теперь имеет прямой доступ к 32 местам хранения по своему желанию. Вам не нужно выделять пространство для конкретного рабочего потока; Windows сделала это для вас.

Конечно, вы можете захотеть скопировать то, что вы считаете своими конкретными данными потока, в это пространство, которое вы отложили, в любом планировщике, который вы настроили для запуска ваших специфических для языка потоков.

Ответ 3

Никогда не изменяли GS в коде x64, поэтому я могу ошибаться, но разве вы не можете изменить GS с помощью PUSH/POP или LGS?

Обновление: руководства Intel также говорят, что mov SegReg, Reg допустим в 64-битном режиме.

Ответ 4

Так как x86_64 имеет гораздо больше регистров, чем x86, один из вариантов, который вы можете рассмотреть, если вы не можете использовать GS, просто будет использовать один из регистров общего назначения (например, EBP) в качестве базового указателя и сделать для разницы с новыми регистрами R8-R15.

Ответ 5

Что произойдет, если вы просто перейдете к потокам ОС? Плохо ли производительность?

Вы можете использовать один слот TLS размера указателя, чтобы сохранить базу вашей области хранения легкого потока. Вам просто нужно поменять один указатель во время переключения контекста. Загрузите один из новых регистров тем, когда вам нужно это значение, и вам не нужно беспокоиться об использовании одного из немногих сохраненных вызовов функций.

Другим поддерживаемым решением будет использование Fiber APIs для планирования ваших легких потоков. Затем вы измените JIT, чтобы сделать правильные вызовы FlsGet/SetValue.

Извините, похоже, что старый код написан, чтобы полагаться на префиксы сегментов для адресации, и теперь LDT просто недоступен для такого рода вещей. Вам придется немного исправить генерации кода.

существующий код сильно использует базовую адресацию с масштабированным индексом +, при этом термин GS как 3-й срок. Я думаю, что я могу использовать использование "lea", за которым следует двухрежимная форма

Звучит неплохо.

такие случаи, как "mov eax, mem", которые принимают префикс, но нуждаются в полной замене, чтобы использовать регистрационную адресацию

Возможно, вы можете перенести их на адрес + смещение адресации. Регистр смещения может быть регистром, содержащим базу вашего TLS-блока.

Ответ 6

Почему бы не использовать GetFiberData или вы пытаетесь избежать двух дополнительных инструкций?

Ответ 7

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 и/или гражданином.

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;
}