2011-10-27 02:41:50 +08:00
|
|
|
|
/*
|
2012-01-10 06:02:47 +08:00
|
|
|
|
Copyright (C) 2011-2012 de4dot@gmail.com
|
2011-10-27 02:41:50 +08:00
|
|
|
|
|
|
|
|
|
This file is part of de4dot.
|
|
|
|
|
|
|
|
|
|
de4dot is free software: you can redistribute it and/or modify
|
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
|
|
de4dot is distributed in the hope that it will be useful,
|
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
|
along with de4dot. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2012-11-01 23:42:02 +08:00
|
|
|
|
using dot10.DotNet;
|
|
|
|
|
using dot10.DotNet.Emit;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
using de4dot.blocks;
|
|
|
|
|
|
2011-12-09 16:02:06 +08:00
|
|
|
|
namespace de4dot.code {
|
2011-10-27 02:41:50 +08:00
|
|
|
|
// A simple class that statically detects the values of some local variables
|
|
|
|
|
class VariableValues {
|
|
|
|
|
IList<Block> allBlocks;
|
2012-11-01 23:42:02 +08:00
|
|
|
|
IList<Local> locals;
|
|
|
|
|
Dictionary<Local, Variable> variableToValue = new Dictionary<Local, Variable>();
|
2011-10-27 02:41:50 +08:00
|
|
|
|
|
|
|
|
|
public class Variable {
|
|
|
|
|
int writes = 0;
|
|
|
|
|
object value;
|
|
|
|
|
bool unknownValue = false;
|
|
|
|
|
|
|
|
|
|
public bool isValid() {
|
|
|
|
|
return !unknownValue && writes == 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public object Value {
|
|
|
|
|
get {
|
|
|
|
|
if (!isValid())
|
|
|
|
|
throw new ApplicationException("Unknown variable value");
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
set { this.value = value; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void addWrite() {
|
|
|
|
|
writes++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setUnknown() {
|
|
|
|
|
unknownValue = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
public VariableValues(IList<Local> locals, IList<Block> allBlocks) {
|
2011-10-27 02:41:50 +08:00
|
|
|
|
this.locals = locals;
|
|
|
|
|
this.allBlocks = allBlocks;
|
|
|
|
|
init();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void init() {
|
|
|
|
|
foreach (var variable in locals)
|
|
|
|
|
variableToValue[variable] = new Variable();
|
|
|
|
|
|
|
|
|
|
foreach (var block in allBlocks) {
|
|
|
|
|
for (int i = 0; i < block.Instructions.Count; i++) {
|
|
|
|
|
var instr = block.Instructions[i];
|
|
|
|
|
|
|
|
|
|
switch (instr.OpCode.Code) {
|
|
|
|
|
case Code.Stloc:
|
|
|
|
|
case Code.Stloc_S:
|
|
|
|
|
case Code.Stloc_0:
|
|
|
|
|
case Code.Stloc_1:
|
|
|
|
|
case Code.Stloc_2:
|
|
|
|
|
case Code.Stloc_3:
|
|
|
|
|
var variable = Instr.getLocalVar(locals, instr);
|
|
|
|
|
var val = variableToValue[variable];
|
|
|
|
|
val.addWrite();
|
|
|
|
|
object obj;
|
|
|
|
|
if (!getValue(block, i, out obj))
|
|
|
|
|
val.setUnknown();
|
|
|
|
|
val.Value = obj;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool getValue(Block block, int index, out object obj) {
|
|
|
|
|
while (true) {
|
|
|
|
|
if (index <= 0) {
|
|
|
|
|
obj = null;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
var instr = block.Instructions[--index];
|
|
|
|
|
if (instr.OpCode == OpCodes.Nop)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
switch (instr.OpCode.Code) {
|
|
|
|
|
case Code.Ldc_I4:
|
|
|
|
|
case Code.Ldc_I8:
|
|
|
|
|
case Code.Ldc_R4:
|
|
|
|
|
case Code.Ldc_R8:
|
|
|
|
|
case Code.Ldstr:
|
|
|
|
|
obj = instr.Operand;
|
|
|
|
|
return true;
|
|
|
|
|
case Code.Ldc_I4_S:
|
|
|
|
|
obj = (int)(sbyte)instr.Operand;
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
case Code.Ldc_I4_0: obj = 0; return true;
|
|
|
|
|
case Code.Ldc_I4_1: obj = 1; return true;
|
|
|
|
|
case Code.Ldc_I4_2: obj = 2; return true;
|
|
|
|
|
case Code.Ldc_I4_3: obj = 3; return true;
|
|
|
|
|
case Code.Ldc_I4_4: obj = 4; return true;
|
|
|
|
|
case Code.Ldc_I4_5: obj = 5; return true;
|
|
|
|
|
case Code.Ldc_I4_6: obj = 6; return true;
|
|
|
|
|
case Code.Ldc_I4_7: obj = 7; return true;
|
|
|
|
|
case Code.Ldc_I4_8: obj = 8; return true;
|
|
|
|
|
case Code.Ldc_I4_M1:obj = -1; return true;
|
|
|
|
|
case Code.Ldnull: obj = null; return true;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
obj = null;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
public Variable getValue(Local variable) {
|
2011-10-27 02:41:50 +08:00
|
|
|
|
return variableToValue[variable];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
abstract class MethodReturnValueInliner {
|
|
|
|
|
protected List<CallResult> callResults;
|
|
|
|
|
List<Block> allBlocks;
|
2012-11-01 23:42:02 +08:00
|
|
|
|
MethodDef theMethod;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
VariableValues variableValues;
|
2012-02-18 14:56:53 +08:00
|
|
|
|
int errors = 0;
|
2012-04-30 18:18:47 +08:00
|
|
|
|
bool useUnknownArgs = false;
|
|
|
|
|
|
|
|
|
|
public bool UseUnknownArgs {
|
|
|
|
|
get { return useUnknownArgs; }
|
|
|
|
|
set { useUnknownArgs = value; }
|
|
|
|
|
}
|
2011-10-27 02:41:50 +08:00
|
|
|
|
|
|
|
|
|
protected class CallResult {
|
|
|
|
|
public Block block;
|
|
|
|
|
public int callStartIndex;
|
|
|
|
|
public int callEndIndex;
|
|
|
|
|
public object[] args;
|
|
|
|
|
public object returnValue;
|
|
|
|
|
|
|
|
|
|
public CallResult(Block block, int callEndIndex) {
|
|
|
|
|
this.block = block;
|
|
|
|
|
this.callEndIndex = callEndIndex;
|
|
|
|
|
}
|
|
|
|
|
|
2012-11-22 16:14:51 +08:00
|
|
|
|
public IMethod getMethodRef() {
|
2012-11-01 23:42:02 +08:00
|
|
|
|
return (IMethod)block.Instructions[callEndIndex].Operand;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-02-18 14:56:53 +08:00
|
|
|
|
public bool InlinedAllCalls {
|
|
|
|
|
get { return errors == 0; }
|
|
|
|
|
}
|
|
|
|
|
|
2012-01-14 19:34:42 +08:00
|
|
|
|
public abstract bool HasHandlers { get; }
|
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
public MethodDef Method {
|
2012-07-29 19:20:35 +08:00
|
|
|
|
get { return theMethod; }
|
2012-01-05 23:23:53 +08:00
|
|
|
|
}
|
|
|
|
|
|
2011-10-27 02:41:50 +08:00
|
|
|
|
protected abstract void inlineAllCalls();
|
|
|
|
|
|
|
|
|
|
// Returns null if method is not a method we should inline
|
2012-11-01 23:42:02 +08:00
|
|
|
|
protected abstract CallResult createCallResult(IMethod method, MethodSpec gim, Block block, int callInstrIndex);
|
2011-10-27 02:41:50 +08:00
|
|
|
|
|
2012-07-29 19:20:35 +08:00
|
|
|
|
public int decrypt(Blocks blocks) {
|
|
|
|
|
if (!HasHandlers)
|
|
|
|
|
return 0;
|
|
|
|
|
return decrypt(blocks.Method, blocks.MethodBlocks.getAllBlocks());
|
|
|
|
|
}
|
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
public int decrypt(MethodDef method, List<Block> allBlocks) {
|
2012-01-14 19:34:42 +08:00
|
|
|
|
if (!HasHandlers)
|
|
|
|
|
return 0;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
try {
|
2012-07-29 19:20:35 +08:00
|
|
|
|
theMethod = method;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
callResults = new List<CallResult>();
|
2012-07-29 19:20:35 +08:00
|
|
|
|
this.allBlocks = allBlocks;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
|
|
|
|
|
findAllCallResults();
|
|
|
|
|
inlineAllCalls();
|
|
|
|
|
inlineReturnValues();
|
2011-10-27 04:06:48 +08:00
|
|
|
|
return callResults.Count;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
}
|
2012-07-31 10:39:34 +08:00
|
|
|
|
catch {
|
|
|
|
|
errors++;
|
|
|
|
|
throw;
|
|
|
|
|
}
|
2011-10-27 02:41:50 +08:00
|
|
|
|
finally {
|
2012-07-29 19:20:35 +08:00
|
|
|
|
theMethod = null;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
callResults = null;
|
2012-07-29 19:20:35 +08:00
|
|
|
|
this.allBlocks = null;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
variableValues = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
bool getLocalVariableValue(Local variable, out object value) {
|
2011-10-27 02:41:50 +08:00
|
|
|
|
if (variableValues == null)
|
2012-11-20 00:58:34 +08:00
|
|
|
|
variableValues = new VariableValues(theMethod.Body.Variables, allBlocks);
|
2011-10-27 02:41:50 +08:00
|
|
|
|
var val = variableValues.getValue(variable);
|
2012-04-30 14:31:09 +08:00
|
|
|
|
if (!val.isValid()) {
|
|
|
|
|
value = null;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2011-10-27 02:41:50 +08:00
|
|
|
|
value = val.Value;
|
2012-04-30 14:31:09 +08:00
|
|
|
|
return true;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void findAllCallResults() {
|
|
|
|
|
foreach (var block in allBlocks)
|
|
|
|
|
findCallResults(block);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void findCallResults(Block block) {
|
|
|
|
|
for (int i = 0; i < block.Instructions.Count; i++) {
|
|
|
|
|
var instr = block.Instructions[i];
|
|
|
|
|
if (instr.OpCode != OpCodes.Call)
|
|
|
|
|
continue;
|
2012-11-01 23:42:02 +08:00
|
|
|
|
var method = instr.Operand as IMethod;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
if (method == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
IMethod elementMethod = method;
|
|
|
|
|
var gim = method as MethodSpec;
|
2012-07-28 10:22:17 +08:00
|
|
|
|
if (gim != null)
|
2012-11-01 23:42:02 +08:00
|
|
|
|
elementMethod = gim.Method;
|
2012-07-28 10:22:17 +08:00
|
|
|
|
var callResult = createCallResult(elementMethod, gim, block, i);
|
2011-10-27 02:41:50 +08:00
|
|
|
|
if (callResult == null)
|
|
|
|
|
continue;
|
|
|
|
|
|
2011-10-28 04:22:52 +08:00
|
|
|
|
if (findArgs(callResult))
|
|
|
|
|
callResults.Add(callResult);
|
2011-10-27 02:41:50 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2011-10-28 04:22:52 +08:00
|
|
|
|
bool findArgs(CallResult callResult) {
|
2011-10-27 02:41:50 +08:00
|
|
|
|
var block = callResult.block;
|
2012-11-22 16:14:51 +08:00
|
|
|
|
var method = callResult.getMethodRef();
|
2012-11-01 23:42:02 +08:00
|
|
|
|
var methodArgs = DotNetUtils.getArgs(method);
|
2012-01-05 23:23:53 +08:00
|
|
|
|
int numArgs = methodArgs.Count;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
var args = new object[numArgs];
|
|
|
|
|
|
|
|
|
|
int instrIndex = callResult.callEndIndex - 1;
|
2011-10-28 04:22:52 +08:00
|
|
|
|
for (int i = numArgs - 1; i >= 0; i--) {
|
2012-01-05 23:23:53 +08:00
|
|
|
|
object arg = null;
|
|
|
|
|
if (!getArg(method, block, ref arg, ref instrIndex))
|
2011-10-28 04:22:52 +08:00
|
|
|
|
return false;
|
2012-01-05 23:23:53 +08:00
|
|
|
|
if (arg is int)
|
2012-11-01 23:42:02 +08:00
|
|
|
|
arg = fixIntArg(methodArgs[i], (int)arg);
|
2012-07-28 10:22:17 +08:00
|
|
|
|
else if (arg is long)
|
2012-11-01 23:42:02 +08:00
|
|
|
|
arg = fixIntArg(methodArgs[i], (long)arg);
|
2012-01-05 23:23:53 +08:00
|
|
|
|
args[i] = arg;
|
2011-10-28 04:22:52 +08:00
|
|
|
|
}
|
2011-10-27 02:41:50 +08:00
|
|
|
|
|
|
|
|
|
callResult.args = args;
|
|
|
|
|
callResult.callStartIndex = instrIndex + 1;
|
2011-10-28 04:22:52 +08:00
|
|
|
|
return true;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
object fixIntArg(TypeSig type, long value) {
|
|
|
|
|
switch (type.ElementType) {
|
2012-07-28 10:22:17 +08:00
|
|
|
|
case ElementType.Boolean: return value != 0;
|
|
|
|
|
case ElementType.Char: return (char)value;
|
|
|
|
|
case ElementType.I1: return (sbyte)value;
|
|
|
|
|
case ElementType.U1: return (byte)value;
|
|
|
|
|
case ElementType.I2: return (short)value;
|
|
|
|
|
case ElementType.U2: return (ushort)value;
|
|
|
|
|
case ElementType.I4: return (int)value;
|
|
|
|
|
case ElementType.U4: return (uint)value;
|
|
|
|
|
case ElementType.I8: return (long)value;
|
|
|
|
|
case ElementType.U8: return (ulong)value;
|
2012-01-05 23:23:53 +08:00
|
|
|
|
}
|
|
|
|
|
throw new ApplicationException(string.Format("Wrong type {0}", type));
|
|
|
|
|
}
|
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
bool getArg(IMethod method, Block block, ref object arg, ref int instrIndex) {
|
2011-10-27 02:41:50 +08:00
|
|
|
|
while (true) {
|
2011-10-28 07:28:08 +08:00
|
|
|
|
if (instrIndex < 0) {
|
|
|
|
|
// We're here if there were no cflow deobfuscation, or if there are two or
|
|
|
|
|
// more blocks branching to the decrypter method, or the two blocks can't be
|
|
|
|
|
// merged because one is outside the exception handler (eg. buggy obfuscator).
|
2012-11-11 12:31:11 +08:00
|
|
|
|
Logger.w("Could not find all arguments to method {0} ({1:X8})",
|
2012-01-04 02:52:40 +08:00
|
|
|
|
Utils.removeNewlines(method),
|
2012-11-01 23:42:02 +08:00
|
|
|
|
method.MDToken.ToInt32());
|
2012-02-18 14:56:53 +08:00
|
|
|
|
errors++;
|
2011-10-28 07:28:08 +08:00
|
|
|
|
return false;
|
|
|
|
|
}
|
2011-10-27 02:41:50 +08:00
|
|
|
|
|
|
|
|
|
var instr = block.Instructions[instrIndex--];
|
|
|
|
|
switch (instr.OpCode.Code) {
|
|
|
|
|
case Code.Ldc_I4:
|
|
|
|
|
case Code.Ldc_I8:
|
|
|
|
|
case Code.Ldc_R4:
|
|
|
|
|
case Code.Ldc_R8:
|
|
|
|
|
case Code.Ldstr:
|
|
|
|
|
arg = instr.Operand;
|
|
|
|
|
break;
|
|
|
|
|
case Code.Ldc_I4_S:
|
|
|
|
|
arg = (int)(sbyte)instr.Operand;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case Code.Ldc_I4_0: arg = 0; break;
|
|
|
|
|
case Code.Ldc_I4_1: arg = 1; break;
|
|
|
|
|
case Code.Ldc_I4_2: arg = 2; break;
|
|
|
|
|
case Code.Ldc_I4_3: arg = 3; break;
|
|
|
|
|
case Code.Ldc_I4_4: arg = 4; break;
|
|
|
|
|
case Code.Ldc_I4_5: arg = 5; break;
|
|
|
|
|
case Code.Ldc_I4_6: arg = 6; break;
|
|
|
|
|
case Code.Ldc_I4_7: arg = 7; break;
|
|
|
|
|
case Code.Ldc_I4_8: arg = 8; break;
|
|
|
|
|
case Code.Ldc_I4_M1:arg = -1; break;
|
|
|
|
|
case Code.Ldnull: arg = null; break;
|
|
|
|
|
|
|
|
|
|
case Code.Nop:
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
case Code.Ldloc:
|
|
|
|
|
case Code.Ldloc_S:
|
|
|
|
|
case Code.Ldloc_0:
|
|
|
|
|
case Code.Ldloc_1:
|
|
|
|
|
case Code.Ldloc_2:
|
|
|
|
|
case Code.Ldloc_3:
|
2012-11-20 00:58:34 +08:00
|
|
|
|
getLocalVariableValue(instr.Instruction.GetLocal(theMethod.Body.Variables), out arg);
|
2011-10-27 02:41:50 +08:00
|
|
|
|
break;
|
|
|
|
|
|
2012-04-30 14:31:09 +08:00
|
|
|
|
case Code.Ldfld:
|
2011-10-27 02:41:50 +08:00
|
|
|
|
case Code.Ldsfld:
|
|
|
|
|
arg = instr.Operand;
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
2012-04-30 14:31:09 +08:00
|
|
|
|
int pushes, pops;
|
2012-11-01 23:42:02 +08:00
|
|
|
|
instr.Instruction.CalculateStackUsage(false, out pushes, out pops);
|
2012-04-30 18:18:47 +08:00
|
|
|
|
if (!useUnknownArgs || pushes != 1) {
|
2012-11-11 12:31:11 +08:00
|
|
|
|
Logger.w("Could not find all arguments to method {0} ({1:X8}), instr: {2}",
|
2012-04-30 14:31:09 +08:00
|
|
|
|
Utils.removeNewlines(method),
|
2012-11-01 23:42:02 +08:00
|
|
|
|
method.MDToken.ToInt32(),
|
2012-04-30 14:31:09 +08:00
|
|
|
|
instr);
|
|
|
|
|
errors++;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < pops; i++) {
|
|
|
|
|
if (!getArg(method, block, ref arg, ref instrIndex))
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
arg = null;
|
|
|
|
|
break;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
2011-10-28 04:22:52 +08:00
|
|
|
|
|
|
|
|
|
return true;
|
2011-10-27 02:41:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void inlineReturnValues() {
|
2012-07-28 10:22:17 +08:00
|
|
|
|
callResults = removeNulls(callResults);
|
2011-10-27 02:41:50 +08:00
|
|
|
|
callResults.Sort((a, b) => {
|
|
|
|
|
int i1 = allBlocks.FindIndex((x) => a.block == x);
|
|
|
|
|
int i2 = allBlocks.FindIndex((x) => b.block == x);
|
2012-11-01 23:42:02 +08:00
|
|
|
|
if (i1 != i2)
|
|
|
|
|
return i1.CompareTo(i2);
|
2011-10-27 02:41:50 +08:00
|
|
|
|
|
2012-11-01 23:42:02 +08:00
|
|
|
|
return a.callStartIndex.CompareTo(b.callStartIndex);
|
2011-10-27 02:41:50 +08:00
|
|
|
|
});
|
|
|
|
|
callResults.Reverse();
|
|
|
|
|
inlineReturnValues(callResults);
|
|
|
|
|
}
|
|
|
|
|
|
2012-07-28 10:22:17 +08:00
|
|
|
|
static List<CallResult> removeNulls(List<CallResult> inList) {
|
|
|
|
|
var outList = new List<CallResult>(inList.Count);
|
|
|
|
|
foreach (var callResult in inList) {
|
|
|
|
|
if (callResult.returnValue != null)
|
|
|
|
|
outList.Add(callResult);
|
|
|
|
|
}
|
|
|
|
|
return outList;
|
|
|
|
|
}
|
|
|
|
|
|
2011-10-27 02:41:50 +08:00
|
|
|
|
protected abstract void inlineReturnValues(IList<CallResult> callResults);
|
|
|
|
|
}
|
|
|
|
|
}
|