Skip to content

Commit cb06f7c

Browse files
committed
feat: add bytecode support to vm (instruction based) pass
1 parent df7106d commit cb06f7c

2 files changed

Lines changed: 125 additions & 72 deletions

File tree

docs/src/18-virtual-machine-instruction.md

Lines changed: 57 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
# Virtual machine (instruction-level)
22

3-
An LLVM pass that replaces arithmetic instructions with calls to a register-based VM. Instead of executing `add`, `sub`, `mul`, etc. directly, operands are stored into a global register file. Then `__vm_dispatch(opcode, dst, src0, src1)` executes the operation via the proper VM handler and writes the result back to a destination register. This means that before calling `__vm_dispatch`, the inputs must be copied into the `src0` and `src1` registers, and the result must be read from the `dst` register.
3+
An LLVM pass that replaces arithmetic instructions with calls to a register-based VM. Instead of executing `add`, `sub`, `mul`, etc. directly, operands are stored into a global register file and a bytecode blob is created for each instruction. Then `__vm_exec(bytecode_ptr)` reads the bytecode `[opcode, dst, src0, src1]`, executes the operation via the proper VM handler and writes the result back to a destination register. This means that before calling `__vm_exec`, the inputs must be copied into the `src0` and `src1` registers and the result must be read from the `dst` register.
44

5-
This is a simplified, instruction-level approach. Commercial tools usually virtualize entire functions or regions and hide control flow inside the VM. Here, we only virtualize individual operations while keeping branches and loops native.
5+
This is a simplified, instruction-level approach. Commercial tools usually virtualize entire functions or regions, use a single bytecode stream with a fetch-decode-execute (FDE) loop and hide control flow inside the VM. Here, we create separate bytecode blobs per instruction and keep branches and loops native.
66

77
Known limitations:
88
- significantly increased code size
99
- significantly increased runtime penalty
10+
- control flow remains visible (not virtualized)
11+
- no bytecode encryption
1012
- the VM can be easily reversed
1113

1214
The source code is available [here](https://github.com/gemesa/phantom-pass/tree/main/src/18-virtual-machine-instruction).
@@ -73,7 +75,7 @@ $ opt -load-pass-plugin=./obf.dylib -passes="virtual-machine<compute>" -S test.l
7375
VirtualMachinePass: instructions replaced in function 'compute'
7476
```
7577

76-
Check the output, note that the arithmetic instructions have been replaced with `__vm_dispatch` calls:
78+
Check the output, note that the arithmetic instructions have been replaced with `__vm_exec` calls:
7779

7880
```
7981
$ cat obf.ll
@@ -84,6 +86,10 @@ target triple = "arm64-apple-macosx15.0.0"
8486
8587
@.str = private unnamed_addr constant [12 x i8] c"Result: %d\0A\00", align 1
8688
@__vm_regs = private global [256 x i64] zeroinitializer
89+
@__vm_bc_0 = private constant [4 x i8] c"\01\02\00\01"
90+
@__vm_bc_1 = private constant [4 x i8] c"\07\02\00\01"
91+
@__vm_bc_2 = private constant [4 x i8] c"\06\02\00\01"
92+
@__vm_bc_3 = private constant [4 x i8] c"\02\02\00\01"
8793
8894
; Function Attrs: mustprogress nofree noinline norecurse nosync nounwind ssp willreturn memory(none) uwtable(sync)
8995
define i32 @compute(i32 noundef %a, i32 noundef %b) local_unnamed_addr #0 {
@@ -92,26 +98,26 @@ entry:
9298
%b_ext = sext i32 %a to i64
9399
store i64 %a_ext, ptr @__vm_regs, align 8
94100
store i64 %b_ext, ptr getelementptr inbounds ([256 x i64], ptr @__vm_regs, i64 0, i64 1), align 8
95-
call void @__vm_dispatch(i8 1, i8 2, i8 0, i8 1)
101+
call void @__vm_exec(ptr @__vm_bc_0)
96102
%vm_result = load i64, ptr getelementptr inbounds ([256 x i64], ptr @__vm_regs, i64 0, i64 2), align 8
97103
%vm_trunc = trunc i64 %vm_result to i32
98104
%a_ext1 = sext i32 %vm_trunc to i64
99105
store i64 %a_ext1, ptr @__vm_regs, align 8
100106
store i64 1, ptr getelementptr inbounds ([256 x i64], ptr @__vm_regs, i64 0, i64 1), align 8
101-
call void @__vm_dispatch(i8 7, i8 2, i8 0, i8 1)
107+
call void @__vm_exec(ptr @__vm_bc_1)
102108
%vm_result2 = load i64, ptr getelementptr inbounds ([256 x i64], ptr @__vm_regs, i64 0, i64 2), align 8
103109
%vm_trunc3 = trunc i64 %vm_result2 to i32
104110
%a_ext4 = sext i32 %vm_trunc3 to i64
105111
store i64 %a_ext4, ptr @__vm_regs, align 8
106112
store i64 255, ptr getelementptr inbounds ([256 x i64], ptr @__vm_regs, i64 0, i64 1), align 8
107-
call void @__vm_dispatch(i8 6, i8 2, i8 0, i8 1)
113+
call void @__vm_exec(ptr @__vm_bc_2)
108114
%vm_result5 = load i64, ptr getelementptr inbounds ([256 x i64], ptr @__vm_regs, i64 0, i64 2), align 8
109115
%vm_trunc6 = trunc i64 %vm_result5 to i32
110116
%a_ext7 = sext i32 %vm_trunc6 to i64
111117
%b_ext8 = sext i32 %a to i64
112118
store i64 %a_ext7, ptr @__vm_regs, align 8
113119
store i64 %b_ext8, ptr getelementptr inbounds ([256 x i64], ptr @__vm_regs, i64 0, i64 1), align 8
114-
call void @__vm_dispatch(i8 2, i8 2, i8 0, i8 1)
120+
call void @__vm_exec(ptr @__vm_bc_3)
115121
%vm_result9 = load i64, ptr getelementptr inbounds ([256 x i64], ptr @__vm_regs, i64 0, i64 2), align 8
116122
%vm_trunc10 = trunc i64 %vm_result9 to i32
117123
ret i32 %vm_trunc10
@@ -129,14 +135,22 @@ entry:
129135
declare noundef i32 @printf(ptr noundef readonly captures(none), ...) local_unnamed_addr #2
130136
131137
; Function Attrs: noinline optnone
132-
define private void @__vm_dispatch(i8 %op, i8 %dst, i8 %src0, i8 %src1) #3 {
138+
define private void @__vm_exec(ptr %bytecode) #3 {
133139
entry:
140+
%op_ptr = getelementptr inbounds i8, ptr %bytecode, i64 0
141+
%dst_ptr = getelementptr inbounds i8, ptr %bytecode, i64 1
142+
%src0_ptr = getelementptr inbounds i8, ptr %bytecode, i64 2
143+
%src1_ptr = getelementptr inbounds i8, ptr %bytecode, i64 3
144+
%op = load i8, ptr %op_ptr, align 1
145+
%dst = load i8, ptr %dst_ptr, align 1
146+
%src0 = load i8, ptr %src0_ptr, align 1
147+
%src1 = load i8, ptr %src1_ptr, align 1
134148
%src0_ext = zext i8 %src0 to i64
135149
%src1_ext = zext i8 %src1 to i64
136-
%src0_ptr = getelementptr inbounds [256 x i64], ptr @__vm_regs, i64 0, i64 %src0_ext
137-
%src0_ptr1 = getelementptr inbounds [256 x i64], ptr @__vm_regs, i64 0, i64 %src1_ext
138-
%a = load i64, ptr %src0_ptr, align 8
139-
%b = load i64, ptr %src0_ptr1, align 8
150+
%src0_reg_ptr = getelementptr inbounds [256 x i64], ptr @__vm_regs, i64 0, i64 %src0_ext
151+
%src1_reg_ptr = getelementptr inbounds [256 x i64], ptr @__vm_regs, i64 0, i64 %src1_ext
152+
%a = load i64, ptr %src0_reg_ptr, align 8
153+
%b = load i64, ptr %src1_reg_ptr, align 8
140154
switch i8 %op, label %default [
141155
i8 1, label %add
142156
i8 2, label %sub
@@ -151,8 +165,8 @@ entry:
151165
add: ; preds = %entry
152166
%add_res = add i64 %a, %b
153167
%dst_ext = zext i8 %dst to i64
154-
%dst_ptr = getelementptr inbounds [256 x i64], ptr @__vm_regs, i64 0, i64 %dst_ext
155-
store i64 %add_res, ptr %dst_ptr, align 8
168+
%dst_ptr1 = getelementptr inbounds [256 x i64], ptr @__vm_regs, i64 0, i64 %dst_ext
169+
store i64 %add_res, ptr %dst_ptr1, align 8
156170
ret void
157171
158172
sub: ; preds = %entry
@@ -244,59 +258,63 @@ undefined8 _compute(int param_1,int param_2)
244258
{
245259
DAT_100008000 = (long)param_2;
246260
DAT_100008008 = (long)param_1;
247-
FUN_100000644(1);
261+
FUN_100000668(&DAT_100000860);
248262
DAT_100008000 = (long)(int)DAT_100008010;
249263
DAT_100008008 = 1;
250-
FUN_100000644(7,2,0,1);
264+
FUN_100000668(&DAT_100000864);
251265
DAT_100008000 = (long)(int)DAT_100008010;
252266
DAT_100008008 = 0xff;
253-
FUN_100000644(6,2,0,1);
267+
FUN_100000668(&DAT_100000868);
254268
DAT_100008000 = (long)(int)DAT_100008010;
255269
DAT_100008008 = (long)param_1;
256-
FUN_100000644(2,2,0,1);
270+
FUN_100000668(&DAT_10000086c);
257271
return DAT_100008010;
258272
}
259273
260-
void FUN_100000644(char param_1,uint param_2,ulong param_3,ulong param_4)
274+
void FUN_100000668(char *param_1)
261275
262276
{
263-
ulong uVar1;
264-
ulong uVar2;
277+
char cVar1;
278+
byte bVar2;
279+
ulong uVar3;
280+
ulong uVar4;
265281
266-
uVar2 = (&DAT_100008000)[param_3 & 0xff];
267-
uVar1 = (&DAT_100008000)[param_4 & 0xff];
268-
if (param_1 == '\x01') {
269-
(&DAT_100008000)[(ulong)param_2 & 0xff] = uVar2 + uVar1;
282+
cVar1 = *param_1;
283+
bVar2 = param_1[1];
284+
uVar4 = (&DAT_100008000)[(byte)param_1[2]];
285+
uVar3 = (&DAT_100008000)[(byte)param_1[3]];
286+
if (cVar1 == '\x01') {
287+
(&DAT_100008000)[bVar2] = uVar4 + uVar3;
270288
return;
271289
}
272-
if (param_1 == '\x02') {
273-
(&DAT_100008000)[(ulong)param_2 & 0xff] = uVar2 - uVar1;
290+
if (cVar1 == '\x02') {
291+
(&DAT_100008000)[bVar2] = uVar4 - uVar3;
274292
return;
275293
}
276-
if (param_1 == '\x03') {
277-
(&DAT_100008000)[(ulong)param_2 & 0xff] = uVar2 * uVar1;
294+
if (cVar1 == '\x03') {
295+
(&DAT_100008000)[bVar2] = uVar4 * uVar3;
278296
return;
279297
}
280-
if (param_1 == '\x04') {
281-
(&DAT_100008000)[(ulong)param_2 & 0xff] = uVar2 & uVar1;
298+
if (cVar1 == '\x04') {
299+
(&DAT_100008000)[bVar2] = uVar4 & uVar3;
282300
return;
283301
}
284-
if (param_1 == '\x05') {
285-
(&DAT_100008000)[(ulong)param_2 & 0xff] = uVar2 | uVar1;
302+
if (cVar1 == '\x05') {
303+
(&DAT_100008000)[bVar2] = uVar4 | uVar3;
286304
return;
287305
}
288-
if (param_1 == '\x06') {
289-
(&DAT_100008000)[(ulong)param_2 & 0xff] = uVar2 ^ uVar1;
306+
if (cVar1 == '\x06') {
307+
(&DAT_100008000)[bVar2] = uVar4 ^ uVar3;
290308
return;
291309
}
292-
if (param_1 == '\a') {
293-
(&DAT_100008000)[(ulong)param_2 & 0xff] = uVar2 << (uVar1 & 0x3f);
310+
if (cVar1 == '\a') {
311+
(&DAT_100008000)[bVar2] = uVar4 << (uVar3 & 0x3f);
294312
return;
295313
}
296-
if (param_1 != '\b') {
314+
if (cVar1 != '\b') {
297315
return;
298316
}
299-
(&DAT_100008000)[(ulong)param_2 & 0xff] = uVar2 >> (uVar1 & 0x3f);
317+
(&DAT_100008000)[bVar2] = uVar4 >> (uVar3 & 0x3f);
300318
return;
301319
}
302320
```

src/18-virtual-machine-instruction/obf.cpp

Lines changed: 68 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class VirtualMachinePass : public PassInfoMixin<VirtualMachinePass> {
3636
SmallSet<StringRef, 8> FunctionNames;
3737
Function *VMDispatcher = nullptr;
3838
GlobalVariable *RegisterFile = nullptr;
39+
unsigned BytecodeCounter = 0;
3940

4041
static constexpr uint8_t REG_SRC0 = 0;
4142
static constexpr uint8_t REG_SRC1 = 1;
@@ -49,6 +50,7 @@ class VirtualMachinePass : public PassInfoMixin<VirtualMachinePass> {
4950

5051
PreservedAnalyses run(Module &M, ModuleAnalysisManager &) {
5152
bool Changed = false;
53+
BytecodeCounter = 0;
5254
RegisterFile = getOrCreateRegisterFile(M);
5355
VMDispatcher = getOrCreateVMDispatcher(M);
5456

@@ -77,6 +79,27 @@ class VirtualMachinePass : public PassInfoMixin<VirtualMachinePass> {
7779
}
7880

7981
private:
82+
GlobalVariable *createBytecode(Module &M, uint8_t Opcode, uint8_t Dst,
83+
uint8_t Src0, uint8_t Src1) {
84+
LLVMContext &Ctx = M.getContext();
85+
IntegerType *I8Ty = Type::getInt8Ty(Ctx);
86+
ArrayType *BytecodeTy = ArrayType::get(I8Ty, 4);
87+
88+
std::vector<Constant *> Bytes = {
89+
ConstantInt::get(I8Ty, Opcode),
90+
ConstantInt::get(I8Ty, Dst),
91+
ConstantInt::get(I8Ty, Src0),
92+
ConstantInt::get(I8Ty, Src1),
93+
};
94+
95+
Constant *Init = ConstantArray::get(BytecodeTy, Bytes);
96+
97+
std::string Name = "__vm_bc_" + std::to_string(BytecodeCounter++);
98+
99+
return new GlobalVariable(M, BytecodeTy, true, GlobalValue::PrivateLinkage,
100+
Init, Name);
101+
}
102+
80103
GlobalVariable *getOrCreateRegisterFile(Module &M) {
81104
if (GlobalVariable *GV = M.getGlobalVariable("__vm_regs")) {
82105
return GV;
@@ -94,34 +117,28 @@ class VirtualMachinePass : public PassInfoMixin<VirtualMachinePass> {
94117
Function *getOrCreateVMDispatcher(Module &M) {
95118
LLVMContext &Ctx = M.getContext();
96119

97-
if (Function *F = M.getFunction("__vm_dispatch")) {
120+
if (Function *F = M.getFunction("__vm_exec")) {
98121
return F;
99122
}
100123

101124
IntegerType *I8Ty = Type::getInt8Ty(Ctx);
102125
IntegerType *I64Ty = Type::getInt64Ty(Ctx);
103126
Type *VoidTy = Type::getVoidTy(Ctx);
127+
PointerType *I8PtrTy = PointerType::getUnqual(I8Ty);
104128

105-
// void __vm_dispatch(uint8_t op, uint8_t dst, uint8_t src0, uint8_t src1);
106-
FunctionType *FTy =
107-
FunctionType::get(VoidTy, {I8Ty, I8Ty, I8Ty, I8Ty}, false);
129+
// void __vm_exec(int8_t* bytecode);
130+
FunctionType *FTy = FunctionType::get(VoidTy, {I8PtrTy}, false);
108131

109132
Function *F =
110-
Function::Create(FTy, Function::PrivateLinkage, "__vm_dispatch", M);
133+
Function::Create(FTy, Function::PrivateLinkage, "__vm_exec", M);
111134

112135
// The dispatcher should not be inlined or optimized away.
113136
// That would defeat the point of the virtualization.
114137
F->addFnAttr(Attribute::NoInline);
115138
F->addFnAttr(Attribute::OptimizeNone);
116139

117-
Argument *OpArg = F->getArg(0);
118-
Argument *DstArg = F->getArg(1);
119-
Argument *Src0Arg = F->getArg(2);
120-
Argument *Src1Arg = F->getArg(3);
121-
OpArg->setName("op");
122-
DstArg->setName("dst");
123-
Src0Arg->setName("src0");
124-
Src1Arg->setName("src1");
140+
Argument *BytecodeArg = F->getArg(0);
141+
BytecodeArg->setName("bytecode");
125142

126143
BasicBlock *EntryBB = BasicBlock::Create(Ctx, "entry", F);
127144
BasicBlock *AddBB = BasicBlock::Create(Ctx, "add", F);
@@ -137,20 +154,35 @@ class VirtualMachinePass : public PassInfoMixin<VirtualMachinePass> {
137154
IRBuilder<> Builder(Ctx);
138155
Builder.SetInsertPoint(EntryBB);
139156

140-
Value *Src0Ext = Builder.CreateZExt(Src0Arg, I64Ty, "src0_ext");
141-
Value *Src1Ext = Builder.CreateZExt(Src1Arg, I64Ty, "src1_ext");
142-
143-
Value *Src0Ptr =
144-
Builder.CreateInBoundsGEP(RegisterFile->getValueType(), RegisterFile,
145-
{Builder.getInt64(0), Src0Ext}, "src0_ptr");
146-
Value *Src1Ptr =
147-
Builder.CreateInBoundsGEP(RegisterFile->getValueType(), RegisterFile,
148-
{Builder.getInt64(0), Src1Ext}, "src0_ptr");
149-
150-
Value *A = Builder.CreateLoad(I64Ty, Src0Ptr, "a");
151-
Value *B = Builder.CreateLoad(I64Ty, Src1Ptr, "b");
152-
153-
SwitchInst *Switch = Builder.CreateSwitch(OpArg, DefaultBB, 8);
157+
// bytecode: [op, dst, src0, src1]
158+
Value *OpPtr = Builder.CreateInBoundsGEP(I8Ty, BytecodeArg,
159+
Builder.getInt64(0), "op_ptr");
160+
Value *DstPtr = Builder.CreateInBoundsGEP(I8Ty, BytecodeArg,
161+
Builder.getInt64(1), "dst_ptr");
162+
Value *Src0Ptr = Builder.CreateInBoundsGEP(I8Ty, BytecodeArg,
163+
Builder.getInt64(2), "src0_ptr");
164+
Value *Src1Ptr = Builder.CreateInBoundsGEP(I8Ty, BytecodeArg,
165+
Builder.getInt64(3), "src1_ptr");
166+
167+
Value *Op = Builder.CreateLoad(I8Ty, OpPtr, "op");
168+
Value *Dst = Builder.CreateLoad(I8Ty, DstPtr, "dst");
169+
Value *Src0 = Builder.CreateLoad(I8Ty, Src0Ptr, "src0");
170+
Value *Src1 = Builder.CreateLoad(I8Ty, Src1Ptr, "src1");
171+
172+
Value *Src0Ext = Builder.CreateZExt(Src0, I64Ty, "src0_ext");
173+
Value *Src1Ext = Builder.CreateZExt(Src1, I64Ty, "src1_ext");
174+
175+
Value *Src0RegPtr = Builder.CreateInBoundsGEP(
176+
RegisterFile->getValueType(), RegisterFile,
177+
{Builder.getInt64(0), Src0Ext}, "src0_reg_ptr");
178+
Value *Src1RegPtr = Builder.CreateInBoundsGEP(
179+
RegisterFile->getValueType(), RegisterFile,
180+
{Builder.getInt64(0), Src1Ext}, "src1_reg_ptr");
181+
182+
Value *A = Builder.CreateLoad(I64Ty, Src0RegPtr, "a");
183+
Value *B = Builder.CreateLoad(I64Ty, Src1RegPtr, "b");
184+
185+
SwitchInst *Switch = Builder.CreateSwitch(Op, DefaultBB, 8);
154186
Switch->addCase(ConstantInt::get(I8Ty, VM_ADD), AddBB);
155187
Switch->addCase(ConstantInt::get(I8Ty, VM_SUB), SubBB);
156188
Switch->addCase(ConstantInt::get(I8Ty, VM_MUL), MulBB);
@@ -164,7 +196,7 @@ class VirtualMachinePass : public PassInfoMixin<VirtualMachinePass> {
164196
const char *Name) {
165197
Builder.SetInsertPoint(BB);
166198
Value *Result = Builder.CreateBinOp(BinOp, A, B, Name);
167-
Value *DstExt = Builder.CreateZExt(DstArg, I64Ty, "dst_ext");
199+
Value *DstExt = Builder.CreateZExt(Dst, I64Ty, "dst_ext");
168200
Value *DstPtr =
169201
Builder.CreateInBoundsGEP(RegisterFile->getValueType(), RegisterFile,
170202
{Builder.getInt64(0), DstExt}, "dst_ptr");
@@ -214,7 +246,6 @@ class VirtualMachinePass : public PassInfoMixin<VirtualMachinePass> {
214246

215247
bool virtualizeInstructions(Function &F, Module &M) {
216248
LLVMContext &Ctx = M.getContext();
217-
Type *I8Ty = Type::getInt8Ty(Ctx);
218249
Type *I64Ty = Type::getInt64Ty(Ctx);
219250

220251
SmallVector<Instruction *, 32> ToVirtualize;
@@ -265,10 +296,14 @@ class VirtualMachinePass : public PassInfoMixin<VirtualMachinePass> {
265296
Builder.CreateStore(AExt, Src0Ptr);
266297
Builder.CreateStore(BExt, Src1Ptr);
267298

268-
Builder.CreateCall(VMDispatcher, {ConstantInt::get(I8Ty, Opcode),
269-
ConstantInt::get(I8Ty, REG_DST),
270-
ConstantInt::get(I8Ty, REG_SRC0),
271-
ConstantInt::get(I8Ty, REG_SRC1)});
299+
GlobalVariable *Bytecode =
300+
createBytecode(M, Opcode, REG_DST, REG_SRC0, REG_SRC1);
301+
302+
Value *BytecodePtr = Builder.CreateInBoundsGEP(
303+
Bytecode->getValueType(), Bytecode,
304+
{Builder.getInt64(0), Builder.getInt64(0)}, "bytecode_ptr");
305+
306+
Builder.CreateCall(VMDispatcher, {BytecodePtr});
272307

273308
Value *DstPtr = Builder.CreateInBoundsGEP(
274309
RegisterFile->getValueType(), RegisterFile,

0 commit comments

Comments
 (0)