Как правильно вызвать функцию Win32/64 из LLVM?

Я пытаюсь вызвать метод из LLVM IR обратно в код C++. Я работаю в 64-битном Visual C++ или, как это описывает LLVM:

Machine CPU:      skylake
Machine info:     x86_64-pc-windows-msvc

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

В основном вызов выглядит так:

struct SomeStruct 
{
    static void Breakpoint( return; } // used to set a breakpoint
    static void Set(uint8_t* ptr, double foo) { return foo * 2; }
};

и LLVM IR выглядит так:

define i32 @main(i32, i8**) {
varinit:
  // omitted here: initialize %ptr from i8**. 
  %5 = load i8*, i8** %instance0

  // call to some method. This works - I use it to set a breakpoint
  call void @"Helper::Breakpoint"(i8* %5)

  // this call fails:
  call void @"Helper::Set"(i8* %5, double 0xC19EC46965A6494D)
  ret i32 0
}

declare double @"SomeStruct::Callback"(i8*, double)

Я подумал, что проблема, вероятно, в том, как работают соглашения о вызовах. Поэтому я попытался внести некоторые коррективы, чтобы исправить это:

// during initialization of the function
auto function = llvm::Function::Create(functionType, llvm::Function::ExternalLinkage, name, module);
function->setCallingConv(llvm::CallingConv::X86_64_Win64);
...

// during calling of the function
call->setCallingConv(llvm::CallingConv::X86_64_Win64);

К сожалению, что бы я ни пытался, я получаю ошибки «недействительной инструкции», о которых этот пользователь сообщает как о проблеме с соглашениями о вызовах: Clang создает исполняемый файл с недопустимой инструкцией . Я пробовал это с X86-64_Win64, Stdcall, Fastcall и без спецификаций соглашения о вызовах - все с тем же результатом.

Я прочитал на https://msdn.microsoft.com/en-us/library/ms235286.aspx в попытке выяснить, что происходит. Затем я посмотрел на выходные данные сборки, которые должны быть сгенерированы LLVM (используя API-вызов targetMachine->addPassesToEmitFile), и обнаружил:

    movq    (%rdx), %rsi
    movq    %rsi, %rcx
    callq   "Helper2<double>::Breakpoint"
    vmovsd  __real@c19ec46965a6494d(%rip), %xmm1
    movq    %rsi, %rcx
    callq   "Helper2<double>::Set"
    xorl    %eax, %eax
    addq    $32, %rsp
    popq    %rsi

Согласно MSDN, аргумент 2 должен быть в %xmm1, так что это тоже кажется правильным. Однако при проверке, все ли работает в отладчике, Visual Studio выдает много вопросительных знаков (например, "недопустимая инструкция").

Любая обратная связь приветствуется.


Код разборки:

00000144F2480007 48 B8 B6 48 B8 C8 FA 7F 00 00 mov         rax,7FFAC8B848B6h  
00000144F2480011 48 89 D1             mov         rcx,rdx  
00000144F2480014 48 89 54 24 20       mov         qword ptr [rsp+20h],rdx  
00000144F2480019 FF D0                call        rax  
00000144F248001B 48 B8 C0 48 B8 C8 FA 7F 00 00 mov         rax,7FFAC8B848C0h  
00000144F2480025 48 B9 00 00 47 F2 44 01 00 00 mov         rcx,144F2470000h  
00000144F248002F ??                   ?? ?? 
00000144F2480030 ??                   ?? ?? 
00000144F2480031 FF 08                dec         dword ptr [rax]  
00000144F2480033 10 09                adc         byte ptr [rcx],cl  
00000144F2480035 48 8B 4C 24 20       mov         rcx,qword ptr [rsp+20h]  
00000144F248003A FF D0                call        rax  
00000144F248003C 31 C0                xor         eax,eax  
00000144F248003E 48 83 C4 28          add         rsp,28h  
00000144F2480042 C3                   ret  

Некоторая информация о памяти отсутствует. Вид памяти:

0x00000144F248001B 48 b8 c0 48 b8 c8 fa 7f 00 00 48 b9 00 00 47 f2 44 01 00 00 62 f1 ff 08 10 09 48 8b 4c 24 20 ff d0 31 c0 48 83 c4 28 c3 00 00 00 00 00 ...

Здесь отсутствуют вопросительные знаки: «62 f1».


Некоторый код полезен, чтобы увидеть, как я компилирую JIT и т. Д. Я боюсь, что он немного длинный, но помогает понять идею ... и я понятия не имею, как создать меньший фрагмент кода.

    // Note: FunctionBinderBase basically holds an llvm::Function* object
    // which is bound using the above code and a name.
    llvm::ExecutionEngine* Module::Compile(std::unordered_map<std::string, FunctionBinderBase*>& externalFunctions)
    {
        //          DebugFlag = true;

#if (LLVMDEBUG >= 1)
        this->module->dump();
#endif

        // -- Initialize LLVM compiler: --
        std::string error;

        // Helper function, gets the current machine triplet.
        llvm::Triple triple(MachineContextInfo::Triplet()); 
        const llvm::Target *target = llvm::TargetRegistry::lookupTarget("x86-64", triple, error);
        if (!target)
        {
            throw error.c_str();
        }

        llvm::TargetOptions Options;
        // Options.PrintMachineCode = true;
        // Options.EnableFastISel = true;

        std::unique_ptr<llvm::TargetMachine> targetMachine(
            target->createTargetMachine(MachineContextInfo::Triplet(), MachineContextInfo::CPU(), "", Options, llvm::Reloc::Default, llvm::CodeModel::Default, llvm::CodeGenOpt::Aggressive));

        if (!targetMachine.get())
        {
            throw "Could not allocate target machine!";
        }

        // Create the target machine; set the module data layout to the correct values.
        auto DL = targetMachine->createDataLayout();
        module->setDataLayout(DL);
        module->setTargetTriple(MachineContextInfo::Triplet());

        // Pass manager builder:
        llvm::PassManagerBuilder pmbuilder;
        pmbuilder.OptLevel = 3;
        pmbuilder.BBVectorize = false;
        pmbuilder.SLPVectorize = true;
        pmbuilder.LoopVectorize = true;
        pmbuilder.Inliner = llvm::createFunctionInliningPass(3, 2);
        llvm::TargetLibraryInfoImpl *TLI = new llvm::TargetLibraryInfoImpl(triple);
        pmbuilder.LibraryInfo = TLI;

        // Generate pass managers:

        // 1. Function pass manager:
        llvm::legacy::FunctionPassManager FPM(module.get());
        pmbuilder.populateFunctionPassManager(FPM);

        // 2. Module pass manager:
        llvm::legacy::PassManager PM;
        PM.add(llvm::createTargetTransformInfoWrapperPass(targetMachine->getTargetIRAnalysis()));
        pmbuilder.populateModulePassManager(PM);

        // 3. Execute passes:
        //    - Per-function passes:
        FPM.doInitialization();
        for (llvm::Module::iterator I = module->begin(), E = module->end(); I != E; ++I)
        {
            if (!I->isDeclaration())
            {
                FPM.run(*I);
            }
        }
        FPM.doFinalization();

        //   - Per-module passes:
        PM.run(*module);

        // Fix function pointers; the PM.run will ruin them, this fixes that.
        for (auto it : externalFunctions)
        {
            auto name = it.first;
            auto fcn = module->getFunction(name);
            it.second->function = fcn;
        }

#if (LLVMDEBUG >= 2)
        // -- ASSEMBLER dump code
        // 3. Code generation pass manager:

        llvm::legacy::PassManager CGP;
        CGP.add(llvm::createTargetTransformInfoWrapperPass(targetMachine->getTargetIRAnalysis()));
        pmbuilder.populateModulePassManager(CGP);

        std::string result;
        llvm::raw_string_ostream str(result);
        llvm::buffer_ostream os(str);

        targetMachine->addPassesToEmitFile(CGP, os, llvm::TargetMachine::CodeGenFileType::CGFT_AssemblyFile);

        CGP.run(*module);

        str.flush();

        auto stringref = os.str();
        std::string assembly(stringref.begin(), stringref.end());

        std::cout << "ASM code: " << std::endl << "---------------------" << std::endl << assembly << std::endl << "---------------------" << std::endl;
        // -- end of ASSEMBLER dump code.

        for (auto it : externalFunctions)
        {
            auto name = it.first;
            auto fcn = module->getFunction(name);
            it.second->function = fcn;
        }

#endif

#if (LLVMDEBUG >= 2)
        module->dump(); 
#endif

        // All done, *RUN*.

        llvm::EngineBuilder engineBuilder(std::move(module));
        engineBuilder.setEngineKind(llvm::EngineKind::JIT);
        engineBuilder.setMCPU(MachineContextInfo::CPU());
        engineBuilder.setMArch("x86-64");
        engineBuilder.setUseOrcMCJITReplacement(false);
        engineBuilder.setOptLevel(llvm::CodeGenOpt::None);

        llvm::ExecutionEngine* engine = engineBuilder.create();

        // Define external functions
        for (auto it : externalFunctions)
        {
            auto fcn = it.second;
            if (fcn->function)
            {
                engine->addGlobalMapping(fcn->function, const_cast<void*>(fcn->FunctionPointer())); // Yuck... LLVM only takes non-const pointers
            }
        }

        // Finalize
        engine->finalizeObject();

        return engine;
    }

Обновление (выполняется)

Видимо у моего Скайлейка проблемы с инструкцией vmovsd. При запуске того же кода на Haswell (сервере) тест завершается успешно. Я проверил вывод сборки на обоих - они абсолютно одинаковы.

Просто чтобы быть уверенным: XSAVE/XRESTORE не должно быть проблемой на Win10-x64, но давайте все равно выясним. Я проверил функции с кодом из https://msdn.microsoft.com/en-us/library/hskdteyh.aspx и XSAVE/XRESTORE из https://insufficientlycomplicated.wordpress.com/2011/11/07/detecting-intel-advanced-vector-extensions-avx-in-visual-studio/ . Последний работает нормально. Что касается первого, то вот результаты:

GenuineIntel
Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
3DNOW not supported
3DNOWEXT not supported
ABM not supported
ADX supported
AES supported
AVX supported
AVX2 supported
AVX512CD not supported
AVX512ER not supported
AVX512F not supported
AVX512PF not supported
BMI1 supported
BMI2 supported
CLFSH supported
CMPXCHG16B supported
CX8 supported
ERMS supported
F16C supported
FMA supported
FSGSBASE supported
FXSR supported
HLE supported
INVPCID supported
LAHF supported
LZCNT supported
MMX supported
MMXEXT not supported
MONITOR supported
MOVBE supported
MSR supported
OSXSAVE supported
PCLMULQDQ supported
POPCNT supported
PREFETCHWT1 not supported
RDRAND supported
RDSEED supported
RDTSCP supported
RTM supported
SEP supported
SHA not supported
SSE supported
SSE2 supported
SSE3 supported
SSE4.1 supported
SSE4.2 supported
SSE4a not supported
SSSE3 supported
SYSCALL supported
TBM not supported
XOP not supported
XSAVE supported

Это странно, поэтому я подумал: почему бы просто не передать инструкцию напрямую.

int main()
{
    const double value = 1.2;
    const double value2 = 1.3;

    auto x1 = _mm_load_sd(&value);
    auto x2 = _mm_load_sd(&value2);

    std::string s;
    std::getline(std::cin, s);
}

Этот код работает нормально. Разборка:

    auto x1 = _mm_load_sd(&value);
00007FF7C4833724 C5 FB 10 45 08       vmovsd      xmm0,qword ptr [value]  

    auto x1 = _mm_load_sd(&value);
00007FF7C4833729 C5 F1 57 C9          vxorpd      xmm1,xmm1,xmm1  
00007FF7C483372D C5 F3 10 C0          vmovsd      xmm0,xmm1,xmm0  

Судя по всему, он не будет использовать регистр xmm1, но тем не менее доказывает, что сама инструкция делает свое дело.


person atlaste    schedule 25.08.2016    source источник
comment
Это должно быть %xmm0.   -  person Ha.    schedule 25.08.2016
comment
@Ха. Да, я согласен, но даже если это так, я бы не ожидал, что он сломается.   -  person atlaste    schedule 26.08.2016
comment
Возможно, ваш сбой не имеет ничего общего с соглашением о вызовах. Недопустимая инструкция, выдаваемая clang, печатается как ud2, а не как вопросительные знаки. Вы можете переключиться в режим дизассемблирования в Visual Studio и пошагово посмотреть, что на самом деле происходит во время звонка.   -  person Ha.    schedule 26.08.2016
comment
@Ха. Может быть, это и происходит, но это странно, поскольку я не делаю ничего странного, верно? Я добавил скриншот того, что я вижу в представлении разборки VS... как вы можете видеть, все это знаки вопроса.   -  person atlaste    schedule 27.08.2016
comment
Можно ли вручную определить содержание вопросительных знаков? Вы можете использовать либо представление памяти, либо * (unsigned char *)(0xaddress) в отладчике.   -  person Ha.    schedule 27.08.2016
comment
@Ха. Я добавил запрошенную вами информацию. По-видимому, я изменил часть кода, поэтому я добавил немного больше информации и обновил материал IR/ASM, который также изменился. Для полноты я также добавил код, который я использую для генерации кода. PS: я пробовал и с double Get() { return 2.2; }; это дает такое же поведение.   -  person atlaste    schedule 27.08.2016
comment
Инструкция позади ?? это vmovsd xmm1, qword ptr [rcx]. Так что это загрузка вашей двойной константы не удалась.   -  person Ha.    schedule 27.08.2016
comment
Вы случайно не используете Windows Vista, Windows 7 без SP1 или Windows Server 2008 R2 без SP1? Эта ОС не поддерживает AVX (vmovsd — это инструкция AVX), даже если процессор поддерживает. (drdobbs.com/parallel/windows -7-и-windows-server-2008-r2-ser/)   -  person Ha.    schedule 27.08.2016
comment
@Ха. Я работаю здесь над профессиональной версией Windows 10 x64, так что это не должно быть проблемой. Тем не менее, я заметил, что LLVM сообщает о некоторых странных особенностях этого процессора. Обратите внимание на наборы AVX512 здесь; Я уверен, что это ошибка ...: SSE4a avx512bw CX16 TBM XSAVE fma4 avx512vl prfchw bmi2 айх xsavec fsgsbase AVX avx512cd avx512pf МРВ POPCNT FMA BMI АЕС rdrnd xsaves SSE4.1 SSE4.2 AVX2 avx512er ссе lzcnt pclmul avx512f F16C SSSE3 MMX ФКУ cmov xop rdseed movbe hle xsaveopt sha sse2 sse3 avx512dq . Кроме того, через секунду я попытаюсь запустить несколько тестов на другой системе...   -  person atlaste    schedule 27.08.2016
comment
@Ха. Только что протестировано на моем сервере (Xeon E5-2650L v3). Это архитектура Haswell. Очевидно, это действительно работает там. Я только что проверил остальное - видимо, сгенерированный код сборки точно такой же. Вы правы, похоже, что выполнение AVX отключено. Iirc У меня есть код, чтобы проверить это, дайте мне минутку...   -  person atlaste    schedule 27.08.2016
comment
@Ха. Просто добавил последнюю информацию. Флаги XSAVE/XRESTORE и CPUID вроде в порядке.   -  person atlaste    schedule 27.08.2016
comment
@Ха. Я только что закончил выяснять детали ошибки, как вы можете прочитать в ответе, который я набросал. Оказывается, проблема в определении уровня SSE в LLVM. Я сделал отчет об ошибке для команды. Тем не менее, это была ваша идея проверить байты памяти, которые привели меня к ответу, поэтому, если вы что-нибудь запишете, я буду более чем счастлив наградить вас наградой.   -  person atlaste    schedule 28.08.2016
comment
Нет, ваш самостоятельный ответ прекрасно резюмирует это. Я не возражаю, когда не могу получить репутацию из-за недостатков сайта. Я наслаждался хорошей тайной и общением с вами, и это то, что действительно важно.   -  person Ha.    schedule 28.08.2016
comment
@Ха. Хорошо. В таком случае я был рад предоставить хорошую тайну :-)   -  person atlaste    schedule 29.08.2016


Ответы (1)


Я только что проверил на другом Intel Haswell, что здесь происходит, и нашел это:

0000015077F20110 C5 FB 10 08          vmovsd      xmm1,qword ptr [rax] 

Видимо на Intel Haswell выдает другую инструкцию байт-кода, чем на моем Skylake.

@Ха. на самом деле был достаточно любезен, чтобы указать мне правильное направление здесь. Да, скрытые байты действительно указывают на VMOVSD, но видимо он закодирован как EVEX. Это все хорошо, но префикс/кодировка EVEX будет введен в последней архитектуре Skylake как часть AVX512, который не будет поддерживаться до Skylake Purley в 2017 году. Другими словами, это является неверная инструкция.

Для проверки я поставил точку останова в X86MCCodeEmitter::EmitMemModRMByte. В какой-то момент я вижу, что bool HasEVEX = [...] оценивается как true. Это подтверждает, что кодеген/эмиттер выдает неверный результат.

Поэтому я пришел к выводу, что это должна быть ошибка в целевой информации LLVM для процессоров Skylake. Это означает, что осталось сделать только две вещи: выяснить, где именно эта ошибка находится в LLVM, чтобы мы могли ее решить и сообщить об ошибке команде LLVM...

Так где же это в LLVM? Трудно сказать ... x86.td.def определяет функции Skylake как «FeatureAVX512», что, вероятно, вызовет X86SSELevel для AVX512F. Это, в свою очередь, даст неправильные инструкции. В качестве обходного пути лучше просто сказать LLVM, что вместо этого у нас есть Intel Haswell, и все будет хорошо:

// MCPU is used to call createTargetMachine
llvm::StringRef MCPU = llvm::sys::getHostCPUName();
if (MCPU.str() == "skylake")
{
    MCPU = llvm::StringRef("haswell");
}

Тест, работает.

person atlaste    schedule 28.08.2016
comment
Это объясняет, почему я не смог найти кодировку инструкций в документации — она слишком новая. Я еще не видел кодировку EVEX, поэтому не узнал ее как таковую. Ну, по крайней мере, я узнал что-то новое сегодня. - person Ha.; 28.08.2016
comment
@Ха. То же самое. Я читал спецификации Intel год назад, и тогда они не описывали EVEX. Это на самом деле имеет смысл, потому что процессоры еще не имеют регистров zmm. Кроме того, Intel сообщила, что Skylake будет поддерживать AVX512, и позже объяснила, что только поздние Skylake Purley (Xeon) / Kings Landing будут поддерживать AVX512 и Skylake AVX512F. Итак, дело в том, что на сегодняшний день он не будет поддерживать AVX512. Это было довольно запутанно имхо. CPUID отражает это, но, видимо, SSELevel так не работает (это просто кодировка). Запутанно, запутанно... В любом случае, спасибо за помощь! - person atlaste; 29.08.2016