Thunks.pdf

(335 KB) Pobierz
Thunks
Thunks
1.1
Chapter Overview
Chapter One
This chapter discusses thunks which are special types of procedures and procedure calls you can use to
defer the execution of some procedure call. Although the use of thunks is not commonplace in standard
assembly code, their semantics are quite useful in AI (artificial intelligence) and other programs. The proper
use of thunks can dramatically improve the performance of certain programs. Perhaps a reason thunks do
not find extensive use in assembly code is because most assembly language programmers are unaware of
their capabilities. This chapter will solve that problem by presenting the definition of thunks and describe
how to use them in your assembly programs.
1.2
First Class Objects
The actual low-level implementation of a thunk, and the invocation of a thunk, is rather simple. How-
ever, to understand why you would want to use a thunk in an assembly language program we need to jump to
a higher level of abstraction and discuss the concept of
First Class Objects
.
A first class object is one you can treat like a normal scalar data variable. You can pass it as a parameter
(using any arbitrary parameter passing mechanism), you can return it as a function result, you can change the
object’s value via certain legal operations, you can retrieve its value, and you can assign one instance of a
first class object to another. An
int32
variable is a good example of a first class object.
Now consider an array. In many languages, arrays are not first class objects. Oh, you can pass them as
parameters and operate on them, but you can’t assign one array to another nor can you return an array as a
function result in many languages. In other languages, however, all these operations are permissible on
arrays so they are first class objects (in such languages).
A statement sequence (especially one involving procedure calls) is generally not a first class object in
many programming languages. For example, in C/C++ or Pascal/Delphi you cannot pass a sequence of
statements as a parameter, assign them to a variable, return them as a function result, or otherwise operate on
them as though they were data. You cannot create arbitrary arrays of statements nor can you ask a sequence
of statements to execute themselves except at their point of declaration.
If you’ve never used a language that allows you to treat executable statements as data, you’re probably
wondering why anyone would ever want to do this. There are, however, some very good reasons for wanting
to treat statements as data and execute them on demand. If you’re familiar with the C/C++ programming
language, consider the C/C++ "?" operator:
expr ? Texpr: Fexpr
For those who are unfamiliar with the "?" operator, it evaluates the first expression (
expr
) and then returns
the value of
Texpr
if
expr
is true, it evaluates and returns
Fexpr
if
expr
evaluates false. Note that this code
does not evaluate
Fexpr
if
expr
is true; likewise, it does not evaluate
Texpr
if
expr
is false. Contrast this with
the following C/C++ function:
int ifexpr( int x, int t, int f )
{
if( x ) return t;
return f;
}
A function call of the form "ifexpr( expr, Texpr, Fexpr);" is not semantically equivalent to
"expr ? Texpr : Fexpr". The
ifexpr
call always evaluates all three parameters while the conditional expres-
sion operator ("?") does not. If either
Texpr
or
Fexpr
produces a side-effect, then the call to
ifexpr
may pro-
duce a different result than the conditional operator, e.g.,
Beta Draft - Do not distribute
© 2001, By Randall Hyde
Page 1279
Chapter One
i = (x==y) ? a++ : b--;
j = ifexpr( x==y, c++, d-- );
Volume Five
In this example either
a
is incremented or
b
is decremented, but not both because the conditional opera-
tor only evaluates one of the two expressions based on the values of
x
and
y.
In the second statement, how-
ever, the code both increments
c
and decrements
d
because C/C++ always evaluates all value parameters
before calling the function; that is, C/C++ eagerly evaluates function parameter expressions (while the con-
ditional operator uses deferred evaluation).
Supposing that we wanted to defer the execution of the statements "c++" and "d--" until inside the func-
tion’s body, this presents a classic case where it would be nice to treat a pair of statements as first class
objects. Rather than pass the value of "c++" or "d--" to the function, we pass the actual statements and
expand these statements inside the function wherever the format parameter occurs. While this is not possible
in C/C++, it is possible in certain languages that support the use of statements as first class objects. Natu-
rally, if it can be done in any particular language, then it can be done in assembly language.
Of course, at the machine code level a statement sequence is really nothing more than a sequence of
bytes. Therefore, we could treat those statements as data by directly manipulating the object code associated
with that statement sequence. Indeed, in some cases this is the best solution. However, in most cases it will
prove too cumbersome to manipulate a statement sequence by directly manipulating its object code. A better
solution is to use a pointer to the statement sequence and CALL that sequence indirectly whenever we want
to execute it. Using a pointer in this manner is usually far more efficient that manipulating the code directly,
especially since you rarely change the instruction sequence itself. All you really want to do is defer the exe-
cution of that code. Of course, to properly return from such a sequence, the sequence must end with a RET
instruction. Consider the following HLA implementation of the "ifexpr" function given earlier:
procedure ifexpr( expr:boolean; trueStmts:dword; falseStmts:dword );
returns( "eax" );
begin ifexpr;
if( expr ) then
call( trueStmts );
else
call( falseStmts );
endif;
end ifexpr;
.
.
.
jmp overStmt1;
stmt1: mov( c, eax );
inc( c );
ret();
overStmt1:
jmp overStmt2
stmt2: mov( d, eax );
dec( d );
ret();
overStmt2:
ifexpr( exprVal, &stmt1, &stmt2 );
(for reasons you’ll see shortly, this code assumes that the
c
and
d
variables are global, static, objects.)
Page 1280
© 2001, By Randall Hyde
Version: 9/9/02
Thunks
Notice how the code above passes the addresses of the
stmt1
and
stmt2
labels to the
ifexpr
procedure.
Also note how the code sequence above jumps over the statement sequences so that the code only executes
them in the body of the
ifexpr
procedure.
As you can see, the example above creates two mini-procedures in the main body of the code. Within
the
ifexpr
procedure the program calls one of these mini-procedures (stmt1 or
stmt2).
Unlike standard HLA
procedures, these mini-procedures do not set up a proper activation record. There are no parameters, there
are no local variables, and the code in these mini-procedures does not execute the standard entry or exit
sequence. In fact, the only part of the activation record present in this case is the return address.
Because these mini-procedures do not manipulate EBP’s value, EBP is still pointing at the activation
record for the
ifexpr
procedure. For this reason, the
c
and
d
variables must be global, static objects; you
must not declare them in a VAR section. For if you do, the mini-procedures will attempt to access these
objects in
ifexpr’s
activation record, not and the caller’s activation record. This, of course, would return the
wrong value.
Fortunately, there is a way around this problem. HLA provides a special data type, known as a thunk,
that eliminates this problem. To learn about thunks, keep reading...
1.3
Thunks
A
thunk
is an object with two components: a pointer containing the address of some code and a pointer
to an execution environment (e.g., an activation record). Thunks, therefore, are an eight-byte (64-bit) data
type, though (unlike a qword) the two pieces of a thunk are independent. You can declare thunk variables in
an HLA program, assign one thunk to another, pass thunks as parameters, return them as function results,
and, in general, do just about anything that is possible with a 64-bit data type containing two double word
pointers.
To declare a thunk in HLA you use the
thunk
data type, e.g.,
static
myThunk: thunk;
Like other 64-bit data types HLA does not provide a mechanism for initializing thunks you declare in a static
section. However, you’ll soon see that it is easy to initialize a thunk within the body of your procedures.
A
thunk
variable holds two pointers. The first pointer, in the L.O. double word of the thunk, points at
some execution environment, that is, an activation record. The second pointer, in the H.O. double word of
the thunk, points at the code to execute for the thunk.
To "call" a thunk, you simply apply the "()" suffix to the thunk’s name. For example, the following
"calls"
myThunk
in the procedure where you’ve declared
myThunk:
myThunk();
Thunks never have parameters, so the parameter list must be empty.
A thunk invocation is a bit more involved than a simple procedure call. First of all, a thunk invocation
will modify the value in EBP (the pointer to the current procedure’s activation record), so the thunk invoca-
tion must begin by preserving EBP’s value on the stack. Next, the thunk invocation must load EBP with the
address of the thunk’s execution environment; that is, the code must load the L.O. double word of the thunk
value into EBP. Next, the thunk must call the code at the address specified by the H.O. double word of the
thunk variable. Finally, upon returning from this code, the thunk invocation must restore the original activa-
tion record pointer from the stack. Here’s the exact sequence HLA emits to a statement like "myThunk();":
push( (type dword myThunk) );
call( (type dword myThunk[4]) );
// Pass execution environment as parm.
// Call the thunk
The body of a thunk, that is, the code at the address found in the H.O. double word of the thunk variable,
is not a standard HLA procedure. In particular, the body of a thunk does not execute the standard entry or
exit sequences for a standard procedure. The calling code passes the pointer to the execution environment
Beta Draft - Do not distribute
© 2001, By Randall Hyde
Page 1281
Chapter One
Volume Five
(i.e., an activation record) on the stack.. It is the thunk’s responsibility to preserve the current value of EBP
and load EBP with this value appearing on the stack. After the thunk loads EBP appropriately, it can exe-
cute the statements in the body of the thunk, after which it must restore EBP’s original value.
Because a thunk variable contains a pointer to an activation record to use during the execution of the
thunk’s code, it is perfectly reasonable to access local variables and other local objects in the activation
record active when you define the thunk’s body. Consider the following code:
procedure SomeProc;
var
c: int32;
d: int32;
t: thunk;
begin SomeProc;
mov( ebp, (type dword t));
mov( &thunk1, (type dword t[4]));
jmp OverThunk1;
thunk1:
push( EBP );
// Preserve old EBP value.
mov( [esp+8], ebp );
// Get pointer to original thunk environment.
mov( d, eax );
add( c, eax );
pop( ebp );
// Restore caller’s environment.
ret( 4 );
// Remove EBP value passed as parameter.
OverThunk1:
.
.
.
t();
// Computes the sum of c and d into EAX.
This example initializes the
t
variable with the value of
SomeProc’s
activation record pointer (EBP) and
the address of the code sequence starting at label
thunk1.
At some later point in the code the program
invokes the thunk which begins by pushing the pointer to
SomeProc’s
activation record. Then the thunk exe-
cutes the PUSH/MOV/MOV/ADD/POP/RET sequence starting at address
thunk1.
Since this code loads
EBP with the address of the activation record containing
c
and
d,
this code sequence properly adds these
variables together and leaves their sum in EAX. Perhaps this example is not particularly exciting since the
invocation of
t
occurs while EBP is still pointing at
SomeProc’s
activation record. However, you’ll soon see
that this isn’t always the case.
1.4
Initializing Thunks
In the previous section you saw how to manually initialize a thunk variable with the environment
pointer and the address of an in-line code sequence. While this is a perfectly legitimate way to initialize a
thunk variable, HLA provides an easier solution: the THUNK statement.
The HLA THUNK statement uses the following syntax:
thunk
thunkVar
:= #{
code sequence
}#;
thunkVar
is the name of a thunk variable and
code_sequence
is a sequence of HLA statements (note that the
sequence does not need to contain the thunk entry and exit sequences. Specifically, it doesn’t need the
"push(ebp);" and "mov( [esp+8]);" instructions at the beginning of the code, nor does it need to end with the
"pop( ebp);" and "ret(4);" instructions. HLA will automatically supply the thunk’s entry and exit sequences.
Here’s the example from the previous section rewritten to use the THUNK statement:
procedure SomeProc;
var
c: int32;
d: int32;
Page 1282
© 2001, By Randall Hyde
Version: 9/9/02
Thunks
t: thunk;
begin SomeProc;
thunk
t :=
#{
mov( d, eax );
add( c, eax );
}#;
.
.
.
t();
// Computes the sum of c and d into EAX.
Note how much clearer and easier to read this code sequence becomes when using the THUNK statement.
You don’t have to stick in statements to initialize
t,
you don’t have to jump over the thunk body, you don’t
have to include the thunk entry/exit sequences, and you don’t wind up with a bunch of statement labels in the
code. Of course, HLA emits the same code sequence as found in the previous section, but this form is much
easier to read and work with.
1.5
Manipulating Thunks
Since a thunk is a 64-bit variable, you can do anything with a thunk that you can do, in general, with any
other qword data object. You can assign one thunk to another, compare thunks, pass thunks a parameters,
return thunks as function results, and so on. That is to say, thunks are first class objects. Since a thunk is a
representation of a sequence of statements, those statements are effectively first class objects. In this section
we’ll explore the various ways we can manipulate thunks and the statements associated with them.
1.5.1 Assigning Thunks
To assign one thunk to another you simply move the two double words from the source thunk to the des-
tination thunk. After the assignment, both thunks specify the same sequence of statements and the same exe-
cution environment; that is, the thunks are now aliases of one another. The order of assignment (H.O.
double word first or L.O. double word first) is irrelevant as long as you assign both double words before
using the thunk’s value. By convention, most programmers assign the L.O. double word first. Here’s an
example of a thunk assignment:
mov(
mov(
mov(
mov(
(type dword srcThunk), eax );
eax, (type dword destThunk));
(type dword srcThunk[4]), eax );
eax, (type dword destThunk[4]));
If you find yourself assigning one thunk to another on a regular basis, you might consider using a macro
to accomplish this task:
#macro movThunk( src, dest );
mov(
mov(
mov(
mov(
#endmacro;
(type dword src), eax );
eax, (type dword dest));
(type dword src[4]), eax );
eax, (type dword dest[4]));
If the fact that this macro’s side effect of disturbing the value in EAX is a concern to you, you can always
copy the data using a PUSH/POP sequence (e.g., the HLA extended syntax MOV instruction):
Beta Draft - Do not distribute
© 2001, By Randall Hyde
Page 1283
Zgłoś jeśli naruszono regulamin