::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::. Mar 00-Aug
00
:::\_____\::::::::::. Issue
8
::::::::::::::::::::::.........................................................
A S S E M B L Y P R O G R A M M I N G J O U R N A L
http://asmjournal.freeservers.com
asmjournal@...
T A B L E O F C O N T E N T S
----------------------------------------------------------------------
Introduction...................................................mammon_
"Teaching Assembly Language Using HLA"....................Randall.Hyde
"Processor Identification - Part II"..............Chris Dragan.&.Chili
"The LCC Intrinsics Utility"...............................Jacob.Navia
"Accessing COM Objects from Assembly"....................Ernest.Murphy
"64-bit Integer/ASCII Conversion"............................X-Calibre
Column: Win32 Assembly Programming
"Win32 AppFatalExit Skeleton"................................Chili
Column: The Unix World
"System Calls in FreeBSD".........................G.Adam.Stanislav
"Loadable Kernel Modules"..................................mammon_
Column: Gaming Corner
"Win32 ASM Game Programming"...........................Chris.Hobbs
Column: Assembly Language Snippets
"SEH.INC"................................................X-Calibre
"SEH.ASM"................................................X-Calibre
Column: Issue Solution
"BCD_Conv"...........................................Angel.Tsankov
----------------------------------------------------------------------
+++++++++++++++++++Issue Challenge++++++++++++++++++
Convert a two-digit BCD to hexadecimal
----------------------------------------------------------------------
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::..............................................INTRODUCTION
by
mammon_
I cannot begin to count the number of subtle and overt hints I have received
that this issue is by far the most tardy APJ release to date. Quite a few
projects have conspired to steal my time away, from Linux essays to
disassembler coding to reverse engineering a hardware/software combo thrown
together by a madman bent on carrying the technology to his grave. Enough to
say, though, that the issue is finally ready for distribution. Not only
that,
but I actually have about four article left over --including Part II of the
ASM
Gaming series-- to include in APJ 9.
The articles in this issue encompass a wide range of topics, from
customizing
the LCC compiler to programming games in asm. Randall Hyde, who I'm sure
needs
no introduction to assembly coders, has provided an excellent article
discussing the teaching of assembly language, and how he developed HLA to
assist. Chili has done a fair amount of work as well, working on everything
from CPU identification and exception handling to preparing an online gaming
article for ASCII publication.
X-Calibre has provided two complete programming packages, one for exception
handling and one for converting 64-bit integers; an introductory COM article
which further demystifies COM has been provided by Ernest Murphy. The Unix
camp
is doubly represented this month, with an introduction to FreeBSD assembly
language [using NASM, of course] and my linux article deferred from the
previous issue. Capping everything off is a quick challenge and solution
provided by Angel Tsankov.
It has been suggested to me many times during the Time Of No Issues that I
should acquire a staff for ensuring that the issues get out on time. I am
open
to suggestions in this area; anyone willing to volunteer their time on a
regular basis is welcome to contact me. Ideally, the mag should have a staff
that solicits articles [hint IRC hint], tests the code in each article, and
edits the articles to enforce formatting [80 col, 3sp tab] and commenting
standards. To date I've been doing the last one only, and as is readily
apparent I put it off as long as possible.
Another note, regarding mirrors. Translation of the APJ issues is perfectly
acceptable and highly encouraged; all I request is an email giving the URL
so
I can link to it from the main page. I should point out that the individual
articles, once removed from the context of the APJ issue, are the property
of
their individual authors, so contact them before 'repackaging'. Regarding
formatting, I have also received a few requests to reformat APJ in HTML or
another markup language to make reading and browsing easier. This I will not
do, for it makes APJ less portable and causes problems copying code from the
magazine to a source file. I have been working on syntax highlighting/tag
files
for vi and nedit; I will post these and any user-contributed translation
files
[e.g. APJ_to_HTML] on the main APJ website.
All pleading and excuses aside, issue 8 is now put to bed, and issue 9 will
be
out faster than you can recite GNU's license agreement. Enjoy the mag...
_m
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
Teaching Assembly Language Using
HLA
by Randall Hyde
I first began teaching assembly language programming at Cal Poly Pomona in
the
Winter Quarter of 1987. I quickly discovered that good pedagogical material
was difficult to come by; even the textbooks available for the course left
something to be desired. As a result, my students were learning very little
assembly language in the ten weeks available to the course. After about two
quarters, I decided to do something about the textbook problem, so I began
writing a text I entitled "How to Program the IBM PC Using 8088 Assembly
Language" (obviously, this was back in the days when schools still used PCs
made by IBM and the main CPU you could always count on was the 8088). "How
to
Program..." became the epitome of a "work in progress." Each quarter I
would
get feedback from the students, update the text, and give it to Kinko's (and
the UCR Printing and Reprographics Department) to run off copies for my
students the very next quarter.
The original "How to Program..." text provided a basic set of library
routines
to print strings, input characters and lines of text, and a few other basic
functions. This allowed the students to quickly begin writing programs
without
having to learn about the INT instruction, DOS, or BIOS. However, I
discovered
that students were spending a significant time each quarter writing their
own
numeric conversion routines, string manipulation routines, etc. One student
commented on "how much easier it was to program in 'C' than assembly
language
since all those conversions and string operations were built into the
language." I replied that the real savings were due more to the 'C'
standard
library than the language itself and that a comparable library for assembly
language programmers would make assembly language programming almost as easy
as
'C' programming. At that moment a little light when on in my head and I sat
down and wrote the first few routines of what ultimately became the "UCR
Standard Library for 80x86 Assembly Language Programmers" (You can still get
a
copy of the UCR stdlib from webster at the URL given above). As I finished
each group of routines in the standard library, I incorporated them into my
courses. This reaped immediate benefits as students spent less time writing
numeric conversion routines and spent more time learning assembly language.
My
students were getting into far more advanced topics than was possible before
the advent of the UCR Stdlib.
In the early 1990's, the 8088 CPU finally died off and IBM was no longer the
major supplier of PCs. Not only was it time to change the title of my text,
but I needed to update references to the 8088 (that were specific to that
chip)
and bring the text into the world of the 80386 and 80486 processors. DOS
was
still King and 16-bit code was still what everyone was writing, but issues
of
optimization and the like were a little outdated in the text. In addition
to
the changes reflecting the new Intel CPUs, I also incorporated the UCR
Standard
Library into the text since it dramatically improved the speed at which
students progressed beyond the basic assembly programming skills. I
entitled
the new version of the text "The Art of Assembly Language Programming," an
obvious knock-off of Knuth's series ("The Art of Computer Programming").
In early 1996 it became obvious to me that DOS was finally dying and I
needed
to modify "The Art of Assembly Language Programming" (AoA) to use Windows as
the development platform. I wasn't interested in having students write
Windows
GUI applications in assembly language (the time spent teaching
event-oriented
programming would interfere with the teaching of basic machine organization
and
assembly language programming), but it was clear that the days of writing
code
that arbitrarily pokes around in memory and accesses I/O addresses directly
(things that AoA taught) were nearly over. So I decided to get started on a
new version of AoA that used Windows as the basic development environment
with
the emphasis on writing console applications. The UCR Standard Library was
the
single most important pedagogical tool I'd discovered that dramatically
improved my students' progress. As I began work on a new version of AoA for
Windows 3.1 my first task was to improve upon the UCR Standard Library to
make
it even easier to use, more flexible, more efficient, and more "high level."
After six months of part time work I eventually gave up on the UCR Stdlib
v2.0.
The idea was right, unfortunately the tools at my disposal (specifically,
MASM
6.11) weren't quite up to the task at hand. I was writing some really
tricky
macros, obviously exploiting code inside MASM that Microsoft's engineers had
never run (i.e., I discovered lots of bugs). I would code in some
workarounds
to the defects only to have the macro package break at the next minor patch
of
MASM (e.g., from MASM 6.11a to MASM 6.11b). There was also a robustness
issue.
Although MASM's macro capabilities are quite powerful and it almost let me
do
everything I wanted, it was very easy to confuse the macro package and then
MASM would generate some totally weird (but absolutely correct) diagnostic
messages that correctly described what was going wrong in the macro but made
absolutely no sense whatsoever at all to a beginning assembly language
student
who use using the macro to print some data to the console device. As it
became
clear that the UCR Stdlib v2.0 would never be robust enough for student use,
I
decide to take a different approach.
About this time, I was talking with my Department Chair about the assembly
language course. We were identifying some of the problems that students had
learning assembly language. One problem, of course, was the paradigm shift
-
learning to solve problems using machine language rather than a high level
language. The second problem we identified is that students get to apply
very
little of what they've learned from other courses to the assembly language
class. A third problem was the primitive tools available to assembly
language
programmers. Energized by this discussion, I decided to see how I could
solve
these problems and improve the educational process.
Problem one, the paradigm shift, had to be handled carefully. After all,
the
whole purpose of having students take an assembly language programming
course
in the first place is to acquaint them with the low-level operation of the
machine. However, I felt it was certainly possible to redefine parts of
assembly language so that would be more familiar to students. For example,
one
might test the carry flag after an addition to determine if an unsigned
overflow has occurred using code like the following:
add eax, 5
jnc NoOverflow
<< code to execute if overflow occurs >>
NoOverflow:
Although this code is fairly straight-forward, you would be surprised how
many
students cannot visualize this code on their own. On the other hand, if you
feed them some pseudo code like:
add eax, 5
if( the carry flag is set ) then
<< code to execute if overflow occurs >>
endif
those same students won't have any problems understanding this code. To
take
advantage of this difference in perspective, I decided to explore changing
the
definition of assembly language to allow the use of the "if condition then
do
something" paradigm rather than the "if a condition is false them skip over
something" paradigm. Fundamentally, this does not change the material the
student has to learn; it just presents it from a different point of view to
which they're already accustomed. This certainly wasn't a gigantic leap
away
from assembly language as it existed in 1996. After all, MASM and other
assemblers were already allowing statements like ".if" and ".endif" in the
code. So I tried these statements out on a few of my students. What I
discovered is that the students picked up the basic "high level" syntax very
rapidly. Once they mastered the high level syntax, they were able to learn
the
low-level syntax (i.e., using conditional jumps) faster than ever before.
What
I discovered is something that Nicoderm CQ is pushing for their smoking
cessation program: "learning assembly language in graduated steps (from high
level to low level) is easier than going about it 'cold turkey.'"
The second problem, students not being able to leverage their programming
skills from other classes, is largely linked to the syntax of Intel x86
assembly language. Many skills students pick up, such as programming style,
indentation, appropriate programming construct selection, etc., are useless
in
a typically assembly language class. Even skills like commenting and
choosing
good variable names are slightly different in assembly language programs.
As a
result, students spend considerable (unproductive) time learning the new
"rules
of the game" when writing assembly language programs. This directly equates
to
less progress over the ten week quarter. Ideally, students should be able
to
applying knowledge like program style, commenting style, algorithm
organization, and control construct selection they learned in a C/C++ or
Pascal
course to their assembly language programs. If they could, they'd be "up
and
writing" in assembly language much faster than before.
The third problem with teaching assembly language is the primitive state of
the
tools. While MASM provides a wonderful set of high level language control
constructs, very little else about MASM supports this "brave new world" of
assembly language I want to teach. For example, MASM's variable
declarations
leave a lot to be desired (the syntax is straight out of the 1960's). As I
noted earlier, as powerful as MASM's macro facilities are, they weren't
sufficient to develop a robust library package for my students. I briefly
looked at TASM, but it's "ideal" mode fared little better than MASM.
Likewise,
while development environments for high level languages have been improving
by
leaps and bounds (e.g., Delphi and C++ Builder), assembly language
programmers
are still using the same crude command line tools popularized in the early
1970's. Codeview, which is practically useless under Windows, is the most
advanced tool Microsoft provides specifically for assembly language
programmers.
Faced with these problems, I decided the first order of business was to
create
a new x86 assembly language and write a compiler for it. I decided to give
this language the somewhat-less-than-original name of "the High Level
Assembler," or HLA (IBM and Motorola both already have assemblers that use a
variant of this name). It took three years, but the first version of HLA
was
ready for public consumption in September of 1999.
I began using HLA in my CS 61 course (machine organization and assembly
language programming) at UCR in the Fall Quarter, 1999. With no pedagogical
material other than a roughly written reference guide to the language, I was
expecting a complete disaster. It turns out that I was pleasantly
surprised.
Although the students did have major problems, the course went far more
smoothly than I anticipated and we managed to cover about the same material
I
normally covered when using MASM.
Although things were going far better than I expected, this is not to say
that
things were going great, or even as smoothly as I would have liked. The
major
problem, of course, was the lack of a textbook. The only material the
students
had to study from were their lecture notes. Clearly something needed to be
done about this. Of course, the whole reason for spending three years
writing
HLA was to allow me to write a new version of AoA. So in November, 1999, I
began work on the new edition of the text. By the start of the Winter
Quarter
in January, 2000, I had roughed together five chapters, about 50% of the
material was brand new, the other 50% was cut, pasted, and updated from the
older version of the text. During the quarter I rushed out two more
chapters
bringing the total to seven. The Winter Quarter went far more smoothly than
the Fall Quarter. Student projects were much better and the progress of the
class outstripped any assembly language course I'd taught prior to that
point.
Clearly the class was benefiting from the use of HLA.
By the start of the Spring Quarter in April, 2000, I'd managed to make one
proofreading pass over the first six chapters and I'd written the first
draft
of the eighth chapter. With a bit of luck, I will have the first draft of
the
text ready by the end of Summer, 2000. At that time I intend to "shop" the
text around to a set of publishers so other schools can benefit from the
work.
Well, this has been a long-winded report of HLA's justification. You're
probably wondering what HLA is and whether it is applicable to you
(especially
if you're a programmer rather than an educator). Fair enough, the rest of
this
article will discuss the HLA system and how you would use it.
HLA is a technically a compiler, not an assembler. HLA v1.x converts an HLA
source file into a MASM-compatible assembly language source file. This MASM
file is then assembled and linked to produce a Win32 executable file. The
HLA
compiler automatically runs the assembler and linker, so these steps are
transparent to the HLA user (other than the few extra seconds it takes to
assemble and link the output file). This whole process takes only a few
seconds (for example, compiling, assembling, and linking the 750-line
"x2p.hla"
program in the HLA examples directory only takes about two seconds on a 266
MHz
Pentium II system with UW SCSI drives). I am planning to emit object code
directly in version 2.0 of HLA. Until then, an HLA user will need
Microsoft's
MASM and linker. For those who would prefer to have HLA generate code for
TASM, NASM, or some other assembler, the HLA compiler source code is
available,
have fun :-).
HLA is a Win32 console application and it generates Win32 applications. By
default, it generates console applications although it does not restrict you
to
writing console applications under Windows. There is absolutely no support
for
DOS applications. While it is possible to write Linux applications with
only
minor changes to HLA, the development process for Linux applications is
convoluted and hardly worthwhile. HLA v2.0 will address portability across
32-bit x86 operating systems. For now, using HLA is practical only under
Win32
OSes (Win 95, 98, NT, and 2000).
When designing the HLA language, I chose a syntax that is very similar to
common imperative high level languages like Pascal/Delphi, Ada, Modula-2,
FORTRAN77, C/C++, and Java. That is not to say that HLA compiles Pascal
programs, but rather, a Pascal programmer will note many similarities
between
Pascal and HLA (and ditto for the other languages). HLA stole many of the
ideas for data declarations from the Algol based languages (Pascal,
Modula-2,
and Ada), it grabbed the ideas for many of its control structures from
FORTRAN77, Ada, and C/C++/Java, and the structure of the HLA Standard
Library
is based on the C Standard Library. So regardless of which high level
language
you're most comfortable with in this set, you'll certainly recognize some
elements of your favorite HLL in HLA.
A carefully written HLA program will look almost exactly like a high level
language program. Consider the following sample program:
program SampleHLApgm;
#include( "stdlib.hhf" )
const
HelloWorld := "Hello World";
begin SampleHLApgm;
stdout.put( "The classical 'Hello World' program: ", HelloWorld, nl );
end SampleHLApgm;
This program does the obvious thing. Anyone with any high level language
background can probably figure out everything except the purpose of "nl"
(which
is the newline string imported by the standard library). This certainly
doesn't look like an assembly language program; there isn't even a real
machine instruction in sight. Of course, this is a trivial example;
nonetheless, I've managed to write reasonable HLA programs that were just
over
1,000 lines of code that contained only one or two identifiable machine
language instructions. If it's possible to do this, how can I get away with
calling HLA an assembly language?
The truth is, you can actually write a very similar looking program with
MASM.
Here's an example I trot out for unbelievers. This code is compilable with
MASM (assuming you include the UCR Standard Library v2.0 and some additional
code I've cut out for brevity:
var
enum colors,<red,green,blue>
colors c1, c2
endvar
Main proc
mov ax, dseg
mov ds, ax
mov es, ax
MemInit
InitExcept
EnableExcept
finit
try
cout "Enter two colors:"
cin c1, c2
cout "You entered ",c1," and ",c2,nl
.if c1 == red
cout "c1 was red"
.endif
except $Conversion
cout "Conversion error occured",nl
except $Overflow
cout "Overflow error occured",nl
endtry
CleanUpEx
ExitPgm ;DOS macro to quit program.
Main endp
As you can see, the only identifiable machine instructions here are the ones
that initialize the segment registers at the beginning of the program (which
is
unnecessary in a Win32 environment). So let me blunt criticism from
"die-hard"
assembly fans right at the start: HLA doesn't open up all kinds of new
programming paradigms that weren't possible before. With some really clever
macros (e.g., enum, cout, and cin in the MASM code), it is quite possible to
do
some really amazing things. If you're wondering why you should bother with
HLA
if MASM is so wonderful, don't forget my comments about the robustness of
these
macros. Both HLA and MASM (with the UCR Standard Library v2.0) work great
as
long as you write perfect code and don't make any mistakes. However, if you
do
make mistakes, the MASM macro scheme gets ugly real quick.
The "die-hard" assembly fan will probably make the observation that they
would
never write code like the MASM code I've presented above; they would write
traditional assembly code. They want to write traditional code. They don't
want this high level syntax forced upon them. Well, HLA doesn't force you
to
use high level control structures rather than machine instructions. You can
always write the low level code if you prefer it that way. Here is the
original HLA program rewritten to use familiar machine instructions:
program SampleHLApgm2;
#include( "stdlib.hhf" )
data
dword 37, 37;
TcHWpStr: dword;
byte "The classical 'Hello World' program: ",0,0,0;
dword 11, 11;
HWstr: dword;
byte "Hello World",0;
begin SampleHLApgm2;
lea( eax, TcHWpStr );
push( eax );
call stdout.puts;
lea( eax, HWstr );
push( eax );
call stdout.puts;
call stdout.newln;
end SampleHLApgm2;
The stdout.puts and stdout.newln procedures come from the HLA Standard
Library.
I will leave it up to the interested reader to translate these into Win API
Write calls if this code isn't sufficiently low level to satisfy. Note that
HLA strings are not simple zero terminated strings like C/C++. This
explains
the extra zeros and dword values in the DATA section (the dword values hold
the
string lengths; I offer these without further explanation, see the HLA
documentation for more details on HLA's string format).
One thing you've probably noticed from this second example is that HLA uses
a
functional notation for assembly language statements. That is, the
instruction
mnemonics look like function calls in a high level language and the operands
look like parameters to those functions. The neat thing about this notation
is
that it easily allows the use of "instruction composition." Instruction
composition, like functional composition, means that you get to use one
instruction as the operand of another. For example, an instruction like
"mov(
mov( 0, eax ), ebx );" is perfectly legal in HLA. The HLA compiler will
compile the innermost instruction first and then substitute the destination
operand of the innermost instruction for the operand position occupied by
the
instruction. HLA's MOV instruction takes the generic form "MOV( source,
destination );" so the former instruction translates to the following two
instruction sequence:
mov( 0, eax ); // intel syntax: mov eax, 0
mov( eax, ebx ); // intel syntax: mov ebx, eax
By and of itself, instruction composition is somewhat interesting, but
programmers striving to write readable code need to exercise caution when
using
instruction composition. It is real easy to write some really unreadable
code
if you abuse instruction composition. E.g., consider:
mov( add( mov( 0, eax ), sub( ebx, ecx)), edx ), mov( i, esi ));
Egads! What does this mess do? Some might consider the inclusion of
instruction composition in HLA to be a fault of the language if it allows
you
to write such unreadable code. However, I've never felt it was the language
syntax's job to enforce good programming style. If there's really a reason
for
writing such messy code, the compiler shouldn't prevent it.
Although you can produce some truly unreadable messes with instruction
composition, if you use it properly it can enhance the readability of your
programs. For example, HLA lets you associate an arbitrary string with a
procedure that HLA will substitute for that procedure name when the
procedure
call appears as an operand of another instruction. Most functions that
return
a value in a register specify that register name as their "returns" string
(the
string HLA substitutes for the procedure call). For example, the "str.eq(
str1, str2)" function compares the two string operands and returns true or
false in AL depending on the result of the comparison. This allows you to
write code like the following:
if( str.eq( str1, "Hello" )) then
stdout.put( "str1 = 'Hello'" nl );
endif;
HLA directly translates the IF statement into the following sequence:
str.eq( str1, "Hello" );
if( al ) then
stdout.put( "str1= 'Hello'" nl );
endif;
(If a register name appears where a boolean expression is expected, as AL
does
in the IF statement above, HLA emits a TEST instruction to see if the
register
contains a non-zero value.)
Arguably, the former version is a little more readable than the latter
version.
Instruction composition, when you use it in this fashion, lets you write
code
that "looks" a little more high level without the compiler having to
generate
lots of extra code (as it would if HLA supported a generalized arithmetic
expression parser).
Like MASM, HLA supports a wide variety of high level control structures.
HLA's
set is both higher level and lower level at the same time. There are two
reasons HLA's control structures aren't always as powerful as MASM's.
First,
with the sole exception of object method invocations, I made a rule that
HLA's
high level control structures would not modify any general purpose registers
behind the programmer's back. MASM, for example, may modify the value in
EAX
for certain boolean expressions it must compute. Second, remember that the
primary goal of HLA is to teach assembly language; yes, it's supposed to
ease
the learning curve, but still the goal is to teach assembly language. It is
possible to get carried away with the high level language features and then
wind up with an "assembler" that lets students write their assembly language
programs in a high level language. In my opinion, MASM went too far with
what
it allows for boolean expressions. HLA, for example, doesn't allow the use
of
the conjunctive and disjunctive operators ( "&&" and "||") in boolean
expressions. I expect my students to generate the appropriate sequence of
low
level instructions themselves. In general, most HLA boolean expressions
compile into two instructions: a CMP and a conditional jump. I didn't want
to
go any farther than this because that would allow the students to avoid
learning how to write this code for themselves.
Although I designed HLA as a tool to teach assembly language programming,
this
is also a tool that I intend to use so I included lots of goodies for
advanced
assembly language programmers. For example, HLA's macro facilities are more
powerful than I've seen in any programming language based macro processor.
One
unique feature of HLA's macro preprocessor is the ability to create "context
free" control structures using macros. For example, suppose that you decide
that you need a new type of looping construct that HLA doesn't provide;
let's
say, a loop that will repeat once for each character in a string supplied as
a
parameter to the loop. Let's call this loop "OnceForEachChar" and decide
on
the following syntax:
OnceForEachChar( SomeString )
<< Loop Body >>
endOnceForEachChar;
On each iteration of this loop, the AL register will contain the
corresponding
character from the string specified as the OnceForEachChar operand. You can
easily implement this loop using the following HLA macro:
macro OnceForEachChar( SomeString ): TopOfLoop, LoopExit;
pushd( -1 ); // index into string.
TopOfLoop:
inc( (type dword [esp] )); // Bump up index into string.
#if( @IsConst( SomeString ))
lea( eax, SomeString ); // Load address of string constant
into EAX.
#else
mov( SomeString, eax ); // Get ptr to string.
#endif
add( [esp], eax ); // Point at next available
character
mov( [eax], al ); // Get the next available character
cmp( al, 0 ); // See if we're at the end
of the string
je LoopExit;
terminator endOnceForEachChar;
jmp TopOfLoop; // Return to the top of the loop and repeat.
LoopExit:
add( 4, esp ); // Remove index into string from stack.
endmacro;
Anyone familiar with MASM's macro processor should be able to figure out
most
of this code. Note that the symbols "TopOfLoop" and "LoopExit" are local
symbols to this macro. Hence, if you repeat this macro several times in the
code, HLA will emit different actual labels for these symbols to the MASM
output file. The "@IsConst" is an HLA compile-time function that returns
true
if its operand is a constant. Obtaining the address for a constant is
fundamentally different than obtaining the address of a string variable
(since
HLA string variables are actually pointers to the string data). The most
interesting feature of this macro definition is the "terminator" line. This
actually defines a second macro that is active only after HLA encounters the
"OnceForEachChar" macro and control returns to the first statement after the
OnceForEachChar invocation. Invocation of "context free" macros always
occur
in pairs; that is, for every "OnceForEachChar" invocation there must be a
matching "endOnceForEachChar" invocation. The following program
demonstrates
this macro in use, it also demonstrates that you can nest this newly created
control structure in your program:
program SampleHLApgm3;
#include( "stdlib.hhf" )
macro OnceForEachChar( SomeString ): TopOfLoop, LoopExit;
pushd( -1 ); // index into string.
TopOfLoop:
inc( (type dword [esp] ));
#if( @IsConst( SomeString ))
lea( eax, SomeString );
#else
mov( SomeString, eax );
#endif
add( [esp], eax );
mov( [eax], al );
cmp( al, 0 );
je LoopExit;
terminator endOnceForEachChar;
jmp TopOfLoop;
LoopExit:
add( 4, esp );
endmacro;
static
strVar: string := ":" nl;
begin SampleHLApgm3;
OnceForEachChar( "Hello" )
stdout.putc( al );
OnceForEachChar( strVar )
stdout.putc( al );
endOnceForEachChar;
endOnceForEachChar;
end SampleHLApgm3;
This program produces the output:
H:
e:
l:
l:
o:
Here's the MASM code the compiler emits for the sequence above (the
"strings"
segment was moved for clarity):
strings segment page public 'data'
align 4
?635_len dword 5
dword 5
?635_str byte "Hello",0,0,0
strings ends
pushd -1
?634__0278_:
inc dword ptr [esp+0] ;(type dword [esp])
lea eax, ?635_str
add eax, [esp+0] ;[esp]
mov al, [eax+0] ;[eax]
cmp al, 0
je ?636__0279_
push eax
call stdio_putc ;putc
pushd -1
?639__027d_:
inc dword ptr [esp+0] ;(type dword [esp])
mov eax, dword ptr ?630_strVar[0] ;strVar
add eax, [esp+0] ;[esp]
mov al, [eax+0] ;[eax]
cmp al, 0
je ?640__027e_
push eax
call stdio_putc ;putc
jmp ?639__027d_
?640__027e_:
add esp, 4
jmp ?634__0278_
?636__0279_:
add esp, 4
In addition to the "terminator" clause, HLA macros also support a "keyword"
clause that let you bury reserved words within a context-free language
construct. For example, the HLA language does not provide a SWITCH/CASE
statement. This omission was intentional. Rather than build the
SWITCH/CASE
statement into the HLA language, I implemented the SWITCH .. CASE .. DEFAULT
..
ENDCASE statement using HLA's macro facilities (as a demonstration of HLA's
power). An HLA SWITCH statement takes the following form:
switch( reg32 )
case( constantList1 )
<< statements >>
case (constantList2 )
<< statements >>
.
.
.
default // This is optional
<< statements >>
endswitch;
The switch macro implements the "switch" and "endswitch" reserved words
using
the macro and terminator clauses in the macro declaration. It implements
the
"case" and "default" reserved words using the HLA "keyword" clause in a
macro
definition. The "keyword" clause is similar to the "terminator" clause
except
it doesn't force the end of the macro expansion in the invoking code. The
actual code for the HLA SWITCH statement is a little too complex to present
here, so I will extend the example of the OnceForEachChar macro to
demonstrate
how you code use the "keyword" clause in a macro.
Let's suppose you wanted to add a "_break" clause to the "OnceForEachChar"
loop
( I'm using "_break" with an underscore because "break" is an HLA reserved
word). You could easily modify the "OnceForEachChar" macro to achieve this
by
using the following code:
macro OnceForEachChar( SomeString ): TopOfLoop, LoopExit;
pushd( -1 ); // index into string.
TopOfLoop:
inc( (type dword [esp] ));
#if( @IsConst( SomeString ))
lea( eax, SomeString );
#else
mov( SomeString, eax );
#endif
add( [esp], eax );
mov( [eax], al );
cmp( al, 0 );
je LoopExit;
keyword _break;
jmp LoopExit;
terminator endOnceForEachChar;
jmp TopOfLoop;
LoopExit:
add( 4, esp );
endmacro;
The "keyword" clause defines a macro ("_break") that is active between the
"OnceForEachChar" and "endOnceForEachChar" invocations. This macro simply
expands to a jmp instruction that exits the loop. Note that if you have
nested
"OnceForEachChar" loops and you "_break" out of the innermost loop, the code
only jumps out of the innermost loop, exactly as you would expect.
HLA's macro facilities are part of a larger feature I refer to as the "HLA
Compile-Time Language." HLA actually contains a built-in interpreter than
executes while it is compiling your program. The compile-time language
provides conditional compilation ( the #IF..#ELSE..#ENDIF statements in the
previous example), interpreted procedure calls (macros), looping constructs
(#WHILE..#ENDWHILE), a very powerful constant expression evaluator,
compile-time I/O facilities (#PRINT, #ERROR, #INCLUDE, and #TEXT..#ENDTEXT),
and dozens of built-in compile time functions (like the @IsConst function
above).
The HLA built-in string functions (not to be confused with the HLA Standard
Library's string functions) are actually powerful enough to let you write a
compiler for a high level language completely within HLA. I mentioned
earlier
that it is possible to write an expression compiler within HLA; I was
serious.
The HLA compile-time language will let you write a sophisticated recursive
descent parser for arithmetic expressions (and other context-free language
constructs, for that matter).
HLA is a great tool for creating low-level Domain Specific Embedded
Languages
(DSELs). DSELs are mini-languages that you create on a project by project
basis to help reduce development time. HLA's compile time language lets you
create some very high level constructs. For example, HLA implements a very
powerful string pattern matching language in the "patterns" module found in
the
HLA Standard Library. This module lets you write pattern matching programs
that use techniques found in language like SNOBOL4 and Icon. As a final
example, consider the following HLA program that translate RPN (reverse
polish
notation) expressions into their equivalent assembly language (HLA)
statements
and displays the results to the standard output:
// This program translates user RPN input into an
// equivalent sequence of assembly language instrs (HLA fmt).
program RPNtoASM;
#include( "stdlib.hhf" );
static
s: string;
operand: string;
StartOperand: dword;
macro mark;
mov( esi, StartOperand );
endmacro;
macro delete;
mov( StartOperand, eax );
sub( eax, esi );
inc( esi );
sub( s, eax );
str.delete( s, eax, esi );
endmacro;
procedure length( s:string ); returns( "eax" ); nodisplay;
begin length;
push( ebx );
mov( s, ebx );
mov( (type str.strRec [ebx]).length, eax );
pop( ebx );
end length;
begin RPNtoASM;
stdout.put( "-- RPN to assembly --" nl );
forever
stdout.put( nl nl "Enter RPN sequence (empty line to quit): " );
stdin.a_gets();
mov( eax, s );
breakif( length( s ) = 0 );
while( length( s ) <> 0 ) do
pat.match( s );
// Match identifiers and numeric constants
mark;
pat.zeroOrMoreWS();
pat.oneOrMoreCset( {'a'..'z', 'A'..'Z', '0'..'9', '_'} );
pat.a_extract( operand );
stdout.put( " pushd( ", operand, " );" nl );
strfree( operand );
delete;
pat.alternate;
// Handle the "+" operator.
mark;
pat.zeroOrMoreWS();
pat.oneChar( '+' );
stdout.put
(
" pop( eax );" nl
" add( eax, [esp] );" nl
);
delete;
pat.alternate;
// Handle the '-' operator.
mark;
pat.zeroOrMoreWS();
pat.oneChar( '-' );
stdout.put
(
" pop( eax );" nl
" pop( ebx );" nl
" sub( eax, ebx );" nl
" push( ebx );" nl
);
delete;
pat.alternate;
// Handle the '*' operator.
mark;
pat.zeroOrMoreWS();
pat.oneChar( '*' );
stdout.put
(
" pop( eax );" nl
" imul( eax, [esp] );" nl
);
delete;
pat.alternate;
// handle the '/' operator.
mark;
pat.zeroOrMoreWS();
pat.oneChar( '/' );
stdout.put
(
" pop( ebx );" nl
" pop( eax );" nl
" cdq(); " nl
" idiv( ebx, edx:eax );" nl
" push( ebx );" nl
);
delete;
pat.if_failure
// If none of the above, it must be an error.
stdout.put( nl "Illegal RPN Expression" nl );
mov( s, ebx );
mov( 0, (type str.strRec [ebx]).length );
pat.endmatch;
endwhile;
endfor;
end RPNtoASM;
Consider for a moment the code that matches an identifier or an integer
constant:
mark;
pat.zeroOrMoreWS();
pat.oneOrMoreCset( {'a'..'z', 'A'..'Z', '0'..'9', '_'} );
pat.a_extract( operand );
stdout.put( " pushd( ", operand, " );" nl );
strfree( operand );
delete;
The "mark;" invocation saves a pointer into the "s" string where the current
identifier starts. The pat.ZeroOrMoreWS pattern matching function skips
over
zero or more whitespace characters. The pat.OneOrMoreCset pattern match
function matches one or more alphanumeric and underscore characters (a crude
approximation for identifiers and integer constants). The pat.a_extract
function makes a copy of the string between the "mark" and the "a_extract"
calls (this corresponds to the whitespace and identifier/constant). The
stdout.put statement emits the HLA machine instruction that will push this
operand on to the x86 stack for later computations. The remaining
statements
clean up allocated string storage space and delete the matched string from
"s".
Although the "pat.xxxxx" statements look like simple function calls, there's
actually a whole lot more going on here. HLA's pattern matching facilities,
like SNOBOL4 and Icon, support success, failure, and backtracking. For
example, if the pat.oneOrMoreChar function fails to match at least one
character from the set, control does not flow down to the pat.a_extract
function. Instead, control flows to the next "pat.alternate" or
"pat.if_failure" clause. Some calls to HLA pattern matching routines may
even
cause the program to back up in the code and reexecute previously called
functions in an attempt to match a difficult pattern (i.e., the backtracking
component). This article is not the place to get into the theory of pattern
matching; however, these few examples should be sufficient to show you that
something really special is going on here. And all these facilities were
developed using the HLA compile-time language. This should give you a small
indication of what is possible when using the HLA compile time language
facilities.
The HLA language is far too rich to describe in this short article (the
*very*
rough documentation for the language is nearly 300 pages long). For more
information, check out the on-line documentation for HLA at
http://webster.cs.ucr.edu. Someday, you'll also be able to learn about HLA
via "The Art of Assembly Language Programming, HLA/Windows version." I will
keep interested individuals updated on the progress of AoA at the Webster
web
site.
HLA is totally free. It is public domain software and there are no
restrictions on its use, the use of the HLA standard library, or the HLA
compiler source code. Do whatever you want with it and have a lot of fun!
rhyde@...
http://webster.cs.ucr.edu
http://www.cs.ucr.edu/docs/webster/
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
Processor Identification - Part
II
by Chris Dragan & Chili
In the first part of this article I'll explain a lot of different ways to
check
for older processors by exploiting bugs, undocumented features, etc. I'll
also
show how to write an invalid-opcode exception handler, calculate the size
of
the prefetch queue and some other things. Finally, in the last part Chris
shows
how to determine the processor clockrate with the RDTSC instruction.
Chris didn't have much free time at the moment and so couldn't contribute
more,
therefore I had to put this article together pretty much myself, and I hope
the
quality didn't go down very much -- since Chris' texts are definitely
better
than mine.
AAD (ASCII Adjust before Division) Instruction
----------------------------------------------
This instruction allows us to distinguish between at least NEC's V-series
and
Intel processors. AAD, usually in preparation for a division using DIV or
IDIV,
works like this:
AL = AH * 10 + AL
AH = 0
Converting the unpacked two-digit BCD number in AX into binary. Thus
being
"0d5h, 0ah" the normal opcode. The difference is that while Intel's chips
allow
one to replace the multiplicand with any number (and by so building your
own
AAD instruction for various number systems), NEC always encodes it as 10
by
default. So by replacing the second byte with a different number, we can
then
check if the operand is actually used, and if not, assume it's a NEC.
mov ax, 0f0fh
db 0d5h, 10h ; opcode for AAD 16
cmp al, 0ffh ; check if multiplicand was 10 or
not
jz _is_Intel
jnz _is_NEC
This should be used as another way (in addition to the one presented in
the
first article on this subject) to distinguish the NEC V20/V30 series from
the
Intel 8086/88.
PUSHA Instruction
-----------------
Here is another good way to differentiate NECs from Intel's 8086/88.
Since
V20 and V30 execute all the 80186 instructions and knowing that PUSHA
executed
on the 8086/88 as "JMP $+2", one can for example, after executing it, set
the
carry flag and then see if it was really set.
clc ; ensure that CF is clear
pusha ; executed on 8086/88 as JMP $+2
stc
jc _is_NEC_or_186plus
jnc _is_808x
<whatever code here>
.
.
.
_is_NEC_or_186plus:
popa ; clean up
Of course the carry flag must not already be set before performing this
test.
POP CS Trick
------------
I'll just show one last way of accomplishing the same. The trick is that,
on a
8086/88 (non-CMOS versions, at least), the opcode "0fh" will perform a POP
CS,
on a 186/88 is an invalid opcode, generating an INT6 exception, while NECs
and
286+ use that encoding as a prefix byte, to indicate new instructions. So,
to
tell NEC's V20/V30 (also V40/V50, I think) and 8086/88 apart, and knowing
that
with the byte string "0fh, 14h, 0c3h", the CPU will perform the following:
8086/88 V20/V30
------- -------
pop cs set1 bl, cl
adc al, 0C3h
It is then easy to write a piece of code that will distinguish between them:
xor al, al ; BTW: clears CF
push cs
db 0fh, 14h, 0c3h ; intruction(s) -- see above
cmp al, 0c3h ; check if ADC was executed
je _is_808x
jne _is_NEC_V20plus
<whatever code here>
.
.
.
_is_NEC_V20plus:
pop ax ; clean up (no POP CS available)
Note that, again, the carry flag must be cleared before execution of this
test.
Also, just a reminder that this is to be used when you know that the
processor
is not a 186 or above but an older one.
Word Write
----------
On the 8086/88 (+ V20/V30), when a word write is performed at offset 0ffffh
in
a segment, one byte will be written at that offset and the other at offset
0,
while an 80186 family processor will write one byte at offset 0ffffh, and
the
other, one byte beyond the end of the segment (offset 10000h). So all we
have
to do is test if it wraps around or not:
mov ax, ds:[0ffffh] ; save original bytes
mov word ptr ds:[0ffffh], 0aaaah
cmp byte ptr ds:[0], 0aah ; did 2nd byte wrap around?
mov ds:[0ffffh], ax ; restore original bytes
je _is_808x
jne _is_8018x
Again, note that this should only be used for the specified processors.
Multi-Prefix Intructions
------------------------
The standard 8086/88 processors have a bug such that they loose
multiple
prefixes if an interrupt occurs, while CMOS versions do not, since this bug
was
fixed in the 80C86/C88 processors (NEC V20/V30 processors also do not have
this
bug -- allowing the following code to also be applicable to them). If
we
execute a string operation with a repeat prefix and also a segment override
for
long enough to be interrupted, then, if we are on a 8086/88 the REP prefix
will
be lost when the instruction is interrupted, since on return, only the
last
prefix will be retained. If instead, we are on a low-power consumption
CMOS
version, the code will successfully complete.
mov cx, 0ffffh
sti
rep lods byte ptr es:[si] ; sure to be interrupted
cli
jcxz _not_standard_808x ; check if REP was completed
<if here, then it's just a standard 8086/88>
.
.
.
Just in case you want to use a piece of code like this without having to
worry
about that bug, here's how to get it work correctly every time (with
interrupts
enabled -- this time with MOVS):
do_REP: rep movs byte ptr es:[di], es:[si] ; may be
interrupted!
jcxz carry_on ; if not, carry on,
loop do_REP ; else, complete REP
carry_on:
Invalid-Opcode Exception Handler (INT6)
---------------------------------------
From the 80186 and upwards, all processors allow one to implement
an
invalid-opcode exception handler, which gives us a great way of telling
the
families of CPUs apart. All one does is, hook the INT6 interrupt vector
with
our own handler and see if some specific instructions trigger an INT6 or
not.
With our handler we trap those exceptions and then toggle a little flag,
that
show us the processor doesn't support that instruction.
In the code below I hooked the INT6 vector by changing the IVT
(Interrupt
Vector Table) directly, but one can also use DOS services for that, test
which
processor we're running on and after that restore things back to what they
were
before (except registers, place some push/pop code yourself according to
your
needs -- by the way, Robert Collins is a god!). Anyway, the code is pretty
much
self-explanatory:
; Hook INT6 -- set up our own handler
push 0 ; point to IVT (0000:0000) -
(1
pop es ; byte saved thanks to
Chris!)
cli
lds ax, es:[6*4] ; get original handler
vector
mov es:[6*4], offset INT6_handler ; then, replace it
with
mov es:[6*4+2], cs ; our own handler
sti
; Test if processor is at least a 80186 -- Executes "SHL DX, 10"?
mov cx, 1 ; set up invalid-opcode flag
shl dx, 0ah
jcxz unknown_CPU
; Test if processor is at least a 80286 -- Executes "SMSW DX"?
smsw dx
jcxz _is_80186
; Test if processor is at least a 80386 -- Executes "MOV EDX, EDX"?
mov edx, edx
jcxz _is_80286
; Test if processor is at least a 80486 -- Executes "XADD DL, DL"?
xadd dl, dl
jcxz _is_80386
<if here, then it's a 80486 or higher processor>
.
.
.
; Restore original INT6 handler address -- for all processors type!
cli
mov es:[6*4], ax ; restore original INT6 offset
mov es:[6*4+2], ds ; restore original INT6 segment
sti
<whatever code here>
.
.
.
; Our own INT6 handler
INT6_handler:
xor cx, cx ; toggle invalid-opcode flag
push bp
mov bp, sp
add word ptr ss:[bp+2], 3 ; adjust the return address
to
; after the invalid opcode
(3
; bytes for all)
pop bp
iret
Note, that for this code: 1) should only be used if you know the processor
is
at least a 80186, 2) if you fiddle with the contents of AX, ES and DS
and
change them before restoring the original INT6 handler don't forget to
first
save and then restore them!, 3) of course the code in the INT6_handler
should
only be executed by means of an INT6!
Maybe a very small extra explanation is required regarding the INT6_handler.
We
need to adjust the return address, since when an invalid opcode exception
is
issued the saved contents of CS and EIP (which are pushed onto the stack)
point
to the instruction that generated the exception, instead of the next one
(as
usually happens for other interrupts).
Instruction Prefetch Queue
--------------------------
16-bit (ie. 8086s, 80186s, V30s) processors have a prefetch queue 6 bytes
in
size and replenish the instruction queue after having at least two bytes
empty
in the queue, while their 8-bit bus versions (ie. 8088s, 80188, V20s) only
have
a 4 byte prefetch queue and initiate the prefetch cycle when there is at
least
one empty byte in it.
So, knowing this about their Bus Interface Unit design, it isn't difficult
to
write some code to distinguish between the two categories. We'll make a
routine
that uses self-modifying code to change the opcode at the fifth byte and
then
see if it was executed or not.
xor cx, cx
cli ; prevent against queue being
emptied
lea di, patch
mov al, 90h ; load NOP opcode
stosb ; patch fifth byte to a NOP
nop
nop
nop
nop
patch: inc cx ; did the INC execute?
sti
jcxz _is_8bit
<if here, then it's an 16-bit processor>
I believe there is enough time for the prefetch queue to fill, though I have
no
chance to confirm it!
Just in case you want to be on the safe side, here's a routine that will
most
certainly work:
xor dx, dx
cli ; prevent against queue being
emptied
lea di, patch+2
mov al, 90h ; load NOP opcode
mov cx, 3
std
rep stosb ; patch fifth byte to a NOP
nop
nop
nop
nop
patch: inc dx ; did the INC execute?
nop
nop
sti
test dx, dx
jz _is_8bit
<if here, then it's an 16-bit processor>
Again, I must stress that this code should only be used for the
specified
processors, since it will without a doubt fail on others.
Do It The Optimized Way!
------------------------
Here is our size-optimized way of determining the processor type. It's
an
algorithm that uses Intel's guidelines and tests between pre-80286,
80286,
80386, 80486 without CPUID and 80486+ with CPUID support.
Chris is using a similar routine in his CPU identification utility.
; Detection of pre-80286/80286/386+ processors
mov ax, 7202h ; set bits 12-14 and clear bit 15
push ax
popf
pushf
pop ax
test ah, 0f0h
js _is_pre286 ; bit 15 of FLAGS is set on pre-286
jz _is_80286 ; bits 12..15 of FLAGS are clear on
286
; processor in real mode (no V86
mode
; on 286)
; <if here, then it's a 80386 or higher processor>
; Detection of 80386/80486(w/out CPUID)/80486+(CPUID compliant)
pushfd
pop eax
mov edx, eax
xor eax, 00240000h ; flip bits 18 (AC) and 21 (ID)
push eax
popfd
pushfd
pop eax
xor eax, edx ; check if both bits didn't toggle
jz _is_80386
shr eax, 19 ; check if only bit 18 toggled
jz _is_80486_without_CPUID
<if here, then it's a 80486 with CPUID or higher processor>
And so, we got the whole code down to a measly 46 bytes!
CR0 Register - Bit 4
--------------------
The 80386 DX may be differentiated from the other models by trying to clear
bit
4 (ET) in the CR0 register. It can be toggled on the 80386 DX, while it
is
hardwired to 1 on any of the other family models. So this gives us a good
way
to differentiate them, by trying to clear that bit and then see if it
got
forced to set or not.
; Test CR0 register -- bit 4 (ET)
mov eax, cr0
mov edx, eax ; save original CR0
and al, 11101111b ; clear bit 4
mov cr0, eax
mov eax, cr0
mov cr0, edx ; restore original CR0
test al, 00010000b ; check if bit 4 was forced high
jz _is_a_80386DX_model
jnz _is_not_a_80386DX_and_therefore_is_some_other_model
Note that I'm not sure if this can safelly/trustfully be done under
protected
mode!
Clockrate
---------
Before Pentium, it was difficult to determine the processor clockrate.
It
typically based on sophisticated timing loops, which were often
unreliable.
With Pentium, Intel introduced RDTSC instruction, which returned number
of
clocks since the processor start. The following code illustrates how to use
it.
; Determine RDTSC support (assuming that CPUID is supported)
mov eax, 1
cpuid
test edx, 10h ; bit 4 is set when RDTSC is
supported
jz _no_rdtsc
; Disable all interrupts but timer IRQ0
in al, 21h
mov ah, al
in al, 0A1h
push ax ; Save previous values
mov al, 0FEh
out 21h, al
mov al, 0FFh
out 0A1h, al
; Assuming that timer runs at 55ms periods, get the clockrate
hlt ; Wait for timer
rdtsc ; Read TSC
mov ebx, eax ; Save lo
mov ecx, edx ; Save hi
hlt ; Wait for timer
rdtsc ; Read TSC
sub eax, ebx ; Difference lo
sbb edx, ecx ; Difference hi
; Calculate clockrate in MHz
mov ecx, 54925
div ecx
mov [Clockrate], eax
; Restore interrupt states
pop ax
out 0A1h, al
mov al, ah
out 21h, al
The above code can be run in real mode, V86 mode or protected mode in ring0.
In
V86 mode it will hang Pentium and Pentium MMX processors, but on
other
processors it will work OK.
In this code, clockrate is determined as: (T2-T1)*PIT/(D*M), where T1 and
T2
are numbers of clocks returned by RDTSC, PIT is the value divided in
the
Programmable Interval Timer (equals 0x1234DD), D is the value by which PIT
is
divided (0x10000) and M is 1000000 (we want it in MHz).
Is This The End?
----------------
I think this is the end as old CPUs are concerned, since a lot of
techniques
have already been covered here (though there are some more), but not for
other
processors, like AMD and IBM and whatever else Chris and I think up before
the
next article.
Take the time to visit Chris' web page, where you can find the source for
his
CPU identification utility (for Netwide Assembler). His place is at:
http://ams.ampr.org/cdragan/
Also, here are some other sources of information that you might want to
take a
look at (available somewhere on the net -- since I don't remember where I
got
them from):
WHATCHIP.ASM (Christy Gemmell)
86BUGS.LST (Harald Feldmann/Hamarsoft)
[distributed with Ralf Brown's Interrupt list]
OPCODES.LST (Potemkin's Hackers Group)
[distributed with Ralf Brown's Interrupt list]
cpu.asm (Robert Mashlan)
WHATCPU.ASM (Dave M. Walker)
COMPTEST 2.60 (Norbert Juffa)
Ralf Brown's Interrupt List:
http://www.cs.cmu.edu/~ralf/files.html
This, in addition to the ones already referenced in the first article of
this
series.
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
The LCC Intrinsics
Utility
Jacob
Navia
Lcc-win32 is a free C compiler system. It features an IDE, a resource
compiler,
a linker, librarian, a windowed debugger, and other goodies.
Here, I would like to describe a special feature of lcc-win32 that will be
surely appreciated by the colleagues that use assembly.
Lcc-win32 understands special macro definitions called intrinsics.This
constructs will be seen as normal function calls by the front end of the
compiler, but will be inline expanded by the back-end.
You can add your own intrinsic macros to the system, allowing you to use the
power and speed of assembly language within the context of a more powerful
and
safer high level language.
I will present here two examples, to give you an idea of how this can look
like.
You will need the source code of lcc-win32, that can be obtained at the home
page:
http://ps.qss.cz/lcc or
ftp://ftp.cs.virginia.edu/pub/lcc-win32
Inlining the strlen function
----------------------------
Lets assume the strlen function of the C library is just to slow for you.
Instead of generating:
pushl Arg
call _strlen
addl $4,%esp
you would like to generate inline the following code:
; Inlined strlen. The input argument is in ECX and points to the
; character string
orl $-1,%eax
loop:
inc %eax
cmpb $0,(%ecx,%eax)
jnz loop
This function then, should be inlined by the compiler. The C interface would
be:
_strlen(str);
The prototype must be:
extern _stdcall _strlen(char *);
The compiler recognizes intrinsic macros because they have an underscore as
the
first character of their names, they are declared _stdcall, and they appear
in
the intrinsics table. Functions that begin with an underscore are few, and
this
avoids looking up the intrinsics table for each function call, what would
slow
down compilation speed.
You take then the file intrin.c, in the sources of lcc-win32 and modify the
intrinsics table. Its declaration is in the middle of the file, and looks
like
this:
static INTRINSICS intrinsicTable[] = {
{"_fsincos",2, 0, fsincos, NULL },
{"_bswap", 1, 0, bswap, bswapArgs },
... many declarations omitted ...
{"_reduceLtb",3, 0, redCmpLtb, paddArgs },
{"_mmxDotProduct",3,0, mmxDotProd, paddArgs },
{"_emms",0, 0, emms, NULL },
{NULL, 0, 0, 0, 0 }
};
You add before the last line, the following line:
{"_strlen",1, 0, strlenGen, strlenArgs },
telling the system that you want an intrinsic called _strlen, that takes one
argument, whose code will be generated by the function strlenGen(), and the
arguments assigned to their respective registers in the function
strlenArgs().
This functions should assign the registers in which you want the arguments
to
the inline macro, and generate the code for the body of the macro.
Basically,
this macros are seen as special calls by the compiler, that instead of
generating a push instruction, will call your <arguments> function, that
should
set the right fields in each node passed to it, to make later the code
generator
generate a move to the registers specified.
Note that all intrinsics should start with an underscore to avoid
conflicting
with user space names.
When a call to this function is detected by the compiler, you will first be
called when pushing the arguments at each call site. Here is the function
strlenArgs() then:
static Symbol strlenArgs(Node p)
{
Symbol r=NULL;
//The global ArgumentsIndex is zero before each call. The compiler
//takes care of that.
switch (ArgumentsIndex) {
case 0: // First argument pushed, from right to left!
if (p->x.nestedCall == 0) {
Symbol w;
r = SetRegister(p,intreg[ECX]);
}
break;
}
// We have seen another argument
ArgumentsIndex++;
// Assign the register to this expression.
if (p->x.nestedCall == 0 && r)
p->syms[2] = r;
// Should never be more than one arguments
if (ArgumentsIndex == 1)
ArgumentsIndex = 0;
return r;
}
You see that in several places we have the test:
if (p->x.nestedCall == 0)
This means that we should check if we have a nested call sequence within the
arguments, i.e. the following C expression:
strlen( SomeFunction() );
True, in the case of strlen this doesnt change anything important, the
result
of the function will be in EAX anyway. But suppose you defined a macro that
takes two arguments, say, some special form of addition sadd(a,b).
In this case we would assign the second argument (from left to right) to
ECX,
and the first to EAX. Consider then the case of:
sadd( SomeFunction(),5);
If we would just assign 5 to ECX, then the call to SomeFunction(), would
destroy the contents of ECX during the call!
This means that when the compiler detects a call within argument passing,
all
arguments WILL BE in the stack, and our code generating function should take
care of popping them into the right registers before proceeding.
In the case of strlen this can really hardly happen, but its important to
see
how this would work in the general case.
Note too that the argument function should increase the global argument
counter
for each argument, and reset it to zero when its done. Again, this is not
necessary for strlen, but for macros that take more arguments this should be
done imperatively.
The SetRegister function takes care of the details of assigning a register.
Here is its short body:
Symbol SetRegister(Node p,Symbol r)
{
Symbol w;
w = p->kids[0]->syms[2];
if (w->x.regnode == NULL || w->x.regnode->vbl == NULL)
p->kids[0]->syms[2] = r;
return r;
}
This function tests that in the given node, the left child isn't already
assigned to a register. It will assign the register only if this is not the
case. Otherwise, the compiler will generate the move.
We come now to the center of the routine: Generating code for the strlen
utility.
static Symbol strlenGen(Node p)
{
static int labelCount;
// OK, the first thing to do is to see if we should pop our arguments.
// If that is the case, pop them into the right registers.
if (p->x.nestedCall) {
print("\tpopl\t%%ecx\n");
}
/*
Here we generate the code for the strlen routine. Note that the % sign is
used
by the assembler of lcc-win32 to mark a register keyword, but our print()
function uses it too to mark (as printf) the beginning of an argument. We
must
double them to get around this collision.
1) Set the counter to minus one
*/
print("\torl\t$-1,%%eax\n");
/*
2) We should generate the label for this instance. All labels must be
unique,
and the easiest way to ensure that we always generate a new label is to
number
them consecutively using a counter. To avoid colliding with other labels, we
use a unique prefix too.
*/
print("_$strlen%d:\n",labelCount);
/*
3) Now we generate the code for the body of the loop searching for the
character zero.
*/
print("\tinc\t%%eax\n");
/* 4) Note the dollar before the immediate constant.*/
print("\tcmpb\t$0,(%%ecx,%%eax)\n");
/*
5) We generate the jump, incrementing our loop counter afterwards
*/
print("\tjnz\t_$strlen%d\n",labelCount++);
/*
Now we are done, the result is in eax, as it should. We finish our function.
Note that no pops are needed, since the ones we did at the beginning
(eventually) are just to compensate for the pushs the compiler generated.
Note too that we shouldn't insert a return statement since this is a macro
that shouldn't cause the current function to return!
*/
}
We compile the compiler, and we obtain a new compiler that will recognize
the
macro we have just created. Compiling the compiler with itself is a good
test
for your new function of course. This should be done at least three times to
be sure that your function is working OK.
Register assignments
--------------------
In general, you can use ECX, EDX, and EAX as you wish. The contents of EBX,
ESI, EBP and EDI should always be saved. If you destroy them unpredictable
results will surely occur.
Lets write a test function for our new compiler:
#include <stdio.h>
#ifdef MACRO
int _stdcall _strlen(char *);
#define strlen _strlen
#else
int strlen(char *);
#endif
int main(int argc, char *argv[])
{
if (argc > 1)
printf("Length of \"%s\" is %d\n", argv[1],
strlen(argv[1]));
return 0;
}
In the C source, we use the conditional MACRO to signify if we should use
our
macro, or just generate a call to the normal strlen procedure for comparison
purposes. We compile this with our new compiler, and add the S parameter to
see
what is generating.
lcc -S DMACRO tstrlen.c
The assembly (that the compiler writes in tstrlen.asm) is then:
_main:
pushl %ebp
movl %esp,%ebp
pushl %edi
.line 9
.line 10
cmpl $1,8(%ebp)
jle _$2
.line 11
movl 12(%ebp),%edi
; Our argument gets assigned to ECX, as our strlenArgs function
; defined
movl 4(%edi),%ecx
; Here is the begin of our macro body
orl $-1,%eax
; This is our generated label
_$strlen0:
inc %eax
cmpb $0,(%ecx,%eax)
jnz _$strlen0
; Our macro ends here, leaving its results in EAX
pushl %eax
movl 12(%ebp),%edi
pushl 4(%edi)
pushl $_$4
call _printf
addl $12,%esp
_$2:
.line 12
xor %eax,%eax
.line 13
popl %edi
popl %ebp
ret
We see that there is absolutely no call overhead. The arguments are assigned
to
the right registers in our function strlenArgs, and the body is expanded
in-line by strlenGen.
Next, we link our executable:
D:\lcc\src74\test>lcclnk tstrlen.obj
And we run a test:
D:\lcc\src74\test>tstrlen abcde
The length of "abcde" is 5
D:\lcc\src74\test>
Here is the strlenGen() function again for clarity.
static void strlenGen(Node p)
{
static int labelCount;
if (p->x.nestedCall) {
print("\tpopl\t%%ecx\n");
}
print("\torl\t$-1,%%eax\n");
print("_$strlen%d:\n",labelCount);
print("\tinc\t%%eax\n");
print("\tcmpb\t$0,(%%ecx,%%eax)\n");
print("\tjnz\t_$strlen%d\n",labelCount++);
}
Another example: inlining the strchr function
---------------------------------------------
To demonstrate a function with two arguments, we inline the strchr function.
This function should return a pointer to the first occurrence of the given
character in a string, or NULL, if the character doesnt appear in the
string.
The implementation could be like this :
_strchr:
movb (%eax),%dl // read a character
cmpb %cl,%dl // compare it to searched for char
je _strchrexit // exit if found with pointer to char as
result
incl %eax // move pointer to next char
orb %dl,%dl // test for end of string
jne strchr // if not zero continue loop
xorl %eax,%eax // Not found. Zero result
strchrexit :
We just scan the characters looking for either zero (end of the string) or
the
given char. The pointer to the string will be in EAX, and the character to
be
searched for will be in ECX. We use EDX as a scratch register.
The next step is then, to write the strchr function for assigning the
arguments.
Here it is :
static Symbol strchrArgs(Node p)
{
Symbol r=NULL;
switch (ArgumentsIndex) {
case 0: // First argument (from right to left) char to be searched.
// We put it in ECX
if (p->x.nestedCall == 0) {
r = SetRegister(p,intreg[ECX]);
}
break;
case 1: // Second argument: pointer to the string. We put it in EAX
if (p->x.nestedCall == 0) {
r = SetRegister(p,intreg[EAX]);
}
break;
}
ArgumentsIndex++;
if (p->x.nestedCall == 0)
p->syms[2] = r;
if (ArgumentsIndex == 2)
ArgumentsIndex = 0;
return r;
}
The next step is finally to write the generating function. Here it is; note
that we need two labels:
static void strchrGen(Node p)
{
static int labelCount;
if (p->x.nestedCall) {
print("\tpopl\t%%ecx\n");
}
print("_$strchr%d:\n",labelCount);
print("\tmovb\t(%%eax),%%dl\n");
print("\tcmpb\t%%cl,%%dl\n");
print("\tje\t_$strchr%d\n",labelCount+1);
print("\tinc\t%%eax\n");
print("\torb\t%%dl,%%dl\n");
print("\tjne\t_$strchr%d\n",labelCount);
print("\txorl\t%%eax,%%eax\n");
print("_$strchr%d:\n",labelCount+1);
labelCount += 2;
}
This facility is not very common in a compiler system, and it allows you to
use assembly language in the routines that are *really* needed in a software
system, leaving to the compiler the tedious work of generating the assembly
for you in the 90% of the code where speed is not so important after all.
Another benefit is that you can't do simple mistakes when passing arguments
to your assembler macros since they are understood as function calls by the
compiler, and all prototype checking is done by the front end. If you
attempt
to use the strchr macro like this:
strchr('\n",string);
the compiler will issue an error.
The lcc-win32 system can be downloaded free of charge from
http://ps.qss.cz/lcc
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
Accessing COM Objects from
Assembly
by Ernest Murphy
Abstract
--------
The COM (Component Object Model) is used by the Windows Operation system in
increasing ways. For example, the shell.dll uses COM to access some of its
API
methods. The IShellLink and IPersistFile interfaces of the shell32.dll will
be
demonstrated to create a shortcut shell link. A basic understanding of COM
is
assumed. The code sample included is MASM specific.
Introduction
------------
COM may seem complicated with its numerous details, but in use these
complications disappear into simple function calls. The hardest part is
understanding the data structures involved so you can define the
interfaces.
I apologize for all the C++ terminology used in here. While COM is
implementation neutral, it borrows much terminology from C++ to define
itself.
In order to use the COM methods of some object, you must first instance or
create that object from its coclass, then ask it to return you a pointer to
it's interface. This process is performed by the API function
CoCreateInstance.
When you are done with the interface you call it's Release method, and COM
and
the coclass will take care of unloading the coclass.
Assessing COM Methods
---------------------
To use COM methods you need to know before hand what the interface looks
like. Even if you "late bind" through an IDispatch interface, you still need
to know what IDispatch looks like.
An COM interface is just table of pointers to functions. Let's start with
the IUnknown interface. If you were to create a component that simply
exports
the IUnknown interface, you have a fully functional COM object (albeit on
the
level of "Hello World"). IUnknown has the 3 basic methods of every
interface,
since all interfaces inherit from IUnknown. Keep in mind all an interface
consists of is a structure of function pointers. For IUnknown, it looks like
this:
IUnknown STRUCT DWORD
; IUnknown methods
QueryInterface IUnknown_QueryInterface
?
AddRef IUnknown_AddRef
?
Release IUnknown_Release
?
IUnknown ENDS
That's it, just 12 bytes long. It holds 3 DWORD pointers to the procedures
that actually implement the methods. It is the infamous "vtable" you may
have
heard of. The pointers are defined as such so we can have MASM do some type
checking for us when compiling our calls.
Since the vtable holds the addresses of functions, or pointers, these
pointers
are typedefed in our interface definition as such:
IUnknown_QueryInterface typedef ptr
IUnknown_QueryInterfaceProto
IUnknown_AddRef typedef ptr IUnknown_AddRefProto
IUnknown_Release typedef ptr IUnknown_ReleaseProto
Finally, we define the function prototypes as follows:
IUnknown_QueryInterfaceProto typedef PROTO :DWORD, :DWORD, :DWORD
IUnknown_AddRefProto typedef PROTO :DWORD
IUnknown_ReleaseProto typedef PROTO :DWORD
In keeping with the MASM32 practice of "loose" type checking, function
parameters are just defined as DWORDs. Lots of work to set things up, but it
does keeps lots of errors confined to compile time, not run time. In
practice,
you can wrap up your interface definitions in include files and keep them
from cluttering up your source code.
One rather big compilation on defining an interface: MASM cannot resolve
forward references like this, so we have to define them backwards, by
defining
the function prototype typedefs first, and the interface table last. The
sample
program later on defines the interfaces this way.
To actually use an interface, you need a pointer to it. The
CoCreateInstance
API can be used to return us this indirect pointer to an interface
structure.
It is one level removed from the vtable itself, and actually points to the
"object" that holds the interface. (This would be clearer had I been
creating
the interface instead of using one. Please wait for a future article for
that).
The place this pointer points to in the object points to the interface
structure. Thus, this pointer is generically named "ppv", for "pointer to
pointer to (void)," where (void) means an unspecified type.
For example, say we used CoCreateInstance and successfully got an
interface
pointer ppv, and wanted to see if it supports some other interface. We can
call
its QueryInterface method and request a new ppv to the other interface we
are
interested in. Such a call would look like this:
mov eax, ppv ; get pointer to the object
mov edx, [eax] ; and use it to find the interface structure
; and then call that method
invoke (IUnknown PTR [edx]).QueryInterface, ppv,
ADDR IID_SomeOtherInterface, ADDR ppv_new
I hope you find this as wonderfully simple as I do. IID_SomeOtherInterface
holds the GUID of the interface we desire, and ppv_new is a new pointer we
can
use to access it. Also note we must pass in the pointer we used, this lets
the
interface know which object (literally "this" object) we are using.
Incidentally, in a previous APJ article on COM, there was an error in how
a
COM interface is invoked. THIS was left out of the COM call. The program
seemed
to work, because the COM invoke was invoked from the main code, not from a
procedure, and did not require a return call before calling ExitProcess. Had
this COM invoke been done from a procedure, a stack error crash would have
resulted.
Note the register must be type cast (IUnknown PTR [edx]). This lets
the compiler know what structure to use to get the correct offset in the
vtable
for the .QueryInterface function (in this case it means an offset of zero
from
edx). Actually, the information contained by the interface name and function
name called disappear at compile time, all that is left is a numeric offset
from an as of yet value unspecified pointer.
We can simplify a COM invoke further with a macro:
coinvoke MACRO pInterface:REQ, Interface:REQ, Function:REQ, args:VARARG
LOCAL istatement, arg
;; invokes an arbitrary COM interface
;; pInterface pointer to a specific interface instance
;; Interface the Interface's struct typedef
;; Function which function or method of the interface to
perform
;; args all required arguments
;; (type, kind and count determined by the
function)
istatement TEXTEQU <invoke (Interface PTR[eax]).&Function,
pInterface>
FOR arg, <args>
; build the list of parameter arguments
istatement CATSTR istatement, <, >, <&arg>
ENDM
mov eax, pInterface
mov eax, [eax]
istatement
ENDM
Thus, the same QueryInterface method as before can be invoked in a single
line:
coinvoke ppv ,IUnknown, QueryInterface,
ADDR IID_SomeOtherInterface, ADDR ppnew
The return parameter for every COM call is an hResult, a 4 byte return
value
in eax. It is used to signal success or failure. Since the most significant
digit is used to indicate failure, you can test the result with a simple:
.IF !SIGN?
; function passed
.ELSE
; function failed
.ENDIF
Again, this can be simplified with some more simple macros:
SUCCEEDED TEXTEQU <!!SIGN?>
FAILED TEXTEQU <!!SUCCEEDED>
(The not ! sign must be doubled since that symbol has special meaning in
MASM macros)
That's about all you need to fully invoke and use interfaces from COM
objects
from assembly. These techniques work with any COM or activeX object.
Back to the Real Word: Using IShellFile and IPersistFile from shell32.dll
-------------------------------------------------------------------------
The shell32.dll provides a simple, easy way to make shell links (shortcuts).
However, it uses a COM interface to provide this service. The sample below
is
based on the MSDN "Shell Links" section for "Internet Tools and
Technologies."
This may be a strange place to find documentation, but there it is.
The "Shell Links" article may be found at
http://msdn.microsoft.com/library/psdk/shellcc/shell/Shortcut.htm
For this tutorial we will access the following members of the IShellLink and
the IPersistFile interfaces. Note every interface includes a "ppi" interface
parameter, this is the interface that we calling to (it is the THIS
parameter).
(The following interface information is a copy of information published
by Microsoft)
IShellLink::QueryInterface, ppi, ADDR riid, ADDR ppv
* riid: The identifier of the interface requested. To get access to the
* ppv: The pointer to the variable that receives the interface.
Description: Checks if the object also supports the requested interface. If
so,
assigns the ppv pointer with the interface's pointer.
IShellLink::Release, ppi
Description: Decrements the reference count on the IShellLink interface.
IShellLink:: SetPath, ppi, ADDR szFile
* pszFile: A pointer to a text buffer containing the new path for the shell
link object.
Description: Defines where the file the shell link points to.
IShellLink::SetIconLocation, ppi, ADDR szIconPath, iIcon
* pszIconPath: A pointer to a text buffer containing the new icon path.
* iIcon: An index to the icon. This index is zero based.
Description: Sets which icon the shelllink will use.
IPersistFile::Save, ppi, ADDR szFileName, fRemember
* pszFileName: Points to a zero-terminated string containing the absolute
path
of the file to which the object should be saved.
* fRemember: Indicates whether the pszFileName parameter is to be used as
the
current working file. If TRUE, pszFileName becomes the current file and the
object should clear its dirty flag after the save. If FALSE, this save
operation is a "Save A Copy As ..." operation. In this case, the current
file
is unchanged and the object should not clear its dirty flag. If pszFileName
is
NULL, the implementation should ignore the fRemember flag.
Description: Perform a save operation for the ShellLink object, or saves the
shell link are creating.
IPersistFile::Release, ppi
Description: Decrements the reference count on the IPersistFile interface.
These interfaces contain many many more methods (see the full interface
definitions in the code below), but we only need concentrate on those we
will
actually be using.
A shell link is the MS-speak name for a shortcut icon. The information
contained in a link (.lnk) file is:
1 - The file path and name of the program to shell.
2 - Where to obtain the icon to display for the shortcut (usually from
the
executable itself), and which icon in that file to use. We will
use
the first icon in the file
3 - A file path and name where the shortcut should be stored.
The use of these interfaces is simple and straightforward. It goes like
this:
* Call CoCreateInstance CLSID_ShellLink for a IID_IShellLink interface
* Queryinterface IShellLink for an IID_IPersistFile interface.
* Call IShellLink.SetPath to specify where the shortcut target is
* Call IShellLink.SetIconLocation to specify which icon to use
* Call IPersistFile.Save to save our new shortcut .lnk file.
* Call IPersistFile.Release
* Call IShellLink.Release
The last two steps will releases our hold on these interfaces, which will
automatically lead to the dll that supplied them being unloaded.
Again, the hard part in this application was finding documentation. What
finally found broke the search open was using Visual Studio "Search in
Files"
to find "IShellLink" and " IPersistFile" in the /include area of MSVC. This
lead me to various .h files, from which I hand translated the interfaces
from C
to MASM.
Another handy tool I could have used is the command line app
"FindGUID.exe,"
which looks through the registry for a specific interface name or coclass,
or
will output a list of every class and interface with their associated GUIDs.
Finally, the OLEView.exe application will let you browse the registry type
libraries and mine them for information. However, these tools come with MSVC
and are proprietary.
Take care when defining an interface. Missing vtable methods lead to
strange
results. Essentially COM calls, on one level, amount to "perform function
(number)" calls. Leave a method out of the vtable definition and you call
the
wrong interface. The original IShellLink interface definition I used from a
inc
file I downloaded had a missing function. The calls I made generated a
"SUCEEDED" hResult, but in some cases would not properly clean the stack
(since
my push count did not match the invoked function's pop count), thus lead to
a
GPF as I exited a procedure. Keep this in mind if you ever get similar
"weird" results.
MakeLink.asm, a demonstration of COM
------------------------------------
This program does very little, as all good tutorial programs should. When
run, it creates a shortcut to itself, in the same directory. It can be
amusing
to run from file explorer and watch the shortcut appear. Then you can try
the
shortcut and watch it's creation time change.
The shell link tutorial code follows. It begins with some "hack code" to
get the full file name path of the executable, and also makes a string with
the same path that changes the file to "Shortcut To ShellLink.lnk" These
strings are passed to the shell link interface, and it is saved (or
persisted in COM-speak).
The CoCreateLink procedure used to actually perform the COM methods and
perform this link creation has been kept as general as possible, and may
have reuse possibilities in other applications.
;---------------------------------------------------------------------
; MakeLink.asm ActiveX simple client to demonstrate basic concepts
; written & (c) copyright April 5, 2000 by Ernest Murphy
;
; contact the author at
ernie@...
;
; may be reused for any educational or
; non-commercial application without further license
;---------------------------------------------------------------------
.386
.model flat, stdcall
option casemap:none
include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\ole32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\ole32.lib
;---------------------------------------------------------------------
CoCreateLink PROTO :DWORD, :DWORD
;---------------------------------------------------------------------
; Interface definitions
; IUnknown Interface
IUnknown_QueryInterfaceProto typedef PROTO :DWORD, :DWORD, :DWORD
IUnknown_AddRefProto typedef PROTO :DWORD
IUnknown_ReleaseProto typedef PROTO :DWORD
IUnknown_QueryInterface typedef ptr
IUnknown_QueryInterfaceProto
IUnknown_AddRef typedef ptr IUnknown_AddRefProto
IUnknown_Release typedef ptr IUnknown_ReleaseProto
IUnknown STRUCT DWORD
; IUnknown methods
QueryInterface IUnknown_QueryInterface
?
AddRef IUnknown_AddRef
?
Release IUnknown_Release
?
IUnknown ENDS
; IShellLink Interface
IShellLink_IShellLink_GetPathProto typedef PROTO :DWORD, :DWORD, :DWORD,
:DWORD, :DWORD
IShellLink_GetIDListProto typedef PROTO :DWORD, :DWORD
IShellLink_SetIDListProto typedef PROTO :DWORD, :DWORD
IShellLink_GetDescriptionProto typedef PROTO :DWORD, :DWORD, :DWORD
IShellLink_SetDescriptionProto typedef PROTO :DWORD, :DWORD
IShellLink_GetWorkingDirectoryProto typedef PROTO :DWORD, :DWORD, :DWORD
IShellLink_SetWorkingDirectoryProto typedef PROTO :DWORD, :DWORD
IShellLink_GetArgumentsProto typedef PROTO :DWORD, :DWORD, :DWORD
IShellLink_SetArgumentsProto typedef PROTO :DWORD, :DWORD
IShellLink_GetHotkeyProto typedef PROTO :DWORD, :DWORD
IShellLink_SetHotkeyProto typedef PROTO :DWORD, :WORD
IShellLink_GetShowCmdProto typedef PROTO :DWORD, :DWORD
IShellLink_SetShowCmdProto typedef PROTO :DWORD, :DWORD
IShellLink_GetIconLocationProto typedef PROTO :DWORD, :DWORD, :DWORD,
:DWORD
IShellLink_SetIconLocationProto typedef PROTO :DWORD, :DWORD, :DWORD
IShellLink_SetRelativePathProto typedef PROTO :DWORD, :DWORD, :DWORD
IShellLink_ResolveProto typedef PROTO :DWORD, :DWORD, :DWORD
IShellLink_SetPathProto typedef PROTO :DWORD, :DWORD
IShellLink_GetPath typedef ptr
IShellLink_IShellLink_GetPathProto
IShellLink_GetIDList typedef ptr IShellLink_GetIDListProto
IShellLink_SetIDList typedef ptr IShellLink_SetIDListProto
IShellLink_GetDescription typedef ptr IShellLink_GetDescriptionProto
IShellLink_SetDescription typedef ptr IShellLink_SetDescriptionProto
IShellLink_GetWorkingDirectory typedef ptr
IShellLink_GetWorkingDirectoryProto
IShellLink_SetWorkingDirectory typedef ptr
IShellLink_SetWorkingDirectoryProto
IShellLink_GetArguments typedef ptr IShellLink_GetArgumentsProto
IShellLink_SetArguments typedef ptr IShellLink_SetArgumentsProto
IShellLink_GetHotkey typedef ptr IShellLink_GetHotkeyProto
IShellLink_SetHotkey typedef ptr IShellLink_SetHotkeyProto
IShellLink_GetShowCmd typedef ptr IShellLink_GetShowCmdProto
IShellLink_SetShowCmd typedef ptr IShellLink_SetShowCmdProto
IShellLink_GetIconLocation typedef ptr IShellLink_GetIconLocationProto
IShellLink_SetIconLocation typedef ptr IShellLink_SetIconLocationProto
IShellLink_SetRelativePath typedef ptr IShellLink_SetRelativePathProto
IShellLink_Resolve typedef ptr IShellLink_ResolveProto
IShellLink_SetPath typedef ptr IShellLink_SetPathProto
IShellLink STRUCT DWORD
QueryInterface IUnknown_QueryInterface
?
AddRef IUnknown_AddRef
?
Release IUnknown_Release
?
GetPath IShellLink_GetPath
?
GetIDList IShellLink_GetIDList
?
SetIDList IShellLink_SetIDList
?
GetDescription IShellLink_GetDescription
?
SetDescription IShellLink_SetDescription
?
GetWorkingDirectory IShellLink_GetWorkingDirectory
?
SetWorkingDirectory IShellLink_SetWorkingDirectory
?
GetArguments IShellLink_GetArguments
?
SetArguments IShellLink_SetArguments
?
GetHotkey IShellLink_GetHotkey
?
SetHotkey IShellLink_SetHotkey
?
GetShowCmd IShellLink_GetShowCmd
?
SetShowCmd IShellLink_SetShowCmd
?
GetIconLocation IShellLink_GetIconLocation
?
SetIconLocation IShellLink_SetIconLocation
?
SetRelativePath IShellLink_SetRelativePath
?
Resolve IShellLink_Resolve
?
SetPath IShellLink_SetPath
?
IShellLink ENDS
; IPersistFile Interface
IPersistFile_GetClassIDProto typedef PROTO :DWORD, :DWORD
IPersistFile_IsDirtyProto typedef PROTO :DWORD
IPersistFile_LoadProto typedef PROTO :DWORD, :DWORD, :DWORD
IPersistFile_SaveProto typedef PROTO :DWORD, :DWORD, :DWORD
IPersistFile_SaveCompletedProto typedef PROTO :DWORD, :DWORD
IPersistFile_GetCurFileProto typedef PROTO :DWORD, :DWORD
IPersistFile_GetClassID typedef ptr IPersistFile_GetClassIDProto
IPersistFile_IsDirty typedef ptr IPersistFile_IsDirtyProto
IPersistFile_Load typedef ptr IPersistFile_LoadProto
IPersistFile_Save typedef ptr IPersistFile_SaveProto
IPersistFile_SaveCompleted typedef ptr
IPersistFile_SaveCompletedProto
IPersistFile_GetCurFile typedef ptr IPersistFile_GetCurFileProto
IPersistFile STRUCT DWORD
QueryInterface IUnknown_QueryInterface ?
AddRef IUnknown_AddRef ?
Release IUnknown_Release ?
GetClassID IPersistFile_GetClassID ?
IsDirty IPersistFile_IsDirty ?
Load IPersistFile_Load ?
Save IPersistFile_Save ?
SaveCompleted IPersistFile_SaveCompleted ?
GetCurFile IPersistFile_GetCurFile ?
IPersistFile ENDS
;---------------------------------------------------------------------
coinvoke MACRO pInterface:REQ, Interface:REQ, Function:REQ, args:VARARG
LOCAL istatement, arg
;; invokes an arbitrary COM interface
;; pInterface pointer to a specific interface instance
;; Interface the Interface's struct typedef
;; Function which function or method of the interface to perform
;; args all required arguments
;; (type, kind and count determined by the function)
istatement TEXTEQU <invoke (Interface PTR[eax]).&Function, pInterface>
FOR arg, <args>
; build the list of parameter arguments
istatement CATSTR istatement, <, >, <&arg>
ENDM
mov eax, pInterface
mov eax, [eax]
istatement
ENDM
; equate primitives
SUCEEDED TEXTEQU <!!SIGN?>
FAILED TEXTEQU <!!SUCEEDED>
MakeMessage MACRO Text:REQ
; macro to display a message box
; the text to display is kept local to
; this routine for ease of use
LOCAL lbl
LOCAL sztext
jmp lbl
sztext:
db Text,0
lbl:
invoke MessageBox,NULL,sztext,ADDR szAppName,MB_OK
ENDM
;---------------------------------------------------------------------
.data
szAppName BYTE "Shell Link Maker", 0
szLinkName BYTE "Shortcut to MakeLink.lnk", 0
szBKSlash BYTE "\", 0
hInstance HINSTANCE ?
Pos DWORD ?
szBuffer1 BYTE MAX_PATH DUP(?)
szBuffer2 BYTE MAX_PATH DUP(?)
;-----------------------------------------------------------------------
.code
start:
;---------------------------------------------
; this bracketed code is just a 'quick hack'
; to replace the filename from the filepathname
; with the 'Shortcut to' title
;
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke GetModuleFileName, NULL, ADDR szBuffer1, MAX_PATH
invoke lstrcpy, ADDR szBuffer2, ADDR szBuffer1
; Find the last backslash '\' and change it to zero
mov edx, OFFSET szBuffer2
mov ecx, edx
.REPEAT
mov al, BYTE PTR [edx]
.IF al == 92 ; "\"
mov ecx, edx
.ENDIF
inc edx
.UNTIL al == 0
mov BYTE PTR [ecx+1], 0
invoke lstrcpy, ADDR szBuffer2, ADDR szLinkName
;----------------------------------------------
; here is where we call the proc with the COM methods
invoke CoInitialize, NULL
MakeMessage "Let's try our Createlink."
invoke CoCreateLink, ADDR szBuffer1, ADDR szBuffer2
MakeMessage "That's all folks !!!"
invoke CoUninitialize
invoke ExitProcess, NULL
;-----------------------------------------------------------------------
CoCreateLink PROC pszPathObj:DWORD, pszPathLink:DWORD
; CreateLink - uses the shell's IShellLink and IPersistFile interfaces
; to create and store a shortcut to the specified object.
; Returns the hresult of calling the member functions of the interfaces.
; pszPathObj - address of a buffer containing the path of the object.
; pszPathLink - address of a buffer containing the path where the
; shell link is to be stored.
; adapted from MSDN article "Shell Links"
; deleted useless "description" method
; added set icon location method
LOCAL pwsz :DWORD
LOCAL psl :DWORD
LOCAL ppsl :DWORD
LOCAL ppf :DWORD
LOCAL pppf :DWORD
LOCAL hResult :DWORD
LOCAL hHeap :DWORD
.data
CLSID_ShellLink GUID <0021401H, 0000H, 0000H, \
<0C0H, 00H, 00H, 00H, 00H, 00H, 00H, 046H>>
IID_IShellLink GUID <00214EEH, 0000H, 0000H, \
<0C0H, 00H, 00H, 00H, 00H, 00H, 00H, 046H>>
IID_IPersistFile GUID <000010BH, 0000H, 0000H, \
<0C0H, 00H, 00H, 00H, 00H, 00H, 00H, 046H>>
.code
; first, get some heap for a wide buffer
invoke GetProcessHeap
mov hHeap, eax
invoke HeapAlloc, hHeap, NULL, MAX_PATH * 2
mov pwsz, eax
; and set up some local pointers (we can't use ADDR on local vars)
lea eax, psl
mov ppsl, eax
lea eax, ppf
mov pppf, eax
; Get a pointer to the IShellLink interface.
invoke CoCreateInstance, ADDR CLSID_ShellLink, NULL,
CLSCTX_INPROC_SERVER,
ADDR IID_IShellLink, ppsl
mov hResult, eax
test eax, eax
.IF SUCEEDED
; Query IShellLink for the IPersistFile
; interface for saving the shortcut
coinvoke psl, IShellLink, QueryInterface, ADDR IID_IPersistFile,
pppf
mov hResult, eax
test eax, eax
.IF SUCEEDED
; Set the path to the shortcut target
coinvoke psl, IShellLink, SetPath, pszPathObj
mov hResult, eax
; add the description.
coinvoke psl, IShellLink, SetIconLocation, pszPathObj, 0
; use first icon found
mov hResult, eax
; change string to Unicode. (COM typically expects Unicode
strings)
invoke MultiByteToWideChar, CP_ACP, 0, pszPathLink, -1, pwsz,
MAX_PATH
; Save the link by calling IPersistFile::Save
coinvoke ppf, IPersistFile, Save, pwsz, TRUE
mov eax, hResult
; release the IPersistFile ppf pointer
coinvoke ppf, IPersistFile, Release
mov hResult, eax
.ENDIF
; release the IShellLink psl pointer
coinvoke psl, IShellLink, Release
mov hResult, eax
.ENDIF
; free our heap space
invoke HeapFree, hHeap, NULL, pwsz
mov eax, hResult ; since we reuse this variable over and over,
; it contains the last operations result
ret
CoCreateLink ENDP
;-----------------------------------------------------------
end start
;-----------------------------------------------------------------------
Bibliography:
-------------
"Inside COM, Microsoft's Component Object Model" Dale Rogerson
Copyright 1997, Paperback - 376 pages CD-ROM edition
Microsoft Press; ISBN: 1572313498
(THE fundamental book on understanding how COM works on a fundamental level.
Uses C++ code to illustrate basic concepts as it builds simple fully
functional COM object)
"Automation Programmer's Reference : Using ActiveX Technology to Create
Programmable Applications" (no author listed)
Copyright 1997, Paperback - 450 pages
Microsoft Press; ISBN: 1572315849
(This book has been available online on MSDN in the past, but it is cheap
enough for those of you who prefer real books you can hold in your hand.
Defines the practical interfaces and functions that the automation libraries
provide you, but is more of a reference book then a "user's guide")
Microsoft Developers Network
http://msdn.microsoft.com
"Professional Visual C++ 5 ActiveX/Com Control Programming" Sing Li
and Panos Economopoulos
Copyright April 1997, Paperback - 500 pages (no CD, files available
online)
Wrox Press Inc; ISBN: 1861000375
(Excellent description of activeX control and control site interfaces.
A recent review of this book on Amazon.com stated "These guys are the
type that want to rewrite the world's entire software base in
assembler." Need I say more?)
"sean's inconsequential homepage"
http://www.eburg.com/~baxters/
Various hardcore articles on low-level COM and ATL techniques. Coded in C++
"Using COM in Assembly Language" Bill Tyler
Assembly Language Journal, Apr-June 99
Mr Tyler keeps a web site at:
http://thunder.prohosting.com/~asm1/
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::...........................................FEATURE.ARTICLE
64-bit Integer/ASCII
Conversion
by X-Calibre
The following routines provide an assembly-language library for converting
64-bit integers to and from ASCII, such as would be required when preparing
user-supplied data for qword arithmetic or FPU instructions. The library
consists of the routines ParseRadixSigned, ParseRadixUnsigned,
PrintRadixSigned, and PrintRadixUnsigned, and the macro Divide64. Wrappers
for
calling the routines from C code have also been provided.
ParseRadix
----------
ParseRadix is a pair of routines for converting an ASCII string to a signed
or
unsigned 64-bit integer, using a given radix as a base. The routines take a
pointer to a string and an integer radix as input, and return a 64-bit
number.
;-------------------------------------------------------------------------
ParseRadixUnsigned PROC
; Input: Pointer to zero-terminated string in ESI, radix in EDI
; Output: Parsed number in EDX::EAX
; Uses: EAX, EBX, ECX, EDX, ESI, EDI
xor ebx, ebx
; result in EDX::EAX
xor eax, eax
xor edx, edx
mov al, [esi]
inc esi
test eax, eax
jz @@endOfParsing
sub eax, 30h
.IF eax > 9
sub eax, 7
.ENDIF
mov bl, [esi]
@@smallParseLoop:
; ASCII to number conversion
sub ebx, 30h
inc esi
mul edi
.IF ebx > 9
sub ebx, 7
.ENDIF
add eax, ebx
mov bl, [esi]
jc @@carry
test ebx, ebx
jnz @@smallParseLoop
ret
@@carry:
inc edx
test ebx, ebx
jz @@endOfParsing
@@bigParseLoop:
; ASCII to number conversion
mov ecx, eax
mov eax, edx
sub ebx, 30h
inc esi
mul edi
xchg eax, ecx
mul edi
.IF ebx > 9
sub ebx, 7
.ENDIF
add eax, ebx
mov bl, [esi]
adc edx, ecx
test ebx, ebx
jnz @@bigParseLoop
@@endOfParsing:
ret
ParseRadixUnsigned ENDP
ParseRadixSigned PROC
; Input: Pointer to zero-terminated string in ESI, radix in EDI
; Output: Parsed number in EDX::EAX
; Uses: EAX, EBX, ECX, EDX, ESI, EDI
.code
; If string does not start with a '-', consider it positive
cmp byte ptr [esi], '-'
jne ParseRadixUnsigned
; Number is negative, first parse the absolute value
inc esi
call ParseRadixUnsigned
; Now negate the absolute value to get the negative result
neg edx
neg eax
sbb edx, 0
ret
ParseRadixSigned ENDP
;-------------------------------------------------------------------------
The following is a wrapper used for calling the ParseRadix routines from C.
The wrapper provides the following C functions:
extern unsigned __int64 __stdcall
ParseRadixUnsignedC(char *lpBuffer, unsigned int radix);
extern signed __int64 __stdcall
ParseRadixSignedC(char *lpBuffer, unsigned int radix);
;-------------------------------------------------------------------------
.386
.Model Flat, StdCall
.code
include ParseRadix.asm
ParseRadixUnsignedC PROC lpBuffer:PTR BYTE, radix:DWORD
push esi
mov esi, [lpBuffer]
push edi
mov edi, [radix]
push ebx
call ParseRadixUnsigned
pop ebx
pop edi
pop esi
ret
ParseRadixUnsignedC ENDP
ParseRadixSignedC PROC lpBuffer:PTR BYTE, radix:DWORD
push esi
mov esi, [lpBuffer]
push edi
mov edi, [radix]
push ebx
call ParseRadixSigned
pop ebx
pop edi
pop esi
ret
ParseRadixSignedC ENDP
END
;-------------------------------------------------------------------------
Divide64
--------
Divide64 is a macro for doing 64-bit division using 32-bit integer
instructions.
Note that this is a 'long division' algorithm. It can easily be expanded to
be able to divide any number by 32 bits. I only use it for 64 bits here to
keep the CPU from getting an exception on overflow when the input is larger
than ((2^32)-1)*divisor, so that printing any 64 bit number with any radix
is possible.
;-------------------------------------------------------------------------
Divide64 MACRO
; Input: 64 bit dividend in EBX::ECX, 32 bit divisor in ESI
; Output: 64 bit result in EBX::EAX, 32 bit remainder in EDX
; Uses: EAX, EBX, ECX, EDX, ESI
; Divide high dword by divisor.
mov eax, ebx
xor edx, edx
div esi
; Put remainder as high dword of the original dividend.
mov ebx, eax
mov eax, ecx
div esi
ENDM
;-------------------------------------------------------------------------
PrintRadix
----------
PrintRadix is a pair of routines for converting signed and unsigned 64-bit
numbers to an ASCII, string, using a given radix as base. These routines
take a
64-bit number and an integer radix as inpit, and return the pointer to a
character buffer.
;-------------------------------------------------------------------------
PrintRadixUnsigned PROC
; Input: 64 bit unsigned number in EBX::ECX, radix in ESI, pointer to
output
; buffer in EDI
; Output: Zero-terminated ASCII string in output buffer, length of string in
; EAX
; Uses: EAX, EBX, ECX, EDX, ESI, EDI, EBP
xor ebp, ebp ; StringLength counter
; If the high dword of the number is larger than the divisor, we
; have to do a 'long division' to prevent overflow.
cmp ebx, esi
jb smallDiv
longDiv:
Divide64
; Convert the remainder to an ASCII char.
add edx, 30h
dec esp
.IF edx > 39h
add edx, 7
.ENDIF
; Store char on stack.
inc ebp
; While result is not 0, we loop.
test eax, eax
mov ecx, eax
mov [esp], dl
jz lowDWORDIsZero
cmp ebx, esi
jae longDiv
smallDiv:
; Set EBX::ECX to EDX::EAX for a normal 64->32 division.
mov edx, ebx
mov eax, ecx
radixLoopSmall:
div esi
; Convert the remainder to an ASCII char.
add edx, 30h
dec esp
.IF edx > 39h
add edx, 7
.ENDIF
; Store char on stack.
inc ebp
mov [esp], dl
; Clean out high dword for next division.
xor edx, edx
; While result is not 0, we loop.
test eax, eax
jnz radixLoopSmall
toBuffer:
mov eax, ebp ; Return stringlength (not including 0-terminator)
toBufferLoop:
; Copy the string from stack to the destination buffer.
inc edi
mov dl, [esp]
inc esp
dec ebp
mov [edi-1], dl
jnz toBufferLoop
; Zero terminate the string.
mov byte ptr [edi], 0
ret
lowDWORDIsZero:
test ebx, ebx
jnz longDiv
; We have the final string, time to copy it to the destination buffer.
jmp toBuffer
PrintRadixUnsigned ENDP
PrintRadixSigned PROC
; Input: 64 bit signed number in EBX::ECX, radix in ESI, pointer to output
; buffer in EDI
; Output: Zero-terminated ASCII string in output buffer, length of string in
; EAX
; Uses: EAX, EBX, ECX, EDX, ESI, EDI, EBP
; If number is non-negative, use the normal PrintRadix
test ebx, ebx
jns PrintRadixUnsigned
; Prefix the number with a - sign
mov byte ptr [edi], '-'
inc edi
; Negate the 64 bit number
neg ebx
neg ecx
sbb ebx, 0
; Do a normal PrintRadix
call PrintRadixUnsigned
inc eax
ret
PrintRadixSigned ENDP
;-------------------------------------------------------------------------
The following is a wrapper used for calling the PrintRadix routines from C.
The wrapper provides the following C functions:
extern unsigned int __stdcall
PrintRadixUnsignedC(char *lpBuffer, unsigned __int64 number,
unsigned int radix);
extern unsigned int __stdcall
PrintRadixSignedC(char *lpBuffer, signed __int64 number,
unsigned int radix);
;-------------------------------------------------------------------------
.386
.Model Flat, StdCall
.code
include PrintRadix.asm
PrintRadixUnsignedC PROC lpBuffer:PTR BYTE, number:QWORD, radix:DWORD
push ebp
mov ecx, dword ptr [number]
push ebx
mov ebx, dword ptr [number+sizeof DWORD]
push esi
mov esi, [radix]
push edi
mov edi, [lpBuffer]
call PrintRadixUnsigned
pop edi
pop esi
pop ebx
pop ebp
ret
PrintRadixUnsignedC ENDP
PrintRadixSignedC PROC lpBuffer:PTR BYTE, number:QWORD, radix:DWORD
push ebp
mov ecx, dword ptr [number]
push ebx
mov ebx, dword ptr [number+sizeof DWORD]
push esi
mov esi, [radix]
push edi
mov edi, [lpBuffer]
call PrintRadixSigned
pop edi
pop esi
pop ebx
pop ebp
ret
PrintRadixSignedC ENDP
END
;-------------------------------------------------------------------------
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::................................WIN32.ASSEMBLY.PROGRAMMING
Win32 AppFatalExit
Skeleton
by Chili
This is just a Win32 application skeleton with a small procedure that
manages
fatal errors, by displaying an information message box and terminating
the
process.
I think the code is pretty much self explanatory and I commented it to
some
degree, so there's not much to say. To close the black window just hit
ESCAPE.
The only one thing that isn't that quite right is the fact that you have
to
code the line numbers by hand and so if you change anything above
previously
coded numbers, you'll have to do them again... oh well!
To assemble get the MASM32 package from:
http://www.pbq.com.au/home/hutch/
--8<---------------------------------------------------------------------------
; SKELETON.ASM
; Win32 AppFatalExit Skeleton
; by Chili for APJ #8
; August 11, 2000
;##############################################################################
; Compiler Options
;##############################################################################
title Win32 AppFatalExit Skeleton
.386
.model flat, stdcall ; 32-bit memory model
option casemap :none ; case sensitive
;##############################################################################
; Includes
;##############################################################################
;// Include Files
include \masm32\include\windows.inc
include \masm32\include\gdi32.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\comctl32.inc
include \masm32\include\comdlg32.inc
include \masm32\include\shell32.inc
;// Libraries
includelib \masm32\lib\gdi32.lib
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\comctl32.lib
includelib \masm32\lib\comdlg32.lib
includelib \masm32\lib\shell32.lib
;##############################################################################
; Equates
;##############################################################################
;// Basic
NULL equ 0
FALSE equ 0
TRUE equ 1
;##############################################################################
; Local Prototypes
;##############################################################################
;// Main Program Procedures.
WinMain PROTO :DWORD, :DWORD, :DWORD, :DWORD
WndProc PROTO :DWORD, :DWORD, :DWORD, :DWORD
AppFatalExit PROTO :DWORD, :DWORD
;##############################################################################
; Local Macros
;##############################################################################
;// Return a value in EAX.
return MACRO arg
IFNB <arg>
mov eax, arg
ENDIF
ret
ENDM
;// Memory-to-memory MOV.
m2m MACRO m1:REQ, m2:REQ
push m2
pop m1
ENDM
;// Memory copy.
mcopy MACRO destination:REQ, source:REQ
cld
lea esi, source
lea edi, destination
mov ecx, sizeof source
rep movsb
ENDM
;// Insert zero terminated string into code section.
szText MACRO name:REQ, text:VARARG
LOCAL lbl
jmp lbl
name db text, 0
lbl:
ENDM
;// Insert zero terminated string into .data section.
dszText MACRO name:REQ, text:VARARG
.data
name db text, 0
.code
ENDM
;// Return in EBX the ASCII size of a DWORD value
dwsize MACRO value:REQ
xor ebx, ebx
mov eax, value
.if eax == 0
inc ebx
.else
mov ecx, 10
.while eax > 0
xor edx, edx
div ecx
inc ebx
.endw
.endif
ENDM
;##############################################################################
; Initialized Data Section
;##############################################################################
.data
;##############################################################################
; Uninitialized Data Section
;##############################################################################
.data?
;##############################################################################
; Constants Section
;##############################################################################
.const
;##############################################################################
; Code Section
;##############################################################################
.code
;==============================================================================
; Beginning of executable code
;==============================================================================
start proc
;// Do some base initialization for the WinMain function and upon its
;// ending, terminate process.
LOCAL hModule :DWORD
;// Get handle to current instance.
invoke GetModuleHandle, NULL
.IF eax == NULL
dszText szGetModuleHandle_157, "GetModuleHandle, ln #157"
invoke AppFatalExit, addr szGetModuleHandle_157,
sizeof szGetModuleHandle_157
.ENDIF
mov hModule, eax
;// Get pointer to the command-line string for the current process.
invoke GetCommandLine
;// Call initial entry point for a Win32-based application.
invoke WinMain, hModule, NULL, eax, SW_SHOWMAXIMIZED
;// End process and all its threads.
invoke ExitProcess, eax
start endp
;==============================================================================
; WinMain Function (Called by the system as the initial entry point for a
; Win32-based application)
;==============================================================================
WinMain proc hInstance :DWORD, ;// handle to current instance
hPrevInstance :DWORD, ;// handle to previous instance
lpCmdLine :DWORD, ;// pointer to command line
nCmdShow :DWORD ;// show state of window
;// Perform initialization, create and display a main window and enter a
;// message retrieval-and-dispatch loop.
LOCAL wc :WNDCLASSEX
LOCAL hwndMain :DWORD
LOCAL msg :MSG
;// Register the window class for the main window.
mov wc.cbSize, sizeof WNDCLASSEX
mov wc.style, CS_OWNDC
mov wc.lpfnWndProc, offset MainWndProc
mov wc.cbClsExtra, 0
mov wc.cbWndExtra, 0
m2m wc.hInstance, hInstance
invoke LoadIcon, NULL, IDI_APPLICATION
.if eax == NULL
dszText szLoadIcon_203, "LoadIcon, ln #203"
invoke AppFatalExit, addr szLoadIcon_203, sizeof szLoadIcon_203
.endif
mov wc.hIcon, eax
invoke LoadCursor, NULL, IDC_ARROW
.if eax == NULL
dszText szLoadCursor_209, "LoadCursor, ln #209"
invoke AppFatalExit, addr szLoadCursor_209, sizeof szLoadCursor_209
.endif
mov wc.hCursor, eax
invoke GetStockObject, BLACK_BRUSH
.if eax == NULL
dszText szGetStockObject_215, "GetStockObject, ln #215"
invoke AppFatalExit, addr szGetStockObject_215,
sizeof szGetStockObject_215
.endif
mov wc.hbrBackground, eax
mov wc.lpszMenuName, NULL
dszText szClassName, "MainWndClass"
mov wc.lpszClassName, offset szClassName
mov wc.hIconSm, NULL
invoke RegisterClassEx, addr wc
.if eax == 0
dszText szRegisterClassEx_227, "RegisterClassEx, ln #227"
invoke AppFatalExit, addr szRegisterClassEx_227,
sizeof szRegisterClassEx_227
.endif
;// Create the main window.
dszText szDisplayName, "Win32 AppFatalExit Skeleton"
invoke CreateWindowEx, NULL, addr szClassName, addr szDisplayName,
WS_POPUP or WS_CLIPSIBLINGS or WS_MAXIMIZE or \
WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL,
hInstance, NULL
;// If the main window cannot be created, terminate the application.
.if eax == NULL
dszText szCreateWindowEx_237, "CreateWindowEx, ln #237"
invoke AppFatalExit, addr szCreateWindowEx_237,
sizeof szCreateWindowEx_237
.endif
mov hwndMain, eax
;// Show the window and paint its contents.
invoke ShowWindow, hwndMain, nCmdShow
invoke UpdateWindow, hwndMain
.if eax == NULL
dszText szUpdateWindow_255, "UpdateWindow, ln #255"
invoke AppFatalExit, addr szUpdateWindow_255,
sizeof szUpdateWindow_255
.endif
;// Start the message loop.
.while TRUE
invoke PeekMessage, addr msg, NULL, 0, 0, PM_REMOVE
.if (eax != 0)
.break .if msg.message == WM_QUIT
invoke TranslateMessage, addr msg
invoke DispatchMessage, addr msg
.endif
.endw
;// Return the exit code to Windows.
return msg.wParam
WinMain endp
;==============================================================================
; WindowProc Function (Application-defined callback function that processes
; messages sent to a window)
;==============================================================================
MainWndProc proc hwnd :DWORD, ;// handle of window
uMsg :DWORD, ;// message identifier
wParam :DWORD, ;// first message parameter
lParam :DWORD ;// second message paramater
;// Dispatch the messages that can be received.
.if uMsg == WM_KEYDOWN
;// Process keyboard input by means of a key press.
.if wParam == VK_ESCAPE
;// Clean up window-specific data objects.
invoke PostQuitMessage, NULL
return 0
.endif
.elseif uMsg == WM_DESTROY
;// Clean up window-specific data objects.
invoke PostQuitMessage, NULL
return 0
.endif
;// Process other messages.
invoke DefWindowProc, hwnd, uMsg, wParam, lParam
ret
MainWndProc endp
;==============================================================================
; Application Fatal Exit Procedure
;==============================================================================
AppFatalExit proc lpszCaption :DWORD, ;// pointer to string to display
in
\ ;// caption of the message box
nSize :DWORD ;// size of caption
;// Display a message box and terminate.
LOCAL uExitCode :DWORD
LOCAL lpBuffer :DWORD
LOCAL szFatalMessage [256]:BYTE
LOCAL nSizeMsg :DWORD
LOCAL szFatalCaption [64]:BYTE
;// Get the calling thread's last-error code value.
invoke GetLastError
mov uExitCode, eax
;// Obtain error message string.
invoke FormatMessage, FORMAT_MESSAGE_ALLOCATE_BUFFER or \
FORMAT_MESSAGE_FROM_SYSTEM, NULL, uExitCode, 0,
addr lpBuffer, 0, NULL
.if eax == NULL
dwsize uExitCode
mov nSizeMsg, ebx
invoke GetLastError
push eax
dwsize eax
add nSizeMsg, ebx
pop eax
dszText szDoubleFmt, "#%lu [& #%lu]"
invoke wsprintf, addr szFatalMessage, addr szDoubleFmt, uExitCode,
eax
add nSizeMsg, 7
.if eax != nSizeMsg
dszText szDoubleMessage, "#??? [& #???]"
mcopy szFatalMessage, szDoubleMessage
.endif
.else
mov nSizeMsg, eax
dwsize uExitCode
add nSizeMsg, ebx
dszText szFmt, "#%lu - %s"
invoke wsprintf, addr szFatalMessage, addr szFmt, uExitCode,
lpBuffer
add nSizeMsg, 4
.if eax != nSizeMsg
dszText szMessage, "#??? - ?????"
mcopy szFatalMessage, szMessage
.endif
invoke LocalFree, lpBuffer ;// Possible errors in LocalFree ignored
.endif
;// Display the application fatal exit message box.
dszText szCaptionFmt, "Fatal: %s"
invoke wsprintf, addr szFatalCaption, addr szCaptionFmt, lpszCaption
add nSize, 6
.if eax != nSize
dszText szCaption, "Fatal: ?????, ln #???"
mcopy szFatalCaption, szCaption
.endif
invoke MessageBox, NULL, addr szFatalMessage, addr szFatalCaption,
MB_ICONHAND or MB_SYSTEMMODAL
;// End process and all its threads.
invoke ExitProcess, eax
AppFatalExit endp
end start
---------------------------------------------------------------------------8<--
::/ \::::::.
:/___\:::::::.
/| \::::::::.
:| _/\:::::::::.
:| _|\ \::::::::::.
:::\_____\:::::::::::............................................THE.UNIX.WORLD
(Message over 64k, truncated.)