forked from nodejs/node
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathx64.cc
More file actions
768 lines (668 loc) · 24.9 KB
/
Copy pathx64.cc
File metadata and controls
768 lines (668 loc) · 24.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
#if HAVE_FFI
#include "ffi/fast.h"
#if defined(__x86_64__) && !defined(_WIN32)
#include <sys/mman.h>
#include <unistd.h>
#include <stdint.h>
#include <limits>
namespace {
using node::ffi::FastFFIType;
// System V x86_64 argument registers in ABI order. V8 Fast API calls enter the
// trampoline with the JS receiver in the first GP slot (rdi), so user GP
// arguments arrive shifted by one register: rsi, rdx, rcx, r8, r9.
constexpr unsigned kRax = 0;
constexpr unsigned kRcx = 1;
constexpr unsigned kRdx = 2;
constexpr unsigned kRsp = 4;
constexpr unsigned kRsi = 6;
constexpr unsigned kRdi = 7;
constexpr unsigned kR8 = 8;
constexpr unsigned kR9 = 9;
constexpr unsigned kR11 = 11;
constexpr unsigned kIncomingGPRegisters[] = {kRsi, kRdx, kRcx, kR8, kR9};
constexpr unsigned kTargetGPRegisters[] = {kRdi, kRsi, kRdx, kRcx, kR8, kR9};
// Floating-point values use xmmN registers in the SysV x86_64 ABI. The receiver
// consumes only a GP register, so FP arguments are already in the target ABI
// locations and the trampoline only needs to shuffle GP arguments.
bool IsFloatType(FastFFIType type) {
return type == FastFFIType::kFloat32 || type == FastFFIType::kFloat64;
}
// The current scalar implementation only distinguishes FP from non-FP ABI
// classes; non-FP values are carried in GP registers.
bool IsGPType(FastFFIType type) {
return !IsFloatType(type);
}
// kBuffer means V8 passes the original JS value; the trampoline must call back
// into C++ to convert it to a native pointer before invoking the target.
bool IsPointerValueType(FastFFIType type) {
return type == FastFFIType::kBuffer;
}
void Emit8(uint8_t** cursor, uint8_t value) {
*(*cursor)++ = value;
}
void Emit32(uint8_t** cursor, uint32_t value) {
for (unsigned i = 0; i < 4; i++) {
Emit8(cursor, (value >> (i * 8)) & 0xff);
}
}
void Emit64(uint8_t** cursor, uint64_t value) {
for (unsigned i = 0; i < 8; i++) {
Emit8(cursor, (value >> (i * 8)) & 0xff);
}
}
void EmitRex(uint8_t** cursor, bool wide, unsigned reg, unsigned rm) {
// REX encodes 64-bit operand size and the high bit of the ModRM reg/rm
// fields. Emit it unconditionally for byte-register operations too so rsi,
// rdi, and r8+ name their low-byte forms instead of legacy high-byte regs.
Emit8(cursor, 0x40 | (wide ? 0x08 : 0) | ((reg >> 3) << 2) | (rm >> 3));
}
void EmitModRM(uint8_t** cursor, unsigned reg, unsigned rm) {
Emit8(cursor, 0xc0 | ((reg & 7) << 3) | (rm & 7));
}
void EmitMov(uint8_t** cursor, unsigned dst, unsigned src) {
// mov dst, src. Used for the receiver-slot shuffle from V8's GP layout to
// the target ABI layout, and for placing converted buffer pointers.
EmitRex(cursor, true, src, dst);
Emit8(cursor, 0x89);
EmitModRM(cursor, src, dst);
}
void EmitMovImm64(uint8_t** cursor, unsigned reg, uintptr_t value) {
// Materialize an absolute address into a scratch register. The generated code
// uses r11 for helper/target calls and sentinels, avoiding ABI argument regs.
EmitRex(cursor, true, 0, reg);
Emit8(cursor, 0xb8 | (reg & 7));
Emit64(cursor, value);
}
void EmitMovImm32(uint8_t** cursor, unsigned reg, uint32_t value) {
// Load the buffer helper's uint32_t argument index into edx.
if (reg >= 8) {
EmitRex(cursor, false, 0, reg);
}
Emit8(cursor, 0xb8 | (reg & 7));
Emit32(cursor, value);
}
void EmitCall(uint8_t** cursor, unsigned reg) {
// call *reg. All targets are absolute addresses first loaded into r11.
EmitRex(cursor, true, 0, reg);
Emit8(cursor, 0xff);
EmitModRM(cursor, 2, reg);
}
void EmitJmp(uint8_t** cursor, unsigned reg) {
// jmp *reg. Scalar signatures that do not need return normalization can tail
// jump to the native target, avoiding the trampoline's call/return overhead.
EmitRex(cursor, true, 0, reg);
Emit8(cursor, 0xff);
EmitModRM(cursor, 4, reg);
}
bool EmitJmpRel32(uint8_t** cursor, uintptr_t target) {
// jmp rel32 is shorter than movabs+jmp and avoids an indirect branch. It is
// only usable when the native target is within the signed 32-bit displacement
// range from the end of the instruction. mmap() placement is not guaranteed,
// so callers must keep the absolute-jump fallback.
constexpr uintptr_t kInstructionSize = 5;
const uintptr_t next_instruction = reinterpret_cast<uintptr_t>(*cursor) +
kInstructionSize;
const intptr_t displacement = static_cast<intptr_t>(target) -
static_cast<intptr_t>(next_instruction);
if (displacement < std::numeric_limits<int32_t>::min() ||
displacement > std::numeric_limits<int32_t>::max()) {
return false;
}
Emit8(cursor, 0xe9);
Emit32(cursor, static_cast<uint32_t>(static_cast<int32_t>(displacement)));
return true;
}
void EmitPushRbp(uint8_t** cursor) {
// At function entry rsp is 8 mod 16. Pushing rbp makes rsp 16-byte aligned so
// the trampoline can safely call C++ helpers and the native target.
Emit8(cursor, 0x55);
}
void EmitPopRbp(uint8_t** cursor) {
Emit8(cursor, 0x5d);
}
void EmitRet(uint8_t** cursor) {
Emit8(cursor, 0xc3);
}
void EmitSubRsp(uint8_t** cursor, unsigned value) {
// Reserve stack spill space with `sub rsp, imm32`. The caller keeps the value
// 16-byte aligned so nested calls preserve the SysV stack alignment rule.
EmitRex(cursor, true, 5, kRsp);
Emit8(cursor, 0x81);
Emit8(cursor, 0xec);
Emit32(cursor, value);
}
void EmitAddRsp(uint8_t** cursor, unsigned value) {
// Release stack spill space with `add rsp, imm32`, matching EmitSubRsp().
EmitRex(cursor, true, 0, kRsp);
Emit8(cursor, 0x81);
Emit8(cursor, 0xc4);
Emit32(cursor, value);
}
void EmitStackMemory(uint8_t** cursor, unsigned reg, unsigned offset) {
// Address [rsp + offset]. x86_64 requires a SIB byte whenever rsp is the base
// register; offsets fit in the small fixed spill frame used by this emitter.
if (offset == 0) {
Emit8(cursor, ((reg & 7) << 3) | 0x04);
Emit8(cursor, 0x24);
} else {
Emit8(cursor, 0x44 | ((reg & 7) << 3));
Emit8(cursor, 0x24);
Emit8(cursor, offset);
}
}
void EmitStoreRsp(uint8_t** cursor, unsigned reg, unsigned offset) {
// Store a 64-bit incoming GP register into its spill slot.
EmitRex(cursor, true, reg, kRsp);
Emit8(cursor, 0x89);
EmitStackMemory(cursor, reg, offset);
}
void EmitLoadRsp(uint8_t** cursor, unsigned reg, unsigned offset) {
// Reload a 64-bit GP register from the spill area.
EmitRex(cursor, true, reg, kRsp);
Emit8(cursor, 0x8b);
EmitStackMemory(cursor, reg, offset);
}
void EmitLoadCallerStack(uint8_t** cursor, unsigned reg, unsigned offset) {
// Reload a stack-passed incoming argument from the caller's frame. With no
// trampoline prologue, the first stack argument is at [rsp + 8] because
// [rsp] holds the return address. If the trampoline pushed rbp first, callers
// pass offset 16 instead.
EmitLoadRsp(cursor, reg, offset);
}
void EmitCmp(uint8_t** cursor, unsigned lhs, unsigned rhs) {
// cmp lhs, rhs. Used to compare the helper result in rax with the invalid
// pointer sentinel loaded into r11.
EmitRex(cursor, true, rhs, lhs);
Emit8(cursor, 0x39);
EmitModRM(cursor, rhs, lhs);
}
void EmitXorEax(uint8_t** cursor) {
// Return nullptr/zero after the helper has already scheduled the JS exception.
Emit8(cursor, 0x31);
Emit8(cursor, 0xc0);
}
void EmitNarrowInstruction(uint8_t** cursor, uint8_t opcode, unsigned reg) {
// movsx/movzx reg32, reg8/reg16. Writing the 32-bit destination also clears
// the upper 32 bits, which is the ABI-normal form V8 observes.
EmitRex(cursor, false, reg, reg);
Emit8(cursor, 0x0f);
Emit8(cursor, opcode);
EmitModRM(cursor, reg, reg);
}
// Native small integer returns may leave upper bits unspecified. Normalize them
// before V8 observes the result. Arguments do not need the same treatment: the
// C ABI passes narrow integer arguments in integer registers and the callee
// consumes the declared low 8/16 bits.
bool EmitNarrowReturn(uint8_t** cursor, FastFFIType type, unsigned reg) {
switch (type) {
case FastFFIType::kUint8:
EmitNarrowInstruction(cursor, 0xb6, reg);
return true;
case FastFFIType::kInt8:
EmitNarrowInstruction(cursor, 0xbe, reg);
return true;
case FastFFIType::kUint16:
EmitNarrowInstruction(cursor, 0xb7, reg);
return true;
case FastFFIType::kInt16:
EmitNarrowInstruction(cursor, 0xbf, reg);
return true;
default:
return false;
}
}
bool NeedsNarrow(FastFFIType type) {
switch (type) {
case FastFFIType::kUint8:
case FastFFIType::kInt8:
case FastFFIType::kUint16:
case FastFFIType::kInt16:
return true;
default:
return false;
}
}
// Stack adjustment must preserve the platform's 16-byte SP alignment rule.
unsigned Align16(unsigned value) {
return (value + 15) & ~15;
}
void* AllocationHintNear(uintptr_t address) {
// A plain mmap hint is advisory: the kernel may ignore it, but unlike
// MAP_FIXED it will not replace an existing mapping. Keeping the trampoline
// close to the native target lets the scalar tail path use jmp rel32 instead
// of an absolute indirect jump on common Linux layouts.
const uintptr_t page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
if (page_size == 0) {
return nullptr;
}
return reinterpret_cast<void*>(address & ~(page_size - 1));
}
void* AllocateCodeNear(uintptr_t target_address, size_t code_size) {
#if defined(__linux__) && defined(MAP_FIXED_NOREPLACE)
const uintptr_t page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
if (page_size != 0) {
const uintptr_t base = target_address & ~(page_size - 1);
// Search a small window around the target first. Shared libraries usually
// leave nearby holes, and keeping the trampoline close enables jmp rel32.
constexpr uintptr_t kMaxPages = 1024;
for (uintptr_t i = 1; i <= kMaxPages; i++) {
const uintptr_t delta = i * page_size;
const uintptr_t candidates[] = {
base + delta,
base >= delta ? base - delta : 0,
};
for (uintptr_t candidate : candidates) {
if (candidate == 0) {
continue;
}
const intptr_t displacement = static_cast<intptr_t>(target_address) -
static_cast<intptr_t>(candidate + 5);
if (displacement < std::numeric_limits<int32_t>::min() ||
displacement > std::numeric_limits<int32_t>::max()) {
continue;
}
void* code = mmap(reinterpret_cast<void*>(candidate),
code_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON | MAP_FIXED_NOREPLACE,
-1,
0);
if (code != MAP_FAILED) {
return code;
}
}
}
}
#endif
return mmap(AllocationHintNear(target_address),
code_size,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANON,
-1,
0);
}
} // namespace
extern "C" bool node_ffi_create_fast_trampoline(
void* target,
const node::ffi::FastFFIType* args,
size_t argc,
node::ffi::FastFFIType result,
node::ffi::FastFFITrampoline* out) {
// Null inputs mean the caller cannot safely create executable code for this
// signature. Report rejection so the generic FFI path can be used instead.
if (target == nullptr || out == nullptr) {
return false;
}
size_t gp_count = 0;
size_t fp_count = 0;
bool has_buffer_args = false;
// Count the incoming register classes before emitting code. This keeps all
// unsupported cases out of the code generator instead of relying on partial
// instruction streams to fail later.
for (size_t i = 0; i < argc; i++) {
if (IsFloatType(args[i])) {
fp_count++;
} else {
gp_count++;
has_buffer_args = has_buffer_args || IsPointerValueType(args[i]);
}
}
// Buffer conversion calls into C++ before invoking the target. Keep the first
// implementation scalar-only so no XMM argument registers need to be preserved
// across helper calls.
if (has_buffer_args && fp_count != 0) {
return false;
}
const size_t incoming_gp_count = gp_count + (has_buffer_args ? 1 : 0);
// rdi is occupied by the V8 receiver, leaving rsi, rdx, rcx, r8, and r9 for
// incoming GP args/options. Scalar signatures can load one more user argument
// from the caller stack into the target's sixth GP slot; buffer signatures
// still stay register-only because the helper path spills the incoming window.
const size_t max_incoming_gp_count = has_buffer_args ? 5 : 6;
if (incoming_gp_count > max_incoming_gp_count || fp_count > 8) {
return false;
}
constexpr size_t kCodeSize = 1024;
// The fixed allocation is intentionally larger than any currently emitted
// path. It keeps the generator simple while the supported signature set is
// small and bounded.
// Generate into writable anonymous memory first; the page is made executable
// only after the instruction stream is complete and the instruction cache is
// synchronized.
const uintptr_t target_address = reinterpret_cast<uintptr_t>(target);
void* code = AllocateCodeNear(target_address, kCodeSize);
if (code == MAP_FAILED) {
return false;
}
uint8_t* cursor = static_cast<uint8_t*>(code);
const bool tail_call = !has_buffer_args && !NeedsNarrow(result);
if (!tail_call) {
EmitPushRbp(&cursor);
}
if (has_buffer_args) {
// Buffer conversion calls a C++ helper before the target call, so spill all
// incoming GP registers that may be clobbered by that helper.
const unsigned frame_size = Align16(incoming_gp_count * 8);
if (frame_size != 0) {
EmitSubRsp(&cursor, frame_size);
}
for (unsigned i = 0; i < incoming_gp_count; i++) {
// V8 presents user GP args in rsi.. and the callback options pointer
// after them. Spill that whole incoming register window at stack slot i.
EmitStoreRsp(&cursor, kIncomingGPRegisters[i], i * 8);
}
unsigned incoming_gp_index = 0;
unsigned target_gp_index = 0;
const unsigned options_offset = gp_count * 8;
// `incoming_gp_index` walks spilled V8 argument slots; `target_gp_index`
// walks the ABI registers expected by the native target. They differ only
// because V8 has an implicit receiver in rdi that the target does not see.
// Reload each original argument in target ABI order. Buffer arguments are
// converted to raw pointers; scalar GP arguments are moved/narrowed.
for (size_t i = 0; i < argc; i++) {
if (!IsGPType(args[i])) {
continue;
}
const unsigned arg_offset = incoming_gp_index * 8;
const unsigned target_reg = kTargetGPRegisters[target_gp_index];
if (IsPointerValueType(args[i])) {
// Call node_ffi_fast_buffer_data(value, options, index). A sentinel
// return means the helper already threw, so return nullptr/zero without
// invoking the native target.
EmitLoadRsp(&cursor, kRdi, arg_offset);
EmitLoadRsp(&cursor, kRsi, options_offset);
EmitMovImm32(&cursor, kRdx, static_cast<uint32_t>(i));
EmitMovImm64(&cursor,
kR11,
reinterpret_cast<uintptr_t>(node_ffi_fast_buffer_data));
// call *r11 clobbers caller-saved registers. The original arguments were
// spilled above specifically so subsequent target args can be restored.
EmitCall(&cursor, kR11);
EmitMovImm64(&cursor, kR11, std::numeric_limits<uintptr_t>::max());
EmitCmp(&cursor, kRax, kR11);
// If rax != sentinel, skip over the early-return sequence and continue
// assembling the target call state.
uint8_t* jne = cursor;
Emit8(&cursor, 0x75); // jne valid_buffer
Emit8(&cursor, 0);
// Conversion failed. Return zero/null directly; the helper has already
// scheduled the JS exception through FastApiCallbackOptions.
EmitXorEax(&cursor);
if (frame_size != 0) {
EmitAddRsp(&cursor, frame_size);
}
EmitPopRbp(&cursor);
EmitRet(&cursor);
jne[1] = static_cast<uint8_t>(cursor - (jne + 2));
if (target_reg != kRax) {
// Helper returns the converted pointer in rax; move it into the ABI
// register assigned to this target argument if needed.
EmitMov(&cursor, target_reg, kRax);
}
} else {
// Non-buffer GP arguments can be restored directly from the spill area.
EmitLoadRsp(&cursor, target_reg, arg_offset);
}
incoming_gp_index++;
target_gp_index++;
}
} else {
// Scalar-only signatures need no spills: V8's receiver occupies rdi, so
// user GP arguments are shifted down from rsi..r9 to rdi..r8.
unsigned gp_index = 0;
for (size_t i = 0; i < argc; i++) {
if (!IsGPType(args[i])) {
// FP arguments are already in xmm0..xmm7 and are not shifted by the rdi
// receiver slot, so the scalar path does not need to touch them.
continue;
}
if (gp_index < 5) {
EmitMov(&cursor,
kTargetGPRegisters[gp_index],
kIncomingGPRegisters[gp_index]);
} else {
// The sixth user GP argument is stack-passed by V8 because the receiver
// consumed rdi. Load it into r9, the target ABI's sixth GP register.
EmitLoadCallerStack(&cursor,
kTargetGPRegisters[gp_index],
tail_call ? 8 : 16);
}
gp_index++;
}
}
// Tail of the trampoline: load the actual library symbol address and call it
// with arguments now arranged according to the native ABI.
if (tail_call) {
// The trampoline has not touched rsp in this path, so a tail jump preserves
// the exact stack shape the native target expects after a normal call.
if (!EmitJmpRel32(&cursor, target_address)) {
EmitMovImm64(&cursor, kR11, target_address);
EmitJmp(&cursor, kR11);
}
} else {
EmitMovImm64(&cursor, kR11, target_address);
EmitCall(&cursor, kR11);
}
if (has_buffer_args) {
// Restore the spill frame allocated for buffer conversion before returning.
const unsigned frame_size = Align16(incoming_gp_count * 8);
if (frame_size != 0) {
EmitAddRsp(&cursor, frame_size);
}
}
EmitPopRbp(&cursor);
// Native small integer returns may leave upper bits unspecified. Normalize
// them before V8 observes the result.
EmitNarrowReturn(&cursor, result, kRax);
EmitRet(&cursor);
const size_t written = cursor - static_cast<uint8_t*>(code);
__builtin___clear_cache(static_cast<char*>(code),
static_cast<char*>(code) + written);
// Enforce W^X after code generation: the trampoline is executable but no
// longer writable once published through FastFFITrampoline.
if (mprotect(code, kCodeSize, PROT_READ | PROT_EXEC) != 0) {
munmap(code, kCodeSize);
return false;
}
out->code = code;
out->size = kCodeSize;
return true;
}
extern "C" void node_ffi_free_fast_trampoline(
node::ffi::FastFFITrampoline* trampoline) {
// Trampoline cleanup is idempotent so metadata destruction can safely run on
// partially initialized or already-released objects.
if (trampoline == nullptr || trampoline->code == nullptr) {
return;
}
munmap(trampoline->code, trampoline->size);
trampoline->code = nullptr;
trampoline->size = 0;
}
#elif defined(_M_X64)
#include <windows.h>
#include <stdint.h>
namespace {
using node::ffi::FastFFIType;
constexpr unsigned kRax = 0;
constexpr unsigned kRcx = 1;
constexpr unsigned kRdx = 2;
constexpr unsigned kRsp = 4;
constexpr unsigned kR8 = 8;
constexpr unsigned kR9 = 9;
constexpr unsigned kR11 = 11;
constexpr unsigned kWin64GPRegisters[] = {kRcx, kRdx, kR8, kR9};
bool IsFloatType(FastFFIType type) {
return type == FastFFIType::kFloat32 || type == FastFFIType::kFloat64;
}
bool IsBufferType(FastFFIType type) {
return type == FastFFIType::kBuffer;
}
void Emit8(uint8_t** cursor, uint8_t value) {
*(*cursor)++ = value;
}
void Emit32(uint8_t** cursor, uint32_t value) {
for (unsigned i = 0; i < 4; i++) {
Emit8(cursor, (value >> (i * 8)) & 0xff);
}
}
void Emit64(uint8_t** cursor, uint64_t value) {
for (unsigned i = 0; i < 8; i++) {
Emit8(cursor, (value >> (i * 8)) & 0xff);
}
}
void EmitRex(uint8_t** cursor, bool wide, unsigned reg, unsigned rm) {
Emit8(cursor, 0x40 | (wide ? 0x08 : 0) | ((reg >> 3) << 2) | (rm >> 3));
}
void EmitModRM(uint8_t** cursor, unsigned reg, unsigned rm) {
Emit8(cursor, 0xc0 | ((reg & 7) << 3) | (rm & 7));
}
void EmitMov(uint8_t** cursor, unsigned dst, unsigned src) {
EmitRex(cursor, true, src, dst);
Emit8(cursor, 0x89);
EmitModRM(cursor, src, dst);
}
void EmitMovaps(uint8_t** cursor, unsigned dst, unsigned src) {
// movaps xmm_dst, xmm_src. Used only for register-to-register argument
// shuffles; it preserves the payload bits for both f32 and f64 arguments.
EmitRex(cursor, false, dst, src);
Emit8(cursor, 0x0f);
Emit8(cursor, 0x28);
EmitModRM(cursor, dst, src);
}
void EmitMovImm64(uint8_t** cursor, unsigned reg, uintptr_t value) {
EmitRex(cursor, true, 0, reg);
Emit8(cursor, 0xb8 | (reg & 7));
Emit64(cursor, value);
}
void EmitCall(uint8_t** cursor, unsigned reg) {
EmitRex(cursor, true, 0, reg);
Emit8(cursor, 0xff);
EmitModRM(cursor, 2, reg);
}
void EmitJmp(uint8_t** cursor, unsigned reg) {
EmitRex(cursor, true, 0, reg);
Emit8(cursor, 0xff);
EmitModRM(cursor, 4, reg);
}
void EmitSubRsp(uint8_t** cursor, unsigned value) {
EmitRex(cursor, true, 5, kRsp);
Emit8(cursor, 0x81);
Emit8(cursor, 0xec);
Emit32(cursor, value);
}
void EmitAddRsp(uint8_t** cursor, unsigned value) {
EmitRex(cursor, true, 0, kRsp);
Emit8(cursor, 0x81);
Emit8(cursor, 0xc4);
Emit32(cursor, value);
}
void EmitRet(uint8_t** cursor) {
Emit8(cursor, 0xc3);
}
void EmitNarrowInstruction(uint8_t** cursor, uint8_t opcode, unsigned reg) {
EmitRex(cursor, false, reg, reg);
Emit8(cursor, 0x0f);
Emit8(cursor, opcode);
EmitModRM(cursor, reg, reg);
}
bool EmitNarrowReturn(uint8_t** cursor, FastFFIType type, unsigned reg) {
switch (type) {
case FastFFIType::kUint8:
EmitNarrowInstruction(cursor, 0xb6, reg);
return true;
case FastFFIType::kInt8:
EmitNarrowInstruction(cursor, 0xbe, reg);
return true;
case FastFFIType::kUint16:
EmitNarrowInstruction(cursor, 0xb7, reg);
return true;
case FastFFIType::kInt16:
EmitNarrowInstruction(cursor, 0xbf, reg);
return true;
default:
return false;
}
}
bool NeedsNarrow(FastFFIType type) {
switch (type) {
case FastFFIType::kUint8:
case FastFFIType::kInt8:
case FastFFIType::kUint16:
case FastFFIType::kInt16:
return true;
default:
return false;
}
}
void FreeCode(void* code, size_t code_size) {
VirtualFree(code, 0, MEM_RELEASE);
}
} // namespace
extern "C" bool node_ffi_create_fast_trampoline(
void* target,
const node::ffi::FastFFIType* args,
size_t argc,
node::ffi::FastFFIType result,
node::ffi::FastFFITrampoline* out) {
if (target == nullptr || out == nullptr || argc > 3) {
return false;
}
for (size_t i = 0; i < argc; i++) {
if (IsBufferType(args[i])) {
return false;
}
}
constexpr size_t kCodeSize = 512;
void* code = VirtualAlloc(
nullptr, kCodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (code == nullptr) {
return false;
}
uint8_t* cursor = static_cast<uint8_t*>(code);
const bool tail_call = !NeedsNarrow(result);
// Win64 uses positional registers. The V8 receiver occupies position 0, so
// public arguments arrive in positions 1..3 and must be shifted down.
for (size_t i = 0; i < argc; i++) {
if (IsFloatType(args[i])) {
EmitMovaps(&cursor, static_cast<unsigned>(i), static_cast<unsigned>(i + 1));
} else {
EmitMov(&cursor, kWin64GPRegisters[i], kWin64GPRegisters[i + 1]);
}
}
EmitMovImm64(&cursor, kR11, reinterpret_cast<uintptr_t>(target));
if (tail_call) {
// The caller already provided Win64 shadow space for the trampoline; after
// the receiver-slot shuffle, the target can reuse the same stack shape.
EmitJmp(&cursor, kR11);
} else {
// Reserve 32 bytes of shadow space plus 8 bytes for 16-byte stack alignment
// before making a nested call from inside the trampoline.
EmitSubRsp(&cursor, 40);
EmitCall(&cursor, kR11);
EmitAddRsp(&cursor, 40);
EmitNarrowReturn(&cursor, result, kRax);
EmitRet(&cursor);
}
const size_t written = cursor - static_cast<uint8_t*>(code);
FlushInstructionCache(GetCurrentProcess(), code, written);
DWORD old_protect;
if (VirtualProtect(code, kCodeSize, PAGE_EXECUTE_READ, &old_protect) == 0) {
FreeCode(code, kCodeSize);
return false;
}
out->code = code;
out->size = kCodeSize;
return true;
}
extern "C" void node_ffi_free_fast_trampoline(
node::ffi::FastFFITrampoline* trampoline) {
if (trampoline == nullptr || trampoline->code == nullptr) {
return;
}
FreeCode(trampoline->code, trampoline->size);
trampoline->code = nullptr;
trampoline->size = 0;
}
#endif // defined(_M_X64)
#endif // HAVE_FFI