PLT Resolvers
AOT compiler mode mainly described in aot.md, please read it first.
Brief SlowPath idea description
JIT/AOT compiler has a SlowPath mechanism. It is used for some opcodes where a call to runtime is required conditionally,
but not always.
During code generation so-called SlowPath code is created, and we put it into a special cold code block at the end of the function.
Unique SlowPath blob is generated for each place it is called, and as it contains saving registers and setting up of so-called
BoundaryFrame for stack walker, it's code is much longer than few runtime-call-related instructions mentioned in the section above.
Code size issue
Speaking about AOT mode, for opcodes like CallStatic, CallVirtual, and opcodes related to Class resolving such
SlowPath also can be used, as we can cache gathered Method or Class pointer into a slot in GOT table (in .aot_got section).
The problem is that such a SlowPath would be actually required only once when we first time reach appropriate method Id
or class Id. So, in order to reduce code size in AOT mode, more tricky solution with PLT Resolvers is used.
Static Call Resolver
For each pair of File (input for ark_aot compiler) and callee method Id (panda_file::File::EntityId) three
consecutive slots are reserved in PLT-GOT table. FirstSlot is filled during AOT file creation and contains method Id.
SecondSlot is filled during AOT file loading into runtime and contains PLT CallStatic Resolver address.
ThirdSlot would actually store Method pointer after resolving, but during AOT file loading it is initialized
to address of SecondSlot, subtracted by GetCompiledEntryPointOffset value.
During calls, first parameter is always a callee Method pointer, so the trick from previous paragraph allows to have
fully transparent resolver for code generation. Lets see arm64 example (GetCompiledEntryPointOffset is 56 = 7 * 8, all function
parameters are already in proper registers):
========= .aot_got ========
; Somewhere in PLT-GOT table
. . .
-YY-16: FirstSlot - method Id
-YY-08: SecondSlot - PLT CallStatic Resolver
-YY-00: ThirdSlot - address of (-YY-08-56) <--------------
. . . |
; start of entrypoint table |
-NN: address of handler 0, NN = N * 8 |
. . . |
-16: address of handler N-1 |
-08: address of handler N |
========== .text ========== |
00: |
. . . |
XX+00: adr x0, #-(YY+XX) ; Put to the x0 address of ThirdSlot ; before resolve ; after resolve
XX+04: ldr x0, [x0] ; Load value stored in ThirdSlot ; (&FirstSlot)-48 ; Method Pointer
XX+08: ldr x30, [x0, #56] ; Load EntryPoint ; SecondSlot value ; Executable code
XX+12: blr x30 ; Call ; Call Resolver ; Call Method
. . .
PLT CallStatic Resolver after saving all registers to the stack and BoundaryFrame generation, have (&FirstSlot)-48
value in x0, so it may load ldr x1, [x0, #48] to get method Id from FirstSlot.
Caller Method pointer could be extracted (into x0) directly from Caller's CFrame, so,
having this two values in x0 and x1 it just call GetCalleeMethod to gather Method pointer.
When we have Method pointer, it is stored into ThirdSlot, allow to load proper executable address, and goes as first
parameter in actual method call. Jump by register value operation is used instead of call to return back directly into code,
not the resolver.
Virtual Call Resolver
For each pair of File (input for ark_aot compiler) and callee method Id (panda_file::File::EntityId) two consecutive
slots are reserved in PLT-GOT table. FirstSlot is filled during AOT file creation and contains method Id.
SecondSlot is filled with zero and after resolving it stores VTable index incremented by 1.
========= .aot_got ========
; Somewhere in PLT-GOT table
. . .
-YY-08: FirstSlot - method Id
-YY-00: SecondSlot, zero or (index+1) <---------------------------
. . . |
; start of entrypoint table |
-NN: address of handler 0, NN = N * 8 |
. . . |
-16: address of handler N-1 |
-08: address of handler N |
========== .text ========== |
00: |
. . . |
; CallVirtual opcode (register allocator used x5 for Class ptr) |
XX+00: adr x16, #-(YY+XX) ; Put to the x16 address of SecondSlot
XX+04: ldr w17, [x16] ; Load value from SecondSlot
XX+08: cbnz w17, #16 ; Jump to XX+24 when non-zero
XX+16: ldr x28, [#CALL_VIRTUAL_RESOLVER] ; Load VirtualCall Resolver address
XX+20: blr x30 ; Call Resolver, x16 is like a "parameter" and "return value"
XX+24: ldr w16, [x5, #4] ; Get Class pointer into x16
XX+28: add w16, w16, w17, lsl #3 ; x16 = Class+(index+1)*8
XX+32: ldr w16, [x16, #160] ; Load Method from VTable (compensating index+1, as VTable start offset is 168)
. . . ; Check IsAbstract
. . . ; Save caller-saved registers
. . . ; Set call parameters
ZZ+00: mov x0, x16 ; x0 = Method address
ZZ+04: ldr x30, [x0, #56] ; Executable code address
ZZ+08: blr x30 ; Call
. . .
Unlike CallStatic, there is no way to use default parameter registers to send/receive values into resolver.
Thus for PLT CallVirtual Resolver convention is the following - first Encoder temporary register
(x16 for arm64 or r12 for x86_84) is a parameter with SecondSlot address and also the same register
works as "return value"
PLT CallVirtual Resolver loads method Id from FirstSlot using address x16-8,
takes caller Method pointer from previous frame and calls GetCalleeMethod entrypoint.
Having Method pointer it is easy to load VTable index value.
Resolver returns index+1 value using x16, and don't call any other functions like PLT CallStatic Resolver do.
Control is returned back into code instead.
Class and InitClass Resolvers
For each pair of File (input for ark_aot compiler) and class Id (panda_file::File::EntityId) which needs to be resolved
three consecutive slots are reserved in PLT-GOT table. FirstSlot is filled during AOT file creation and contains class Id.
SecondSlot and ThirdSlot are filled with zeroes and after resolving they both store Class pointer, but have different meaning.
When SecondSlot in non-zero it means that Class is known to be in Initialized state already.
========= .aot_got ========
; Somewhere in PLT-GOT table
. . .
-YY-16: FirstSlot - class Id
-YY-08: SecondSlot, zero or "Inialized Class" pointer <-----------
-YY-00: ThirdSlot, zero or Class pointer |
. . . |
; start of entrypoint table |
-NN: address of handler 0, NN = N * 8 |
. . . |
-16: address of handler N-1 |
-08: address of handler N |
========== .text ========== |
00: |
. . . |
; Shared resolved slow path for PLT resolver |
YY+00: ldr x17, x28, [CLASS_INIT_RESOLVER] ; Load InitClass Resolver address
YY+04: br x17 ; Jump to resolver, x16 works like a "parameter" and "return value"
. . . |
; LoadAndInitClass opcode (w7 register allocated for result) |
XX+00: adr x16, #-(YY+8+XX) ; Put to the x16 address of SecondSlot
XX+04: ldr w7, [x16] ; Load value from SecondSlot
XX+08: cbnz w7, #20 ; Jump to XX+28 when non-zero
XX+12: bl YY - (XX+08) ; Call shared slow path for PLT resolver, x16 works like a "parameter" and "return value"
XX+16: mov w7, w16 ; Class should be in w7
XX+20: ... ; run next opcode
. . .
For class-related resolvers convention is the following - first Encoder temporary register
(x16 for arm64 or r12 for x86_84) is a parameter with Slot address, and it is also used as "return value".
PLT InitClass Resolver loads class Id from FirstSlot using address x16-8,
takes caller Method pointer from previous frame and calls InitializeClassById entrypoint.
It stores gathered Class pointer into ThirdSlot, and also does the same for SecondSlot but under condition.
The condition is whether Class state is Initialized, as returning from InitializeClassById entrypoint in some corner
cases can happen when Class is yet only in Initializing state.
PLT Class Resolver receives x16 addressing ThirdSlot, so it loads class Id from FirstSlot using address x16-16.
Another entrypoint is called here - ResolveClass. Gathered Class pointer value is stored into ThirdSlot only.
Both Resolvers returns Class pointer value using x16 back into code.
Resolver Encoding
As all 4 resolvers have a lot of similar parts, their generation in implemented in one method - EncodePltHelper.
Moreover, it is placed in platform-independent file code_generator/target/target.cpp, although there are actually several
differences in what's happening for arm64 and x86_64.
Main difference between two supported platforms is a main temporary register to use in Resolver.
For arm64 we use LR register (x30), and for x86_64 third Encoder temporary - r14 is used.
One more issue is that first Encoder temporary register (x16 for arm64 or r12 for x86_84) used as parameter
in 3 Resolvers (all but CallStatic) is actually a caller-saved for arm64, but callee-saved for x86, leading to some
difference.
Lets briefly discuss all steps which happen consecutively in any Resolver:
-
Save LR and FP register to stack. On
arm64is is just a onestp x29, x30, [sp, #-16]instruction,while onx86caller return address is already on stack, so we load it into temporary (we need it forBoundaryFrame), and pushrbpto the stack. -
Create BoundaryFrame. It actually copies the
SlowPathbehavior of usualBoundaryFrameclass constructor, but with one special trick: for 3 out of 4 Resolvers (all but CallStatic) "return address" and "previous frame" values which are already on stack (see previous step) directly became the upper part ofBoundaryFramestack part. -
Save caller-saved registers. In CallStatic resolver we prepare place on the stack and save registers there. In three other Resolvers caller-saved registers are saved directly into appropriate places in previous CFrame. Stack pointer is temporarily manually adjusted in this case to allow
SaveCallerRegistersfunction to do it's job. Moreover, forarm64we manually addx16to live registers set. -
Prepare parameters for Runtime Call. This step is described above separately in each resolver description.
-
Save callee-saved registers. Adjust stack pointer (second time for
CallStaticResolver, and the only time for other) and callSaveRegisterstwo times - for float and scalar registers. -
Make a Runtime Call. This step is done using
MakeCallAotfunction with properly calculated offset. Resolvers are placed after all functions in AOT file, but distance to.aot_gotsection can be calculated in the same way like for usual code generation. -
Load callee-saved registers. Reverse what was done two steps above -
LoadRegistersfor float and scalar registers, then adjust the stack pointer back. -
Restore previous Frame. Works similar to
BoundaryFrameclass destructor. -
Process gathered result. First,
arm64non-CallStaticResolvers need to manually restorex16from the place it was saved. Onx86_64this step is not required, asr12appears to be callee-saved register and is restored already. Main logic of this step is described above separately in each resolver description. -
Load caller-saved registers. Registers are loaded in the same manner they were saved. So, in CallStatic we have to adjust stack pointer after loading, while in other Resolvers it is temporarily manually adjusted to previous frame before calling
LoadCallerRegistersfunction. -
Restore LR and FP. Nothing special, symmetric to the very first step.
-
Leave Resolver. Jump to the callee Method in
CallStaticResolver, and do a usual "return" in others.