Sunday, March 15, 2009

Support for the LookupSwitch opcode in AbcExplorationLib

The LookupSwitch AVM2 opcode is used to represent the ActionScript switch statement.

This is an interesting branch instruction because it has multiple targets. It is almost a direct translation of the switch statement because it has two parameters, a default case and an array of possible targets.

Recently support for this opcode was added to AbcExplorationLib.

Given the following ActionScript code:


var x;
for(x = 1;x < 5;x++) {
switch(x) {
case 1:
print("one");
break;
case 2:
print("two");
break;
case 3:
print("three");
break;
case 4:
print("four");
break;
}
}


We compile this file to a .abc file using the following command:


$ java -jar /opt/flex3sdk/lib/asc.jar testswitch.as

testswitch.abc, 280 bytes written


By using a little IronPython example included with AbcExplorationLib we can see how this library interprets the LookupSwitch opcode:


$ mono /opt/IronPython-2.0/ipy.exe ../ipyexample/abccontents.py testswitch.abc
...
Instructions:
getlocal_0
pushscope
pushbyte 1
getglobalscope
swap
setslot 1
jump dest177
dest12:
label
jump dest74
dest17:
label
findpropertystrict M.print
pushstring "one"
callprop M.print
pop
jump dest165
dest30:
label
findpropertystrict M.print
pushstring "two"
callprop M.print
pop
jump dest165
dest43:
label
findpropertystrict M.print
pushstring "three"
callprop M.print
pop
jump dest165
dest56:
label
findpropertystrict M.print
pushstring "four"
callprop M.print
pop
jump dest165
dest69:
label
jump dest165
dest74:
getglobalscope
getslot 1
setlocal_1
pushbyte 1
getlocal_1
ifstrictneq dest91
pushshort 0
jump dest143
dest91:
pushbyte 2
getlocal_1
ifstrictneq dest104
pushshort 1
jump dest143
dest104:
pushbyte 3
getlocal_1
ifstrictneq dest117
pushshort 2
jump dest143
dest117:
pushbyte 4
getlocal_1
ifstrictneq dest130
pushshort 3
jump dest143
dest130:
pushfalse
iffalse SolvedReference dest141
pushshort 4
jump dest143
dest141:
pushshort 4
dest143:
kill
lookupswitch dest69 dest17,dest30,dest43,dest56,dest69
dest165:
getglobalscope
getslot 1
increment
setlocal_1
getlocal_1
getglobalscope
swap
setslot 1
kill
dest177:
getglobalscope
getslot 1
pushbyte 5
iflt dest12
returnvoid


As described in the documentation the parameters of the LookupSwitch instruction are specified as relative byte offsets that specify the target. In order to give a higher level representation of the code, these relative offsets are converted to symbolic references. This process is detailed in the "Using F# Active Patterns to encapsulate complex conditions" post.

This process starts in the following functions:


static member ReadAndProcessInstructions(aInput:BinaryReader,
count,
constantPool:ConstantPoolInfo) =
let instructionsAndOffsets =
(AvmMethodBody.ReadingInstructions([],
aInput,
count,
constantPool)) in
let destinations =
AvmMethodBody.CollectDestinations(instructionsAndOffsets,
Map.empty,
instructionsAndOffsets)
in
AvmMethodBody.UpdateCodeWithDestinations(
destinations,
instructionsAndOffsets,[]) |> List.to_array


The CollectDestinations method collects all the absolute offsets used by branch instructions and stores them in a dictionary with a generated label. The UpdateCodeWithDestinations method modifies the instruction list use the generated labels.

For example to add support in CollectDestinations the following code was added:


static member CheckLookupSwitchCase baseOffset
(totalInstructions:(int64*AbcFileInstruction)
list) =
fun (destinations:Map<int64,string>) target ->
match target with
| UnSolvedReference(relativeOffset) when
(AvmMethodBody.IsDestinationDefined(int(baseOffset+relativeOffset),
totalInstructions)) ->
destinations.Add(int64(baseOffset+relativeOffset),
sprintf "dest%d" (baseOffset+relativeOffset))
| _ -> destinations

static member CollectDestinations(instructions:(int64*AbcFileInstruction) list,
destinations:Map<int64,string>,
totalInstructions:(int64*AbcFileInstruction) list) =
match instructions with
...
| ((offset,(LookupSwitch(defaultBranch,cases)))::rest) ->
let baseOffset = int(offset) in
AvmMethodBody.CollectDestinations(rest,
Seq.append [defaultBranch] cases |>
Seq.fold (AvmMethodBody.CheckLookupSwitchCase baseOffset totalInstructions ) destinations,
totalInstructions)
...


Then the code is modified in the UpdateCodeWithDestinations method to use the generated labels.


static member UpdateCodeWithDestinations(destinations:Map<int64,string>,
instructions,
resultingInstructions) =
let processedInstructions =
match instructions with
...
| ((offset,LookupSwitch(defaultCase,cases))::rest) ->
(offset,
LookupSwitch(AvmMethodBody.SolveSwitchCase defaultCase offset destinations,
Array.map (fun c->AvmMethodBody.SolveSwitchCase c offset destinations) cases))::rest
| _ -> instructions
...