Semantics
Let's give meaning to it all.
The goal of this chapter is to ask and answer the following questions:
- What was the hardest instruction to implement?
- How big share of the cases were you able interpret?
- Which part of the documentation was lacking?
1. What are Semantics?
- isbn:978-1-84628-691-9
Today we are going to talk about semantics. Program semantics is about assigning meaning to programs. When we can talk about what program mean, it is easier to explain what they do.
We are going to discuss some different approaches to write down the semantics of a program. They all essentially turn programs syntax into mathematical logic.
1.1. Preface: Natural Deduction
- Natural deduction, Wikipedia.
If you are unfamiliar with Natural Deduction and Gentzen-style proofs, please refer to the wikipediea page on the topic. The sort story is that we refer to logical rules like this:
Which means that implies .
If we want multiple ways of reaching the conclusion, we can make more rules. For example, conjunction only requires one rule, both and has to be true but disjunction has two rules: either has to be true or has to be true.
1.2. Axiomatic Semantics, Flowcharts, and Hoare Triplets
- Hoare logic, Wikipedia.
- homepage.divms.uiowa.edu/~slonnegr/plf/Book/Chapter11.pdf
- doi:10.1007/978-94-011-1793-7_4
The first approach to assigning meaning to programs was Axiomatic Semantics. Here the meaning of the program is described by assigning precondition and postconditions to all statements in a program.
In this world we can describe the semantics of a program by denoting three parts, the precondition , the program , and the postcondition , this is also called Hoare triplets.
These mean that if the world satisfies before executing then the world will satisfy after.
The great thing about this approach is that we can compose the proofs of correctness of parts of the program into a proof of total correctness. Assume the program , where is executed after . Then we can describe the correctness of , like so:
If you are unfamiliar with the syntax above, it is natural deduction see the section on natural deduction. Essentially it means that given , and , and that the postcondition of implies the precondition of , are all true, we can also prove that .
This approach is every effective at describing the meaning of specific programs. This means that it is very good for doing program verification, but is not used often for program analysis in general.
1.3. Denotational Semantics
- Denotational semantics, Wikipedia.
- ncatlab.org/nlab/files/ScottStrachey-MathematicalSemantics.pdf
- www.youtube.com/watch?v=yorSVVbhsCY
- www.youtube.com/watch?v=rlyqoYoUumc
Another approach for writing down the semantics of the program is Denotational Semantics. The idea behind denotational semantics is to map the semantics of the program we want to analyse to a program to something we know well. This can either be another programming language or math.
Consider a very simple expressional language called , which has addition, variables and natural numbers :
If want to give meaning to the program x + 5
, then we can define a map from expressions to a function from a store to integers:
Here we explain that the semantic +
means the same as math symbol , that variables are the same as looking up the variable in a store, and that numbers should just be read as natural numbers.
Now we can see that x + 5
can be calculated in the store , using normal math
If you think this just looks like functional programming, you would be right. It also turns that it is best at describing expressional languages
1.4. Operational Semantics
- Operational semantics, Wikipedia.
- homepages.inf.ed.ac.uk/gdp/publications/Origins_SOS.pdf
- www.cs.cmu.edu/~crary/819-f09/Plotkin81.pdf
Finally, we can introduce Operational Semantics, which is the semantics we will focus on this semester. Operational semantics, describes it's semantics as changes to a state. This makes it ideal for describing imperative languages like the JVM bytecode. Furthermore, the Structural Operational Semantics are defined exactly as you would write a interpreter, which is handy because you are going to write one.
The Structural Operational Semantics or Small Step Semantics are written as judgments of the type which means given the environment , the state of the program is turned into .
The thing that makes this small step semantics is that we we only care about a single operation.
The Natural Operational Semantics or Big Step Semantics, are describing running the program until it halts. where is the final value of the program. Big step semantics often looks nicer than small step semantics, because it does not have to care about execution order.
Big Step semantics have the benefit of being easier to read, however, it has some big disadvantages, namely: we cannot reason about programs that run forever, and we cannot turn big step semantics into a working implementation. In contrast, small step semantics are easy to convert into an interpreter, and we can always recover the big step semantics from the operational semantics by simply applying the single step semantics until the program has terminated with a value:
1.5. Transition System and Traces
Using our new found definition of single step semantics, we can define the meaning of a program as a Transition System: where is the set program states, is the transition relation (defined by the single step semantics) and are possible initial states.
A is the possible infinite sequence of states and operations of the program.
The meaning of a program is now the set of traces that it exhibit:
This is also called the Maximal Trace Semantics. We can now define properties like, does a program halt, using relatively well defined math:
But more about this next time.
2. What are the Semantics of the JVM?
In this section, we are going to introduce some of the semantics of a limited JVM. It is, however, incomplete and you would have to complete it on your own.
2.1. The Values
The JVM is dynamically typed, this means that every value caries around information about its type. There are three kinds of values, stack values , local values and heap values .
The stack values are an extention of the local values. Ints are signed 32 bit integers, floats are 32 bit floating point values (IEEE 754 Standard (JLS ยง1.7)), and refs are also only 32 bits. Bytes are 8 bits unsinged, chars and shorts are singed and 8 and 16 bits respectively.
The heap also contains arrays, which has a length, a type and the content, and classes which has a name and the values of the fields, which is a mapping from names to stack values.
In this course we'll not cover long and double as they are a pain in the ... Furthermore, we'll try to avoid inner classes as well as bootstrap methods.
2.2. The Final States
Our program can end in either or .
2.3. The Context
As with all single step semantics rules, the JVM is run in a context. The context is the bytecode . For now we will define a simple operation which looks up the bytecode instruction at , is program counter, e.i., the name of the method and the offset in that method. We use the following short hands if then and . Furthermore, and .
2.4. The Stack
The JVM is a stack based virtual machine, this means that instead of having registers to put intermediate values in it uses a stack.
The stack is a list of values: . denote the empty stack and we add and remove elemenets from the end of the stack. A stack with the integers 1, 2, and 3, looks like this: . Most of our operations only operate on the stack and the program counter so we define our first simple SOS judgment like this:
Here are some examples. First we have the noop operation:
Then we have the add operation:
2.5. The Locals
In the JVM saves local variables of type to a local array . This is were the inputs to the method goes and any data that should be saved on the method stack instead of in the heap. The local array is indexed normally .
We can therefore extend our JVM judgements to the form:
We can run every operation that does not use the local variables, like so:
We can now also write rules that interact with the locals:
2.6. The Method Stack
The JVM also has a method stack , which is a stack of the tuples we have already introduced.
We can now define the last level of the semantic hirachy.
Ofcause we can use the operations we defined before, by lifting them into the world of method stacks:
But now we can also do things like returning from the program:
Or returning from a method:
2.7. The Heap
Finally we can add information about the heap . The heap is a mapping from references or static variables to . This also present our last SOS judgement:
And, our last lifting operation:
The heap allows use to talk about arrays and classes:
2.8. What about the rest?
So this is a very limited definition of semantics the JVM, we have not covered threads or exceptions, and we probably wont in this course. But even with these restrictions there are still many undefined rules left.
Write out dup
as single step semantics
Write dup
as single step semantic, given the definitions above. You can use the following resources:
- [ https://github.com/kalhauge/jvm2json/blob/main/CODEC.txt? ]: the decompiled codec (search for
<ByteCodeInst>
). - [ https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions? ]: the full list of instructions.
- [ https://docs.oracle.com/javase/specs/jvms/se22/html/jvms-4.html? ]: the class file format. And, finally
- [ https://docs.oracle.com/javase/specs/jvms/se22/html/jvms-6.html#jvms-6.5? ]: the official specification of each instruction.
It is now up to you to help each other by using this new found language to communicate the semantics of the rules of the JVM to each other through single step semantics.
Help each other
Choose one or more operations from the decompiled code and create a rule at this overleaf project:
[ https://www.overleaf.com/2251384699ytnrvmqwpmyh#5063de? ]
You can come back to this activity as needed when doing the rest of the activities.
I hope you can use it as a Wiki for explaining to each other the semantics of the JVM.
Note that Overleaf is not run by DTU, your data is therefore owned by them
. Your participation in this excessive is therefore purely optional. I will share the results on request.
3. Getting Started
The goal of today is to get familiar with the semantics of the JVM as well as starting writing our interpreter.
3.1. Get familiar with Java Bytecode
Now that we have some knowledge of java bytecode we should try to read it:
Inspect the Bytecode
First take a look at the decompiled code of the cases. First look at Simple.json. Now find the assertFalse
method (it's listed in the methods). Now find the code.bytecode
section, which should look like:
[
{ "field": { "class": "jpamb/cases/Simple", "name": "$assertionsDisabled", "type": "boolean" }, "offset": 0, "opr": "get", "static": true },
{ "condition": "ne", "offset": 3, "opr": "ifz", "target": 6 },
{ "class": "java/lang/AssertionError", "offset": 6, "opr": "new" },
{ "offset": 9, "opr": "dup", "words": 1 },
{ "access": "special", "method": { "args": [], "is_interface": false, "name": "<init>", "ref": { "kind": "class", "name": "java/lang/AssertionError" }, "returns": null }, "offset": 10, "opr": "invoke" },
{ "offset": 13, "opr": "throw" },
{ "offset": 14, "opr": "return", "type": null }
]
Especially, notice the opr, it explains what operations to run:
in# | opr | stack | description |
---|---|---|---|
00 | get | [] | Get the boolean and put it on the stack |
01 | ifz | [bool] | if it is not equal to zero (true) jump to the 6th instruction (e.i. return) |
02 | new | [] | otherwise create a new AssertionError object. |
03 | dup | [ref] | dublicate the reference |
04 | invoke | [ref, ref] | call the init method on the AssertionErrror (consuming the top reference). |
05 | throw | [ref] | throw the assertion error. |
06 | return | [] | otherwise return. |
3.2. Write an Interpreter
We should now be able to write an interpreter using the rules. For example, the rule
Converts to the following python code:
def step_push(self, b):
self.stack.append(b["value"]["value"])
self.pc += 1
Write an Interpreter
Write an interpreter for the JVM, that given a method id and an input, prints the query on the last line:
$ ./interpreter "jpamb.cases.Simple.assertInteger:(I)V" "(1)"
... alot ...
... of intermediate ...
... results ...
ok
Here are some good advice getting started:
- Start small, one method at a time. You don't have to cover the entire language. Also you don't need a method stack or a heap for most of the cases
- do them later.
- It's okay to hack some things, like
getting
the$assertionsDisabled
static field. You can assume that is always befalse
. - Print out the state at every step, this will help you debug.
- Look at (and extend)
solutions/interpret.py
for inspiration.
Test it!
You can use the bin/test.py
tool from JPAMB to test that your tool performs correctly. It will also build a report (designated by the -o
flag) that you can inspect errors. The same method filters for bin/evaluate.py
works here as well:
$ python bin/test.py --filter-methods=justReturn\: -o - -- python solutions/interpret.py
8t> starting: python solutions/interpret.py 'jpamb.cases.Simple.justReturn:()I' '()'
8t> read decompiled classfile decompiled/jpamb/cases/Simple.json
8t> STEP 0:
8t> PC: 0 {'offset': 0, 'opr': 'push', 'value': {'type': 'integer', 'value': 0}}
8t> LOCALS: []
8t> STACK: []
8t> STEP 1:
8t> PC: 1 {'offset': 1, 'opr': 'return', 'type': 'int'}
8t> LOCALS: []
8t> STACK: [0]
8t> DONE ok
8t> LOCALS: []
8t> STACK: []
8t> done
A specially effective technique is to save the report to a git repository with the -o
parameter, then you get an automatic semantic diff between versions of your tool.
Assuming you are on a linux machine and your project repository is next to jpamb, you can run the following command to get an instant golden test.
$ python ../jpamb/bin/test.py -o golden.log -- ./bin/interpreter
$ git add golden.log
some time parses
$ python ../jpamb/bin/test.py -o golden.log -- ./bin/interpreter
$ git diff golden.log
3.3. Make your first Dynamic analysis.
Write your first Dynamic Analysis
We are going to write our first dynamic analysis given our interpreter. Its called a random tester.
- Given a method
- Run with random input , make sure to limit the run-depth so that it does not run forever.
- Report any behavior witnessed with
100%
, and answer50%
on anything else (or don't answer).