65.9K
CodeProject 正在变化。 阅读更多。
Home

QOR 架构方面

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (3投票s)

2013年7月11日

CPOL

24分钟阅读

viewsIcon

18391

downloadIcon

126

Querysoft 开放运行时:架构兼容性方面。

JIT Assembler

QOR - 架构模块:ArchQOR

本文是关于 Querysoft Open Runtime (QOR) 系列文章的第三篇,QOR 是一个用于 C++ 软件的开源、面向切面的框架。介绍性文章可以在此处找到。与本文相关的源代码设计为独立工作。您无需将其与之前文章中的代码结合使用。

引言:需要一些汇编

QOR 架构模块抽象了 QOR 目标的不同硬件平台,或者换句话说,它是 QOR 的架构方面。原则上这应该很容易,因为我们已经要求一个可用的 C++ 编译器针对一个平台,以便 QOR 支持它。其余的硬件特性应该由操作系统处理,所以应该没有太多其他事情要做。然而,事实证明,像往常一样,这要复杂一些。

QOR 是一个 C++ 框架,我们在某些地方不遗余力地确保它仍然是一个纯粹的 C++ 框架。在某些领域,传统上做到这一点很困难,特别是在目标操作系统或编译器实际上需要汇编语言代码才能编译的情况下。例如,为了支持结构化异常处理、C++ 异常处理以及 Microsoft Visual C++ 在 32 位 Windows 上提供的额外调试和安全支持,而无需包含任何 Microsoft 代码,一些汇编语言元素是必不可少的。

以下是我们临时的 Win32 异常处理和 SEH 实现中的一个示例函数,它无法在不使用汇编的情况下实现。

__QCMP_DECLARE_NAKED void JumpToFunction( void* target, void* targetStack, void* targetEBP )
{
    __asm
    {
        mov eax,[esp + 0x4] // target
        mov ebp,[esp + 0xC] // targetEBP
        mov esp,[esp + 0x8] // targetStack
        jmp eax
    }
}

如果您从未需要任何内联汇编来使您的 C++ 代码工作,您可能会问这是怎么回事。答案是它通常由操作系统或构建工具附带的 C 和 C++ 库提供。在 Windows 上使用 MSVC,它甚至会以微小的静态库的形式偷偷进入您的原生应用程序,而无需您请求就进行链接。对于 QOR 来说,这是不可接受的,因为我们希望 100% 开源,不包含任何专有代码,并且我们的实现是完全开放和透明的。

然而,我们无法完全避免汇编语言,事实上,即使我们能,我们也会有所损失,因为有少数几件事情仍然确实应该用汇编来完成。例如,几乎所有图像处理核心的数学运算都可以受益于在新处理器上使用 SIMD(单指令多数据)扩展,使得缩放和转换图像等实际操作比其他方式快很多倍。

那么为什么不走捷径呢?

我们能不能妥协一下,在这里或那里包含一些内联汇编或一些预构建的静态库,毕竟即使 Microsoft Platform SDK 在某些地方也这样做?

__inline PVOID GetCurrentFiber( void ) { __asm mov eax, fs:[0x10] }

答案是不,有3个原因。

我们使用哪种汇编语言?

这不仅仅取决于我们目标哪种处理器,而且就 x86 而言,汇编源的语法至少有两种非常不同的变体,AT&T 语法和 Intel 语法。下面的简单示例表明,不仅格式不同,而且两者之间的操作数顺序也相反。上面的 `GetCurrentFiber` 代码在默认的 AT&T 模式下无法与 GCC 汇编器 GAS 一起工作,这也是 MinGW 环境在您尝试将其与 Windows Platform SDK 头文件一起使用时会崩溃的原因之一。

AT&T 语法
movl %esp, %ebp
Intel 语法
mov ebp, esp

与不同的 C++ 编译器不同,我们无法通过 CompilerQOR 这样的库来克服 AT&T 和 Intel 语法之间的差异。为了支持两者,我们必须为 x86 系统准备两套源代码,每种语法一套。我们仍然需要一个完整的支持库,以允许用户选择他们的汇编器,NASM、FASM、TASM 或 HLASM,而不是像其他一些框架那样将他们锁定在 MASM 或 GAS 中。

我们如何在不要求 SSE4 的情况下支持 SSE4?

第二个挑战是在 QOR 编译时考虑目标硬件的差异。例如,如果一个编译为使用 SSE4 的 QOR 无法在不支持 SSE4 的机器上运行,我们如何才能充分利用支持 SSE4 的 x86 机器上的 SSE4?

在支持 SSE4 的最新 x86 CPU 上,我们可以这样做

pmovsxbd xmm0, m32

但是如果只有 SSE2,我们必须这样做才能达到相同的结果。

movd xmm0, m32
punpcklbw xmm0, xmm0
punpcklwd xmm0, xmm0
psrad, xmm, 24

我们如何处理禁用 asm 的 64 位编译器?

内联汇编被排除的第三个原因是,它不被微软的 64 位编译器支持。如果我们确实需要 64 位 Windows 支持,那么源代码树中就不能允许内联汇编器。我们可以像微软那样通过大量的条件编译和目标特定的内联函数来解决这个问题,但这会将我们锁定在使用微软或英特尔编译器上,因为只有这些编译器支持这些内联函数,而我们已经为打破这种限制付出了很多努力,正如在 CompilerQOR 中所看到的那样。

紧急救援,即时编译

解决这些问题以及支持非 x86 硬件目标的答案是创建一个即时(JIT)汇编器,它不依赖于任何现有汇编器,无论是 GAS 还是 MASM,AT&T 还是 Intel。

像 JIT 编译器一样,JIT 汇编器是在代码已经在目标机器上运行时才汇编代码的。因此,它可以在汇编发生之前检测目标机器上的功能是否存在。它可以在仍然能够在旧硬件上运行的同时,利用可用的最佳技术。微软的 .NET 语言和 Java 都有 JIT 编译器。我们不想用 JIT 编译语言取代 C++,而是只 JIT 汇编少量必要的汇编语言代码,使 C++ 在不同硬件之间可移植,成功集成到不同操作系统中,并能够利用可用的高级处理器功能。

不仅 JIT 编译所有内容会对性能造成很大的影响,而且这意味着 JIT 编译器本身将被排除在外,成为一个独立的依赖项,无法利用使系统其余部分可移植的优点。你不能 JIT 一个 JITer,借用一句话。这个问题在过去曾被 JVM 解决过,JVM 在使用 Java 之前需要安装,并通过中间语言或 PCode 来解决。我不认为这些是完整的解决方案,而只是将问题转移并引入了许多新问题,甚至是全新的语言供开发人员学习才能有效地调试。

QOR 将包含一个 JIT 汇编器,它构成了其架构抽象模块的主体,允许硬件依赖关系被抽象化,并使得可以在运行时从 C++ 生成特定于目标的汇编例程,从而充分利用 QOR 的可移植性。

幸运的是,我不需要从头开始编写 JIT 汇编器,因为 Petr Kobalicek 已经创建了令人惊叹的 AsmJit 项目。我将以此为起点,并进行适当的调整,使其符合 QOR 的原则和实践。

我仍然会像从头开始一样讲解设计过程,但这并不意味着 Petr 最初就是这么想的。

JIT 汇编器设计

一种简单诱人的 JIT 汇编器实现方式是为目标处理器的每一个操作编写一个等效的 C++ 函数。对于 x86 来说,这将是数百个函数,代码量并不过分。为了执行汇编程序,我们只需依次调用这些函数。每个函数在被调用时都会执行单个汇编级指令。

Wrong way to do an assembler

其中 lea 的实现可能类似于

__declspec( __naked ) void lea( void )
{
    _asm{ lea }
}

不幸的是,这将是一个糟糕的解决方案,原因有二。首先,与真正的汇编相比,性能会非常非常差,因为每次指令调用 C++ 函数都会产生开销,可能慢 20 倍。其次,它无法工作,因为这种调用开销还会破坏整个汇编函数工作所需的必要状态。C++ 编译器在构成汇编函数的多个函数调用之间不会保留寄存器,也不会保留堆栈。

Why it won't work that way

但是,如果我们稍微修改一下这个想法,将汇编指令批处理呢?我们仍然为每条汇编指令调用一个 C++ 函数,但不是立即在硬件上执行该指令,而是将指令以其将要执行的精确形式保存到缓冲区中。一旦将整批指令写入缓冲区,我们可以将缓冲区地址转换为具有正确签名的函数指针,并像调用与应用程序其余部分一起编译的常规 C++ 函数一样调用汇编函数。我们甚至可以保留缓冲区,以便可以重复调用该函数,而无需每次都重新汇编它。

A better way to do an assembler

这就是 JIT 汇编器的本质。我们调用一系列等效于我们想要执行的汇编指令的 C++ 函数。这些指令被写入一个缓冲区。一旦完成,我们的缓冲区被复制或更改为可执行内存,然后可以被视为一个函数。所有编译的代码仍然是用 C++ 编写的,包括生成特定架构汇编函数的函数。

无论我们针对何种架构,这个过程都保持不变,因此许多代码和外部接口可以泛化,由当前和未来的特定架构实现共享。那么,我将从这些通用的基类和用于指令级汇编器的共享代码开始,深入探讨具体细节。

通用汇编器概述

我将描述一系列最初相互无关的简单类,然后自下而上地将它们组合起来,形成汇编器大纲。

我们从一个简单的缓冲区类开始,它能够将字节和各种大小的字读写到一个可增长的连续缓冲区中。这相当于上面设计中在增量缓冲区写入阶段描述的代码缓冲区。

CAsmBuffer collaboration

注意 `take` 函数,它在写入后提供对缓冲区的访问。

接下来我们有一个抽象的通用汇编器类,它只规定汇编器应该能够报告其生成代码的大小并将其重定位到给定目的地。

CAbstractAssembler collaboration

为了提高汇编器的性能,我们可以通过使用 Zone 中间分配器来避免大量的增量内存分配和重新分配,该分配器每次从操作系统获取更大的块,并以最小的开销按请求分发它们。

CZone collaboration

跟踪任何汇编或编译过程的进展非常重要,因为错误不可避免会发生。为此,ArchQOR 定义了一个抽象的 Logger 类,可以在下游实现以跟踪汇编过程。

CLogger inheritance

将汇编好的缓冲区最终转换为可执行代码的专业工作由 CodeGenerator 完成。目前,我们希望立即执行代码,但将来我们可以使用不同的 `CCodeGeneratorBase` 子类,像常规汇编器一样将可执行模块写入磁盘。

CJitCodeGenerator collaboration

将缓冲区从数据字节转换为可执行代码可能看起来微不足道,或者更像黑魔法,这取决于您对此思考了多少。实际上,这并不意味着对缓冲区中的字节做任何事情,但它可能意味着与操作系统进行特殊协商,以使该内存可执行,或将缓冲区内容传输到操作系统乐于执行的内存中。这些操作由内存管理器代表代码生成器执行,由于与操作系统的交互,内存管理器分为操作系统通用部分和操作系统特定部分。操作系统特定部分最终将位于 SystemQOR 库中,但我们还没有这个库,因此目前需要一些临时处理。

CVirtualMemoryManager collaboration

基础工作已经奠定,现在我们准备创建抽象的批处理操作 CPU,将我们描述的大多数类联系起来。

CCPUBase collaboration

`CCPUBase` 仍然是完全通用的,但正如协作图所示,它能够与低级缓冲区、内存管理器、抽象代码生成器以及我们决定部署的任何日志记录进行交互。这些事情现在都由所有特定架构实现来处理,除非我们发现需要覆盖它们。

为了管理 CPU 指令集的可选扩展,包括内置浮点单元,我们需要一种添加此类扩展的方法。目前,我们只创建一个空的抽象 `CISetExtension` 类。

CISetExtension collaboration

汇编器目标

像围绕它们构建的计算设备一样,处理器种类繁多,但至少可以将其分为一些有用的类别。

  • CPU(中央处理器)是主要的工作马通用计算设备,大多数应用程序代码都以它们为目标。这些是我们希望使用 QOR 的操作系统所针对的处理器。
  • FPU(浮点单元)是专业的数学处理器,通常内置于 CPU 中或与 CPU 紧密耦合。这些处理器可加快非整数算术的执行速度。
  • GPU (图形处理单元) 是武器库的最新成员。这些专用单元设计用于实时处理视频数据,通常具有大规模并行性,但适应性不如 CPU 甚至 FPU,它们直到最近才通过 CUDA 和 OpenCL 等发展变得真正可编程。

对于 QOR,我们将重点放在 CPU 上,并兼顾 FPU,并保留以后引入 GPU 编程的可能性。这将为我们提供一个可移植框架所需的一切,并有可能在以后进行扩展,而不会使 ArchQOR 本身成为一个过于庞大的项目。

Assembler targets

这种支持硬件配置的通用结构体现在抽象的 `CLogicBase` 类以及从它派生出来的特定架构类中。

CLogicBase inheritance

由于我们专注于 x86 CPU,FPU 和 GPU 类目前只是简单的占位符。

ArchQOR 包

在我们为 x86 架构特化通用类之前,让我们将迄今为止开发的通用类置于库的上下文中。

我们将为 ArchQOR 库创建一个主头文件 `ArchQOR.h`。

//ArchQOR.h
...
#ifndef _QARCH_H_
#define _QARCH_H_

#include "CompilerQOR.h"            //Source Compiler definition and framework config
#include "ArchQOR/Defs.h"            //Basic definitions for architecture configuration
#include "ArchQOR/ArchitectureSelection.h"    //Select and configure the architecture platform
#include "ArchQOR/Machine.h"        //Define a Machine representative class

#endif//_QARCH_H_

一个基本定义头文件,用于枚举我们希望支持的架构 `Defs.h`。

//Defs.h
...
//Basic definitions for ArchQOR

#ifndef _QARCH_DEFS_H_
#define _QARCH_DEFS_H_

//Platforms
//Possible values for _QARCH_ARCHITECTURE
#define _QARCH_X86PC        1    //PC x86 based boxes, Intel, AMD etc
#define _QARCH_ARMMOBILE    2    //ARM based Smart phone and tablet type SOC platforms

#endif//_QARCH_DEFS_H_

一个预处理器包含控制头文件,用于选择性地仅包含目标架构的头文件 `ArchitectureSelection.h`。

#ifndef _QARCH_ARCHITECTURESELECTION_H_
#define _QARCH_ARCHITECTURESELECTION_H_

#ifndef    _QARCH_ARCHITECTURE
#    define _QARCH_ARCHITECTURE _QARCH_X86PC
        __QCMP_MESSAGE( "Target Architecture defaulted to x86 PC." )
#endif

#if    ( _QARCH_ARCHITECTURE == _QARCH_X86PC )
    __QCMP_MESSAGE( "Target Architecture x86 PC." )
#    include "ArchQOR/x86/Config/x86PCSelection.h"

#elif    ( _QARCH_ARCHITECTURE == _QARCH_ARMMOBILE )
    __QCMP_MESSAGE( "Target Architecture ARM mobile device." )
#    include "ArchQOR/ARM/Config/ARMSelection.h"
#endif

#endif//_QARCH_ARCHITECTURESELECTION_H_

一个 `Machine` 对象头文件,用于声明 `CMachine` 类型作为 ArchQOR 暴露的根公共对象 `Machine.h`。

//Machine.h
...
#ifndef _QARCH_MACHINE_H_
#define _QARCH_MACHINE_H_

namespace nsArch
{
    //------------------------------------------------------------------------------
    class __QOR_INTERFACE( __ARCHQOR ) CMachine : public CArchVPackage
    {
    public:

        CMachine();
        virtual ~CMachine();
    };
}

__QCMP_LINKAGE_C __QOR_INTERFACE( __ARCHQOR ) nsArch::CMachine& TheMachine( void );

#endif//_QARCH_MACHINE_H_

顶层头文件就这些了。在项目中定义 `_QARCH_ARCHITECTURE` 允许我们选择一个架构,或者它默认为 x86。这些选择导致的头文件负责定义 `CArchVPackage` 基类,`CMachine` 从中派生。

这样就完成了我们可以构建对特定架构支持的通用基础设施。

x86 汇编器

过去二十年来,主导的 CPU 系列是英特尔的 x86 处理器在个人电脑和大型设备中,以及各种制造商的基于 ARM 的处理器在手持和嵌入式设备中。目前,ArchQOR 将支持 x86 架构,它们本身内部存在足够的变体,并且肯定足够复杂,可以彻底测试 JIT 汇编器概念并让我忙碌几个月。

x86 汇编语言主要由操作码标识的指令与操作数组合而成,操作数可以是寄存器内存位置或立即值,即值本身作为指令的一部分。

寄存器可以是通用寄存器,也可以是专用寄存器。x86 系列处理器多年来新增了许多寄存器。除了最初的 16 位 8086 寄存器及其 32 位扩展版本之外,现在还有用于内置 FPU 的 x87 寄存器、用于与每代 x86 处理器不断增长的巨大指令集一起使用的多媒体寄存器扩展多媒体寄存器。我们还需要在汇编器中使用标签,以便我们可以进行跳转和循环。

为了保持我们最初设计无法保持的寄存器状态,我们还需要一个概念:**变量**。这也将有助于最终将汇编代码与编译的 C++ 代码集成。我们将需要**通用、x87、MM 和 XMM 变量**用于 x86 汇编,以与不同类型的寄存器匹配。

这些对象构成了 x86 汇编器操作数类层次结构。

COperand inheritence

为了生成有效的 x86 指令,我们需要能够定义指令是什么以及枚举存在哪些有效的操作码操作数组合。为此,我们定义了一个 `InstructionDescription struct` 和一个指令描述表。

//------------------------------------------------------------------------------
struct InstructionDescription
{

    //------------------------------------------------------------------------------
    // Instruction groups.
    // This should be only used by assembler, because it's Assembler
    // specific grouping. Each group represents one 'case' in the Assembler's 
    // main emit method.

    enum G
    {
        // Group categories.
        G_EMIT,
        G_ALU,
        G_BSWAP,
        ...
        G_MMU_RM_IMM8, 
        G_MMU_RM_3DNOW // Group for 3dNow instructions
    };


    //------------------------------------------------------------------------------
    // Instruction core flags.
    enum F
    {
        F_NONE = 0x00, // No flags.
        F_JUMP = 0x01, // Instruction is jump, conditional jump, call or ret.
        F_MOV = 0x02, // Instruction will overwrite first operand - o[0].
        F_FPU = 0x04, // Instruction is X87 FPU.
        // Instruction can be prepended using LOCK prefix (usable for multithreaded applications).
        F_LOCKABLE = 0x08,
        F_SPECIAL = 0x10, // Instruction is special case, this is for HLA. 
        F_SPECIAL_MEM = 0x20// Instruction always performs memory access. 
        //This flag is always combined with F_SPECIAL and signalizes
        //that there is implicit address which is accessed (usually EDI/RDI or ESI/EDI).
    };

    // --------------------------------------------------------------------------
    // Instruction operand flags.
    enum O
    {
        // X86, MM, XMM
        O_GB = 0x0001,
        O_GW = 0x0002,
        ...
        O_FM_4_8_10 = O_FM_4 | O_FM_8 | O_FM_10,
        // Don't emit REX prefix.
        O_NOREX = 0x2000
    };

    Cmp_unsigned__int16 code; // Instruction code.
    Cmp_unsigned__int16 nameIndex; // Instruction name index in instructionName[] array.
    Cmp_unsigned__int8 group; // Instruction group, used also by HLA
    Cmp_unsigned__int8 flags; // Instruction type flags. 
    // First and second operand flags (some groups depends to these settings, used also by HLA). 
    Cmp_unsigned__int16 oflags[ 2 ];
    Cmp_unsigned__int16 opCodeR; // If instruction has only memory operand, this is register opcode. 
    Cmp_unsigned__int32 opCode[ 2 ]; // Primary and secondary opcodes.

    //------------------------------------------------------------------------------
    // Get the instruction name (null terminated string).
    inline const char* getName() const 
    { 
        return instructionName + nameIndex; 
    }
    ...
};
//------------------------------------------------------------------------------

//Instruction description table

# define MAKE_INST(code, name, group, flags, oflags0, oflags1, opReg, opCode0, opCode1) \
{ code, code##_INDEX, group, flags, { oflags0, oflags1 }, opReg, { opCode0, opCode1 } }
# define G(g) InstructionDescription::G_##g
# define F(f) InstructionDescription::F_##f
# define O(o) InstructionDescription::O_##o

const InstructionDescription instructionDescription[] =
{
// Instruction code (enum) | instruction name | instruction group|
//     instruction flags| oflags[0] | oflags[1] | r| opCode[0] | opcode[1]
MAKE_INST(INST_ADC , "adc" , G(ALU) , F(LOCKABLE) , O(GQDWB_MEM) , O(GQDWB_MEM)|O(IMM) , 2, 0x00000010, 0x00000080),
MAKE_INST(INST_ADD , "add" , G(ALU) , F(LOCKABLE) , O(GQDWB_MEM) , O(GQDWB_MEM)|O(IMM) , 0, 0x00000000, 0x00000080),
MAKE_INST(INST_ADDPD , "addpd" , G(MMU_RMI) , F(NONE) , O(XMM) , O(XMM_MEM) , 0, 0x66000F58, 0),
MAKE_INST(INST_ADDPS , "addps" , G(MMU_RMI) , F(NONE) , O(XMM) , O(XMM_MEM) , 0, 0x00000F58, 0),
MAKE_INST(INST_ADDSD , "addsd" , G(MMU_RMI) , F(NONE) , O(XMM) , O(XMM_MEM) , 0, 0xF2000F58, 0),
MAKE_INST(INST_ADDSS , "addss" , G(MMU_RMI) , F(NONE) , O(XMM) , O(XMM_MEM) , 0, 0xF3000F58, 0),
MAKE_INST(INST_ADDSUBPD , "addsubpd" , G(MMU_RMI) , F(NONE) , O(XMM) , O(XMM_MEM) , 0, 0x66000FD0, 0),
...
};

现在我们可以从抽象基类派生一个 x86 特定批处理 CPU,并使用指令描述表将 x86 指令发射到代码缓冲区中。

//------------------------------------------------------------------------------
class __QOR_INTERFACE( __ARCHQOR ) Cx86CPUCore : public CCPUBase
{
    friend class CInstEmitter;

public:

    Cx86CPUCore( nsArch::CCodeGeneratorBase* codeGenerator ) __QCMP_THROW;      
    virtual ~Cx86CPUCore() __QCMP_THROW;
...

    //------------------------------------------------------------------------------
    //Emit single opCode without operands.
    inline void _emitOpCode( Cmp_unsigned__int32 opCode) __QCMP_THROW
    {
        // instruction prefix
        if( opCode & 0xFF000000 )
        {
            _emitByte( (Cmp_unsigned__int8)( ( opCode & 0xFF000000 ) >> 24 ) );
        }

        // instruction opcodes
        if( opCode & 0x00FF0000 )
        {
            _emitByte((Cmp_unsigned__int8)( ( opCode & 0x00FF0000 ) >> 16 ) );
        }

        if( opCode & 0x0000FF00 )
        {
            _emitByte((Cmp_unsigned__int8)(( opCode & 0x0000FF00 ) >>  8 ) );
        }
        // last opcode is always emitted (can be also 0x00)
        _emitByte((Cmp_unsigned__int8)( opCode & 0x000000FF ) );
    }

...
    //------------------------------------------------------------------------------
    //Emit SIB byte.
    inline void _emitSib( Cmp_unsigned__int8 s, Cmp_unsigned__int8 i, Cmp_unsigned__int8 b ) __QCMP_THROW
    { 
        _emitByte( ( ( s & 0x03 ) << 6 ) | ( ( i & 0x07 ) << 3 ) | ( b & 0x07 ) ); 
    }

    //------------------------------------------------------------------------------
    //Emit REX prefix (64-bit mode only).
    inline void _emitRexR( Cmp_unsigned__int8 w, Cmp_unsigned__int8 opReg, 
      Cmp_unsigned__int8 regCode, bool forceRexPrefix ) __QCMP_THROW
    {
...
    }

...    
    // Emit instruction where register is inlined to opcode.
    void _emitX86Inl(Cmp_unsigned__int32 opCode, Cmp_unsigned__int8 i16bit, 
      Cmp_unsigned__int8 rexw, Cmp_unsigned__int8 reg, bool forceRexPrefix) __QCMP_THROW;
...    
    // Emit MMX/SSE instruction.
    void _emitMmu(Cmp_unsigned__int32 opCode, Cmp_unsigned__int8 rexw, 
      Cmp_unsigned__int8 opReg, const COperand& src, Cmp_int_ptr immSize) __QCMP_THROW;

    // Emit displacement.
    LabelLink* _emitDisplacement(LabelData& l_data, 
                   Cmp_int_ptr inlinedDisplacement, int size) __QCMP_THROW;

    // Emit relative relocation to absolute pointer @a target. It's needed
    // to add what instruction is emitting this, because in x64 mode the relative
    // displacement can be impossible to calculate and in this case the trampoline
    // is used.
    void _emitJmpOrCallReloc(Cmp_unsigned__int32 instruction, void* target) __QCMP_THROW;

    // Helpers to decrease binary code size. These four emit methods are just
    // helpers thats used by assembler. They call emitX86() adding NULLs
    // to first, second and third operand, if needed.

    // Emit X86/FPU or MM/XMM instruction.
    void _emitInstruction(Cmp_unsigned__int32 code) __QCMP_THROW;

    // Emit X86/FPU or MM/XMM instruction.
    void _emitInstruction(Cmp_unsigned__int32 code, const COperand* o0) __QCMP_THROW;

...    
    void EmitInstructionG_ENTER( Cmp_unsigned__int32 code, const COperand*& o0, 
         const COperand*& o1, const COperand*& o2, Cmp_unsigned__int32& bLoHiUsed, 
         bool& assertIllegal, const COperand** _loggerOperands, const CImm* immOperand, 
         Cmp_unsigned__int32 immSize, Cmp_uint_ptr beginOffset, const InstructionDescription* id, 
         Cmp_unsigned__int32 forceRexPrefix )  __QCMP_THROW;
    void EmitInstructionG_IMUL( Cmp_unsigned__int32 code, const COperand*& o0, 
         const COperand*& o1, const COperand*& o2, Cmp_unsigned__int32& bLoHiUsed, 
         bool& assertIllegal, const COperand** _loggerOperands, const CImm* immOperand, 
         Cmp_unsigned__int32 immSize, Cmp_uint_ptr beginOffset, const InstructionDescription* id, 
         Cmp_unsigned__int32 forceRexPrefix )  __QCMP_THROW;
    void EmitInstructionG_INC_DEC( Cmp_unsigned__int32 code, const COperand*& o0, 
         const COperand*& o1, const COperand*& o2, Cmp_unsigned__int32& bLoHiUsed, 
         bool& assertIllegal, const COperand** _loggerOperands, const CImm* immOperand, 
         Cmp_unsigned__int32 immSize, Cmp_uint_ptr beginOffset, const InstructionDescription* id, 
         Cmp_unsigned__int32 forceRexPrefix )  __QCMP_THROW;
    void EmitInstructionG_J( Cmp_unsigned__int32 code, const COperand*& o0, 
         const COperand*& o1, const COperand*& o2, 
         Cmp_unsigned__int32& bLoHiUsed, bool& assertIllegal, 
         const COperand** _loggerOperands, const CImm* immOperand, Cmp_unsigned__int32 immSize, 
         Cmp_uint_ptr beginOffset, const InstructionDescription* id, 
         Cmp_unsigned__int32 forceRexPrefix )  __QCMP_THROW;

...
    //------------------------------------------------------------------------------
    // Simplifed version of relocCode() method.
    inline Cmp_uint_ptr relocCode( void* dst ) const __QCMP_THROW
    {
        return relocCode( dst, (Cmp_uint_ptr)dst );
    }

    // Embed

    void embed( const void* data, Cmp_uint_ptr length ) __QCMP_THROW;//Embed data into instruction stream.
    void embedLabel( const CLabel& label ) __QCMP_THROW;//Embed absolute label pointer (4 or 8 bytes).

...
    //------------------------------------------------------------------------------
    inline void SetEmitOptions( Cmp_unsigned__int32 EmitOptions )
    {
        m_uiEmitOptions = EmitOptions;
    }

...
protected:

    Cmp_unsigned__int32 m_uiProperties;     //Properties.
    Cmp_unsigned__int32 m_uiEmitOptions;    //Emit flags for next instruction (cleared after emit).

    Cmp_int_ptr m_iTrampolineSize;   //Size of possible trampolines.
    LabelLink* m_pUnusedLinks;       //Linked list of unused links (LabelLink* structures)


public:

    nsCodeQOR::PodVector< LabelData > m_LabelData;  //Labels data.
    nsCodeQOR::PodVector< RelocData > m_RelocData;  //Relocations data.

};

通过这种批处理器设计,可以有一个函数,根据其操作码和参数生成任何 x86 指令,而不是为每条指令编写一个函数。实际上,如果该函数完成所有任务,它将是一个庞大的怪物,就像在单个函数中实现 `printf` 一样。相反,我们将指令集分为需要特定验证检查或特定逻辑才能发出的指令组,然后为每个组编写一个函数。所有这些函数都作用于生成单个指令所需的一组通用数据。这被封装在指令发射器类 `CInstEmitter` 中。

//------------------------------------------------------------------------------
class CInstEmitter
{

public:

    CInstEmitter( Cx86CPUCore& CPUParam, Cmp_unsigned__int32 codeParam, 
      const COperand* o0Param, const COperand* o1Param, 
      const COperand* o2Param ) __QCMP_THROW;

    bool BeginInstruction( void ) __QCMP_THROW;
    bool PrepareInstruction( void ) __QCMP_THROW;
    void FinishImmediate( const COperand* pOperand, Cmp_unsigned__int32 immSize ) __QCMP_THROW;
    void EndInstruction( void ) __QCMP_THROW;
    void CleanupInstruction( void ) __QCMP_THROW;
    bool LockInstruction( void ) __QCMP_THROW;
    void InstructionImmediate( void ) __QCMP_THROW;
    void InstructionIllegal( void ) __QCMP_THROW;

    void InstructionG_EMIT( void ) __QCMP_THROW;
    void InstructionG_ALU( void ) __QCMP_THROW;
    void InstructionG_BSWAP( void ) __QCMP_THROW;
...    
    void InstructionG_MOV_PTR( void ) __QCMP_THROW;
    void InstructionG_MOVSX_MOVZX( void ) __QCMP_THROW;
#        if ( _QARCH_WORDSIZE == 64 )
    void InstructionG_MOVSXD( void ) __QCMP_THROW;
#        endif
    void InstructionG_PUSH( void ) __QCMP_THROW;
    void InstructionG_POP( void ) __QCMP_THROW;
    void InstructionG_R_RM( void ) __QCMP_THROW;
    void InstructionG_RM_B( void ) __QCMP_THROW;

...    
        void InstructionG_MMU_RM_3DNOW( void ) __QCMP_THROW;

protected:

    Cmp_unsigned__int32 m_uiCode;
    const COperand* m_pO0;
    const COperand* m_pO1;
    const COperand* m_pO2;
    Cmp_unsigned__int32 m_bLoHiUsed; 
    bool m_bAssertIllegal;
    const COperand* m_aLoggerOperands[ 3 ];
    const CImm* m_pImmOperand;
    Cmp_unsigned__int32 m_uiImmSize;
    Cmp_uint_ptr m_uiBeginOffset;
    const InstructionDescription* m_pId;
    Cmp_unsigned__int32 m_uiForceRexPrefix;
    Cx86CPUCore& m_CPU;

private:

    __QCS_DECLARE_NONASSIGNABLE( CInstEmitter );
};

`Cx86CPUCore` 函数 `_emitInstruction` 用于包装所有这些内容并正确发射单个指令。

void Cx86CPUCore::_emitInstruction( Cmp_unsigned__int32 code, const COperand* o0, 
     const COperand* o1, const COperand* o2 ) __QCMP_THROW
{
    const InstructionDescription* id = &instructionDescription[ code ];
    CInstEmitter Emitter( *this, code, o0, o1, o2 );
    if( Emitter.BeginInstruction() && Emitter.PrepareInstruction() && 
                Emitter.LockInstruction() )
    {
        switch( id->group )
        {
        case InstructionDescription::G_EMIT:
            Emitter.InstructionG_EMIT();
            break;
        case InstructionDescription::G_ALU:
            Emitter.InstructionG_ALU();
            break;
...
    Emitter.CleanupInstruction();
}

现在我们有了按组编写 x86 格式指令的代码,我们还需要能够调用以生成特定指令的函数。为此,`Cx86CPUCore` 以两种方式进行了扩展。一个派生类层次结构添加了与每个处理器代相关的指令,以便我们可以针对特定级别的指令集。一个并行的指令集扩展类层次结构附加到特定的 `Cx86CPUCore` 实例,实现了已添加到 x86 指令集的多媒体扩展代。

CPU inheritance

在编译时,我们从这些派生链中选择级别,以构成我们在运行时所需的最低规格 x86 目标。然后,在运行时,我们可以检查可执行文件的主机是否足够强大以满足这些要求。例如,对于需要与 32 位 Windows 集成的汇编函数,我们只需要 i386 级别的指令,尽管由于 i386 缺少 `cpuid` 指令,出于实际目的,我们不会支持低于 i486 的目标。

下面是 `Selection.h` 头文件的一部分,它将定义 `CMainInstructionSet` 和 `CFloatingPointUnit` 名称为正确的类,例如 `Ci686CPU` 和 `Ci686FPU`。

...
#ifndef _QARCH_ISET_I786
#    error ("_QARCH_ISET_I786 not defined")
#endif

#ifndef _QARCH_X86LEVEL
    __QCMP_MESSAGE( "Target instruction set level not set. Defaulting to i686." )
#    define _QARCH_X86LEVEL _QARCH_ISET_I686
#endif

#if ( _QARCH_X86LEVEL == _QARCH_ISET_I786 )
    __QCMP_MESSAGE( "Target i786 instruction set." )
#    include "ArchQOR/x86/Assembler/BatchCPU/i786CPU.h"
    namespace nsArch
    {
        typedef nsx86::Ci786CPU CMainInstructionSet;
        typedef nsx86::CP7FPU CFloatingPointUnit;
#    define _QARCH_X87_FPU_EXTENSION_CLASS ,public CFloatingPointUnit
#    define _QARCH_X87_FPU_EXTENSION_INIT ,CFloatingPointUnit( (Cx86CPUCore&)(*this) )
    }
#elif ( _QARCH_X86LEVEL == _QARCH_ISET_I686 )
    __QCMP_MESSAGE( "Target i686 instruction set." )
#    include "ArchQOR/x86/Assembler/BatchCPU/i686CPU.h"
    namespace nsArch
    {
        typedef nsx86::Ci686CPU CMainInstructionSet;
        typedef nsx86::CP6FPU CFloatingPointUnit;
#    define _QARCH_X87_FPU_EXTENSION_CLASS ,public CFloatingPointUnit
#    define _QARCH_X87_FPU_EXTENSION_INIT ,CFloatingPointUnit( (Cx86CPUCore&)(*this) )
    }
#elif ( _QARCH_X86LEVEL == _QARCH_ISET_I586 )
    __QCMP_MESSAGE( "Target i586 instruction set." )
#    include "ArchQOR/x86/Assembler/BatchCPU/i586CPU.h"
    namespace nsArch
    {
        typedef nsx86::Ci586CPU CMainInstructionSet;
        typedef nsx86::CPentiumFPU CFloatingPointUnit;
#    define _QARCH_X87_FPU_EXTENSION_CLASS ,public CFloatingPointUnit
#    define _QARCH_X87_FPU_EXTENSION_INIT ,CFloatingPointUnit( (Cx86CPUCore&)(*this) )
    }
#elif ( _QARCH_X86LEVEL == _QARCH_ISET_I486 )
...

选择的 `CMainInstructionSet` 和 `CFloatingPointUnit` 类随后用于派生 `CCPU` 类。

这给我们提供了一个低级汇编器,它可以创建适用于 x86 机器的可执行指令序列,但还不能轻易地从 C++ 编译器的角度创建有效的函数。要从低级指令序列到带有前导和 C++ 调用约定的完全成熟函数,我们需要构成高级汇编器的一组构造。

高级汇编器 (HLA) 设计

高级汇编器围绕“可发出对象(Emittable)”的概念展开,即可以发出到指令和指令流中的对象。该流经过多个阶段处理,然后用于驱动低级汇编器将实际指令发出到缓冲区中。这种额外的抽象层允许创建完整的有效函数,并控制参数传递、调用约定、返回值以及与现有编译函数集成所需的一切。它还允许将来包含额外的处理阶段,例如积极优化。与低级汇编器一样,HLA 分为可在不同架构之间重用的抽象类和特定于架构的类。

class __QOR_INTERFACE( __ARCHQOR ) Emittable
{
public:

    // Create new emittable.
    // Never create Emittable by new operator or on the stack, use
    // Compiler::newObject template to do that.
    Emittable( nsArch::CHighLevelAssemblerBase* c, Cmp_unsigned__int32 type ) __QCMP_THROW;

    // Destroy emittable.
    // Note Never destroy emittable using delete keyword, High Level Assembler
    // manages all emittables in internal memory pool and it will destroy
    // all emittables after you destroy it.
    virtual ~Emittable() __QCMP_THROW;

    // [Emit and Helpers]

    virtual void prepare( CHLAssemblerContextBase& cc ) __QCMP_THROW;        // Step 1. Extract emittable variables, update statistics, ...
    virtual Emittable* translate( CHLAssemblerContextBase& cc ) __QCMP_THROW;             // Step 2. Translate instruction, alloc variables, ...
    virtual void emit( CHighLevelAssemblerBase& a ) __QCMP_THROW;            // Step 3. Emit to Assembler.
    virtual void post( CHighLevelAssemblerBase& a ) __QCMP_THROW;            // Step 4. Last post step (verify, add data, etc).

    // [Utilities]
    
    virtual int getMaxSize() const __QCMP_THROW;                    // Get maximum size in bytes of this emittable (in binary).
    virtual bool _tryUnuseVar( CommonVarData* v ) __QCMP_THROW;            // Try to unuse the variable. Returns true only if the variable will be unused by the instruction, otherwise false is returned.

    //------------------------------------------------------------------------------
    inline nsArch::CHighLevelAssemblerBase* getHLA() const __QCMP_THROW        // Get associated HLA instance.
    { 
        return m_pHLAssembler; 
    }

    //------------------------------------------------------------------------------
    // Get emittable type, see EMITTABLE_TYPE.
    inline Cmp_unsigned__int32 getType() const __QCMP_THROW 
    { 
        return m_ucType; 
    }

    //------------------------------------------------------------------------------
    // Get whether the emittable was translated.
    inline Cmp_unsigned__int8 isTranslated() const __QCMP_THROW 
    { 
        return m_ucTranslated; 
    }

    //------------------------------------------------------------------------------
    // Get emittable offset in the stream
    // Emittable offset is not byte offset, each emittable increments offset by 1
    // and this value is then used by register allocator. Emittable offset is
    // set by compiler by the register allocator, don't use it in your code.
    inline Cmp_unsigned__int32 getOffset() const __QCMP_THROW 
    { 
        return m_uiOffset; 
    }

    //------------------------------------------------------------------------------
    inline Cmp_unsigned__int32 setOffset( Cmp_unsigned__int32 uiOffset ) __QCMP_THROW
    {
        m_uiOffset = uiOffset;
        return m_uiOffset;
    }            

    // [Emittables List]

    //------------------------------------------------------------------------------
    // Get previous emittable in list.
    inline Emittable* getPrev() const __QCMP_THROW 
    { 
        return m_pPrev; 
    }

    //------------------------------------------------------------------------------
    inline void setPrev( Emittable* pPrev ) __QCMP_THROW
    {
        m_pPrev = pPrev;
    }

    //------------------------------------------------------------------------------
    // Get next emittable in list.
    inline Emittable* getNext() const __QCMP_THROW 
    { 
        return m_pNext; 
    }

    //------------------------------------------------------------------------------
    inline void setNext( Emittable* pNext ) __QCMP_THROW
    {
        m_pNext = pNext;
    }

    //------------------------------------------------------------------------------
    // Get comment string.
    inline const char* getComment() const __QCMP_THROW 
    { 
        return m_szComment; 
    }

    void setComment( const char* str ) __QCMP_THROW;// Set comment string to str.

    void setCommentF( const char* fmt, ... ) __QCMP_THROW;
    // Format comment string using fmt string and variable argument list.

protected:

    //------------------------------------------------------------------------------
    // Mark emittable as translated and return next.
    inline Emittable* translated() __QCMP_THROW
    {
        //assert(_translated == false);

        m_ucTranslated = true;
        return m_pNext;
    }

    // High Level Assembler where this emittable is connected to.
    nsArch::CHighLevelAssemblerBase* m_pHLAssembler;
    Cmp_unsigned__int8 m_ucType;        // Type of emittable, see EMITTABLE_TYPE.
    Cmp_unsigned__int8 m_ucTranslated;    // Whether the emittable was translated, see translate().
    Cmp_unsigned__int8 m_ucReserved0;    // Reserved flags for future use.
    Cmp_unsigned__int8 m_ucReserved1;    // Reserved flags for future use.
    Cmp_unsigned__int32 m_uiOffset;        // Emittable offset.
    Emittable* m_pPrev;                    // Previous emittable.
    Emittable* m_pNext;                    // Next emittable.
    const char* m_szComment;            // Embedded comment string (also used by a Comment emittable).

private:

    __QCS_DECLARE_NONCOPYABLE( Emittable );
};

首先,我们将低级指令封装在一个高级指令可发出对象 `CEInstruction` 中,然后是 **Prolog**、**Epilog** 和 **Function Prototype** 可发出对象、跳转**目标**、**Return** 和 **Call**,最后是一个函数可发出对象 `CEFunction` 来掌握所有这些并符合目标 ABI。

在通用层面,**Alignment**、**Comment**、**Data**、**Dummy** 和 **FunctionEnd** 可发出对象以及上面详述的基类 `CEmittable` 独立于我们使用的底层汇编语言,因此它们可以在不同平台之间重用。

CEmittable inheritance

`CEFunction` 类维护生成单个函数的参数和不变量,但跟踪函数生成过程的开发状态数据由一个专门用于 x86 的高级汇编器上下文类 `Cx86HLAContext` 管理。它在函数创建时跟踪变量、寄存器分配、流写入点和作用域。

//------------------------------------------------------------------------------
// HLA context is used during assembly and normally developer doesn't
// need access to it. The context is used per function (it's reset after each
// function is generated).
class __QOR_INTERFACE( __ARCHQOR ) Cx86HLAContext : public CHLAssemblerContextBase
{
public:

    Cx86HLAContext( nsArch::CHighLevelAssemblerBase* compiler ) __QCMP_THROW;
    ~Cx86HLAContext() __QCMP_THROW;                        

    void _clear() __QCMP_THROW;                                        // Clear context, preparing it for next function generation.
    
    void allocVar( VarData* vdata, Cmp_unsigned__int32 regMask, Cmp_unsigned__int32 vflags ) __QCMP_THROW;
...

    CMem _getVarMem( VarData* vdata ) __QCMP_THROW;

    VarData* _getSpillCandidateGP() __QCMP_THROW;
    VarData* _getSpillCandidateMM() __QCMP_THROW;
    VarData* _getSpillCandidateXMM() __QCMP_THROW;
    VarData* _getSpillCandidateGeneric(VarData** varArray, Cmp_unsigned__int32 count) __QCMP_THROW;

    //------------------------------------------------------------------------------
    inline bool _isActive( VarData* vdata) __QCMP_THROW 
    { 
        return vdata->nextActive != 0; 
    }

    void _addActive( VarData* vdata ) __QCMP_THROW;
    void _freeActive( VarData* vdata ) __QCMP_THROW;
    void _freeAllActive() __QCMP_THROW;
    void _allocatedVariable( VarData* vdata ) __QCMP_THROW;

    //------------------------------------------------------------------------------
    inline void _allocatedGPRegister(Cmp_unsigned__int32 index) __QCMP_THROW 
    { 
        _state.usedGP |= nsCodeQOR::maskFromIndex(index); _modifiedGPRegisters |= nsCodeQOR::maskFromIndex(index); 
    }

...


    // [Operand Patcher]
    void translateOperands( COperand* operands, Cmp_unsigned__int32 count ) __QCMP_THROW;
...

    // [Backward Code]
    void addBackwardCode( EJmp* from ) __QCMP_THROW;

    void addForwardJump( EJmp* inst ) __QCMP_THROW;

    // [State]

    StateData* _saveState() __QCMP_THROW;
    void _assignState(StateData* state) __QCMP_THROW;
    void _restoreState(StateData* state, Cmp_unsigned__int32 targetOffset = INVALID_VALUE) __QCMP_THROW;

    // [Memory Allocator]

    VarMemBlock* _allocMemBlock(Cmp_unsigned__int32 size) __QCMP_THROW;
    void _freeMemBlock(VarMemBlock* mem) __QCMP_THROW;
    void _allocMemoryOperands() __QCMP_THROW;
    void _patchMemoryOperands( nsArch::CEmittable* start, nsArch::CEmittable* stop ) __QCMP_THROW;
    
    nsCodeQOR::Zone _zone;                    // Zone memory manager.
    nsArch::CHighLevelAssemblerBase* _compiler;                    // Compiler instance.
    EFunction* _function;                    // Function emittable.
    nsArch::CEmittable* _start;                // Current active scope start emittable.
    nsArch::CEmittable* _stop;                // Current active scope end emittable.
    nsArch::CEmittable* _extraBlock;                    // Emittable that is used to insert some code after the function body.
    StateData _state;                        // Current state (register allocator).
    VarData* _active;                        // Link to circullar double-linked list containing all active variables (for current state).
    ForwardJumpData* _forwardJumps;            // Forward jumps (single linked list).        
    Cmp_unsigned__int32 _unrecheable;        // Whether current code is unrecheable.
    Cmp_unsigned__int32 _modifiedGPRegisters;        // Global modified GP registers mask (per function).
    Cmp_unsigned__int32 _modifiedMMRegisters;        // Global modified MM registers mask (per function).
    Cmp_unsigned__int32 _modifiedXMMRegisters;        // Global modified XMM registers mask (per function).
    Cmp_unsigned__int32 _allocableEBP;                // Whether the EBP/RBP register can be used by register allocator.

    int _adjustESP;                                // ESP adjust constant (changed during PUSH/POP or when using stack ).
    Cmp_unsigned__int32 _argumentsBaseReg;        // Function arguments base pointer (register).
    Cmp__int32 _argumentsBaseOffset;            // Function arguments base offset.
    Cmp__int32 _argumentsActualDisp;            // Function arguments displacement.
    Cmp_unsigned__int32 _variablesBaseReg;        // Function variables base pointer (register).
    Cmp__int32 _variablesBaseOffset;            // Function variables base offset.
    Cmp__int32 _variablesActualDisp;            // Function variables displacement.
    VarMemBlock* _memUsed;                        // Used memory blocks (for variables, here is each created mem block that can be also in _memFree list).
    VarMemBlock* _memFree;                        // Free memory blocks (freed, prepared for another allocation).
    
    Cmp_unsigned__int32 _mem4BlocksCount;        // Count of 4-byte memory blocks used by the function.
    Cmp_unsigned__int32 _mem8BlocksCount;        // Count of 8-byte memory blocks used by the function.
    Cmp_unsigned__int32 _mem16BlocksCount;        // Count of 16-byte memory blocks used by the function.
    Cmp_unsigned__int32 _memBytesTotal;            // Count of total bytes of stack memory used by the function.
    bool _emitComments;                            // Whether to emit comments.
    nsCodeQOR::PodVector< EJmp* > _backCode;    // List of emittables which need to be translated. These emittables are filled by addBackwardCode().
    Cmp_uint_ptr _backPos;                        // Backward code position (starts at 0).
};

为了将 JIT 汇编的函数集成到现有编译的 C++ 中,我们需要能够双向调用。`CFunctionPrototype` 协助 JIT 汇编的函数调用现有的编译函数和其他 JIT 汇编的函数。

CFunctionPrototype collaboration Cx86HLAIntrinsics inheritance

高级汇编器类本身提供了一个接口,用于创建新变量和函数,以及将可发出对象插入到流中。

高级汇编器内联函数类基于高级汇编器,提供了一个虚拟汇编语言接口,为我们提供了原始的每个指令一个函数的设计,但在当前正在构建的可发出函数上下文中,并且以变量而不是寄存器的形式。

现在我们有了一个能够生成与我们的 C++ 代码库集成的函数的高级 x86 汇编器,以及一组基类,以便将来将支持扩展到非 x86 架构。

为了锦上添花,让高级汇编器更易于使用,ArchQOR 定义了一组仿函数模板。这些模板使得 JIT 函数可以被视为对象以生成它们,并被视为函数以调用它们。下面是 3 参数模板的代码。

template< typename RET, typename P1, typename P2, typename P3 >
class CJITFunctor3 : public CJITFunctorBase
{
public:
    typedef RET( *FP )( P1, P2, P3 );

    //------------------------------------------------------------------------------
    CJITFunctor3( CHighLevelAssemblerBase* pHLA ) : CJITFunctorBase( pHLA )
    , m_pFunc( 0 )
    {
    }

    //------------------------------------------------------------------------------
    ~CJITFunctor3()
    {
    }

    //------------------------------------------------------------------------------
    RET operator()( P1 p1, P2 p2, P3 p3 )
    {
        if( !m_bGenerated )
        {
            m_pFunc = Generate();
        }
    
        if( !m_pFunc )
        {
            throw "Null function pointer exception";
        }
        return (m_pFunc)( p1, p2, p3 );
    }

protected:

    virtual FP Generate( void ) = 0;

    FP m_pFunc;

};

用法:实践是检验真理的唯一标准

为了演示高级 JIT 汇编器的用法,我们将创建一个快速的 `memcpy` 函数,它以 4 字节为单位工作,而不是标准 `memcpy` 的每次 1 字节。

函数的签名将是:`void MemCpy32( Cmp_unsigned__int32* destination, const Cmp_unsigned__int32* source, Cmp_uint_ptr count );`

步骤1是根据我们想要的签名从 `CJITFunctor3` 派生一个新仿函数类型。

//A function object for the MemCpy32 JIT function
class CJITmemcpy32 : public nsArch::CJITFunctor3< void, Cmp_unsigned__int32*, 
      const Cmp_unsigned__int32*, Cmp_uint_ptr >
{
public:

    //Construction is minimal
    CJITmemcpy32( CHighLevelAssemblerBase* pHLA ) : CJITFunctor3( pHLA ){}

protected:

    //Overrides Generate to make the JIT function on demand
    virtual FP Generate( void );
};

步骤 2 是实现 `Generate` 函数,使用高级汇编器构造 `MemCpy32`。

CJITmemcpy32::FP CJITmemcpy32::Generate( void )
{
    //Get the x86 specific High Level Assembler
    Cx86HLAIntrinsics& HLA( *( dynamic_cast< Cx86HLAIntrinsics* >( m_pHLA ) ) );

    //Create a new function
    HLA.newFunction( CALL_CONV_DEFAULT, FunctionBuilder3< Void, Cmp_unsigned__int32*, 
        const Cmp_unsigned__int32*, Cmp_unsigned__int32 >() );

    //Labels for the exit point and loop
    CLabel LoopLabel = HLA.newLabel();
    CLabel ExitLabel = HLA.newLabel();

    //Variables for the function parameters
    CGPVar dst( HLA.argGP( 0 ) );
    CGPVar src( HLA.argGP( 1 ) );
    CGPVar cnt( HLA.argGP( 2 ) );

    // Allocate variables to registers (if they are not allocated already).
    HLA.alloc( dst );
    HLA.alloc( src );
    HLA.alloc( cnt );

    // Exit if length is zero.
    HLA.test( cnt, cnt );
    HLA.jz( ExitLabel );

    // Loop begin.
    HLA.bind( LoopLabel );

    // Copy DWORD (4 bytes).
    CGPVar tmp( HLA.newGP( VARIABLE_TYPE_GPD ) );
    HLA.mov( tmp, dword_ptr( src ) );
    HLA.mov( dword_ptr( dst ), tmp );

    // Increment dst/src pointers.
    HLA.add( src, 4 );
    HLA.add( dst, 4 );

    // Loop until --cnt is zero.
    HLA.dec( cnt );
    HLA.jnz( LoopLabel );

    // Exit.
    HLA.bind( ExitLabel );

    // Finish.
    HLA.endFunction();

    // Part 2:

    // Make JIT function.
    FP fn = reinterpret_cast< FP >( HLA.make() );

    // Ensure that everything is ok.

    if( fn )
    {
        m_bGenerated = true;
    }

    return fn;
}

步骤3是创建 `CJITmemcpy32` 实例并为其设置一些数据以供其操作。

// Create the JIT MemCopy32 function object. At this stage no assembly is generated
CJITmemcpy32 MemCopy32( &TheMachine().HLAssembler() );

// Create some data.
Cmp_unsigned__int32 dstBuffer[128];
Cmp_unsigned__int32 srcBuffer[128] = {1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89};

第四步是好玩的部分。现在我们可以“神奇地”调用一个函数,它实际上并不存在,除了作为第二步的“源”,但它却能工作。

MemCopy32( dstBuffer, srcBuffer, 128 );

在调试器中单步执行此调用会发现,该仿函数会检查函数是否已生成,发现尚未生成,因此在调用它之前即时生成它。第二次调用 `MemCopy32` 将跳过函数生成阶段,因此会获得更快的执行结果。

这是最终 JIT 生成函数的实际代码的反汇编视图:-

00030000  push        ebp  
00030001  mov         ebp,esp  
00030003  push        ebx  
00030004  push        esi  
00030005  sub         esp,10h  
00030008  mov         ecx,dword ptr [ebp+8]  
0003000B  mov         edx,dword ptr [ebp+0Ch]  
0003000E  mov         ebx,dword ptr [ebp+10h]  
00030011  test        ebx,ebx  
00030013  je          00030022  
00030015  mov         esi,dword ptr [edx]  
00030017  mov         dword ptr [ecx],esi  
00030019  add         edx,4  
0003001C  add         ecx,4  
0003001F  dec         ebx  
00030020  jne         00030015  
00030022  add         esp,10h  
00030025  pop         esi  
00030026  pop         ebx  
00030027  mov         esp,ebp  
00030029  pop         ebp  
0003002A  ret  

与本文相关的代码实现了对高级汇编器的所述测试,并且还利用低级汇编器完成了一项小型但至关重要的任务,即除了使用汇编语言之外无法完成的任务:查询主机 CPU 的功能、版本和品牌信息。以下是它在我的开发笔记本电脑上运行时提供的一些输出示例。

Test output

一次学习经历

我不是,也从未声称是汇编语言方面的专家。许多年前,我能胜任 Z80 汇编,但 x86 是另一回事,我一直觉得它特别难以阅读,更不用说编写了。移植和优化 Petr Kobalicek 的 JIT 汇编器让我学到了很多,我希望我的 x86 汇编能力提高到了几乎合格的水平。

我认为我一直缺失的最重要的一点是对 x86 汇编编程中约定的重要性的认识。拥有如此庞大的指令集和如此多的寄存器,似乎有成千上万种方法可以完成任何给定的任务。虽然汇编器本身不会抱怨你选择哪种,机器也会执行你给它的任何东西,但这还不够。除了机器对正确汇编语言的要求之外,还有大量关于如何使用堆栈、如何传递参数、如何使用和保留寄存器、哪些“通用”寄存器实际用于返回值、临时变量和参数的规则。操作系统使用哪个段寄存器用于什么目的,以及我尚未发现的十几种其他事情。一旦你知道了这些事情,阅读反汇编列表或他人的汇编代码就会容易得多,因为你知道会发生什么。同样,如果你不需要想出如何做传递参数等常见事情,因为你只需遵循约定,那么编写汇编代码就会容易得多。

我从未遇到过关于这些约定的良好文档,我想这就是我如此努力地掌握 x86 汇编的原因。考虑到这些约定是硬件相关的、部分依赖于操作系统、部分依赖于编译器,并且部分只是武断的传统,如果没有太多连贯的文档,那也就不足为奇了。

未来方向

ArchQOR 已经启动并运行,但仍有许多工作要做。64 位代码生成未经测试,将会出现问题。我还需要确保 ArchQOR 在 CompilerQOR 支持的所有编译器上可靠运行,但我不会用一堆额外的项目文件和相关杂物来负担本文的源代码。

ArchQOR 及其 JIT 汇编器有许多可能的改进和扩展,无疑也有许多 Bug 需要查找并修复。

支持 Haswell 更新的 x86 指令集扩展 AVX 和 AVX2。这将需要大量的代码更改,因为最大参数数量从 3 个增加到 5 个,并且引入了新的指令格式。

对 ARM CPU 和 NEON SIMD 扩展的支持是必不可少的,这样 QOR 才能在基于 ARM 的移动设备上运行。现在,ArchQOR 中的代码布局使得实现这一目标变得非常清晰,但所需的知识,特别是实现高级汇编器的知识是广泛的,而我大部分都不具备。

此版本的 ArchQOR 也绑定到 Windows 平台,这与 QOR 的概念不符。ArchQOR 代码的 99% 同样适用于 x86 Linux 和其他 x86 操作系统。我们将在未来的文章中探讨如何从操作系统中抽象 ArchQOR 并最终抽象整个框架。

既然我们可以针对特定级别的 x86 硬件,CompilerQOR 就可以支持依赖于目标硬件的编译器内部函数,所以我需要回到 CompilerQOR,并至少更新 MSVC 支持,以启用需要 i586 或 i686 目标的内部函数。

如果您仔细检查与本文相关的源代码,您会注意到一个 CodeQOR 文件夹,其中包含几个小类。这些是 ArchQOR 需要的依赖项,但通常会存在于 CodeQOR 模块中。本系列文章的下一篇将是关于 CodeQOR 的,我相信它会比这篇更有趣。

致谢

  • ArchQOR 的大部分代码完全基于 AsmJit,因此代码功劳应归于 Petr Kobalicek,没有他的工作,这一切都不会实现。Bob 不喜欢站外链接,但 AsmJit 很容易通过 Google 搜索到。
  • 感谢我的专家校对员和最好的妹妹。所有剩余的错误都是她完成工作后我添加的。
  • 感谢 Harold Aptroot 指出缺少 AVX/AVX2 支持以及添加它所涉及的问题。
  • Microsoft、Windows、Visual C++ 和 Visual Studio 是 Microsoft 的商标。
  • 提及的所有其他商标和商号均被承认为其各自所有者的财产,他们对本文或相关源代码的任何内容概不负责。

历史

  • 初始版本 - 2013年7月9日。
© . All rights reserved.