LLVM中的栈映射与补丁点

原文地址:http://llvm.org/docs/StackMaps.html

定义

在本文我们将所有充当LLVM客户端的组件统称为“运行时”。包括LLVMIR生成器,目标代码消费者,以及代码修补程序。

栈映射(stack map)记录在一个特定指令地址处的活动值。这些活动值不是指生存期跨越该栈映射的LLVM值。相反,它们只是运行时要求在这个点存活的值。例如,它们可能是在与包含该栈映射的函数无关的那点上,运行时重启程序执行所需的值。

LLVM在目标代码一个指定的栈映射节中发布栈映射数据。对每个栈映射,这个栈映射数据包含一个记录。这个记录保存了栈映射的指令地址,并对每个被映射值的包含一个项。每个项将值的位置编码为寄存器,栈偏移,或常量。

一个补丁点是为在运行时修补插入新指令序列所保留空间的指令地址。补丁点看上去很像对LLVM的调用。它们接受遵循一个调用规范的实参,并可能返回一个值。它们也暗示栈映射的生成,这允许运行时定位补丁点,并找出该点处活动值的位置。

动机

这个功能目前是实验性的,但在一些设置中是潜在有用的,最明显的是作为一个运行时(JIT)编译器。Patchpoint固有函数应用的例子有,对运行时多态方法分发实现一个内联调用缓存(callcache),或在动态类型语言中,比如JavaScript,优化属性提取。

这里记述的固有函数目前由开源的WebKit项目的JavaScript编译器使用,参考FTL JIT,但它们被设计为用在任何需要栈映射或代码修补的时候。因为这些固有函数是实验性,LLVM版本间的兼容性不能保证。

在这个文档里描述的栈映射功能独立于在计算栈映射中的描述的功能。GCFunctionMetadata提供了指向由GCRoot固有函数捕捉的一个收集堆内部指针的位置,这也可以视为一个“栈映射”。不同于上面调用的栈映射,GCFunctionMetadata栈映射接口不提供将任意类型的活动寄存器值关联到指令地址的功能,它也不指定结果栈映射的格式。这里描述的栈映射有向垃圾收集运行时提供更多信息的潜力,但在本文档里对此不作描述。

固有函数

可以使用以下两类固有函数来实现栈映射与补丁点:llvm.experimental.stackmap与llvm.experimental.patchpoint。这两类固有函数生成一个栈映射记录,它们都允许某种形式的代码修补。它们可以独立使用(即llvm.experimental.patchpoint隐含地产生一个栈映射,无需额外调用llvm.experimental.stackmap)。使用哪个依赖于为代码修补预留空间是否必要,以及固有函数实参是否应该根据调用惯例降级(lowering)。Llvm.experimental.stackmap不预留空间,它也不预期任何调用实参。如果运行时在栈映射的地址处修补代码,它将破坏性地覆盖程序代码。这不同于llvm.experimental.patchpoint,它为原地修补预留空间,不会覆盖周围的代码。它还根据其调用惯例降级指定数量的实参。这允许被修补代码无需编排就地进行函数调用。

这些固有函数任何一个的实例在栈映射节中生成一个栈映射记录。记录包括一个ID,允许运行时唯一地识别这个栈映射,以及从包含函数代码起始处的偏移。

固有函数llvm.experimental.stackmap

语法

declarevoid

      @llvm.experimental.stackmap(i64<id>, i32 <numShadowBytes>, ...)

概观

固有函数llvm.experimental.stackmap记录在栈映射中指定值的位置,它不生成任何代码。

操作数:

第一个操作数是一个要编码在栈映射内的ID。第二个操作数是跟在这个固有函数后屏蔽字节数。随后的可变数目操作数是位置将被记录在这个栈映射的活动值。

将这个固有函数用作一个简朴的栈映射,不支持代码修补,屏蔽字节数可以设置为0。

语义:

栈映射固有函数不就地生成任何代码,除非需要nop覆盖其阴影(参见下面)。不过,它从函数入口的偏移保存在栈映射中。这是紧跟着这个栈映射前的指令的相对指令地址。

栈映射ID使得运行时能够定位期望的栈映射记录。LLVM将这个ID直接传递给栈映射记录而不检查其唯一性。

LLVM保证指令的影子在栈映射指令偏移之后,在这中间不会遭遇基本块的末尾,也不会对llvm.experimental.stackmap或llvm.experimental.patchpoint进行另一次调用。这使得运行时可以响应由外部代码触发的事件,对这个点的代码进行修补。跟在栈映射后指令的代码可能发布在栈映射的影子中,这些指令可能被析构性修补所改写。没有影子字节,这个析构性修补将改写当前函数外的程序代码或数据。我们不允许覆盖栈映射影子,因此运行时无需考虑这个边角。

例如,一个带有8字节影子的栈映射:

call void @runtime()

call void (i64, i32, ...)* @llvm.experimental.stackmap(i64 77, i32 8,

                                                      i64* %ptr)

%val = load i64* %ptr

%add = add i64 %val, 3

ret i64 %add

可能要求一字节的nop填充:

0x00callq _runtime

0x05nop                <--- stack mapaddress

0x06movq (%rdi), %rax

0x07addq $3, %rax

0x0apopq %rdx

0x0bret                <---- end of 8-byteshadow

现在,如果运行时需要使编译后代码失效,它可以如下修补栈映射地址处的8字节代码:

0x00callq _runtime

0x05movl  $0xffff, %rax <--- patched codeat stack map address

0x0acallq *%rax         <---- end of8-byte shadow

这样,在对运行时的普通调用返回后,将执行对一个特殊入口点的修补后调用,它可以从由栈映射定位的值重建一个栈帧。

固有函数llvm.experimental.patchpoint.*

语法

declarevoid

  @llvm.experimental.patchpoint.void(i64<id>, i32 <numBytes>,

                                     i8*<target>, i32 <numArgs>, ...)

declarei64

  @llvm.experimental.patchpoint.i64(i64<id>, i32 <numBytes>,

                                    i8*<target>, i32 <numArgs>, ...)

概观

固有函数llvm.experimental.patchpoint.*创建对指定<target>的一个函数调用,并记录在栈映射中指定值的位置。

操作数:

第一个操作数是一个ID,第二个操作数是为可修补区域保留的字节数,第三个操作数是一个函数的目标地址(可能是空),第四个操作数指定后面多少个可变操作数被视为函数调用实参。余下可变数目操作数是将位置在栈映射中记录的活动值。

语义:

补丁点固有函数产生一个栈映射。它也发布一个对由<target>指定地址的函数调用,如果这个地址不是一个常量null。这个函数调用与其实参根据在该固有函数调用点指定的调用惯例降级。该固有函数非空返回类型的版本也根据调用惯例返回一个值。

在PowerPC上,注意<target>必须是用于该间接调用预期目标的ABI函数指针,特别地,在为ELFV1 ABI编译时,<target>是通常用作C/C++函数指针表示的函数描述符地址。

请求0个补丁点实参是有效的。在这个情形下,所有可变操作数像llvm.experimental.stackmap.*那样处理。区别是空间将仍然对修补预留,将发布一个调用,而且允许一个返回值。

实参的位置通常不记录在栈映射中,因为它们已经被调用惯例固定了。余下的活动值的位置将被记录,这可以是一个寄存器,栈位置,或常量。已经引入一个特殊的调用惯例用于栈映射,anyregcc,它强制参数载入寄存器,但允许动态分配这些寄存器,除了余下的活动值,这些实参寄存器的位置也将记录在栈映射中。

补丁点也发布nop来涵盖指令编码空间至少<numBytes>。因此,用户必须确保<numBytes>足够在支持的目标机器上编码对目标地址的一个调用。如果该调用目标是常量null,那么没有最小的要求。0字节的空目标补丁点是有效的。

运行时可能修补为补丁点发布的代码,包括调用序列与nop。不过,运行时可能不对LLVM在预留空间发布的代码做任何假设。部分修补是不允许的。运行时必须修补所有的预留字节,如果需要以nop填充。

这个例子展示了,对每个本地(native)调用惯例,一个预留15个字节,在$rdi中带有的一个实参,并在$rax中的返回一个值的补丁点:

%target = inttoptr i64 -281474976710654 to i8*

%val = call i64 (i64, i32, ...)*

         @llvm.experimental.patchpoint.i64(i64 78, i32 15,

                                           i8* %target, i32 1, i64* %ptr)

%add = add i64 %val, 3

ret i64 %add

可能生成:

0x00movabsq $0xffff000000000002, %r11 <--- patch point address

0x0acallq   *%r11

0x0dnop

0x0enop                               <---end of reserved 15-bytes

0x0faddq    $0x3, %rax

0x10movl    %rax, 8(%rsp)

注意将不会记录栈映射位置。如果被修补的代码类型不需要对特定的调用惯例寄存器固定,那么可以使用anyregcc惯例:

%val= call anyregcc @llvm.experimental.patchpoint(i64 78, i32 15,

                                                  i8* %target, i32 1,

                                                  i64* %ptr)

现在栈映射表明了%ptr实参的位置以及返回值:

StackMap: ID=78, Loc0=%r9 Loc1=%r8

修补代码序列现在可能使用恰好分配在%r8中的实参,并返回分配在%r9中的值:

0x00movslq 4(%r8) %r9              <---patched code at patch point address

0x03nop

...

0x0enop                            <---end of reserved 15-bytes

0x0faddq    $0x3, %r9

0x10movl    %r9, 8(%rsp)

栈映射格式

在一个LLVM模块中,一个栈映射或补丁点固有函数的出现强制代码发布创建一个栈映射节。这个节的格式如下:

Header{

  uint8 : Stack Map Version (current version is 1)

  uint8 : Reserved (expected to be 0)

  uint16 : Reserved (expected to be 0)

}

uint32: NumFunctions

uint32: NumConstants

uint32: NumRecords

StkSizeRecord[NumFunctions]{

  uint64 : Function Address

  uint64 : Stack Size

}

Constants[NumConstants]{

  uint64 : LargeConstant

}

StkMapRecord[NumRecords]{

  uint64 : PatchPoint ID

  uint32 : Instruction Offset

  uint16 : Reserved (record flags)

  uint16 : NumLocations

  Location[NumLocations] {

    uint8 : Register | Direct | Indirect | Constant | ConstantIndex

    uint8 : Reserved (location flags)

    uint16 : Dwarf RegNum

    int32 : Offset or SmallConstant

  }

  uint16 : Padding

  uint16 : NumLiveOuts

  LiveOuts[NumLiveOuts]

    uint16 : Dwarf RegNum

    uint8 : Reserved

    uint8 : Size in Bytes

  }

  uint32 : Padding (only if required to alignto 8 byte)

}

每个位置的第一个字节编码了如下一个表明如何解析RegNum与Offset域的类型:

编码

类型

描述

0x1

Register

Reg

值在一个寄存器

0x2

Direct

Reg + Offset

帧索引值

0x3

Indirect

[Reg + Offset]

溅出值

0x4

Constant

Offset

小常量

0x5

ConstIndex

Constants[Offset]

大常量

在通常的情形里,值可通过一个寄存器得到,这时Offset域将是0。溅出到一个栈地址的值被编码为Indirect位置。运行时必须从一个栈地址载入这些值,通常以[BP + Offset]的形式。如果一个alloca值被直接传递给一个栈映射固有函数,作为一个优化,LLVM可能将帧索引折叠进该栈映射,以避免分配一个寄存器或栈槽。这些帧索引将被编码为BP+ Offset形式的Direct位置。LLVM也可能将常量直接发布在栈映射里,在Constant位置的Offset,或由ConstaIndex位置援引的常量池中,来进行优化。

在每个调用点,也记录了一个“出口存活(liveout)”寄存器列表。这些是生命期超过这个栈映射的寄存器,因此必须由运行时保存。在以一个缺省将大多数寄存器保留为被调用者保存的调用惯例使用补丁点固有函数时,这是一个重要的优化。

出口存活寄存器列表的每项包含一个DWARF寄存器号及字节大小。栈映射格式故意忽略了子寄存器信息。取而代之,运行时必须谨慎地解析这个信息。例如,如果栈映射报告一个字节在%rax,这个值可能要么在%al,要么在%ah。在实践中这无关紧要,因为运行时将只是保存%rax。不过,如果栈映射报告16个字节在%ymm0,运行时可以安全地通过仅保存%xmm0来优化。

栈映射格式是LLVM SVN修订与运行时之间的契约。它目前是实验性的,在短期内可能会改变,但尽量减少运行时更新需求是重要的。因此,栈映射数据由简单性与可扩展性驱动。表示的紧凑性是其次的,因为期望运行时在编译一个模块后立即解析数据,并以自己的格式编码信息。因为运行时控制节的分配,对多个模块,它可以重用同一个栈映射空间。

栈映射支持目前仅对64位平台实现。不过,一个32位实现应该能够使用相同的格式,浪费微不足道的空间。

栈映射节

通过经由LLVM C APILLVMCreateSimpleMCJITMeoryManager()提供自己的内存管理器,一个JIT编译器可以很容易地访问这个节。在创建内存管理器时,JIT提供一个回调函数LLVMMemoryManagerAllocateDataSectionCallback()。在LLVM创建这个节时,它调用这个回调函数并传递这个节的名字。这时JIT可以经历这个节在内存中的地址,在后面解析它来恢复栈映射数据。

在Darwin上,栈映射节名是“__llvm_stackmaps”。段名是“__LLVM_STACKMAPS”。

栈映射用法

在本文档里描述的栈映射支持可以用来精确地确定在一个指定代码位置处的值的所在。LLVM不维护这些值与任何更高级实体间的任何映射。仅给出LLVM保存的ID,偏移及位置次序,运行时必须能够解释栈映射记录。

注意这与调试信息的目的大不一样,调试信息是尽最大努力追踪每条指令处具名变量的位置。

这个设计的一个重要动机是,在执行到达与一个栈映射关联的指令地址时,允许运行时征用一个栈帧。运行时必须能够重建一个栈帧,并使用由栈映射提供的信息重启执行。例如,执行可能在一个解释器或同一个函数的一个重编译版本中重启。

这个用法限制了LLVM优化。显然,LLVM必须不能越过一个栈映射移动写操作。不过,读操作也必须被谨慎处理。如果读操作可能触发一个异常,把它提升到一个栈映射以上是有效的。例如,给定当前类型系统的状态,运行时可能确定不进行类型检查执行一个读操作是安全的,如果类型系统改变了,同时读操作函数的某个激活存在于栈上,读操作会变得不安全。运行时可以通过立即修补任何落在当前调用点与该读操作之间的栈映射位置来阻止该读操作的后续执行(通常,运行时只是修补所有的栈映射位置来使该函数无效)。如果编译器已经把读操作提升到这个栈映射以上,在运行时拿回控制之前,程序将崩溃。

为了实施这些语义,栈映射与补丁点固有函数被认为可能读及写所有的内存。这可能比某些用户所预期的更多地限制了优化。这个限制可以通过将调用点标记为“readonly”来避免。在未来,我们还可以允许向固有函数调用添加元数据来表达别名,因此允许优化将某些读操作提升到栈映射以上。

直接栈映射项

正如在栈映射节所展示的,一个直接栈映射位置记录了帧索引的地址。这个地址本身是运行时请求的值。这不同于援引请求值必须被读入的栈位置的间接位置。直接位置可以传递一个alloca的地址,而间接位置处理寄存器溅出。

例如:

entry:

  %a = alloca i64...

  llvm.experimental.stackmap(i64 <ID>,i32 <shadowBytes>, i64* %a)

在编译后,或之后任何时刻,运行时可以确定这个alloca在栈上的相对位置。这不同于寄存器以及间接位置,因为在执行到达栈映射的指令地址时,运行时可以只读这些位置中的值。

在入口块的alloca被一个固有函数直接消费时,这个功能要求LLVM特殊地对待它们。(这也是llvm.gcroot.intrinisc施加的要求)。LLVM转换必须不能以任何中介值替换这个alloca。运行时可以简单地通过检查栈映射的位置是一个直接位置类型来验证。

支持的架构

对StackMap生成及相关固有函数的支持对每个后端要求一些编程。今天,仅支持LLVM后端的一个子集。当前支持的架构有X86_64,PowerPC,以及Aarch64。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值