分享
 
 
 

Interfacing DJGPP with Assembly-Language Procedures

王朝other·作者佚名  2006-01-10
窄屏简体版  字體: |||超大  

Interfacing DJGPP with Assembly-Language Procedures

Written by Matthew Mastracci

Last Revised: 10/11/1997

---------------------------------------------------

Table of Contents

=---------------------------------------------------

Section

=---------------------------------------------------------------------------

1.0 Introduction

1.1 A few words

1.2 Where to get NASM

2.0 Assembly language with DJGPP

2.1 Inline assembly (AT&T style)

2.2 When and when not to use NASM

3.0 DJGPP and NASM

3.1 Introduction to NASM

3.2 Using NASM with makefiles

3.3 Using NASM with RHIDE

3.4 Getting used to NASM

3.5 A note on memory references

3.6 Returning 64-bit values

3.7 Name-mangling

3.8 Internal data structures

3.9 Exportable data structures

3.10 A note about labels

3.11 Accessing external symbols

4.0 Advanced NASM topics

4.1 Accessing real-mode interrupts

4.2 Direct memory access (to protected-mode memory)

4.3 Direct memory access (to real-mode memory)

4.4 malloc() and NASM

4.5 Hooking interrupts

4.6 _CRT0_FLAG_LOCK_MEMORY

4.7 Real-mode callback functions

4.8 Doubleword-aligned accesses

5.0 Contacting the author

5.1 Closing comments

5.2 Getting DJGPPASM.DOC

=---------------------------------------------------

1.0 - Introduction

=---------------------------------------------------

1.1 - A few words

=---------------------------------------------------

Most programmers have no trouble dealing with assembly language in

real mode. Your code is in one segment, your data is in another and you

have complete, unhindered access to the entire system. Things like

"general protection faults", "segment violations" and "page faults" seemed

like things only Windows programmers had to deal with.

In protected mode, however, things are different. Instead of

segments, we have to use selectors. You aren't allowed to write to any

memory location you please and the absolute addresses you took for granted

in real mode don't work in quite the same way. The purpose of this

tutorial is to ease the real to protected mode transition and help the

reader to grasp the important fundamental concepts that are important to

well-behaved assembly language.

1.2 - Where to get NASM

=---------------------------------------------------

The easiest place to get NASM from is Simtel. There is a large list of

Simtel mirrors in the DJGPP FAQ. It's in the "/pub/simtelnet/msdos/asmutl"

directory, with the name NASM???.ZIP (where ??? is the latest version

number).

=---------------------------------------------------

2.0 - Assembly language with DJGPP

=---------------------------------------------------

2.1 - Inline assembly (AT&T style)

=---------------------------------------------------

DJGPP allows you code full-blown inline assembly with one catch: you

have to use a semantically different set of opcodes referred to as

"AT&T-syntax." On top of that, Gas, DJGPP's assembler, is only used to

getting code straight from GCC, which means that it has very limited

syntax-checking and may not do exactly what you think you told it to.

To begin, let's look at the format of an inline-assembler statement in

a function:

unsigned short int AddFour(unsigned short int x)

{

unsigned int y;

__asm__ __volatile__(

"movw %w1, %%ax\n"

"addw $4, %%ax\n"

"movw %%ax, %w0\n"

: "=r" (y)

: "r" (x)

);

return y;

}

This function (as you may have guessed), adds four to a given

parameter and returns the value. You may, however, wonder about the

strange arrangement of registers and variables for the opcodes. Compared to

Intel-syntax asm, they're backwards! In essence, standard x86 opcodes

"act" from right to left, ie: mov ax, bx gets the value of ax from bx; add

ax, 4 adds 4 to ax. AT&T opcodes, however, acts from left to right. Here

are some examples:

Intel AT&T

mov bx, ax movw %%ax, %%bx

add ax, 4 addw $4, %%ax

Basically, the format of an inline assembly statement is:

__asm__ [__volatile__](

"opcodes" :

output-vars :

input-vars :

modified-regs

);

- "__asm__" instructs the compiler to treat the parameters of the

statement as pure assembly and to pass them to the assembler (Gas) as

written. ;

- "__volatile__" is an optional statement which instructs the compiler not

to move any of your opcodes around from where you place them or combine

them as it pleases. This is probably a good idea, as Gas is designed to

take pure GCC output and may take some unwanted liberties with the code;

- output-vars is a list of constraints indicating which variables will be

modified by the routine: use the format "=r" (x), where r is the type of x

and x is the output variable to be modified;

- input-vars is a list of constraints indicating which variables will be

used by the routine: use the format "r" (x), where r is the type of x and x

is the input variable to be referenced in the routine;

- modified-regs is a list of hard registers which are "clobbered" by the

function.

Inside the actual assembly statements, you'll notice that you don't

actually say "movw x, %%ax" or something like that. That's because the

compiler "renames" them, in the order you "mention" them. That means that

the first variable defined as an output variable will be "%w0", and, if it

exists, the next output variable will be "%w1", if it doesn't exists, the

first input variable will be "%w1" instead. In essence, the numbering

starts at output-vars at zero and counts up through the end of output-vars

and then through input-vars, incrementing by one each time.

Inline assembly is convenient in DJGPP and quite fast, but its

complexity in programming and general unreliability makes it difficult to

use for long programming tasks. In the next section, we will explore an

alternative, Intel-syntax-based method that works incredibly well with

DJGPP.

2.2 - When and when not to use NASM

=---------------------------------------------------

There are a few points to consider when choosing whether NASM will be

the best assembly-language compiler for you to use or even whether a

combination of the two methods would be better.

1. Using the __asm__ directive means that you can inline your assembly

language function, which you can't do with NASM. On top of that, you can

tell the compiler which registers you clobber and it can work around that.

2. GAS was only designed to take input from the compiler, not from the

programmer. There are a few things you need to watch out for, as its

error-checking is fairly minimal.

3. NASM follows the TASM/MASM format for assembly in most cases. If you're

used to these compilers, the adjustment period is much less, where as with

AT&T syntax, you have to practice a little more to get used to it.

4. NASM is a solid compiler that won't optimize behind your back. Each

instruction you enter is compiled exactly as you enter it. Also, NASM

supports MMX, which (as far as I can tell) isn't supported by GAS.

My personal preference is this: use NASM for your major assembly

routines (ie: the sprite-drawing routines in a graphic library or callback

functions) and use the __asm__ directive for minor functions that you want

to inline at any point in the future. If you're planning on creating a .S

file, you can probably save a lot of headache by just creating it as a .ASM

file and compiling it with NASM instead of GAS.

=---------------------------------------------------

3.0 - DJGPP and NASM

=---------------------------------------------------

3.1 - Introduction to NASM

=---------------------------------------------------

There is a freeware package named the NASM, Netwide ASseMbler,

project. It is a fully-functional assembler with most of the capabilities

of commercial products such as MASM or TASM, but (in the spirit of DJGPP

and other freeware compilers) costs you nothing. Obtain the latest version

of the package, create a %DJDIR%\NASM directory and unZIP the archive

inside the directory with the -d option. This will create the proper

directories for the program. Finally, either copy the NASM executable to

your %DJDIR%\BIN directory or add the directory to your path.

3.2 - Using NASM with makefiles

=---------------------------------------------------

If you use GNU's make to handle your projects, setting up NASM as a

compiler is fairly easy. For each file, the dependancies/method line

should look like this:

filename.o : filename.asm ; nasm -f coff filename.asm

You could also create a rule for making .o files from .asm files, to

save time:

%.o : %.asm ; nasm -f coff $<

3.3 - Using NASM with RHIDE

=---------------------------------------------------

If you want to include a NASM-compiled file in your project, follow

these steps for each .asm file:

1. Open the project window;

2. Select the .asm file you want to compile with NASM;

3. Hit Ctrl-O for the file's local options;

4. Select "User" for compiler;

5. Enter "nasm -f coff $(SOURCE_FILE)" in the "Compiler" text area; and

6. Set the error-checking to "built-in C-compiler".

This should work for all versions of RHIDE, starting at version 1.2.

In future versions, the author may have implemented a new system for using

external compilers. Check the program updates for more information.

3.4 - Getting used to NASM

=---------------------------------------------------

To begin, let's create a sample assembly language program:

nasmtest.asm:

[BITS 32]

[GLOBAL _AddFour__FUi]

[SECTION .text]

; ---------------------------------------------------------------------------

; Prototype: unsigned int AddFour(unsigned int x);

; Returns: x + 4

; ---------------------------------------------------------------------------

x_AddFour equ 8

_AddFour__FUi:

push ebp

mov ebp, esp

mov eax, [ebp + x_AddFour]

add eax, 4

mov esp, ebp

pop ebp

ret

Let's also create a C++ program to test it:

nasmtest.cc:

#include <stdio.h>

extern unsigned int AddFour(unsigned int);

int main(void)

{

printf("AddFour(4) = %i\n", AddFour(4));

return 0;

}

To make things easier, create a makefile like so:

nasmtest.mak:

nasmtest.exe : nasmtest.cc nasmtest.o ; gcc nasmtest.cc -o nasmtest.exe nasmtest.o -v -Wall

nasmtest.o : nasmtest.asm ; nasm -f coff nasmtest.asm

Type "make -f nasmtest.mak" to create the executable. When you

run it, it should print "AddFour(4) = 8".

Let's go through the assembler source:

[BITS 32]

This line instructs NASM that you want it to prefix 16-bit code and

default to 32-bit code, not the other way around. You must have this in

any functions designed to run in protected mode. If you don't have it (in

a version of NASM prior to 0.91) you'll probably generate a page fault (try

it), because all of your memory references will have the top word truncated

(ie: ebp = 0x47282567, chopped to 0x2567).

[GLOBAL _AddFour__FUi]

This line tells NASM that the label is to be exported and will be

accessable as an external symbol to other modules. There are other things

that can be placed here, but we will learn about those later.

[SECTION .text]

This defines the .text segment, which is the segment placed first in

the assembled file. It contains any executable code (exportable or not)

for the module. There are other types of segments which we will learn

about later.

; ---------------------------------------------------------------------------

; Prototype: unsigned int AddFour(unsigned int x);

; Returns: x + 4

; ---------------------------------------------------------------------------

These lines give the prototype of the function for your C++ function,

for easy reference. They aren't necessary, but help you with remembering

what your function takes as parameters and returns.

x_AddFour equ 8

As NASM has no direct support for parameter structures (as of yet), we

can define our function's parameters with this method. These are used for

referencing parameters placed on the stack by the calling function. Each

parameter will take a minimum of four bytes (padded with zeros), which

helps you keep your code 32-bit.

_AddFour__FUi:

This label declaration defines a function called "AddFour." There is,

however, a suffix which represents the parameters of the function. This

suffix is unique to C++ (the label would be "_AddFour" in C) to account for

external function overloading. Each suffix begins with "__F" to say that

it is, in fact, a function. Following this, there are characters

representing the data types (some of them):

Character Data Type

c char

d double

f float

i int

s short

v void

x long long int

In addition, there are prefixes you can add to the characters to

change them:

Prefix Meaning

U unsigned (ie: Uc is 'unsigned char')

P pointer (ie: Pi is 'int *')

R reference (ie: RUc is 'unsigned char &')

push ebp

mov ebp, esp

These lines are analagous to the "push bp/mov bp, sp" opcodes in

real-mode. They are used to preserve the 32-bit values of the caller's

stack frame.

mov eax, [ebp + x_AddFour]

add eax, 4

We load eax with the 32-bit unsigned int from the stack and add four

to it. Note that all function return values are returned in eax (extended

to edx if necessary).

mov esp, ebp

pop ebp

ret

This simply restores the caller's stack frame and returns to the

caller.

3.5 - A note on memory references

=---------------------------------------------------

In NASM, memory references work a little differently than they do in

most other assemblers. To specify the address of a variable (a symbol), you

don't use the "offset" function. Instead, you just specify the name of the

symbol as so:

mov esi, output_variable ; loads ESI with the *address* of output_variable

mov output_variable, esi ; illegal! trying to load into immediate value

If you want to access the contents of a memory location, put the

variable name in square brackets:

mov esi, [output_variable] ; loads ESI with the *value* of output_variable

mov [output_variable], eax ; loads output_variable with the contents of eax

That's all there is. If you're ever confused, just imagine that the

compiler is just replacing each of the symbols with its absolute constant

value (which it is, as a matter of fact). That way (if output_variable is

at offset 2362), this makes no sense:

mov 2342, esi

But these do make sense:

mov esi, 2342

mov [2342], eax

3.6 - Returning 64-bit values

=---------------------------------------------------

One of the great things about DJGPP is its built-in support for 64-bit

integers (signed and unsigned). How can we use these in NASM? Simple: we

just pass the low byte as we would pass a regular int, but we pass the high

byte in edx. Consider:

[BITS 64]

[GLOBAL _BigNum__FUiUi]

[SECTION .text]

; ---------------------------------------------------------------------------

; Prototype: unsigned long long int BigNum(unsigned int a, unsigned int b);

; Returns: unsigned 64-bit integer, with a as the high word and b as the low

; word

; ---------------------------------------------------------------------------

a_BigNum equ 8

b_BigNum equ 12

_BigNum__FUiUi:

push ebp

mov ebp, esp

mov edx, [ebp + b_BigNum]

mov eax, [ebp + a_BigNum]

mov esp, ebp

pop ebp

ret

Let's create a quick C++ program to test this:

#include <stdio.h>

extern unsigned long long int BigNum(unsigned int a, unsigned int b);

int main(void)

{

unsigned int a = 0x11111111, b = 0x22222222;

printf("The 64-bit integer made from 0x%0x and 0x%0x is 0x%016Lx (%Lu)", a,

b, BigNum(a, b), BigNum(a, b));

return 0;

}

The range of the 64-bit 'unsigned long long int' is from 0 to

18,446,744,073,709,551,615. That's 18 quintillion, or 18x10^18!

3.7 - Name-mangling

=---------------------------------------------------

As mentioned before, C++ functions are "mangled" to account for how

it allows the programmer to overload the parameters of a function. If you

don't want your functions names to have annoying trailers like "__Fv" or

"__FUiUicP11__dpmi_regs", there's another way to do it. If you load up

one of DJGPP's supplied include files, you'll notice this near the start:

#ifdef __cplusplus

extern "C" {

#endif

int foo(int bar);

void fx(void);

...

#ifdef __cplusplus

}

#endif

The extern "C" declaration tells the compiler that the function was

originally compiled with a regular C-compiler, not a fancy C++-compiler

that mangles your function names. This way, you can write your assembly

language functions will just a preceeding underscore (like _foo or _bar).

Before you get mad at C++ for mangling your simple names, take a

look at what C++ does to your complicated class names!

3.8 - Internal data structures

=---------------------------------------------------

Many of the functions you will write with the assembler will require

various variables that won't be seen outside the scope of the module. To

do this, let's create a new section below the function AddFour. Put this

at the end of nasmtest.asm:

[SECTION .data]

AddConstant dd 00000004h

Now modify AddFour like so:

push ebp

mov ebp, esp

mov eax, [ebp + x_AddFour]

add eax, [AddConstant]

mov esp, ebp

pop ebp

ret

The code now references data inside the data segment (everything in

.data is placed in the data segment referenced by the selector in ds).

With NASM, you must place the variable's name in square brackets to

indicate a reference to a memory location. If you forget the brackets, the

compiler will treat it as the actual offset of the variable, often leading

to unwanted side-effects.

3.9 - Exportable data structures

=---------------------------------------------------

What if we want a function to make a variable accessable to a module

it gets linked with? Let's add a version string to the module for the

calling program to read. Add this in the .data section, under AddConstant:

_VersionString db "NASMTEST.ASM - Version 0.0a", 00h

Now, at the top of the file, under the first [GLOBAL ...] declaration,

add:

[GLOBAL _VersionString]

This label is now exportable as a variable in the calling program.

Add the following lines to nasmtest.cc, under the declaration of AddFour:

extern char VersionString[];

And under the first printf() statement, add:

printf("VersionString = '%s'\n", VersionString);

Compile this, and you should see the exported string from the assembly

module. Note that the variable can be accessed internally as well.

Consider:

; ---------------------------------------------------------------------------

; Prototype: void NewVersion(char c);

; Returns: nothing

; ---------------------------------------------------------------------------

c_NewVersion equ 8

_NewVersion__Fc:

push ebp

mov ebp, esp

mov al, [ebp + c_NewVersion] ; mov eax, [ebp + c_NewVersion]

; would be just as valid, as the

; stack is padded with zeros

mov byte [_VersionString + 26], al ; 26 is the offset of the 'a'

; in VersionString from position

mov esp, ebp ; zero

pop ebp

ret

Add the following two lines as well:

NewVersion('g');

printf("VersionString = '%s'\n", VersionString);

Don't forget to export the function with [GLOBAL ...] and declare the

external function in your C++ source.

What if we wanted to offer a complicated structure? Add the following

declaration to nasmtest.cc:

extern struct {

unsigned int x, y;

char * VersionStringPointer;

} DataValues;

Now add the following in the .data section:

_DataValues:

dd 00000001 ; unsigned int x

dd 00000002 ; unsigned int y

dd _VersionString ; char * VersionStringPointer

; Loaded with offset of version string,

; effectively a pointer

And export it with:

[GLOBAL _DataValues]

Now you can check that the structure works:

printf("x = %i, y = %i\n", DataValues.x, DataValues.y);

// This line uses a char * instead of char like before

printf("VersionString = '%s'", DataValues.VersionStringPointer);

Other variable types may be passed back and forth in this method. Just

remember to ensure that the structures on both sides are exactly the same.

3.10 - A note about labels

=---------------------------------------------------

In essence, there are three types of labels within NASM:

1) Exportable references: labels which are declared in a [GLOBAL ...]

statement at the top of a file, accessable by other modules linked with

this one. These labels reference functions, structures and variables, and

begin with an underscore, ie:

_InitMode13h__Fv: ; Exportable function

; function code

_DataTable: ; Exportable structure

db 00h

db 00h

_XCoord dw 0000h ; Exportable variable

2) Internal references: labels referencing functions, structures and

variables which are private to the module are written without a prefix

character:

SaveRegs: ; Internal function

; function code

DescriptorTable: ; Internal structure

dw 0000h, 0000h

RAMPointer: dd 00000000h ; Internal variable

3) Internal jump targets: labels which will be the target of conditional

and unconditional jumps are written with a period as a prefix:

.LoopStart:

loop .LoopStart

jmp .DoneProc

.DoneProc:

Adhering to these guidelines will ensure your code works properly with

DJGPP and most debuggers.

3.11 - Accessing external symbols

=---------------------------------------------------

Your assembly language module can access public symbols from other

modules it gets linked with as well. Create two strings in the .data

section like so:

; Note that 0ah is used instead of \n, as \n is not interpreted by

; printf, but processed by the compiler instead.

PrintTemplate1 db "VersionString = '%s'", 0ah, 00h

PrintTemplate2 db "unsigned int Exported = 0x%08x", 0ah, 00h

Now create an assembler function to access some external symbols:

; ---------------------------------------------------------------------------

; Prototype: void PrintStrings(void);

; Returns: nothing

; ---------------------------------------------------------------------------

_PrintStrings__Fv:

push ebp

mov ebp, esp

; Remember that C pushes parameters from right to left, not

; left to right like Pascal!

; Note that we push the pointer for a string...

push dword _VersionString

push dword PrintTemplate1

call _printf

; ... but push the actual value of most everything else

push dword [_Exported]

push dword PrintTemplate2

call _printf

mov esp, ebp

pop ebp

ret

We'll have to tell NASM that we have a few external symbols, so it

doesn't give us any errors. Put these by the [GLOBAL ...] definitions:

[GLOBAL _PrintStrings__Fv]

[EXTERN _printf]

[EXTERN _Exported]

Now remove any code previously contain in main() and add the following

lines:

external void PrintStrings(void);

unsigned int Exported;

int main(void)

{

Exported = 0xdeadbeef;

PrintStrings();

return 0;

}

=---------------------------------------------------

4.0 - Advanced NASM topics

=---------------------------------------------------

4.1 - Accessing real-mode interrupts

=---------------------------------------------------

When you write your C++ code, you usually don't worry about having to

call a real-mode interrupt. Usually, you just define a register structure

and call __dpmi_int or one of the various other wrappers provided by DJGPP.

In assembly language, however, you don't have it quite so easy.

The easiest way is to call the DPMI server's function 0x03000, which

simulates a real-mode interrupt. Create a new file with the following:

inttest.asm:

[BITS 32]

[SECTION .text]

SaveRegs:

mov [SaveEAX], eax

mov [SaveEBX], ebx

mov [SaveECX], ecx

mov [SaveEDX], edx

mov [SaveESI], esi

mov [SaveEDI], edi

mov [SaveEBP], ebp

pushf

pop eax

mov [SaveFlags], ax

ret

RestoreRegs:

pushf

pop eax

mov ax, [SaveFlags]

push eax

popf

mov eax, [SaveEAX]

mov ebx, [SaveEBX]

mov ecx, [SaveECX]

mov edx, [SaveEDX]

mov esi, [SaveESI]

mov edi, [SaveEDI]

mov ebp, [SaveEBP]

ret

[SECTION .data]

; Register set structure for int 31h, function 3000h

RegSet

SaveEDI dd 00000000

SaveESI dd 00000000

SaveEBP dd 00000000

dd 00000000 ; Note: reserved

SaveEBX dd 00000000

SaveEDX dd 00000000

SaveECX dd 00000000

SaveEAX dd 00000000

SaveFlags dw 0000

SaveES dw 0000

SaveDS dw 0000

SaveFS dw 0000

SaveGS dw 0000

SaveIP dw 0000

SaveCS dw 0000

SaveSP dw 0000

SaveSS dw 0000

SaveRegs and RestoreRegs can now be called from your program to fill

and examine the values stored in the structure RegSet. Now you can simply

wrap an interrupt call like:

int 10h

like so:

; Pass our registers to the interrupt

SaveRegs

; Function 3000h: Simulate real-mode interrupt

mov ax, 3000h

; Interrupt to simulate: int 10h

mov bx, 0010h

; Copy zero bytes from our stack (you probably won't need otherwise)

mov cx, 0000h

; Load es with ds, so es:edi points to RegSet

push ds

pop es

mov edi, RegSet

; Do it

int 31h

; Get the return values back

RestoreRegs

As you can see, there is a lot of overhead involved in calling a

real-mode interrupt from protected-mode. The message? If you don't have

to, don't. Use these sparingly, in parts of your code that aren't time

critical.

Let's try this with a real-life example. Add these two export

declarations:

[GLOBAL _InitMode13h__Fv]

[GLOBAL _RestoreTextMode__Fv]

Next, add their respective functions:

; ---------------------------------------------------------------------------

; Prototype: void InitMode13h(void);

; Returns: nothing

; ---------------------------------------------------------------------------

_InitMode13h__Fv:

push ebp

mov ebp, esp

; ah = 00h, Sets video mode with int 10h to mode al

mov ax, 0013h

call SaveRegs

; Simulate real-mode interrupt

mov ax, 0300h

; Of int 10h

mov bx, 0010h

; Copy nothing from the stack

mov cx, 0000h

; Load es with ds, so es:edi points to RegSet

push ds

pop es

mov edi, RegSet

; Do it

int 31h

; Load results back into registers

call RestoreRegs

mov esp, ebp

pop ebp

ret

; ---------------------------------------------------------------------------

; Prototype: void RestoreTextMode(void);

; Returns: nothing

; ---------------------------------------------------------------------------

_RestoreTextMode__Fv:

push ebp

mov ebp, esp

; ah = 00h, Sets video mode with int 10h to mode al

mov ax, 0003h

call SaveRegs

; Simulate real-mode interrupt

mov ax, 0300h

; Of int 10h

mov bx, 0010h

; Copy nothing from the stack

mov cx, 0000h

; Load es with ds, so es:edi points to RegSet

push ds

pop es

mov edi, RegSet

; Do it

int 31h

; Load results back into registers

call RestoreRegs

mov esp, ebp

pop ebp

ret

And now create the test program:

inttest.cc:

#include <conio.h>

#include <go32.h>

#include <sys/farptr.h>

extern void InitMode13h(void);

extern void RestoreTextMode(void);

int main(void)

{

int x, y;

InitMode13h();

_farsetsel(_dos_ds);

for (y = 0; y < 100; y++) {

for (x = 0; x < 100; x++) {

_farnspokeb(0xa0000 + y * 320 + x, ((x * y) >> 2) % 256);

}

}

getch();

RestoreTextMode();

return 0;

}

This should produce a 100x100 square in the top left of your screen,

containing a neat-looking pattern. In a nutshell, that's how you can use

interrupts to perform tasks that are complicated, but aren't called enough

times to warrent optimization. You may have noticed that the routine isn't

quite instant however (if you compiled the program with optimizations off).

In the next section, we will explore ways to increase the speed of direct

memory accesses of all kinds.

4.2 - Direct memory access (to protected-mode memory)

=---------------------------------------------------

As we've seen earlier, data segment pointers can be accessed simply by

using square brackets (ie: mov ax, [DataSegmentWord]). In fact, this

applies to all standard pointers, including those obtained from malloc().

They are all 32-bit near pointers because, hey, in protected-mode,

everything is "near."

This simplifies buffer-to-buffer copying a little. You can copy a

large amount of memory (up to 256k) between two external malloc()'d

pointers by simply writing:

cld ; Forward copy, inc esi/edi

mov esi, [_PointerFrom] ; ds:esi is the source

mov edi, [_PointerTo] ; es:edi is the dest, assumes es = ds

mov ecx, Count ; Count is the actual number of bytes

rep movsd ; divided by 4 (ie: double-words)

Most of the time, you will find es equal to ds while executing your

code. Don't rely on this, however, unless you are absolutely sure. It's a

good idea to set es to ds at the start of the code, to ensure you don't end

up writing to another selector and causing a GPF or worse. If your code

can't afford to waste cycles (and is called repeatedly), consider setting

es to ds in another function and calling it once before the other calls.

Be sure that you aren't calling anything that may change the value of es

under your nose, though.

4.3 - Direct memory access (to real-mode memory)

=---------------------------------------------------

But what if you want to access something from real-mode linear memory?

Here's where it gets a little tricky. You can't simply pull a descriptor

for your particular segment from thin air and access it that way. DJGPP

provides a convenient way to get a descriptor for something like this,

however:

int __dpmi_segment_to_descriptor(int _segment);

You simply pass it the real-mode segment you want a descriptor for and

it returns the descriptor you can use to access it. That's all it takes.

Let's add a function to inttest.asm to do this for us:

; ---------------------------------------------------------------------------

; Prototype: int InitGraphics(void);

; Returns: -1 on error, else zero

; ---------------------------------------------------------------------------

_InitGraphics__Fv:

push ebp

mov ebp, esp

push dword 0a000h ; Push segment for function

call ___dpmi_segment_to_descriptor ; Note the triple underscore

cmp eax, -1 ; Check for error

jne .SelectorOkay ; No error, selector is valid

mov [VideoRAM], 0ffffh ; Error, make selector invalid

jmp .Done ; End function, will return -1

.SelectorOkay:

mov [VideoRAM], ax ; Load selector from ax

xor eax, eax ; Clear eax to return zero

.Done:

mov esp, ebp

pop ebp

ret

We'll also have to add a variable to the .data section:

VideoRAM dw 0000h

And add some declarations at the beginning:

[GLOBAL _InitGraphics__Fv]

[EXTERN ___dpmi_segment_to_descriptor]

Now that we have a descriptor that references segment 0xa000, we can

create a PutPixel routine on its way to being much faster than before:

; ---------------------------------------------------------------------------

; Prototype: void PutPixel(unsigned int x, unsigned int y,

; unsigned char Color);

; Returns: nothing

; ---------------------------------------------------------------------------

x_PutPixel equ 8

y_PutPixel equ 12

Color_PutPixel equ 16

_PutPixel__FUiUiUc:

push ebp

mov ebp, esp

mov eax, [ebp + y_PutPixel]

mov ebx, eax

shl eax, 6 ; Note: x shl 6 + x shl 8 = 320 * x

shl ebx, 8

add ebx, eax

add ebx, [ebp + x_PutPixel]

mov fs, [VideoRAM]

mov al, [ebp + Color_PutPixel]

mov [fs:ebx], al

mov esp, ebp

pop ebp

ret

Now our main procedure becomes:

int main(void)

{

int x, y;

InitGraphics();

InitMode13h();

for (y = 0; y < 100; y++) {

for (x = 0; x < 100; x++) {

PutPixel(x, y, ((x * y) >> 2) % 256);

}

}

getch();

RestoreTextMode();

return 0;

}

You can remove the #include<...> statements at the start of the file,

with the exception of conio.h. Once you've added the extern declarations,

it should compile and run like before.

One important aspect of __dpmi_segment_to_descriptor is that the

number of selectors available to it is finite. Thus, you should only call

the InitGraphics() function once (along with any others that use this

function), at the beginning of the program. In any case, calling it more

than once is redundant and will put a drain on resources and speed.

4.4 - malloc() and NASM

=---------------------------------------------------

One of the most important functions for managing allocated memory is

malloc(). It allows you to assign any pointer to a contigious block of

memory and access it like a structure, array, variable or even as a

function.

Let's look at an example:

alloctst.cc

#include <stdio.h>

extern void FillString(char * StringToFill);

int main(void)

{

char * TestString;

TestString = (char *)malloc(20);

FillString(TestString);

printf("TestString = '%s'\n", TestString);

free(TestString);

return 0;

}

alloctst.asm

[BITS 32]

[GLOBAL _FillString__FPc]

[SECTION .text]

; ---------------------------------------------------------------------------

; Prototype: void FillString(char * StringToFill);

; Returns: nothing

; ---------------------------------------------------------------------------

StringToFill_FillString equ 8

_FillString__FPc:

push ebp

mov ebp, esp

mov edi, [ebp + StringToFill_FillString]

mov esi, Filler

mov ecx, 20

rep movsb

mov esp, ebp

pop ebp

ret

[SECTION .data]

Filler db "<- Buffer filled ->", 00h

As you may have guessed, the program copies the filler (without any

sort of bounds checking) to the passed string and then prints it. In this

way, malloc() can be used for many different applications in your program.

In graphical applications, you may find these buffers useful for storing

sprites, sound and tile data. With a little bit of effort, you can create

dynamically-loaded drivers that you can load and unload into your program

as needed. This could be useful for creating a standard set of procedures

for sound output with a number of drivers that can be loaded for each

different sound card.

4.5 - Hooking interrupts

=---------------------------------------------------

Interrupt-hooking is an integral part of many different types of

programs, from timer-synchronization in games to serial port monitoring in

communication programs. Let's try a quick example. Create a makefile and

enter the following program:

hookint.asm:

[BITS 32]

[GLOBAL _TickHandler__Fv]

[GLOBAL _TickHandler__Size]

[EXTERN _Timer1]

[EXTERN _Timer2]

[EXTERN _Flag1]

[EXTERN _Flag2]

[EXTERN _TickCount]

[SECTION .text]

_TickHandler__Fv:

inc dword [_TickCount]

dec dword [_Timer1]

dec dword [_Timer2]

cmp dword [_Timer1], 0

jz .SetFlag1

jmp .TestFlag2

.SetFlag1:

mov dword [_Flag1], 1

.TestFlag2:

cmp dword [_Timer2], 0

jz .SetFlag2

jmp .Done

.SetFlag2:

mov dword [_Flag2], 1

.Done:

ret

; Calculate size of function by subtracting offsets

_TickHandler__Size dd $-_TickHandler__Fv

[SECTION .data]

hookint.cc:

// Adapted from the DJGPP test program "libc\go32\timer.c"

#include <stdio.h>

#include <pc.h>

#include <dpmi.h>

#include <go32.h>

#define LOCK_VARIABLE(x) _go32_dpmi_lock_data((void *)&x, (long)sizeof(x));

extern void TickHandler(void);

extern int TickHandler__Size;

int Timer1 = 1, Timer2 = 1, Flag1, Flag2, TickCount = 0;

int main()

{

_go32_dpmi_seginfo OldHandler, NewHandler;

printf("Grabbing timer interrupt...\n");

_go32_dpmi_get_protected_mode_interrupt_vector(8, &OldHandler);

_go32_dpmi_lock_code(TickHandler, (long)TickHandler__Size);

LOCK_VARIABLE(Timer1);

LOCK_VARIABLE(Timer2);

LOCK_VARIABLE(Flag1);

LOCK_VARIABLE(Flag2);

LOCK_VARIABLE(TickCount);

NewHandler.pm_offset = (int)TickHandler;

NewHandler.pm_selector = _go32_my_cs();

_go32_dpmi_chain_protected_mode_interrupt_vector(8, &NewHandler);

while (!kbhit())

{

if (Flag1)

{

printf("Timer 1 expired: %i\n", TickCount);

Flag1 = 0;

Timer1 = 5;

}

if (Flag2)

{

printf("Timer 2 expired: %i\n", TickCount);

Flag2 = 0;

Timer2 = 7;

}

}

getkey();

printf("Releasing timer interrupt...\n");

_go32_dpmi_set_protected_mode_interrupt_vector(8, &OldHandler);

return 0;

}

What does this program do? Let's examine it:

_go32_dpmi_get_protected_mode_interrupt_vector(8, &OldHandler);

This line reads the selector and offset of the previous handler for

INT 8 (the timer interrupt) into the structure OldHandler for use later.

_go32_dpmi_lock_code(TickHandler, (long)TickHandler__Size);

LOCK_VARIABLE(Timer1);

LOCK_VARIABLE(Timer2);

LOCK_VARIABLE(Flag1);

LOCK_VARIABLE(Flag2);

LOCK_VARIABLE(TickCount);

Here, we ensure that the handler doesn't get paged out from under our

noses and cause a page fault. As the function call for locking a variable

is fairly complicated to type repeatedly, we define a macro to save some

time. The size of the locked code is calculated at compile-time by our

variable in the source file and passed as an int for us to use.

NewHandler.pm_offset = (int)TickHandler;

NewHandler.pm_selector = _go32_my_cs();

_go32_dpmi_chain_protected_mode_interrupt_vector(8, &NewHandler);

We then get the selector/offset pair for our new handler and add it to

the interrupt chain for INT 8. Note that _go32_my_cs() returns the

selector for the program's code. At this point, a wrapper has been created

for our function so that it will execute every time the interrupt is

called. You won't need to terminate your routine with iret, as you

normally do, because the function to chain the vector will create a special

wrapper to ensure that the routine will run like normal. You cannot,

howver, use any special system functions while in your interrupt routine

(like printf, fopen, fread, etc.), as most of these are non-reentrant.

_go32_dpmi_set_protected_mode_interrupt_vector(8, &OldHandler);

Once we have finished with it, we can return the handler to its

previous state by passing the old handler's address we saved before.

Our protected-mode interrupt handler is quite simple. It increments

the variable TickCount for every timer tick and decrements Timer1 and

Timer2. If either of the two timers is equal to zero, it sets the flag

associated with the timer. The flags are latched, meaning that you must

zero them after you have dealt with them. This also means that if you

aren't able to catch one or more timer expiries (because the system is

busy), you'll only have to service it once.

Interrupt handlers can be quite complicated if necessary. The only

restriction is that you can't call any functions that aren't re-entrant.

This includes most of the system functions and any of yours that fall under

the same category. In most cases, you should stay within your function as

much as possible to prevent possible conflicts.

4.6 - _CRT0_FLAG_LOCK_MEMORY

=---------------------------------------------------

It is possible to lock all of your program's memory using one of the

CRT0 startup flags, _CRT_FLAG_LOCK_MEMORY. This flag effectively disables

virtual memory (disk swapping). It's a great feature if you want to write

a program that shouldn't be swapped to disk at any time (perhaps a game).

To use it, you simply add the following lines to your programs:

#include <crt0.h>

int _CRT0_STARTUP_FLAGS = _CRT0_FLAG_LOCK_MEMORY;

This will ensure that your code, data and allocated memory will all

be locked, without the need to use _go32_dpmi_lock_data(). The amount of

available memory will decrease, however.

4.7 - Real-mode callback functions

=---------------------------------------------------

Interrupt handlers are great for handling interrupts, but what if

you want a real-mode program to call one of your functions when a certain

event occurs? If you were in real-mode too, you could just get the segment

and offset of your function and pass it on to the other program and be done

with it. In protected mode, there's one more step: a wrapper. Just like

before, there's a great library function that does all the tough stuff for

you:

_go32_dpmi_allocate_real_mode_callback_wrapper_retf()

How do we use this? It's easy. Let's look at another example:

rmcbtest.cc:

#include <go32.h>

#include <dos.h>

#include <dpmi.h>

#include <conio.h>

#include <stdio.h>

#include <crt0.h>

int _CRT0_STARTUP_FLAGS = _CRT0_FLAG_LOCK_MEMORY;

/* To make sure the name doesn't get mangled */

extern "C" void MouseCallback(_go32_dpmi_registers * r);

extern volatile int MouseButtons;

extern volatile int MouseX;

extern volatile int MouseY;

_go32_dpmi_registers regs;

int main(void)

{

clrscr();

_go32_dpmi_seginfo info;

/* Set up the handler */

info.pm_offset = (int)MouseCallback;

_go32_dpmi_allocate_real_mode_callback_retf(&info, &regs);

__dpmi_regs r;

/* Set the horizontal range valid from 0 to 1000 */

r.x.ax = 0x07;

r.x.cx = 0;

r.x.dx = 1000;

__dpmi_int(0x33, &r);

/* Set the vertical range valid from 0 to 1000 */

r.x.ax = 0x08;

r.x.cx = 0;

r.x.dx = 1000;

__dpmi_int(0x33, &r);

/* Install the real-mode callback routine */

r.x.ax = 0x0c;

r.x.cx = 0x1f; /* 0x1f traps on movements and RMB/LMB presses */

r.x.dx = info.rm_offset;

r.x.es = info.rm_segment;

__dpmi_int(0x33, &r);

while (!MouseButtons) {

printf("(%i, %i)\n", MouseX, MouseY);

delay(250);

}

/* Clean up handler */

r.x.ax = 0x0c;

r.x.cx = 0;

r.x.dx = 0;

r.x.es = 0;

__dpmi_int(0x33, &r);

_go32_dpmi_free_real_mode_callback(&info);

printf("Mouse button pressed.");

return 0;

}

rmcbtest.asm:

[BITS 32]

[GLOBAL _MouseCallback]

[GLOBAL _MouseX]

[GLOBAL _MouseY]

[GLOBAL _MouseButtons]

[SECTION .text]

; ---------------------------------------------------------------------------

; Prototype: void MouseCallback(__dpmi_regs * r)

; Returns: nothing

; ---------------------------------------------------------------------------

Pointer_MouseCallback equ 8

_MouseCallback:

push ebp

mov ebp, esp

mov esi, [ebp + Pointer_MouseCallback]

xor eax, eax

mov ax, [esi + 16] ; offset of bx in __dpmi_regs

mov [_MouseButtons], eax

mov ax, [esi + 24] ; offset of cx in __dpmi_regs

mov [_MouseX], eax

mov ax, [esi + 20] ; offset of dx in __dpmi_regs

mov [_MouseY], eax

mov esp, ebp

pop ebp

ret

[SECTION .data]

_MouseX dd 00000000h

_MouseY dd 00000000h

_MouseButtons dd 00000000h

The code is fairly straight-forward and similar to the code we

created for handling real-mode interrupts. To set up the callback, we

set the pm_offset member of the info struct and pass it to the routine

allocation function (you know, the one with the excessively long name).

The function creates a wrapper and passes the wrapper's entry points

back in the rm_segment and rm_offset in the info struct.

You might have noticed that it also requires a global variable (this

is important: you can't give it any other type of variable). The function

then uses this variable and passes a pointer to it to your function. If

you write your function correctly, you can save yourself a lot of trouble

by passing it to a struct internal to your asm function. Be careful with

this, though, it could change at any time and break your programs.

To access the register struct, just load esi from [ebp + 8] and then

use [esi + n] to access the registers. You'll need to count the offset

manually from DPMI.H, but don't worry too much, there aren't very many

members.

4.8 - Doubleword-aligned accesses

=---------------------------------------------------

In some cases, on 386 machines an up, accessing memory aligned to a

word or double-word boundary is faster than an unaligned access. On higher-

end machines, double-word boundaries offer the greatest benefit. In NASM,

we can tell the assembler that we want to align an entire program segment to

a double-word boundary easily:

[segment .text ALIGN=4]

How can we tell the assembler that we want to align data or functions

to a double-word boundary? It's actually quite simple. Using the assembler

variables "$$" (the start address of the current segment), "$" (the address

of the current opcode) and the "times" directive, we can create a statement

like so:

times ($$ - $) & 3 nop ; Align the next instruction/data to

; a double-word boundary, assuming

; segment is aligned to double-word

It seems fairly lengthy, and indeed, it is. If you have NASM version

0.94 or later, however, the macro facility comes in handy:

%define align times ($$ - $) & 3 nop

Now if you want to align any of your data/procedures, just use the

align keyword as if it were a directive. Make sure that you put it before

the label, or else you'll end up jumping into the nop's and slowing down

your program:

align

_PutPixel__FUiUiUi:

To see this in action, load up the INTTEST.ASM file and add the %define

line listed above to the top of the file and our new "align" keyword before

each of the functions. Also add the keyword before the definition of the

VideoRAM variable:

align

VideoRAM dw 0000h

Add "ALIGN=4" to each of the segment definitions as well:

[segment .text ALIGN=4]

[segment .data ALIGN=4]

Compile the program and then load it into a debugger (FSDB works well

for this). Trace through the program up to the InitGraphics() function call

and step into the function. Go back a few bytes and notice how there are a

few nop's before the actual function. Also look at the starting address of

the function. It'll end in either 0, 4, 8, or C, meaning it's double-word

aligned. If you want, remove the "align" macros from the file and

recompile. Notice how the function aren't aligned anymore. Neat, huh?

=---------------------------------------------------

5.0 - Contacting the author

=---------------------------------------------------

5.1 - Closing comments

=---------------------------------------------------

This document is still in an unfinished state, so there may be some

errors (glaring or otherwise), omissions or misinformation. If you happen

to stumble across any of these (even typos), feel free to send me email to:

mmastrac@acs.ucalgary.ca

5.2 - Getting DJGPPASM.DOC

=---------------------------------------------------

The latest public version of this document is always available at:

http://www.ucalgary.ca/~mmastrac/djgppasm.doc

The examples created in the document are available in a separate

zip-file at:

http://www.ucalgary.ca/~mmastrac/djnasmex.zip

This manual compiled using MC v1.05 by Matthew Mastracci

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有