Wednesday, July 19, 2006

Visual Basic Secrets

Visual Basic Secrets

Visual Basic Secrets

Copyright� 2002 by Kevin Wilson


Introduction
Using Pointers In Visual Basic
VarPtr, StrPtr, and ObjPtr
ByRef / ByVal
AddressOf and Callbacks
Accessing "Hidden" API's


Introduction:

Visual Basic is called a "Rapid Application Development (RAD) Development Tool" because it was designed to take care of the Windows "ground work" for you, thus allowing you to concentrate on the important stuff like the program's functionality and documentation.

For example, when you open VB and add a standard "Form" to your project, there's A LOT that goes into putting that form on the screen when you hit "F5" to execute the program and simply display the form. You have to call the "CreateWindow" to actually create the Form and give it it's properties that make up it's interface. You then have to modify it's text font, forecolor, backcolor, device context, etc. by calling various Win32 API's. Lastly, you have to hook into the Windows messages that are being sent to the newly created form by subclassing it and then catching and processing each Windows messages properly via a "WindowProc" callback function. More complex interfaces require more complex object creation and handling functionality to be programmed into the form. C and C++ programmers actually have to create all that object creation, message handling, and object destruction code by hand (or have a template of it generated).

Visual Basic's ability to do the "basics" for you like this is a powerful thing to programmers who know how to correctly use VB as a development tool, but also puts a lot of power into the hands of people that don't know much about programming. Visual Basic is mocked by C/C++ programmers because of this. They say, "Anyone can develop with VB, but it takes a real programmer to develop with C/C++." I say that the SMART programmer chooses Visual Basic because VB eliminates potential bugs in your object creation, message handling, and object destruction routines, VB offers easier and quicker handling of Windows events, VB gives you a more robust interface capabilities, VB gives you easier access to COM objects and third party controls, VB is easier to read because it is very close to reading English where C/C++ is VERY cryptic, VB allows you easy access to the Win32 API (which gives the programmer the ability to tap into the power of Windows), and on top of ALL THAT... Visual Basic has the ability to hook into the power and speed of C/C++ via components, libraries, and other code written in C/C++. Heh... where's the bragging rights now? :)

Here's the thing though... even VB programmers that have been in the industry for years don't realize the real power of VB because they don't grasp (or realize) a few key concepts and functionalities that VB offers. These concepts aren't taught, or at least are not emphasized the way they should, so I call them "VB SECRETS".

^ TOP ^

Using Pointers In Visual Basic:

I was once asked in a job interview a question that I now realize was a TRICK QUESTION. The question was, "Does Visual Basic have or use 'pointers' ?" The obvious answer to anyone that uses Visual Basic is "NO". You don't see pointer declarations and macros in VB like you do in C/C++... and that's what I think the interviewer was getting at. She accepted my answer with that reasoning. However, the correct answer should have been "YES".

Visual Basic (like just about every other programming language) does use pointers... EXTENSIVELY. The difference is, Visual Basic hides them from you whenever possible, or calls them something different so as to not burden you with the formalities and protocols required when using them.

I will proceed to explain how you can use pointers to access information held in variables directly (VarPtr / StrPtr / ObjPtr), pass information to functions by pointers (ByRef / ByVal), and retrieve and pass pointers to functions (AddressOf).

^ TOP ^

VarPtr, StrPtr, and ObjPtr:

The VB Functions "VarPtr" (Variable Pointer), "StrPtr" (String Pointer), and "ObjPtr" (Object Pointer) are UNDOCUMENTED, UNSUPPORTED functions that Microsoft has made available in Visual Basic 5.0, and 6.0. These functions (along with many others) are no longer available in VB.net. These functions allow you to get the address in memory where VB variables (pointers) are, as well as the address in memory where the actual data that the variables point to are. The reason these functions are so useful is because if you know the memory address of data, you can manipulate it, copy it, or pass it around directly instead of relying on VB to do it for you. This is MUCH faster and (in some cases) gives you the ability to do things that VB on it's own can't do.

This is what Microsoft's MSDN says about "VarPtr":

This function can be used to get the address of a variable or an array element. It takes the variable name or the array element as the parameter and returns the address. However, you should be aware that unlocked Dynamic Arrays may be reallocated by Visual Basic, so you must be very careful when you use VarPtr to get the address of an array element.

The following example gets the address of a variable:

Dim lngVariableAddress As Long
Dim dblMyVariable As Double
lngVariableAddress = VarPtr(dblMyVariable)

This example gets the address of the fourth element of an array:

Dim lngElementAddress As Long
Dim lngArrayOfLongs(9) As Long
' The following will get the address of the 4th element in the array
lngElementAddress = VarPtr(lngArrayOfLongs(3))

Limitations: The VarPtr function cannot be used to get the address of an array...

This is what Microsoft's MSDN says about "StrPtr":

Strings in Visual Basic are stored as BSTR's. If you use the VarPtr on a variable of type String, you will get the address of the BSTR, which is a pointer to a pointer of the string. To get the address of the string buffer itself, you need to use the StrPtr function. This function returns the address of the first character of the string. Take into account that Strings are stored as UNICODE in Visual Basic.

To get the address of the first character of a String, pass the String variable to the StrPtr function.

Example:

Dim lngCharAddress As Long
Dim strMyVariable As String
strMyVariable = "Some String"
lngCharAddress = StrPtr(strMyVariable)

You can use this function when you need to pass a pointer to a UNIOCODE string to an API call.

This is what Microsoft's MSDN says about "ObjPtr":

ObjPtr takes an object variable name as a parameter and obtains the address of the interface referenced by this object variable.

One scenario of using this function is when you need to do a collection of objects. By indexing the object using its address as the key, you can get faster access to the object than walking the collection and using the Is operator. In many cases, the address of an object is the only reliable thing to use as a key.

Example:

objCollection.Add MyObj1, CStr(ObjPtr(MyObj1))
...
objCollection.Remove CStr(ObjPtr(MyObj1))

Note that the "Limitations" at the end of the information about "VarPtr", it said that you can't use VarPtr to get the address of an array. That's true... to a point. You can't pass the variable "MyArray" to it (because VB stores arrays in an OLE object called a "SafeArray"), but if you get the address of the first element of the array "MyArray(0)", you have the address of the whole array because arrays elements are stored in memory contiguously (in numeric order from the first to the last). So if a Win32 API, or a C/C++ function asks for a pointer to a byte array, like this:

Option Explicit
Private Type POINTAPI
X As Long
Y As Long
End Type


'BOOL Polyline(
' HDC hDC, // handle of device context
' CONST POINT *lpPT, // address of array containing endpoints
' int cPoints // number of points in the array
');

Private Declare Function Polyline Lib "GDI32.DLL" (ByVal hDC As Long, _

ByRef
lpPT As Any
, ByVal cPoints As Long) As Long

You could call it like this:

Private Sub Form_Load()
Dim ThePoints() As POINTAPI
Me
.AutoRedraw = True
Me
.Visible = True
Me
.Move 0, 0, Me.Width, Me.Height
ReDim ThePoints(1 To 5) As POINTAPI
ThePoints
(1).X = 0: ThePoints(1).Y = 0
ThePoints
(2).X = 100: ThePoints(2).Y = 0
ThePoints
(3).X = 100: ThePoints(3).Y = 100
ThePoints
(4).X = 0: ThePoints(4).Y = 100
ThePoints
(5).X = 0: ThePoints(5).Y = 0
If Polyline(Me.hDC, ByVal VarPtr(ThePoints(1)), 5) = 0 Then Debug.Print "FAILED!"
Me
.Refresh
Erase ThePoints
End Sub

NOTE: Be careful about storing pointers to dynamic arrays because when arrays are reallocated, resized, or redim'ed... it is very possible that you'll have a completely new memory address for the actual data.

For more information on VarPtr, StrPtr, and ObjPtr, see the following:

http://support.microsoft.com/default.aspx?scid=kb;en-us;Q199824
http://msdn.microsoft.com/library/en-us/dnw32dev/html/ora_apiprog6_topic1.asp
http://msdn.microsoft.com/library/en-us/dnovba00/html/LightningStrings.asp
http://msdn.microsoft.com/library/en-us/dnovba01/html/Lightweight.asp

^ TOP ^

ByRef / ByVal:

By far the biggest problem VB programmers run into while working with Win32 API's (or any exported C/C++ function for that matter) is correctly passing the required parameters to the function. Inserting a "ByRef" where a "ByVal" should've been (or visa versa), or passing a value or variable when the function was expecting a pointer can be the one difference between the function being called working perfectly or causing Windows to crash and burn. Understanding how to pass parameters correctly takes an understanding of how Windows programs work with "calling stacks" and memory allocation between the calling program and the function being called.

First of all, lets discuss what a "call stack" is and how it relates to memory allocation when passing parameters to a function. The "call stack" is simply a spot in memory where the variables and values being passed to and from a function are stored. It's called a "stack" because parameter values follow one after the other in memory and are accessed with that assumption. Because of this, parameters are in a way "stacked on top of eachother" to make up all the information being given to the function. When a parameter is added to a function's call stack, it is said to be "pushed" onto the call stack. When a parameter is removed (or cleaned up) from a function's call stack, it is said to be "popped" off the call stack. The terms "stack", "push", and "pop" are assembler terms (yes, we are that low-level at this point) and if you were to decompile a program or DLL into assembly, you'd see lines with the words "push", "pop", etc.

When Visual Basic calls a Win32 API (or any exported C/C++ function), it expects the function to use the "Standard Calling Convention" ( __stdcall ) as apposed to the default C/C++ calling convention ( __cdecl ). This means that when the function is called, parameters are passed into memory (or pushed onto the stack) from right to left, and the function being called is responsible for cleaning up the memory passed to it (or popping the memory passed off of the stack). If you try to call an exported function that is declared with any other calling convention but __stdcall, Visual Basic will not know how to handle the stack and parameters being passed back and forth so you will get a message from VB saying "Bad DLL Calling Convention".

For a more in-depth and advanced explanation of how memory is allocated and deallocated when calling parameters, and what call stacks are and how they work in Windows, I strongly recommend reading an EXCELLENT book by Dan Appleman (Desaware) called "Dan Appleman's Win32 API Puzzle Book and Tutorial for Visual Basic Programmers".

Now lets back up a little and get out of the inner workings of Windows memory and get back to working with VB. When you call a function, you can pass parameters to it in one of two ways. You can pass it an explicit value that you want the function to take and work with, or you can pass it a pointer to information already present in memory. When you're passing simple information like numbers, sizes, flags, etc. you want to pass the information in ByVal (meaning By Value) because you want it to take the value of what you are passing, not the memory address of where that value is currently being held. Now when you want to pass more complex data to a function like a data type, an array of values, or an object reference, you need to pass a reference (or pointer) to the function telling it where in memory the data is. This is done by specifying the ByRef (meaning By Reference) keyword. The function will then go to that point in memory and read the data that applies. The exception to this is when you pass strings to Win32 API calls (or any C/C++ exported function). Always pass strings ByVal to API's (unless you're passing a string array... in which case you'd use ByRef, or just pass the first element of the array ByVal).

So at this point you're saying, "I already know about passing parameters ByRef/ByVal". Yes, but did you realize that what you're doing when you pass "ByRef" is passing a pointer? If you take that concept a step further, you can do things like make function interfaces more generic (thus opening them up for more broad application) by altering the "ByRef" to "ByVal" and passing an explicit pointer to the data you want to pass. So instead of declaring your function like this:

Option Explicit

Private Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type

'int FillRect(
' HDC hDC, // handle to device context
' CONST RECT *lpRC, // pointer to structure with rectangle
' HBRUSH hBR // handle to brush
');

Private Declare Function FillRect Lib "USER32.DLL" (ByVal hDC As Long, _
ByVal lpRC As Long, ByVal hBR As Long) As Long
Private Declare Function CreateSolidBrush Lib "GDI32.DLL" (ByVal crColor As Long) As Long
Private Declare Function DeleteObject Lib "GDI32.DLL" (ByVal hObject As Long) As Long

Private Sub Form_Load()
Dim hBrush As Long
Dim MyRect As RECT

Me.AutoRedraw = True
Me.Visible = True
Me.Move 0, 0, Me.Width, Me.Height
With MyRect
.Top = 0: .Left = 0: .Right = 100: .Bottom = 100
End With

hBrush = CreateSolidBrush(vbRed)
If hBrush = 0 Then Exit Sub
If FillRect(Me.hDC, VarPtr(MyRect), hBrush) = 0 Then Debug.Print "FAILED!"
Me.Refresh
DeleteObject hBrush
End Sub

If you think about it, this gives you all kinds of options when declaring functions and parameters. You're not restricted anymore to the exact variable type. You could make EVERYTHING "Long" variable types and pass pointers around to everything (as long as you were careful about how you did it). So you're having trouble passing that monster custom type around, FORGET ABOUT IT... pass it with a pointer. So you're having problems passing that object around, FORGET ABOUT IT... pass it with a pointer. VB 5.0 won't let you return variable arrays (or funky types and objects) as the return type for a function, FORGET ABOUT IT... pass back a long value representing where the array is in memory and use the CopyMemory API to copy it down into a local array. See where I'm going with this? :)

If you just said, "NO"... using VarPtr, StrPtr, and ObjPtr in conjunction with ByRef and Byval allows you to pass around data in ANY format if you know what you're doing.

^ TOP ^

AddressOf and Callbacks:

The "AddressOf" operator is all about callbacks. "But what is a call back?" you ask. A callback is the Windows equivalent of a VB "Event". In fact, VB events (on a very low level) are just about always triggered by callback functions that catch the original event in the form of a Windows Message. Callbacks are most often seen within the Win32 API (and other C/C++ code) where notification of user and/or Windows activity is required or desired within your application. You don't see callbacks within VB much because VB handles messaging and notification via "Events", which are much easier and safer to deal with compared to callbacks and all that goes into them.

Lets say that you wanted to receive notification of EVERY message that Windows is sending to a Form within your project (even ones that you'd never use), along with a few custom messages that may be sent to your Form via some other API call(s). What you would do is setup a callback function that is recognized by Windows ("WindowProc") and then tell Windows (via the "SetWindowLong" API) to send all of it's messages meant for your Form to your callback function instead, so you can inspect them and/or react to them... and then send them on their way (via the "CallWindowProc" API). Doing this is called "Sub-Classing" and is a very powerful (but at the same time very dangerous) technique that you can use to redraw your Form, it's menus, and/or it's contents in a custom manner (or whatever you want to do with your Form).

There two draw-backs to using "AddressOf":

1) You can only retrieve the address to a function or sub (public or private) contained within a Standard VB Module. There's no way around this.

2) It can only be called as part of a parameter list to a function or sub. The way to get around it is like this:

Option Explicit

Public Sub Main()
Dim lngProcAddress As Long
lngProcAddress = GetProcAddress(AddressOf WindowProc)
End Sub

Public Function GetProcAddress(ByVal lngAddressOf As Long) As Long
GetProcAddress = lngAddressOf
End Function

Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
' <>
End Function

You'll notice that we pass the "AddressOf" with the name of the function we want to get the address (memory pointer) of to the function "GetProcAddress" which simply returns back that value. Simple concept and is very effective. The addresses of functions and subs doesn't change so you could store the address of the functions and subs you want to reference this way so you don't have to repeatedly call AddressOf, etc.

"So lets see it in action!" you say... OK!

Here's an example of "sub-classing" as mentioned above:

Option Explicit

Private Const GWL_WNDPROC = (-4)
Private lngPrevProc As Long
Private lngHWND As Long

Private Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA" _

(ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare Function CallWindowProc Lib "USER32.DLL" Alias "CallWindowProcA" _

(ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long

' This is the CALLBACK function that receives the messages for the specified hWnd
Private Function WindowProc(ByVal hWnd As Long, _

ByVal uMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
' Display the messages and their information in the IMEDIATE window
' NOTE: You can find out what messages are being passed by comparing the value of

' "uMsg" to Windows Messages (WM_*) constant values defined in the WINUSER.H file
Debug.Print _

"hWnd=" & hWnd & ", uMsg=" & uMsg & ", wParam=" & wParam & ", lParam=" & lParam

' Forward on the messages to where they were originally supposed to go. This MUST
' here or the window will become unresponsive because it will stop recieving messages
WindowProc = CallWindowProc(lngPrevProc, hWnd, uMsg, wParam, lParam)
End Function

' This function starts a new sub-classing
Public Function Subclass_Start(ByVal hWnd As Long) As Boolean
' Stop any previous sub-class
If Subclass_Stop = False Then Exit Function
' Attempt to start a new sub-class
lngPrevProc = SetWindowLong(hWnd, GWL_WNDPROC,
AddressOf WindowProc)
If lngPrevProc <> 0 Then
lngHWND = hWnd
Subclass_Start = True
End If
End Function

' This function stops any existing sub-classing
Public Function Subclass_Stop() As Boolean
' If no previous sub-class was started, just exit
If lngPrevProc = 0 Or lngHWND = 0 Then
Subclass_Stop = True
Else
' Set the message handling procedure back to what it originally was
If SetWindowLong(lngHWND, GWL_WNDPROC, lngPrevProc) <> 0 Then
Subclass_Stop = True
End If
End If
' Clear the variables used
lngPrevProc = 0
lngHWND = 0
End Function

Option Explicit

Private Sub Form_Load()
Subclass_Start Me.hWnd
End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
Subclass_Stop
End Sub

Here's an example of "enumeration", which is a VERY popular way for Windows to give you back information about "information lists" (like a list of all windows, a list of all the objects on a window, a list of all the installed fonts, etc):

Option Explicit

Private lngWinHandle() As Long
Private lngWinHandleCount As Long

Private Declare Function EnumWindows Lib "USER32.DLL" (ByVal lpEnumFunc As Long, _

ByVal lParam As Long) As Long

' This is the CALLBACK that enumerates through all windows in the current desktop
Private Function EnumWindowsProc(ByVal hWnd As Long, ByVal lParam As Long) As Long
' Incrament the array of window handles
lngWinHandleCount = lngWinHandleCount + 1
ReDim Preserve lngWinHandle(1 To lngWinHandleCount) As Long
' Add the information to the array
lngWinHandle(lngWinHandleCount) = hWnd
' Tell the function to keep going
EnumWindowsProc = 1
End Function

Public Function GetAllWindows(ByRef Return_Handles() As Long, _

Optional ByRef Return_WinCount As Long) As Boolean
' Clear any previous information
Erase lngWinHandle
lngWinHandleCount = 0
' Start enumerating through the windows
If EnumWindows(
AddressOf EnumWindowsProc, 0) <> 0 Then
Return_Handles = lngWinHandle
Return_WinCount = lngWinHandleCount
GetAllWindows = True
End If
Erase lngWinHandle
lngWinHandleCount = 0
End Function

Option Explicit
Private Sub Form_Load()
Dim lngWindows() As Long
Dim lngWindowsCount As Long
Dim lngCounter As Long
Dim strWindows As String
If GetAllWindows(lngWindows, lngWindowsCount) = True Then
If lngWindowsCount > 0 Then
For lngCounter = 1 To lngWindowsCount
strWindows = strWindows & " " & lngWindows(lngCounter) & Chr(13)
Next
End If
Me.AutoRedraw = True
Me.Print strWindows
End If
Erase lngWindows
End Sub

^ TOP ^

Accessing "Hidden" API's:

This part is definately the most "secret" of all the secrets decribed on this page. There are indeed MANY hidden Win32 API calls in Windows... the trick is to find them and find out how to call them because Microsoft sure won't tell you.

"But why hide them?" you ask? Because Microsoft adds extra functionality to the API that only they can gain access to. This gives their products an advantage when (running under Windows) over everyone else's because only they have access to more powerfull functionality, faster functionality, or extra functionality via these hidden API calls when everyone else has to make do with the regular, documented functionality exposed by Windows and the documented in the MSDN. "Unfair advantage" you say? DEFINATELY! But who ever said that Microsoft does business fairly... or ethically for that matter! These kinds of business practices are what are constantly keeping Microsoft in court and on newspaper headlines.

"What kind of hidden API's are out there, and how do I find out what they are and how to use them?" you ask? EXCELLENT question, and that's why I've included this here. There are many web pages out on the internet dedicated to finding these hidden API's and exposing their functionality to "level the playing field" and give the more cool functionality to developers like you and me! Here's a few good web pages on this:

http://www.geocities.com/SiliconValley/4942/index.html
http://www.users.qwest.net/~eballen1/nt.sekrits.html
http://www.mvps.org/vbnet/code/shell/undocshelldlgs.htm
http://www.mvps.org/vbnet/code/shell/undocformatdlg.htm
http://www.mvps.org/vbnet/code/shell/undocchangeicondlg.htm
http://www.mvps.org/vbnet/code/shell/undocshpaths.htm
http://www.ercb.com/ddj/1992/ddj.9211.html

You can find a few of these "hidden API's" in the modCOMDLG32.bas module under the "VB Standard Modules". They look like this:

Public Declare Function DLG_FindFile Lib "shell32.dll" Alias "#90" _
(ByVal pidlRoot As Long, ByVal pidlSavedSearch As Long) As Long
Public Declare Function DLG_FindComputer Lib "shell32.dll" Alias "#91" _
(ByVal pidlRoot As Long, ByVal pidlSavedSearch As Long) As Long

You'll notice that they are aliased by a number "#90", "#91", etc. These are called "Ordinal Numbers" and they are a way for you to expose API's through a DLL without exposing it's name. So if you wrote a function, and you wanted to use it but didn't want anyone else to, you could expose it by a number. This doesn't give anything away as to what it does or why it's there, but at the same time gives you access to it.

Sneaky, huh?! :)

WELL! That's all folks. If I think of any other "secrets" or neat "hidden" or "obscure" functionality within VB (or remember any that I meant to put here), I'll add them. Happy coding! =)

Tuesday, July 18, 2006

World of Game Hacking - Breaking the packet encrypting in Tibia 7.72

World of Game Hacking - Breaking the packet encrypting in Tibia 7.72


Breaking the packet encrypting in Tibia 7.72 PDF Print E-mail
Written by Arne Banan
Friday, 16 June 2006

Discuss this article on the forums. (5 posts)

This tutorial will show you how to use Tibia's own encryption routines from an injected
DLL, this will require som reversing and assembly coding, if you have not done any of
that before this tutorial probably isn't for you but I'll try to explain things as best I
can.This tutorial will not cover the packet format, mainly because I don't know it.
Neither will I include what encryption routine it uses or how to code it, if you want to
make an .EXE you can just rip the encryption routine and include it as inline asm in
your program...
This is the first tutorial I have EVER written, so bare with me Embarassed.

Tools used: WPE Pro 0.9a, OllyDbg 1.10 and MASM 8.2

stretch

First, let's see how these packets looks like, so hook up Tibia with WPE and start
capturing. I wrote some random letters in the chat and this is what I got:

wpe

The first two bytes are, quite clearly, the size of the data. But there are no letters in
the rest which means that it's probably encrypted, which further means that we got
some reversing to do.

Now, what would be the easiest way to find the encryption routine? Let's take a look
at the send()'s prototype from MSDN:

int send(
SOCKET s,
const char* buf,
int len,
int flags
);

Tibia will surely write to that buffer during or shortly after the encryption routine, let's
put a breakpoint on it.

Start Olly, open tibia.exe from Olly and log in, the first packet sent by Tibia is
unencrypted and probably exchanges the key so we should not break here. When
you're logged in open up the command plugin (it can be found at www.ollydbg.de)
and write "bp send" which puts a breakpoint on the first instruction of send() in
ws2_32.dll. Write something in the chat and watch Olly break. Watch the stack and
you'll see Olly's neatly formatted arguments:

0012E5BC 0052F255 /CALL to send from Tibia.0052F24F
0012E5C0 0000010C |Socket = 10C
0012E5C4 00719540 |Data = Tibia.00719540
0012E5C8 00000012 |DataSize = 12 (18.)
0012E5CC 00000000 \Flags = 0

So, the send data is at 00719540? Then let's check it, this is what I got:

00719540 10 00 EA D6 02 D5 6B 58 29 44 88 E5 25 48 ED DE .êÖ ÕkX)Dˆå%HíÞ
00719550 23 26 #&

Yours is probably different. These are the first 18 bytes, since that was all it's going to
send (DataSize = 12 (18.). We can see the unencrypted first two bytes and then the
rest of the packet. Strange though, I wrote something shorter when I captured the
packet with WPE but the size is still 10 in hex. It still makes sense that the first two
bytes is the size because all the bytes after the first two are 16 (10 in hex). So what
does this mean? It could be that it is some kind of padding to make all the packets a
certain length or that the encryption routine needs a certain length, it's something we
have to find out. But now, let's put a breakpoint on memory access. Write something
more in the chat and it breaks!

0050B5AC |. 881438 MOV BYTE PTR DS:[EAX+EDI],DL
0050B5AF |. 8B06 MOV EAX,DWORD PTR DS:[ESI]
0050B5B1 |. 8B56 08 MOV EDX,DWORD PTR DS:[ESI+8]
0050B5B4 |. 886C10 01 MOV BYTE PTR DS:[EAX+EDX+1],CH
0050B5B8 |. 8B46 08 MOV EAX,DWORD PTR DS:[ESI+8]
0050B5BB |. 8B8C24 D000000>MOV ECX,DWORD PTR SS:[ESP+D0]
0050B5C2 |. 83C0 02 ADD EAX,2
0050B5C5 |. 8946 08 MOV DWORD PTR DS:[ESI+8],EAX
0050B5C8 |. 5F POP EDI
0050B5C9 |. 5E POP ESI
0050B5CA |. 64:890D 000000>MOV DWORD PTR FS:[0],ECX
0050B5D1 |. 81C4 D4000000 ADD ESP,0D4
0050B5D7 \. C2 0400 RETN 4

Hmm... There's nothing at 00719540 yet and if you scroll up we can't see any
mathematical computations or anything of the like, which often shows that it's a
encryption routine, but it might be in one of those calls up there. Let's go back and
check there later if we don't find anything by running again.

00508CD0 /$ 8B4424 04 MOV EAX,DWORD PTR SS:[ESP+4] ; MOVs the first argument into EAX. The first argument happens to be 00719542, that's right after the packet size!
00508CD4 |. 8B08 MOV ECX,DWORD PTR DS:[EAX] ; MOVs four bytes from EAX (00719542) into ECX.
00508CD6 |. 8B50 04 MOV EDX,DWORD PTR DS:[EAX+4] ; MOVs four bytes from EAX+4 (00719546) into EDX.
00508CD9 |. 53 PUSH EBX
00508CDA |. 55 PUSH EBP
00508CDB |. 56 PUSH ESI ; Save som registers on the stack.
00508CDC |. 8B7424 14 MOV ESI,DWORD PTR SS:[ESP+14] ; MOVs the second argument into ESI.
00508CE0 |. 57 PUSH EDI ; Save EDI too...
00508CE1 |. 33C0 XOR EAX,EAX ; Nulls EAX
00508CE3 |. BF 08000000 MOV EDI,8
00508CE8 |> 8BDA /MOV EBX,EDX ; Start of a big loop full of XORs, ADDs, SUBs, SHRs and SHLs, probably the encryption.
; After the loop.
00508DEF |. 8B4424 14 MOV EAX,DWORD PTR SS:[ESP+14] ; MOVs the first argument into EAX, again.
00508DF3 |. 5F POP EDI
00508DF4 |. 5E POP ESI
00508DF5 |. 5D POP EBP ; POPs the registers off the stack.
00508DF6 |. 8908 MOV DWORD PTR DS:[EAX],ECX ; MOVs ECX to the first four bytes in EAX (00719542).
00508DF8 |. 8950 04 MOV DWORD PTR DS:[EAX+4],EDX ; MOVs EDX to the fours bytes at EAX+4 (00719546)
00508DFB |. 5B POP EBX ; POP EBX too...
00508DFC \. C3 RETN ; Return to the calling function.

void 00508CD0(char*,char*);

Well, my friend, I think we just stumbled upon the encryption routine! This function
takes the eight bytes from the first argument and does some long computations, some
using the second argument, on them an then puts them back. You can see that the
first eight bytes are now completely different than from before, it seems that the
encryption routine only encrypts eight bytes at a time which explains the packet
lengths. This is not a normal calling convention and the arguments are only MOVed
from the stack, not removed from it so the calling function must remove the
arguments from the stack after the call. If we want to use this function we need to
have a buffer with the data to be encrypted and the second argument which seems
to be some sort of key. Let's see in more detail what the second argument is by
following the RETN back to the calling function.

00508F60 /$ 8B4424 04 MOV EAX,DWORD PTR SS:[ESP+4] ; MOVs the first argument into EAX.
00508F64 |. 85C0 TEST EAX,EAX ; Checks if the argument is null.
00508F66 |. 74 0D JE SHORT Tibia.00508F75 ; If it is, return.
00508F68 |. 83C1 04 ADD ECX,4 ; Else add ECX with 4.
00508F6B |. 51 PUSH ECX ; PUSHes the second argument, the key, to the encryption function.
00508F6C |. 50 PUSH EAX ; PUSHes the first argument to the encryption function.
00508F6D |. E8 5EFDFFFF CALL Tibia.00508CD0 ; CALLs the encryption function.
00508F72 |. 83C4 08 ADD ESP,8 ; Removes the arguments from the stack by adding 8 to ESP.
00508F75 \> C2 0400 RETN 4 ; Returns to the calling function.

This didn't make the whereas of the key any clearer. But it shows that we have to
manually remove the arguments from the stack after calling the encryption function.
The first argument here is the buffer to be encrypted and ECX should contain the
address of the key - 4. You can use this function instead of the on at 00508CD4 if you
want to. Let's go to the calling function and check how ECX get it's value:

004CBEB0 > 8D4F 02 LEA ECX,DWORD PTR DS:[EDI+2]
004CBEB3 . 3BF1 CMP ESI,ECX
004CBEB5 . 0F8D B0000000 JGE Tibia.004CBF6B
004CBEBB . 8D96 40957100 LEA EDX,DWORD PTR DS:[ESI+719540] ; The address to the bytes to be encrypted are stored in EDX.
004CBEC1 . 52 PUSH EDX ; PUSHes the first argument.
004CBEC2 . B9 749D7100 MOV ECX,Tibia.00719D74 ; 00719D74 + 4 is the address to the key.
004CBEC7 . E8 94D00300 CALL Tibia.00508F60 ; CALLs the function before the encryption routine.
004CBECC . 83C6 08 ADD ESI,8 ; ADDs 8 to ESI to encrypt the next 8 bytes, or end the loop.
004CBECF .^EB DF JMP SHORT Tibia.004CBEB0 ; Continue looping.

We land in the middle of a long function, in a short loop. The first three instructions are
used to see if the necessary bytes are encrypted and it's time to end the loop. The
address to the key is hardcoded at 00719D74 and is increased by 4 in the function it
calls which makes the address to the key 00719D78.


Now we have all the information needed to encrypt a packet, the decryption routine
can be found the same way but by putting a brakpoint at recv and then a memory
breakpoint on that buffer. The decryption function is located at 00508E00 and uses
the same arguments as the encryption function at 00508CD0, the first argument
should be a pointer to the buffer and the second a pointer to the key. The functions
en/decrypts 8 bytes every function call so we need to add 8 to the pointer and call it
again until the whole buffer is encrypted. They don't remove the arguments from the
stack which means we have to ADD 8 to ESP after every a CALL or else we'll mess the
stack up.
We also want to try if our functions work by sending a packet to the server, but we
need to find the address to the socket for that. The address is DMA though, so put a
breakpoint on the send function using bp send again and make it brake. Press Alt + F9
to get back to tibia.exe and step through the RETN.

0052F240 . FF7424 0C PUSH DWORD PTR SS:[ESP+C] ; /Flags
0052F244 . FF7424 0C PUSH DWORD PTR SS:[ESP+C] ; |DataSize
0052F248 . FF7424 0C PUSH DWORD PTR SS:[ESP+C] ; |Data
0052F24C . FF71 04 PUSH DWORD PTR DS:[ECX+4] ; |Socket
0052F24F . FF15 0CB65500 CALL DWORD PTR DS:[<&WS2_32.#19>] ; \send
0052F255 C2 0C00 RETN 0C

And you get here:

004CBFBA . 8B0D 509D7100 MOV ECX,DWORD PTR DS:[719D50] ; MOVs the first four bytes from [00719D50] into ECX.
004CBFC0 . 8B11 MOV EDX,DWORD PTR DS:[ECX] ; MOVs the first four bytes from [ECX] to EDX.
004CBFC2 . 6A 00 PUSH 0 ; PUSHes the last argument to [EDX+18], which is also the last argument used in the call to send.
004CBFC4 . 83C6 02 ADD ESI,2 ; ESI is the size of the packet to send minus the first two bytes which are added here.
004CBFC7 . 56 PUSH ESI ; PUSHes the third argument to [EDX+18] and also to send.
004CBFC8 . 68 42957100 PUSH Tibia.00719542 ; PUSHes the data to be sent.
004CBFCD . FF52 18 CALL DWORD PTR DS:[EDX+18] ; CALLs the function before send, which resolves the socket from [ECX+4]

We should mimic this code when we want to send our packets, or just take the socket
from [ECX+4].

Now we can start coding our DLL, it will be really short, simple and just send a packet
making you say: "It works...". Here it is:

.486
.model flat, stdcall
option casemap :none

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\macros\macros.asm

includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib ; The usually includes and options...

.data
packetsize db 18h,00h ; The packet header with the size.
packet db 0Fh,00h,96h,01h,0Bh,00h,49h,74h,20h,77h,6Fh,72h,6Bh,73h,2Eh,2Eh,2Eh ; The main packet, you can easily find those by breaking at the encryption routine.
padding db 00,00,00,00,00,00,00 ; Padding to make our packet 24 bytes and thus encryptable if we didn't do this we would overwrite our code, I think...

.code
DllEntry proc hInst:DWORD, reason:DWORD, unused:DWORD ; The usually arguments passed to DllEntry...
.if reason == DLL_PROCESS_ATTACH ; Check if we're just getting attached using the .if macro.
push 00719D78h ; PUSH the address to the key.
push offset packet ; PUSH the first 8 bytes to be encrypted.
mov edi, 00508CD0h ; Load the functions address into EDI.
call edi ; CALL the encryption function.
add DWORD PTR[esp], 8 ; ADDs 8 bytes to the first argument to the encryption function.
call edi ; And CALL it again using the same arguments.
add DWORD PTR[esp], 8 ; ADDs 8 bytes to the first argument again.
call edi ; And CALLs it.
add esp, 8 ; Clear the stack of the arguments.
push 0 ; The last arguments to the send function.
push 1Ah ; The total size to send.
push offset packetsize ; The address to the buffer to send.
mov eax, DWORD PTR[00719D50h]
mov ecx, DWORD PTR[eax]
mov eax, DWORD PTR[ecx] ; Finds the address to the socket and the function to call.
call DWORD PTR[eax+18h] ; CALL the function used to send the packet.
.endif
ret ; Return.
DllEntry endp
end DllEntry

I think the comments are quite self explainable. Use a DLL injector to inject it, there is
one in the tools section by Max_Power. A simple DLL injection tool isn't very hard to
code though, I think you can get quite alot of info on it using Google.

Thanks goes too:
g3nuin3 & hunter for their great tutorial on breaking the packet enryption in Water
Margin, that's what got me going on breaking the Tibia packet enryption.
And all the members of WOGH!

That wasn't a lot of thanking... Undecided

>

Wednesday, July 12, 2006

test

Why no posts on the site?

BCB - VC compatibility : 探訪動態連結函式庫

探訪動態連結函式庫


探 訪 動 態 連 結 函 式 庫 (Dynamic Linking Libraries,DLLs)


打Windows 從 娘 胎 出 生 後 , 動 態 連 結 函 式 庫(Dynamic Linking Libraries,DLLs) 宛 如 這 個 新 生 兒 的 血 液 一 般 的 重 要 ,DLLs 一 直 扮 演 著Windows 的 基 石 這 個 角 色 。 到 了Win32 雄 霸 一 方 的世 紀 ,DLLs 的 威 風 更 是 不 減 當 年 , 幾 乎 所 有 的Win32 API 都 以隱 身 在DLLs 中 的 形 式 存 在 ,Windows 血 液 裡 流 的 盡 乎 都 是DLLs 。 在Windows 下DLL 通 常 是 以 『 副 檔 名 為DLL 的 檔 案 』 存 在 ,DLL 內 可 以 包 含 提 供 外 部 呼 叫 的 函 式 、 資 源 (Resource ) 以 及各 種 的 變 數 , 當DLLs 被 應 用 程 式 載 入 時 這 些DLLs 內 含 的 資料 都 會 變 為 應 用 程 式 所 屬 行 程 的 一 部 份 。 我 們 可 以 把DLL 當 成 一 個 函 式 庫 (Library ) , 但 因 為 還 要 能 夠 被 動 態 載 入, 這 個 函 式 庫 自 然 比 傳 統 的 函 式 庫 複 雜 些 。

DLL 一 二 說

DLLs 的 出 現 提 供 了 程 式 設 計 師 一 個 將 程 式 模 組 化 的 方法 , 別 於C++ 類 別 的 建 構 時 期 模 組 化 ,DLL 乃 是 執 行 時 期 模組 化 , 因 此 程 式 設 計 師 可 以 在 建 構 程 式 時 將 所 需 要 用 到的 函 式 分 門 別 類 的 製 造 成DLLs 的 形 式 。 但 為 什 麼 我 們 要 使用DLLs 呢 ?DLLs 充 其 量 不 就 你 把 程 式 碼 、 資 源 等 獨 立 到 另一 個 檔 案 裡 頭 去 , 到 底 有 什 麼 好 處 值 得 我 們 大 費 周 章 地把 部 份 程 式 寫 成DLLs 的 形 式 呢 ? 使 用DLL 的 好 處 大 致 可 以 如下 歸 類 :

1 、 有 效 率 的 重 複 使 用 程 式 碼

當 程 式 設 計 師 所 撰 寫 的 程 式 碼 一 多 起 來 , 必 然 地 會 發現 有 很 多 程 式 碼 是 在 做 相 同 的 事 情 , 通 常 當 程 式 設 計 師遇 到 這 些 重 複 的 程 式 碼 , 最 平 常 不 過 的 方 法 就 是 把 這 些重 複 的 程 式 碼 大 則 獨 立 成 『 函 式 』 (Function ) 小 則 獨 立成 『 巨 集 』 (Marco ) 。 但 是 , 當 這 些 函 式 不 單 單 只 在 單一 應 用 程 式 內 會 用 到 , 而 是 在 撰 寫 許 多 應 用 程 式 時 都 得用 到 時 , 這 些 常 用 的 函 式 通 常 就 會 被 製 作 成 函 式 庫 來 使用 , 例 如C 語 言 的Runtime Library (RTL ) ; 但 編 譯 器 在 編 譯 應用 程 式 遇 到 函 式 庫 時 , 會 把 這 些 隱 身 在 函 式 庫 裡 頭 的 函式 實 體 內 容 如 同 我 們 在 程 式 裡 頭 撰 寫 這 些 函 式 的 原 始 碼般 地 加 進 應 用 程 式 的 執 行 檔 中 , 也 就 是 說 當 你 所 用 的 函式 庫 越 多 時 , 你 的 執 行 檔 也 就 相 對 的 會 越 來 越 龐 大 , 這個 做 法 也 就 是 我 們 所 謂 的 靜 態 連 結 (Static Linking ) 。 因此 為 了 避 免 應 用 程 式 的 過 分 龐 大 , 有 人 提 出 了 動 態 連 結(Dynamic Linking ) 的 做 法 , 所 謂 動 態 連 結 就 是 提 供 了 一 個做 法 讓 我 們 不 需 要 把 應 用 程 式 的 執 行 檔 變 得 如 此 龐 大 ,但 一 樣 可 以 享 用 這 些 使 用 頻 率 高 的 函 式 。 也 就 是 說 這 些函 式 會 在 程 式 執 行 時 才 被 載 入 , 而 不 是 直 接 編 譯 在 執 行檔 中 。 這 樣 一 來 可 以 讓 我 們 更 有 效 率 使 用 這 些 函 式 。 但相 對 的 , 當 你 所 撰 寫 的 程 式 得 交 給 他 人 使 用 時 , 你 除 了得 把 你 所 編 譯 好 的EXE 檔 交 給 他 之 外 , 還 得 一 併 把 編 譯 好的DLLs 檔 交 給 他 , 否 則 執 行 起 來 一 定 會 產 生 不 可 預 期 的 錯誤 。

2 、 區 分 程 式 碼

依 據 先 前 的 第 一 點 , 若 有 朝 一 日 發 現 這 些 被 包 裝 在DLLs 之 中 的 函 式 的 實 作 方 法 有 點 錯 誤 或 是 發 現 有 更 好 的 做 法時 , 需 要 更 動 僅 只 有 部 份 的DLLs 原 始 碼 , 重 新 將 修 改 過 的DLLs 給 編 譯 後 就 可 以 達 成 更 新 程 式 的 目 的 , 至 於 應 用 程 式 端連 動 都 不 需 要 動 一 下 ; 當 然 了 , 前 提 是 這 些 個 修 改 過 的函 式 名 稱 與 傳 入 的 參 數 型 別 宣 告 都 不 能 夠 更 動 。 根 據 這些 個 特 性 , 咱 們 可 以 把 整 個 應 用 程 式 中 的 函 式 依 照 功 能或 目 的 分 類 , 並 將 這 些 分 類 好 的 函 式 組 合 成 許 多 個DLLs 模組 , 將 執 行 檔 給 分 割 城 數 個 小 檔 案 , 讓 這 些DLLs 模 組 分 工合 作 來 完 成 應 用 程 式 所 要 達 到 的 目 的 。 這 樣 一 來 , 對 於應 用 程 式 的 維 護 以 及 更 新 就 不 需 要 大 費 周 章 地 從 頭 再 編譯 一 次 了 , 僅 需 把 要 有 修 改 到 的DLLs 從 新 編 譯 就 可 以 達 成程 式 的 更 新 。

3 、 節 省 記 憶 體 的 使 用 量

先 前 提 到 :DLLs 的 載 入 是 在 應 用 程 式 執 行 時 才 被 載 入 ,甚 至 還 可 以 是 可 以 在 應 用 程 式 所 需 用 到 函 式 時 才 被 載 入。 此 外DLLs 還 有 一 個 很 重 要 的 特 性 : 若 不 同 的 應 用 程 式 但需 要 相 同 的DLL 中 的 函 式 時 ,DLL 僅 在 第 一 個 使 用 到DLL 的 應用 程 式 執 行 時 載 入 , 只 要 這 個 應 用 程 式 尚 未 結 束 而 其 他的 應 用 程 式 又 正 好 需 要 使 用 到 同 一DLL 中 的 函 式 時 ,DLL 不需 要 再 重 新 被 載 入 到 記 憶 體 中 就 可 以 供 第 一 個 應 用 程 式以 外 需 要 用 到DLL 的 應 用 程 式 使 用 , 一 直 到 沒 有 任 何 應 用程 式 使 用 這 個DLL 時 ,DLL 才 會 跟 著 最 後 一 個 使 用DLL 的 應 用程 式 一 起 從 記 憶 體 裡 頭 消 失 , 因 此 使 用DLLs 來 包 裝 常 用 的函 式 是 個 不 錯 能 夠 節 省 記 憶 體 與 系 統 資 源 的 做 法 。

4 、 將 程 式 推 向 國 際 舞 台

DLL 在 設 計 時 , 就 已 經 被 設 計 不 只 是 能 夠 放 入 函 式 而 已, 還 能 夠 被 放 入 許 多 的 資 源 (Resource ) , 如 : 可 以 放 入選 單 資 料 、 字 串 資 料 、 圖 形 資 料 等 等 … 。 也 因 此DLL 很 常被 拿 來 作 為 應 用 程 式 邁 向 國 際 舞 台 的 一 個 墊 腳 石 。 你 可以 在 應 用 程 式 被 執 行 時 檢 查 執 行 應 用 程 式 的 作 業 系 統 語言 版 本 , 之 後 把 當 地 語 言 版 本 的Resource DLL 給 載 入 , 讓 所有 的 文 字 及 畫 面 都 達 到 當 地 語 言 化 的 目 的 。 而 這 個 功 能在C++Builder 3 當 中 經 由Borland C++Builder 部 門 工 程 師 的 努 力 ,已 經 幫 我 們 把 這 些 煩 人 的 步 驟 給 簡 化 了 許 多 , 我 們 僅 須按 下 選 單 上 的New 並 選 擇Resource DLL Wizard , 並 將 將 需 要 更 改的 文 字 給 更 改 成 不 同 的 語 言 , 並 不 須 在 執 行 時 檢 查 執 行平 台 的 語 言 為 何 。 其 餘 煩 瑣 的 工 作 都 已 經 被Borland 工 程 師給 完 成 了 。 我 們 最 後 只 需 要 重 新 編 譯 這 個Resource DLL 並 附在 應 用 程 式 中 就 可 以 完 成 一 個 國 際 化 的 應 用 程 式 了 。

既 然DLLs 在Windows 上 頭 是 那 麼 的 重 要 且 使 用DLLs 還 有 那 麼多 的 好 處 , 當 然 值 得 咱 們 來 好 好 的 了 解 一 下 。 先 來 討 論DLLs 的 基 本 架 構 。

Import 還 是Export ?

我 們 已 經 知 道 當DLL 被 應 用 程 式 載 入 時 ,DLL 內 所 含 有 的資 料 都 會 成 為 應 用 程 式 所 屬 行 程 中 的 一 部 份 , 這 到 底 怎麼 辦 到 的 ? 其 實 在DLL 被 應 用 程 式 載 入 時 ,DLL 會 被 先 設 定一 個 基 底 位 址 (Base Address ) , 若 這 個 基 底 位 址 並 沒 有 和應 用 程 式 中 的 其 他 資 源 互 相 衝 突 , 則 這 個DLL 檔 會 被 映 射到 載 入 端 行 程 內 的 相 同 位 址 上 讓 載 入 端 應 用 。 那DLL 中 到底 有 多 少 資 訊 可 以 被 載 入 呢 ? 先 看 看 圖 一 :

圖 一 是 使 用C++Builder 內 附 上 的tdump.exe 工 具 列 出 一 個DLL 檔內 所 有 的Section ,tdump 是 個 非 常 好 用 的 工 具 , 之 後 使 用 次會 不 少 , 先 說 明 用 法 , 用 法 很 簡 單 :

tdump inputfile outputfile

這 樣 就 可 以 把inputfile 裡 頭 的 資 料 給 格 式 化 輸 出 到outputfile 裡 頭 , 當 然 了 , 你 的inputfile 得 是tdump 認 得 的 檔 案 :DOS 下的 執 行 檔 、PE 格 式 執 行 檔 ( 註1 ) 、.OBJ 檔 、.LIB 檔 , 其 餘的 檔 案 若 為 文 字 格 式 就 直 接 輸 出 , 而Binary 格 式 的 檔 案 就以HEX Dump 格 式 輸 出 。 此 外 , 在Microsoft Visual C++ 裡 頭 的dumpbin.exe 也 提 供 相 同 功 能 來 分 析 這 些 檔 案 。

圖 一 中 的 這 些Section 表 示 著 :

Section
包 含
意 義
.text 應 用 程 式 或DLL 的 程 式 碼 這 個section 包 含 了 一 般 性 的 程 式 碼 ,就 是 先 前 提 到 的 除 了 自 己 所 撰 寫 的 程 式 碼 外 還 有Runtime Library 的 程 式 碼 。
.data 具 有 初 始 值 的 資 料 這 個section 存 放 了 在 編 譯 時 期 就 已 經具 有 初 始 值 的 資 料 ; 包 括 了 全 域 變 數 (global variable ) 、靜 態 變 數 (static variable ) 以 及 ”Hello World ” 這 一 類 字 串等 等 … 。
.tls 執 行 緒 內 部 儲 存 空 間 thread local storage
.idata 輸 入 名 稱 表 這 個section 包 含 了 有 『 從 其 他DLLs 中輸 入 過 來 的 函 式 與 資 料 』 的 相 關 資 訊 。
.edata 輸 出 名 稱 表 這 個section 恰 好 與.idate 相 反 , 是 存 放了 由 此EXE 或DLL 輸 出 給 外 頭 使 用 的 函 式 與 資 料 的 相 關 資 訊。
.rsrc 資 源 若 你 使 用 過Microsoft Visual C++ 或 是Borland Resoure Workshop 來 觀 察 過EXE 或DLL , 你 所 看 到 的 那 些resource date 就 是 儲 存 在 這 個section 裡 。 也 就 是 編 譯 器 將 應 用 程 式 所 需要 用 到 的resource date 都 整 理 好 一 起 放 到 這 個section 裡 頭 。
.reloc 修 正 表 資 訊 這 個section 裡 頭 含 有 一 個base relocationsg 是 一 個 調 整 值 , 先 前 說 過 當 …. 會 , 但 若 無 法 載 入 到 預 設的 位 址 , 就 會 依 據 這 個 調 整 值 來 作 調 整 。

除 了 了 解 這 些section 之 外 , 你 還 必 須 知 道 的 另 一 個 觀 念是 所 謂 的 相 對 虛 擬 位 址 (Relative Virtual Address ,RVA) 。PE 格式 執 行 檔 中 有 許 多 資 料 的 位 址 都 是 以RVA 表 示 。 簡 單 的 來說RVA 是 某 一 項 資 料 從 檔 案 被 映 射 進 來 的 起 點 算 起 的 偏 移值 (offset ) 。 舉 個 例 子 , 我 們 說Windows 載 入 器 把 一 個PE 格式 執 行 檔 檔 映 射 到 虛 擬 位 址 空 間0x400000 處 , 如 果 在 此 執行 檔 中 有 一 個 函 式 的 函 式 指 標 起 始 於0x40C000 , 那 麼 這 個函 式 指 標 的RVA 就 是0x C000 :

虛 擬 位 址 (0x40C000 ) ─ 基 底 位 址 (0x400000 ) = RVA (0xC000 )

只 要 把 相 對 虛 擬 位 址 加 上 基 底 位 址 , 相 對 虛 擬 位 址 就可 以 被 轉 換 為 一 個 有 用 的 指 標 。 『 基 底 位 址 』(Base Address) 也 是 另 一 個 重 要 名 詞 , 通 常 基 底 位 址 是 用 來 描 述 被 映 射到 記 憶 體 中 的EXE 或DLL 的 起 始 位 址 。

另 外 圖 一 中 『Key to section flags 』 是 這 些section 的 屬 性 旗標 種 類 , 如 : 唯 讀 、 共 享 或 可 寫 入 等 等 … 每 個section 的 屬相 可 以 從 『Object table 』 最 後 一 欄 的 『Flag 』 中 看 出 。

了 解 這 些 基 本 知 識 後 , 再 回 頭 詳 細 看 一 看 在 圖 一 中 的輸 出 輸 入section , 我 們 已 經 知 道 , 製 作DLL 的 主 要 目 的 是 製造 一 個 模 組 化 的 函 式 或 資 料 供 其 他 的 程 式 應 用 , 而 這 種提 供 給 其 他EXE 和DLL 使 用 的 方 式 就 稱 為 輸 出 (export ) , 反之 若 取 用 其 他 的EXE 或DLL 中 的 函 式 , 就 稱 為 輸 入 。 在DLL 中, 你 可 以 輸 出 任 何 想 要 輸 出 的 資 料 , 如 函 式 、 類 別 (class ) 或 是 資 源 等 等 … , 我 們 把 重 點 放 在 輸 出 輸 入 函 式 的 部份 , 我 們 先 來 觀 察 一 般 的 輸 出 函 式 :

一 樣 可 以 使 用 先 前 提 到 的tdump 來 觀 察DLL 中 的 輸 出 表 格, 由 圖 二 中 可 以 看 到 輸 出 函 式 表 格 包 含 了Ordinal 、RVA 以 及Name 三 個 欄 位 表 示 。Name 就 是 輸 出 的 函 式 名 稱 而RVA ─ 相 對 虛 擬位 址 在 前 頭 已 經 介 紹 過 了 , 至 於Ordinal 則 是 輸 出 表 格 中 輸出 函 式 的 序 號 。 這 些 輸 出 函 式 透 過 這 個 表 格 上 的 函 式 名稱 與 函 式 序 號 讓 外 界 認 得 。 當 載 入 端 最 初 在 載 入DLL 時 並不 知 道DLL 內 的 輸 出 函 式 的 正 確 位 址 只 知 道 函 式 的 序 號 與名 稱 , 但 在 動 態 連 結 的 過 程 中 會 建 立 起 一 個 連 連 看 表 格將 載 入 端 的 函 式 呼 叫 與 被 載 入 端 內 的 函 式 正 確 位 址 給 連結 起 來 。

那 我 們 要 怎 樣 才 能 夠 達 成 輸 出 的 動 作 , 其 實 很 簡 單 ,你 只 要 在 你 的 應 用 程 式 中 需 要 輸 出 的 函 式 前 頭 加 上 :__declspec(dllexport) 即 可 , 如 :

__declspec(dllexport) void Function(void);

這 樣 一 來 就 會 把Function(void) 這 個 函 式 給 放 到 輸 出 表 格上 頭 了 。

先 前 還 有 提 到 還 可 以 將C++ 類 別 透 過DLL 來 輸 出 , 可 以 的一 樣 是 加 上__declspec(dllexport) , 如 :

class __declspec(dllexport) __stdcall MyClass : public TObject{ … …};

當 我 們 在 看 輸 出 表 格 時 , 會 發 現 函 式 的 輸 出 表 格 前 頭好 像 有 個 如 圖 三 一 般 的 資 料 :

這 就 是 函 式 的 輸 入 表 格 , 這 裡 列 出 來 的 是USER32.DLL 裡 頭被 我 們 使 用 到 的 函 式 。 一 樣 的 怎 樣 建 立 輸 入 表 格 呢 ? 一般 的Win32 API 都 已 經 在 其 所 屬 的Header File 裡 頭 定 義 好 了 ,我 們 只 需 加 上#include 就 可 以 安 心 使 用 了 , 而對 於 自 行 打 造 的DLL 中 的 輸 出 函 式 我 們 在 載 用 此DLL 的 載 入端 就 得 在 函 式 的 宣 告 前 面 加 上__declspec(dllimport) , 如 :

__declspec(dllimport) void Function(void);

目 的 是 告 訴 編 譯 器Function(void) 這 個 函 式 是 由 外 部 輸 入的 。

Name Mangling

嗯 ! 經 過 了 先 前 的 說 明 , 讀 者 們 應 該 知 道 若 要 在DLLs 中將 函 式 輸 出 , 僅 需 要 在 函 式 的 宣 告 中 加 上__declspec(dllexport) 即 可 , 若 要 載 入 別 的DLLs 中 的 函 式 則 須 在 函 式 的 宣 告 中 加上__declspec(dllimport) , 但 這 僅 止 於C 編 譯 器 , 在C++ 編 譯 器 裡頭 是 行 不 通 的 , 怎 麼 說 呢 ? 就 拿 一 個 多 載 (overloading )的 例 子 來 說 , 如 果 函 式 的 名 稱 都 相 同 ( 當 然 所 傳 的 參 數型 別 不 同 ) , 編 譯 器 應 該 會 如 何 處 理 ? 到 底 那 個 才 是 我們 真 正 使 用 到 的 函 式 呢 ? 其 實 在 編 譯 器 做 編 譯 動 作 時 ,對 這 些 同 名 的 函 式 都 動 了 點 手 腳 讓 同 名 的 函 式 偷 偷 地 變成 不 同 名 稱 , 以 下 面 同 名 的 三 個 函 式 為 例 :

int Func(int X);

int Func(float X);

void Func(double *d);

使 用C++Builder 3.0 所 編 譯 出 來 的 函 式 名 稱 為 : ( 註2 )

@Func$qf

@Func$qi

@Func$qpd

而 使 用Visual C++ 6.0 所 編 譯 出 來 的 函 式 名 稱 為 : ( 註3 )

?Func@@YAHH@Z

?Func@@YAHM@Z

?Func@@YAXPAN@Z

編 譯 器 這 個 偷 偷 修 改 函 式 名 稱 的 行 為 稱 為 『name mangling 』 , 但 除 了 函 式 名 稱 被 改 變 外 你 是 否 發 現 還 現 另 一 件 很嚴 重 的 事 情 , 就 是 同 的 編 譯 器 竟 然 有 著 不 同 的name mangling 做 法 , 這 表 示 著 若 咱 們 若 使 用Borland C++Builder 編 譯 器 來 開發 應 用 程 式 時 將 無 法 使 用 一 個 經 由Microsoft Visual C++ 所 編譯 器 完 成 的 函 式 庫 。 此 外 ,Naming Mangling 的 作 用 不 止 於 多載 的 函 式 上 ,C++ 程 式 中 所 有 的global 函 式 以 及class 中 所 有的 成 員 (members ) 都 會 被name mangling 這 個 動 作 給 整 型 一 下。 那 這 樣 不 就 沒 戲 唱 了 ! 若 要 在C++Builder 下 使 用Visual C++ 所 編 譯 的DLLs 豈 不 都 得 擁 有DLLs 的 原 始 碼 才 能 囉 ? 其 實 不然 , 有 方 法 可 抑 制name mangling 的 作 用 , 就 是 在 函 式 的 宣 告錢 加 上extern “C ” 這 個 修 飾 詞 , 強 制 將 函 式 以C 語 言 的 行台 重 現 , 而 非 以C++ 語 言 的 形 態 出 現 。 但 是 要 注 意 , 多 載函 式 可 不 能 加 上extern ”C ” 這 個 修 飾 詞 , 因 為 這 個 會 造 成一 堆 名 稱 相 同 的 函 式 , 若 你 硬 是 要 使 用extern “C ” 在 多 載含 上 , 編 譯 器 一 定 會 送 你 一 個ERROR ( 如 下 ) 做 獎 品 。

in C++Builder :

[C++Error]Project1.cpp(13): Only one of s set of overloaded functions can be “C ”.

in Visual C++

error C2733 : Second C linkage of overloaded function ‘Func ’ not allowed

因 此 若 拿 原 先 多 載 的 例 子 給 加 上extern “C ” :

extern “C ” int Func(int X);

則 會 被 編 譯 器 給 編 譯 成 :

in Visual C++ :

_Func

in C++Builder :

_Func

似 乎 兩 個 編 譯 器 已 經 達 成 了 一 致 的 輸 出 。 嗯 ! 這 樣 一來 就 可 以 把DLLs 互 相 使 用 了 , 不 不 不 ! 還 沒 有 那 麼 簡 單 ,還 有 一 個 重 要 的 議 題 『Call Conventions 』 , 不 過 這 個 問 題 在此 先 不 提 , 放 到 後 頭 在 提 , 先 談 談 怎 樣 使 用C++Builder 來 建立DLLs 吧 !

 

建 立DLL

使 用C++Builder 來 建 立DLL 並 不 是 什 麼 難 事 , 只 需 要 按 下 幾下 滑 鼠 的 左 鍵 即 可 。 在C++Builder 下 建 立DLLs 大 致 分 成 兩 種方 法 , 先 按 選 單 上 的File|New 後 會 出 現New Items 對 話 盒 (Dialog ) ( 圖 四 ) :

  1. 選 擇New Items 對 話 盒 中 的Console Wizard 選 項 按 下OK 後 再 選 擇DLL 即 可 。 ( 圖 五 )
  2. 選 擇New Items 對 話 盒 中 的DLL 那 個 選 項 按 下OK 即 可 。
方 法 一 是 建 立 一 個 標 準 的DLL Project , 不 允 許 使 用 任 何VCL 類 別 , 而 方 法 二 所 打 造 出 來 的DLL 則 是 可 建 立 包 含 了VCL 類別 的DLL ( 當 然 你 也 可 以 不 使 用VCL 類 別 ) , 而 這 兩 個 功 能在Visual C++ 中 差 可 比 擬 的 是Win32 Dynamic-Link Library 與MFC AppWizard(dll) 。

按 完OK 或Finish 後 你 會 看 到DLL Project 與 些 許 程 式 碼 的 產 生, 這 段 由C++Builder 自 動 產 生 的 程 式 碼 中 分 兩 大 部 分 , 第 一部 份 是 個 很 長 一 串 的 註 解 , 最 後 就 是 所 謂 的DLL 進 入 點 。先 來 了 解 這 一 長 串 的 註 解 , 由 方 法 一 與 方 法 二 製 造 出 來的DLL Project 註 解 有 點 不 相 同 , 不 過 內 容 大 致 上 差 不 多 ,內 容 如 是 說 : 如 果 我 們 的DLL 內 使 用 到 了 字 串 物 件 如 :AnsiString , 或 是 在 輸 出 函 式 的 參 數 或 回 傳 值 使 用 到 長 字 串 的 話 ,就 必 須 加 入MEMMGR.LIB 這 個 函 式 庫 。 另 外 , 若 我 們 在 另 一 個模 組 ( 如DLL ) 中 使 用 了 例 如new 或GetMem 等 方 法 來 配 置 記 憶體 , 而 在 不 同 的 模 組 ( 如EXE 應 用 程 式 ) 中 使 用 了 這 塊 記憶 體 或 呼 叫FreeMem 等 方 法 來 釋 放 記 憶 體 , 則MEMMGR.LIB 也 是必 須 被 加 入 的 。 此 外 還 有 一 個 值 得 注 意 的 , 就 是MEMMGR.LIB 必 須 加 在 所 有 要 用 到 函 式 庫 的 最 前 頭 , 以 便 在 其 他 函 式庫 之 前 優 先 載 入 並 接 手 相 關 的 記 憶 體 維 護 。 同 時 要 記 住的 是 若 你 使 用 了MEMMGR.LIB 這 個 函 式 庫 , 那 麼 當 你 移 交DLL 或是 應 用 程 式 時 , 你 必 須 連 同BORLNDMM.DLL 一 併 移 交 給 使 用 者。 不 過 在 這 段 聲 明 的 倒 數 第 二 段 中 有 提 到 , 若 要 避 免 額外 的 檔 案 付 給 使 用 者 ( 越 多 的 檔 案 對 使 用 這 來 說 是 一 種負 擔 ) , 你 可 以 將 有 關 字 串 的 資 料 改 由char * 或 是shortstring 來 傳 送 , 這 樣 可 以 不 動 用 到BORLNDMM.DLL 與MEMMGR.LIB 來 作 記 憶體 的 配 置 。 另 外 , 聲 明 的 最 後 一 段 中 有 提 到 , 若 你 在 Project\Options 裡 頭 的Link 一 頁 勾 選 了Use Dynamic RTL 一 項 時 , 就 不 須 額 外 手動 將MEMMGR.LIB 給 加 到Project 裡 頭 了 , 因 為C++Builder 會 自 動 幫你 做 這 個 動 作 。

緊 接 著 咱 們 來 看 一 下DLL 的 進 入 點

DLL 的 進 入 點 :DLLMain

以 下 是 由 方 法 二 生 產 出 來 的DLL 原 始 碼 :

  1. //---------------------------------------------------------------------------
  2. #include
  3. #pragma hdrstop
  4. //---------------------------------------------------------------------------
  5. int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
  6. {
  7. return 1;
  8. }
  9. //---------------------------------------------------------------------------
在Win32 平 台 的 程 式 設 計 中 , 明 言 規 定DLL 的 進 入 點 函 式 定義 為DllMain , 可 是 我 們 怎 麼 看 到 的 是DllEntryPoint 呢 ? 這 是Borland 對 於 標 準 的DLL 所 動 的 一 點 點 手 腳 , 當 然 你 若 執 意 把DllEntryPoint 給 改 成DllMain 也 是 可 以 的 , 但 是 你 要 注 意 , 當 你 更 動DllEntryPoint 時 ,C++Builder 工 具 列 上 頭 的Run 會 被Disable 喔 ! 這 是 就 是 擺明 了 不 給 你 編 譯 , 即 使 你 使 用Project\Build All 也 會 送 給 你錯 誤 訊 息 , 不 過 你 可 以 在DllMain 函 式 之 後 加 上#define WinMain 就 可 以 編 譯 了 , 不 過 筆 者 並 不 建 議 在C++Builder 下 這 麼 用 。

早 在 討 論 輸 入 輸 出 表 時 就 已 經 知 道 若 不 考 慮 到 各 個 編譯 器 間DLL 的 互 相 引 用 , 我 們 大 可 把 輸 出 函 式 這 麼 寫 :

__declspec(dllexport) int MyFunc (void);

若 要 輸 出 整 個 類 別 則 可 以 這 樣 寫 :

class __declspec(dllexport) MyClass : public TObject{...};

但 是 注 意 , 這 若 這 樣 寫 僅 可 以 在 自 己 寫 作DLL 的 編 譯 器中 來 使 用 這 個DLL 了 , 並 沒 辦 法 達 到 其 他 編 譯 器 也 可 以 使用 的 目 的 , 大 大 的 抹 殺 了 軟 體 元 件 的 構 想 , 更 何 況DLL 是採 用 模 組 化 的 設 計 。 當 然 了 ! 類 別 的 輸 出 當 然 不 在 考 慮範 圍 內 , 但 若 對 函 式 的 輸 出 咱 們 還 是 乖 一 點 , 加 上extern “C ” 來 遏 止name mangling 對 我 們 函 式 名 稱 所 動 的 手 腳 :

extern “C ” __declspec(dllexport) int MyFunc (void);

有 了 這 些 基 礎 知 識 後 , 咱 們 來 來 真 正 撰 寫 一 個 有 用 的DLL 試 試 看 吧 ! 那 要 做 些 什 麼 呢 ? 咱 們 就 做 個 簡 單 的 訊 息 視窗 即 可 , 怎 麼 做 , 其 實 說 穿 了 就 是 把Windows API MessageBox 給稍 微 包 裝 一 下 , 不 過 還 是 用MessageBox 這 個 函 式 來 實 作 內 容, 讀 者 們 可 能 覺 得 , 這 麼 無 聊 還 在 包 裝 一 次MessageBox 函 式幹 嘛 ? 這 個 嘛 ! 不 過 做 個 測 試 嘛 !

先 使 用Consol Wizard 來 產 生DLL Project , 並 將 此Project 儲 存 為ShowMsg.bpr , 在DLL 的 進 入 點DllEntryPoint 之 後 加 上 以 下 的 程 式 碼 :

  1. extern “C ” __declspec(dllexport) int ShowMsg(char *pText,HWND hWnd)
  2. {
  3. return MessageBox(hWnd,pText,"Information",MB_OK);
  4. }
嗯 ! 這 樣 一 來 咱 們 只 要 呼 叫ShowMsg() 函 式 , 並 將 要 秀 出 來的 文 字 與 視 窗 代 碼 當 作 參 數 來 傳 即 可 。 緊 接 著 只 需 按 下Ctrl+F9 就 開 始 編 譯 了 並 產 生 出ShowMsg.dll 了 。

使 用DLLs 中 的 函 式 ─DLLs 的 載 入

知 道 了 怎 樣 建 立 起 一 個DLL 後 , 接 著 就 是 了 解 如 何 使 用DLL 裡 頭 所 提 供 的 函 式 的 時 候 了 , 在 使 用 這 些 函 式 之 前 還 必須 做 個 更 重 要 的 動 作 , 就 是 將DLL 給 載 入 。 載 入DLLs 的 方 法大 致 上 可 以 分 成 兩 個 :

Implicit Linking 與Explicit Linking 。

Implicit Linking

Implicitly Link ( 隱 式 聯 結 ) 又 稱 靜 態 載 入 , 所 謂 靜 態 載入 是 指 程 式 在 聯 結 時 期 即 與DLLs 所 對 應 的import libraries 做靜 態 鏈 結 , 於 是 可 執 行 檔 中 便 對 所 有 的DLL 函 式 都 有 一 份重 定 位 表 格(relocation table) 和 待 修 正 記 錄(fixup record) 。 當程 式 被Windows 載 入 器 載 入 記 憶 體 中 , 載 入 器 會 自 動 修 正 所有 的fixup records , 而 這 個fixup records 就 是 記 錄 由DLL 中 所 有輸 出 資 源 的 正 確 位 址 , 也 就 是 先 前 提 到 的RVA 加 上DLL 被 載入 的 基 底 位 址 , 經 過 這 樣 的 程 序 動 態 聯 結 便 順 利 產 生 。也 就 是 說 , 程 式 開 始 執 行 時 , 會 用 靜 態 載 入 方 式 所 使 用到 的DLLs 都 載 入 到 行 程 的 記 憶 體 裡 。 先 來 看 看 靜 態 載 入 放是 的 優 點 :

1 、 靜 態 載 入 方 式 所 使 用 到 的 這 個DLL 會 在 應 用 程 式 執行 時 載 入 , 然 後 就 可 以 呼 叫 出 所 有 由DLL 中 匯 出 的 函 式 ,就 好 像 是 包 含 在 程 式 中 一 般 。

2 、 動 作 較 為 簡 單 , 載 入 的 方 法 由 編 譯 器 負 責 處 理 ,咱 們 不 須 動 腦 筋 。
而 缺 點 是 :

1 、 當 這 個 程 式 靜 態 載 入 方 式 所 使 用 到 的 這 個DLL 不 存在 時 , 這 個 程 式 在 開 始 時 就 出 現 無 法 找 到DLL 的 訊 息 而 導致 應 用 程 式 無 執 行 。

2 、 編 譯 時 需 要 加 入 額 外 的import library 。

3 、 若 是 要 載 入 的DLLs 一 多 , 載 入 應 用 程 式 的 速 度 會 便慢 。

4 、 若 遇 到 不 同 品 牌 的C++ 編 譯 器 時 , 靜 態 載 入 可 就 沒有 這 麼 簡 單 處 理 了 , 因 為 當 函 式 經 過Calling Conventions 的 處理 後 , 若 要 使 用 其 他 品 牌 編 譯 器 所 致 造 出 的DLL 須 得 大 動干 戈 才 行 。
Implicit Linking 範 例 :

以 先 前 建 立 的ShowMsg.DLL 為 例 子 , 我 們 已 知 這 個DLL 僅 輸出 一 個 函 式 :ShowMsg , 且 知 道 這 個 函 式 的 原 始 定 義 :

extern “C ” __declspec(dllexport) int ShowMsg(char *pText,HWND hWnd);

因 此 , 若 我 們 要 載 入 這 個 函 式 則 必 須 在 應 用 程 式 中 加入 此 輸 入 函 式 的 宣 告 :

extern “C ” __declspec(dllimport) int ShowMsg(char *pText,HWND hWnd);

此 外 , 還 要 加 上 這 個DLL 的import library File , 要 產 生import library 的 方 法 有 兩 個 :

1 、Project\Options 的Linker 中 的Geretate import library 勾 選 , 在正 常 情 況 下 , 預 設 值 是 勾 選 起 來 的 。

2 、 若 不 小 心 把lib 檔 案 給 刪 除 掉 了 , 也 可 以 利 用implib.exe 這 個C++Builder 所 附 上 的 工 具 來 產 生lib 檔 ,implib 是 文 字 模 式下 的 程 式 , 因 此 必 須 到 文 字 模 式 下 使 用 , 以 我 們 現 在 的例 子 來 說 , 使 用 方 式 為 :implib ShowMsg.lib ShowMsg.dll , 這 樣就 會 產 生ShowMsg.lib 檔 了 。
緊 接 著 就 是 加 入 這 個lib 檔 到 咱 們 的Project 裡 頭 , 可 以使 用 【Project\Add to Project … 】 來 加 入lib 檔 。 如 此 一 來 就 可以 在 應 用 程 式 的 任 何 地 方 使 用ShowMsg 函 式 了 。

不 過 在 此 我 們 發 現 一 個 小 問 題 , 若 每 次 要 匯 入DLL 裡 頭的 函 式 , 還 必 須 把 函 式 的 原 始 定 義 給 抄 過 來 ( 雖 然 說 複製 ─ 貼 上 這 個 動 作 很 簡 單 ) , 但 有 沒 有 更 好 的 辦 法 呢 ?有 的 , 咱 們 可 以 在DLL 原 始 碼 的Header File 裡 動 點 手 腳 , 讓要 使 用DLL 的 應 用 程 式 只 需include 這 個Header File 就 可 以 了 ,怎 麼 做 呢 ? 就 是 使 用 前 置 處 理 符 號 , 若 是 使 用Borland C++ 或 是Borland C++Builder 來 編 譯DLL 都 必 須 加 上#define __DLL__ 這 個宣 告 , 但 是 我 們 從C++Builder 所 幫 我 建 立 的Project 裡 面 看 不到 這 個 , 原 因 是C++Builder 已 經 在Make file 裡 幫 我 們 加 上-WD 這個 編 譯 參 數 來 達 成#define __DLL__ 所 要 的 目 的 了 。 所 以 我 們可 以 把ShowMsg.h 給 改 成 :

程 式 列 表 一 、ShowMsg.h

  1. //---------------------------------------------------------------------------
  2. #ifndef ShowMsgH
  3. #define ShowMsgH
  4. //---------------------------------------------------------------------------
  5. #ifdef __DLL__
  6. #define DLLAPI extern "C" __declspec(dllexport)
  7. #else
  8. #define DLLAPI extern "C" __declspec(dllimport)
  9. #endif
  10. DLLAPI int ShowMsg(char *pText,HWND hWnd);
  11. #endif
程 式 列 表 二 、ShowMsg.cpp
  1. #include
  2. #pragma hdrstop
  3. #include
  4. USEFILE("ShowMsg.h");
  5. //---------------------------------------------------------------------------
  6. #pragma argsused
  7. int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void*)
  8. {
  9. return 1;
  10. }
  11. //---------------------------------------------------------------------------
  12. int ShowMsg(char *pText,HWND hWnd)
  13. {
  14. return MessageBox(hWnd,pText,"Information",MB_OK);
  15. }
這 樣 一 來 , 只 需 要 在 應 用 程 式 裡 加 入 這 個Header File 並 將ShowMsg.lib 檔 給 加 入 這 個Project 裡 頭 , 就 大 功 告 成 了

Explicit Linking

而 所 謂Explicitly link ( 顯 式 聯 結 ) 又 稱 動 態 載 入 , 若 是使 用 動 態 載 入 就 是 需 要 時 才 載 入DLL , 然 後 在 使 用 過 後 即釋 放DLL , 嗯 ! 似 乎 是 很 不 錯 的 選 擇 , 這 種 方 法 的 優 點 有:

1 、DLL 只 要 需 要 時 才 會 載 入 到 記 憶 體 中 , 可 以 更 有 效的 使 用 記 憶 體 。

2 、 應 用 程 式 載 入 的 速 度 較 使 用 隱 式 鏈 結 時 快 , 因 為當 程 式 開 始 載 入 時 並 不 需 要 把DLL 給 載 入 到 行 程 中 。

3 、 編 譯 時 不 須 額 外 的import library 檔 。

4 、 讓 我 們 可 以 更 清 楚DLL 的 載 入 流 程 。
但 不 光 只 有 優 點 也 是 點 缺 點 的 , 缺 點 就 是 必 須 寫 多 一點 程 式 碼 。 首 先 , 必 須 使 用LoadLibrary 這 個Windows API 來 手 動載 入DLL , 並 使 用GetProcessAddress 來 取 得 所 要 使 用 的 函 式 的函 式 指 標 , 最 後 不 需 要 用 到 此DLL 時 使 用FreeLibrary 將DLL 釋放 。 所 以 , 在 學 會 動 態 載 入DLL 時 , 必 須 先 知 道 函 式 指 標的 用 法 。

Explicit Linking 範 例 :

咱 們 就 拿 一 個 常 見 的DLL ─ 控 制TWAIN32 界 面 的DLL : 『Eztw32.dll 』 來 作 動 態 載 入 的 示 範 :

eztw32.dll 裡 頭 有 四 十 多 個 輸 出 函 式 可 以 使 用 , 不 過 只有 以 下 這 四 個 函 式 是 我 們 所 要 用 到 的 :

1 、void __stdcall TWAIN_SelectImageSource(HWND hwnd);

功 能 : 用 來 選 擇 所 要 使 用 的TWAIN 介 面 裝 置

2 、int __stdcall TWAIN_AcquireToClipboard(HWND hwnd, unsigned int pixmask);

功 能 : 經 由TWAIN 介 面 將 資 料 置 放 到 剪 貼 簿 中

3 、int __stdcall TWAIN_LoadSourceManager(void);

功 能 : 呼 叫TWAIN 介 面 裝 置 程 式

4 、int __stdcall TWAIN_UnloadSourceManager(void);

功 能 : 關 閉TWAIN 介 面 裝 置 程 式

再 度 使 用tdump 來 觀 察eztw32.dll 裡 頭 的 輸 出 函 式 表 :

Exports from EZTW32.dll

50 exported name(s), 50 export addresse(s). Ordinal base is 1.

Ordinal RVA Name

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

0000 00001000 DllMain

...

0003 000012c0 TWAIN_AcquireToClipboard

...

0029 00001650 TWAIN_LoadSourceManager

...

0038 000010a0 TWAIN_SelectImageSource

...

0046 00001980 TWAIN_UnloadSourceManager

...

先 確 認 函 式 的 名 稱 以 及 函 式 的 序 號 , 等 一 下 會 用 到 。

接 著 就 是 開 始 將DLL 載 入 了 ,Win32 API 有 兩 個 函 式 提 供 了將DLL 載 入 的 功 能 , 分 別 是LoadLibrary 與LoadLibraryEx 。 通 常 都使 用LoadLibrary , 先 看 看LoadLibrary 的 原 始 定 義 :

HINSTANCE LoadLibrary(

LPCTSTR lpLibFileName // address of filename of executable module

);

嗯 ! 只 需 要 傳 入 檔 案 名 稱 即 可 , 若 以 我 們 所 要 使 用 的Eztw32.dll 為 例 :

HINSTANCE hDLL;

hDll = LoadLibrary( “Eztw32.dll ”);

若 我 們 沒 有 指 定 副 檔 名 , 則 自 動 會 以 『.DLL 』 為 副 檔 名, 或 許 會 感 到 納 悶 , 那 我 還 沒 有 指 定DLL 的 路 徑 啊 ! 正 確的 做 法 應 該 是 要 指 定 路 徑 的 , 但 是 天 曉 得 使 用 者 會 把DLL 給 放 到 哪 裡 去 呢 ? 所 以 使 用LoadLibrary 這 個 函 式 時 , 若 參數 中 沒 有 指 明 路 徑 , 系 統 會 依 特 定 的 次 序 來 找 尋DLL 的 存在 與 否 , 若 不 存 在 則LoadLibrary 函 式 則 會 回 傳NULL , 以 下 就是 搜 尋 次 序 :

1 、 被 執 行 的 應 用 程 式 所 存 在 的 路 徑 。

2 、 目 前 的 目 錄 。

3 、Windows 系 統 目 錄 , 對Windows 95/98 說 是Windows\System , 而Windows NT 則 是Winnt\System32 。 目 錄 名 稱 可 以 使 用GetSystemDirectory 這個API 來 取 得 。

4 、Windows 目 錄 。 目 錄 名 稱 可 以 使 用GetWindowsDirectory 這 個API 來 取 得 。

5 、 最 後 由 設 定 的PATH 環 境 變 數 來 尋 找 。
這 也 就 是 為 什 麼 我 們 的Windows\System 目 錄 下 有 這 麼 多DLL 的 原 因 之 一 了 。

把DLL 載 入 記 憶 體 後 最 重 要 的 工 作 就 是 把 函 式 指 標 指 向函 式 在 記 憶 體 中 正 確 的 位 址 , 要 做 到 這 個 動 作 得 透 過GetProcAddress 這 個API 來 幫 忙 :

FARPROC GetProcAddress(

HMODULE hModule, // handle to DLL module

LPCSTR lpProcName // name of function

);

GetProcAddres 函 式 的 第 一 個 參 數 是 經 由LoadLibrary 所 取 得 的DLL 的Handle , 而 第 二 個 參 數 是 函 式 的 名 稱 或 是 函 式 的 輸 出 序號 經 由 函 式 名 稱 取 得 函 式 的 指 標 , 以eztw32.dll 中 的TWAIN_SelectImageSource 函 式 為 例 , 應 由 函 式 名 稱 取 得 函 式 的 位 址 的 方 法 為 : :

GetProcAddress(hDLL, ”TWAIN_SelectImageSource ”);

若 經 由 函 式 輸 出 序 號 取 得 函 式 的 位 址 則 為 :

GetProcAddress(hDLL, MAKEINTRESOURCE (39));

在 此 要 注 意 序 號 的 起 始 值 是1 不 是0 , 經 由 tdump 所 列 出來 的Ordinal 是 由 起 始 值 開 始 的 位 移 植 , 而Ordinal Base 為1 ,因 此TWAIN_SelectImageSource 的 序 號 是39 而 不 是 38 。

接 著 在 此 先 複 習 一 下 函 式 指 標 的 使 用 方 式 , 一 樣TWAIN_SelectImageSource 函 式 為 例 ,TWAIN_SelectImageSource 函 式 的 原 始 定 義 為 :

void __stdcall TWAIN_SelectImageSource(HWND hwnd);

那 就 可 以 用 :

void (__stdcall *TWAIN_SelectImageSource)(HWND hwnd);

來 宣 告TWAIN_SelectImageSource 為 一 個 函 式 指 標 , 之 後 再 用:

TWAIN_SelectImageSource = (void (__stdcall *)(HWND hwnd))

GetProcAddress(hDLL, ”TWAIN_SelectImageSource ”);
來 取 得 函 式 的 位 址 , 之 後 就 在 程 式 中 若 要 使 用TWAIN_SelectImageSource 這 個 函 式 就 可 以 像 是 靜 態 載 入 般 使 用 了 。 但 這 樣 似 乎 得打 蠻 多 字 的 , 偷 懶 的 我 通 常 都 使 用 另 一 個 方 法 , 就 是 使用typedef 來 自 訂 型 別 。 方 法 如 下 :

typedef void (__stdcall *_TWAIN_SelectImageSource)(HWND hwnd);

_TWA IN_SelectImageSource TWAIN_SelectImageSource;

TWAIN_SelectImageSource = (_TWAIN_SelectImageSource)

GetProcAddress(hDLL, ”TWAIN_SelectImageSource ”);

先 使 用typedef 把_TWAIN_SelectImageSource 給 定 義 成 一 個 特 殊 的型 別 , 之 後 就 可 以 直 接 引 用 , 的 確 是 可 以 少 打 點 字 。

最 後 當DLL 裡 頭 的 函 式 不 再 需 要 使 用 時 , 咱 們 就 得 使 用FreeLibrary 將DLL 從 記 憶 體 裡 頭 卸 下 來 :

BOOL FreeLibrary(

HMODULE hLibModule // handle to loaded library module

);

使 用 方 法 很 簡 單 只 需 將LoadLibrary 所 傳 回 來 的DLL Handle 當參 數 傳 給FreeLibrary 傳 入 即 可 。

懂 得 這 些 動 態 載 入DLL 的 流 程 後 , 就 可 以 實 際 動 手 來 做做 看 。 筆 者 發 現TWAIN32 這 些 功 能 實 在 很 適 合 包 裝 成 一 個 物件 , 當 物 件 誕 生 時 , 立 即 自 動 去LoadLibrary 並 將 函 式 指 標的 位 址 給 連 結 起 來 , 當 這 個 物 件 被 摧 毀 時 , 就 自 動 去FreeLibrary , 嗯 ! 似 乎 不 錯 , 不 過 詳 細 的 做 法 就 不 多 做 解 釋 了 , 相信 讀 者 看 了 下 面 的 程 式 列 表 應 該 就 懂 了 。

程 式 列 表 三 、CTWAIN.h :

  1. //---------------------------------------------------------------------------
  2. #ifndef CTWAINH
  3. #define CTWAINH
  4. #include
  5. //---------------------------------------------------------------------------
  6. // 用typedef 自 訂 函 式 指 標 的 型 別
  7. typedef int (__stdcall *_TWAIN_AcquireToClipboard)
  8. (HWND hwnd, unsigned int pixmask);
  9. typedef int (__stdcall *_TWAIN_LoadSourceManager)
  10. (void);
  11. typedef void (__stdcall *_TWAIN_SelectImageSource)
  12. (HWND hwnd);
  13. typedef int (__stdcall *_TWAIN_UnloadSourceManager)
  14. (void);
  15. class CTWAIN
  16. {
  17. public:
  18. __fastcall CTWAIN(void);
  19. __fastcall ~CTWAIN(void);
  20. void __fastcall SelectImageSource(HWND hWnd);// 選 擇TWAIN32 設 備
  21. void __fastcall Acqure(HWND hWnd);// 經 由TWAIN32 設 備 取 得 資 料
  22. Graphics::TPicture *Picture;
  23. protected:
  24. private:
  25. // 宣 告 函 式 指 標
  26. _TWAIN_AcquireToClipboard TWAIN_AcquireToClipboard;
  27. _TWAIN_LoadSourceManager TWAIN_LoadSourceManager;
  28. _TWAIN_SelectImageSource TWAIN_SelectImageSource;
  29. _TWAIN_UnloadSourceManager TWAIN_UnloadSourceManager;
  30. HINSTANCE hDLL;
  31. };
  32. //---------------------------------------------------------------------------
  33. extern bool __stdcall CheckTWAINDLL();// 檢 驗DLL 檔 案 是 否 存 在 的函 式
  34. #endif
程 式 列 表 四 、CTWAIN.cpp :
  1. //---------------------------------------------------------------------------
  2. #include
  3. #pragma hdrstop
  4. #include "CTWAIN.h"
  5. //---------------------------------------------------------------------------
  6. #pragma package(smart_init)
  7. __fastcall CTWAIN::CTWAIN()
  8. {
  9. hDLL = ::LoadLibrary("Eztw32.dll");// 載 入DLL 到 行 程 的 記 憶 體 內
  10. // 取 得 所 需 函 式 的 函 式 指 標
  11. TWAIN_AcquireToClipboard = (_TWAIN_AcquireToClipboard)
  12. ::GetProcAddress(hDLL,"TWAIN_AcquireToClipboard");
  13. TWAIN_LoadSourceManager = (_TWAIN_LoadSourceManager)
  14. ::GetProcAddress(hDLL,"TWAIN_LoadSourceManager");
  15. TWAIN_SelectImageSource = (_TWAIN_SelectImageSource)
  16. ::GetProcAddress(hDLL,"TWAIN_SelectImageSource");
  17. TWAIN_UnloadSourceManager = (_TWAIN_UnloadSourceManager)
  18. ::GetProcAddress(hDLL,"TWAIN_UnloadSourceManager");
  19. Picture = new Graphics::TPicture;
  20. }
  21. //---------------------------------------------------------------------------
  22. __fastcall CTWAIN::~CTWAIN()
  23. {
  24. delete Picture;
  25. ::FreeLibrary(hDLL);// 從 記 憶 體 中 釋 放DLL
  26. }
  27. //---------------------------------------------------------------------------
  28. void __fastcall CTWAIN::SelectImageSource(HWND hWnd)// 選 擇TWAIN32 設備
  29. {
  30. TWAIN_SelectImageSource(hWnd);
  31. // 使 用DLL 裡 頭 的TWAIN_SelectImageSource 函 式
  32. }
  33. //---------------------------------------------------------------------------
  34. void __fastcall CTWAIN::Acqure(HWND hWnd)// 經 由TWAIN32 設 備 取 得 資料
  35. {
  36. if (TWAIN_LoadSourceManager() > 0)
  37. // 使 用DLL 裡 頭 的TWAIN_LoadSourceManager 函 式
  38. {
  39. if (TWAIN_AcquireToClipboard(hWnd,0)>0)
  40. // 使 用DLL 裡 頭 的TWAIN_AcquireToClipboard 函 式
  41. {
  42. Clipboard()->Open();
  43. if (Clipboard()->HasFormat(CF_PICTURE))
  44. Picture->Assign(Clipboard());
  45. Clipboard()->Clear();
  46. Clipboard()->Close();
  47. }
  48. TWAIN_UnloadSourceManager();
  49. // 使 用DLL 裡 頭 的TWAIN_UnloadSourceManager 函 式
  50. }
  51. }
  52. //---------------------------------------------------------------------------
  53. bool __stdcall CheckTWAINDLL()// 檢 驗DLL 檔 案 是 否 存 在 的 函 式
  54. {
  55. HANDLE hDll = ::LoadLibrary("EZTW32.DLL");
  56. if(hDll == NULL)
  57. {
  58. ::MessageDlg("Error! Could not found EZTWtw32.dll",
  59. mtInformation,TMsgDlgButtons()<
  60. return false;
  61. }
  62. ::FreeLibrary(hDll);
  63. return true;
  64. }
  65. //---------------------------------------------------------------------------
當 建 構 好 這 個CTWAIN 類 別 後 , 若 我 們 要 使 用 這 剛 剛 建 好 的類 別 , 我 們 可 以 寫 成 兩 個 函 式 :

1 、void SelectSource(HWND hWnd);

選 擇TWAIN32 設 備 的 來 源

2 、void Acquire(HWND hWnd);

經 由 選 定 的TWAIN32 設 備 來 源 將 資 料 取 得

  1. //---------------------------------------------------------------------------
  2. void SelectSource(HWND hWnd)// 選 擇TWAIN32 設 備 的 來 源
  3. {
  4. if (CheckTWAINDLL())
  5. {
  6. CTWAIN TWAIN;
  7. TWAIN.SelectImageSource(hWnd);
  8. }
  9. }
  10. //---------------------------------------------------------------------------
  11. void Acquire(HWND hWnd)// 經 由 選 定 的TWAIN32 設 備 來 源 將 資 料 取得
  12. {
  13. if (CheckTWAINDLL())
  14. {
  15. CTWAIN TWAIN;
  16. TWAIN.AcqureScanner(hWnd);
  17. if (TWAIN.Picture->Graphic != NULL)
  18. {
  19. //Do the processing that you want to do with the TWAIN.Picture
  20. TWAIN.Picture->Graphic = NULL;
  21. }
  22. }
  23. }
  24. //---------------------------------------------------------------------------
相 形 之 下 , 似 乎 靜 態 載 入 的 方 式 方 便 多 了 , 不 用 下 這 麼多 工 夫 在 手 動 載 入DLL 並 手 動 分 派 各 函 式 的 實 際 位 址 , 若遇 到 一 大 堆 輸 出 函 式 豈 不 光Keyin GetProcAddress 那 一 段 就 得花 上 許 多 時 間 。 但 若 用 到 不 同 品 牌 的C++ 編 譯 器 所 產 生 的DLL 時 , 採 用 動 態 載 入 加 上 以 函 式 輸 出 序 號 來 取 得 函 式 指 標的 方 式 , 僅 需 利 用TDUMP 製 造 初 一 張 序 號 表 再 加 上 勤 勞 的打 字 即 可 無 須 再 動 什 麼 腦 筋 。 稍 後 會 提 到 怎 麼 樣 在C++Builder 下 使 用 靜 態 載 入 來 載 入Visual C++ 所 編 譯 出 來 的DLL 。 這 樣 相形 之 下 , 反 而 又 會 覺 得 用 動 態 載 入 方 便 些 。

Calling Conventions

因 為 不 同 的 語 言 間 有 不 同 的 傳 遞 參 數 的 方 法 , 而C/C++ 編 譯 器 為 了 能 夠 使 用 由 其 他 語 言 開 發 出 來 的 函 式 庫 加 上了 這 些 參 數 傳 遞 方 式 不 同 的 方 式 稱 為calling conventions ( 呼叫 慣 例 ) , 如 在C++Builder 裡 頭 常 常 看 到 的__fastcall ; 一 般常 用 的 呼 叫 慣 例 有 以 下 四 種 :

呼 叫 慣 例
說 明
參 數 傳 遞 方 式
__cdecl 傳 遞 的 參 數 由 呼 叫 函式 清 除 。 由 右 往 左 的 次 序 將 參數 傳 遞 到 堆 疊 之 中 。
__stdcall 傳 遞 的 參 數 由 被 呼 叫函 式 清 除 。 一 般 使 用 於Win32 的 標 準 呼 叫 , 在DLLs 間 的 呼 叫大 部 分 都 使 用__stdcall 。 由 右 往 左 的 次 序 將 參數 傳 遞 到 堆 疊 之 中 。
__fastcall 傳 遞 的 參 數 由 被 呼 叫函 式 清 除 。 在C++Builder 中__fastcall 為VCL 元 件 使 用 的 內 定 呼叫 慣 例 。 in C++Builder :

由 左 往 右 的 次 序 傳 遞 參 數 , 第 一 個 參 數 由EAX 傳 遞 , 第二 個 由EDX 傳 遞 , 第 二 個 由ECX 傳 遞 , 其 餘 操 超 過 的 參 數 再交 由 堆 疊 傳 遞 。

in Visual C++ :

由 左 往 右 的 次 序 傳 遞 參 數 , 第 一 個 參 數 由ECX 傳 遞 , 第二 個 由EDX 傳 遞 , 其 餘 操 超 過 的 參 數 再 交 由 堆 疊 傳 遞 。

__pascal 傳 遞 的 參 數 由 被 呼 叫函 式 清 除 , 在Windows 3.1 時 期__pascall 為 標 準 用 法 , 但 到 了Windows 95/NT 後 以 鮮 少 使 用 。 由 左 往 右 的 次 序 將 參數 傳 遞 到 堆 疊 之 中 。Visual C++ 已 經 不 支 援__pascal 此 呼 叫 方式 。 因 此 在 此 不 做__pascal 的 討 論 。

但 光 是 說 說 很 難 了 解 到 這 些 參 數 傳 遞 方 式 有 何 異 同 ,以 下 函 式 分 別 使 用__cdecl 、__stdcall 與__fastcall 三 種 呼 叫 慣例 當 做 範 例 :

void calltype MyFunc(char c, shorty s, int i , double f);

當 我 使 用 這 個 函 式 :

MyFunc( “x ”,12,8192,2.7183);

時 會 被 編 譯 器 編 譯 成 如 以 下 四 圖 :

 

由 圖 八 與 圖 九 中 可 以 看 出__fastcall 在 兩 個 編 譯 器 中 有 顯著 的 不 同 ,C++Builder 使 用 了 三 個 暫 存 器 來 存 放 參 數 , 讓 傳遞 的 速 度 更 為 加 快 , 這 也 就 是VCL 類 別 中 的 預 設 呼 叫 慣 例為__fastcall 的 原 因 。 暫 且 撇 開__fastcall 的 不 同 , 呼 叫 慣 例造 成 的 還 不 只 這 一 樣 差 異 , 還 有 著 與name mangling 有 點 類 似的 麻 煩 , 就 是 函 式 的 名 稱 更 動 問 題 。 當 你 使 用 不 同 的 呼叫 慣 例 時 , 編 譯 器 還 是 對 動 點 手 腳 , 動 什 麼 手 腳 , 筆 者用 以 下 的 範 例 做 觀 察 , 先 定 義 四 個 函 式 分 別 使 用__fastcall 、__stdcall 與__cdecl 與 不 指 定 四 種 呼 叫 慣 例 :

  1. #define DLLEXP extern "C" __declspec(dllexport)
  2. DLLEXP int MyFunc_Default(char *c,int X)
  3. {
  4. return X;
  5. }
  6. DLLEXP int __fastcall MyFunc_Fast(char *c,int X)
  7. {
  8. return X;
  9. }
  10. DLLEXP int __stdcall MyFunc_Std(char *c,int X)
  11. {
  12. return X;
  13. }
  14. DLLEXP int __cdecl MyFunc_Cdecl(char *c,int X)
  15. {
  16. return X;
  17. }
定 義 好 後 分 別 使 用C++Builder 與Visual C++ 將 這 四 個 函 式 編 譯成DLL 檔 , 編 譯 完 成 後 使 用tdump 與impdef 來 觀 察 這 些 函 式 的輸 出 結 果 : (IMPDEF 的 用 法 為 IMPDEF def_file dll_file )

 

經 由 上 述 簡 單 的 實 驗 可 以 將 兩 者 的 差 異 列 出 , 並 且 可以 找 出 編 譯 器 預 設 的 呼 叫 慣 例 :

呼 叫 慣 例
原 始 函 式
Borland C++Builder
Microsoft Visual C++
__cdecl MyFunc_cdcel _MyFunc_cdcel MyFunc_cdcel
__stdcall MyFunc_std MyFunc_std _MyFunc_std@8
__fastcall MyFunc_fast @MyFunc_fast @MyFunc_fast@8
MyFunc_default _MyFunc_default MyFunc_default
預 設 呼 叫 慣 例
__cdecl __cdecl

由 表 上 可 以 看 出 兩 者 間 的 差 異 還 不 少 呢 ! 而 為 什 麼 要討 論 到 這 一 點 呢 ? 因 為 接 著 就 要 討 論 到 如 何 拿Visual C++ 所編 譯 的DLL 到C++Builder 裡 頭 使 用 。

在Borland C++Builder 下 使 用Microsoft Visual C++ 所 編 譯 的DLLs

若 已 經 解 決 了name mangling 與calling convention 的 理 想 狀 況 下, 由C++Builder 下 來 呼 叫Visual C++ 所 編 譯 出 來 的DLLs 應 該 不 是難 事 才 對 。 但 不 幸 的 , 只 對 了 一 半 , 怎 麼 說 , 幸 運 的 那一 半 是 , 咱 們 可 以 利 用 先 前 提 過 的 『 動 態 載 入 』 方 式 來載 入DLLs 中 的 函 式 , 即 使 因 為calling convention 的 問 題 導 致 函式 名 稱 在 編 譯 後 會 被 更 動 , 但 是 只 要 知 道 函 式 的 輸 出 序號 就 可 以 照 樣 載 入 ; 不 幸 的 那 一 半 是 若 採 用 『 靜 態 載 入』 的 方 法 就 又 會 遇 上 了 個 大 難 題 :Borland 與Microsoft 所 使 用的OBJs 檔 案 格 式 不 相 同 ;Borland 採 用Intel 所 訂 定 的OMF (Object Module Format ) 格 式 而Microsoft 採 用COFF (Common Object File Format ) 格 式 , 因 此 若 要 拿Visual C++ 所 編 譯 的DLLs 與LIBs 來 使 用 ,僅 有DLLs 能 夠 用 而 已 ,LIBs 毫 無 用 武 之 地 , 但 謝 天 謝 地 ,Borland 提 供 了 一 個 工 具IMPLIB.EXE , 可 以 直 接 從 任 何 編 譯 器 所 編 譯出 的DLLs 裡 頭 將OMF 格 式 的LIBs 給 製 造 出 來 。 因 此 製 造C++Builder 相 容 的OMF 格 式LIBs 不 算 是 個 大 問 題 了 。 但 對 於Calling Convention 所 產 生 的 問 題 就 比 較 麻 煩 些 , 接 下 來 咱 們 就 來 討 論 如 何使 用 靜 態 載 入 來 載 入Visual C++ 所 製 造 出 來 的DLLs 。

要 將Visual C++ 所 製 造 出 了DLLs 搬 到C++Builder 來 用 大 致 上 分三 個 步 驟 :

A 、 檢 驗 輸 出 函 式 的 設 呼 叫 慣 例

由 先 前 呼 叫 慣 例 的 實 驗 裡 看 得 出 來 , 兩 種 編 譯 器 的 預設 呼 叫 慣 例 皆 為__cdecl , 而__cdecl 與__stdcall 的 參 數 傳 遞 方式 兩 個 編 譯 器 也 相 同 , 但 是 在C++Builder 下__fastcall 為VCL 的標 準 呼 叫 慣 例 , 而 且 為 了 加 速 參 數 的 傳 遞 , 參 數 傳 遞 的方 法 與Visual C++ 不 同 , 若 是 硬 搬 到C++Builder 來 使 用 , 恐 有問 題 出 現 ; 因 此 若 要 拿Visual C++ 所 編 譯 的DLL 來 使 用 , 切 記只 能 使 用__cdecl 與__stdcall 這 兩 種 呼 叫 慣 例 。

B 、 查 驗 經 過 編 譯 器 編 譯 後 的 函 式 名 稱

由 呼 叫 慣 例 的 實 驗 結 果 裡 看 出 ,__cdecl 與__stdcall 在 兩 個編 譯 器 下 所 編 譯 出 的 正 式 名 稱 稍 有 不 同 , 以 一 個 簡 單 的 void MyFunction(void); 為 例 :

呼 叫 慣 例 Borland C++Builder Microsoft Visual C++
__cdecl _MyFunction MyFunction
__stdcall MyFunction _MyFunction@4

我 們 必 須 檢 查 看 看 哪 些 函 式 是 使 用__stdcall 而 哪 些 函 式是 使 用__cdecl , 接 下 來 下 一 個 步 驟 就 是 轉 換 這 些 名 稱 , 將名 稱 由C++Builder 不 認 得 變 成 認 得 。

C 、 製 作OMF 格 式 的LIBs

由 於 經 過 編 譯 器 處 理 過 後 的 函 式 名 稱 已 經 與 原 先 函 式名 稱 不 相 同 了 , 因 此 若 直 接 轉 換 成LIBs 檔 也 無 啥 效 用 , 必須 動 點 手 腳 。 先 前 已 經 學 過IMPDEF 的 使 用 方 法 , 先 前 是 用在 觀 察 輸 出 函 式 , 現 在 也 是 , 但 還 多 了 動 手 腳 的 部 份 。

利 用 先 前 測 試 的 那 個VC6TEST.DLL 來 製 造DEF 檔 案 : (__fastcall 的 呼 叫 慣 例 部 份 記 得 要 先 除 去 )

IMPDEF VC6TEST.DEF VC6TEST.DLL

製 造 出 來 的DEF 檔 :

LIBRARY VC6TEST.DLL

EXPORTS

MyFunc_Cdecl @1

MyFunc_Default @2

_MyFunc_Std@8 =_MyFunc_Std @3

這 時 候 我 們 就 來 動 手 腳 , 把C++Builder 不 認 得 給 改 承 認 得的 。 改 成 如 下 :

EXPORTS

;use this type of aliasing

;(Borland name) = (Name exported by Visual C++)

MyFunc_Std = _MyFunc_Std@8

_MyFunc_Cdecl = MyFunc_Cdecl

_MyFunc_Default = MyFunc_Default

最 後 再 將DEF 檔 給 還 原 成LIB 檔 , 什 麼 ? 你 有 沒 有 說 錯 把DEF 這 個 文 字 檔 變 回LIB 檔 ? 是 的 , 其 實 靜 態 載 入 所 需 要 用 到的LIB 檔 案 只 是 個 函 式 表 格 罷 了 , 並 沒 有 真 正 函 式 內 容 在裡 頭 , 因 此 我 們 可 以 經 由 動 過 手 腳 的DEF 檔 給 還 原 成LIB 檔:

IMPLIB VC6TEST.lib VC6TEST.def

動 完 手 腳 後 不 忘 還 要 去 檢 查 一 下 是 否 函 式 輸 出 , 可 以使 用TLIB 來 觀 察 最 後 的test.lib 是 否 正 確 輸 出 :

TLIB VC6TEST.lib,VC6TEST.txt

Publics by module

_MyFunc_cdecl size = 0

_MyFunc_cdecl

_MyFunc_default size = 0

_MyFunc_default

MyFunc_std size = 0

MyFunc_std

可 以 由 觀 察test.txt 來 檢 驗 輸 出 的 正 確 與 否 , 在 確 定 輸出 名 稱 無 誤 後 就 可 以 直 接 拿 進C++Builder 做 先 前 已 經 說 明 過的 靜 態 載 入 測 試 了 。 我 的 測 試 方 法 為 建 立 一 個Console Application 來 載 入 這 個DLL 做 測 試 。

  1. #pragma hdrstop
  2. #include
  3. //---------------------------------------------------------------------------
  4. USELIB("VC6test.lib");
  5. //---------------------------------------------------------------------------
  6. #pragma argsused
  7. #define DLLIMP extern "C" __declspec(dllimport)
  8. DLLIMP int MyFunc_default(char *c,int X);
  9. DLLIMP int __stdcall MyFunc_std(char *c,int X);
  10. DLLIMP int __cdecl MyFunc_cdecl(char *c,int X);
  11. #include
  12. #include
  13. int main(int argc, char **argv)
  14. {
  15. printf("int x = MyFunc_std(\"x\",123);\n");
  16. int x = MyFunc_std("x",123);
  17. printf("Result : x = %d\n",x);
  18. printf("int y = MyFunc_cdecl(\"x\",2323);\n");
  19. int y = MyFunc_cdecl("x",2323);
  20. printf("Result : y = %d\n",y);
  21. getch();
  22. return 0;
  23. }
 

輸 出 結 果 :

int x = MyFunc_std("x",123);

Result : x = 123

int y = MyFunc_cdecl("x",2323);

Result : y = 2323

這 樣 一 來 算 大 功 告 成 , 但 在 筆 者 的 經 驗 中 使 用Visual C++ 來 寫 作DLL 供Borland C++Builder 使 用 , 有 以 下 幾 點 是 值 得 注 意的 :

  1. 抑 制name mangling 的 行 為
    一 定 要 記 住 為 輸 出 函 式 加 上 extern “C ” 來 抑 制 name mangling 的 動 手 腳 。
  2. 禁 止 輸 出 類 別
為 什 麼 要 禁 止 類 別 的 輸 出 呢 ? 因 為Visual C++ 處 理 類 別 的 方式 與C++Builder 有 著 很 大 的 不 同 。 所 以 少 用 為 妙 。
結 語

這 次 筆 者 大 多 著 墨 於DLLs 的 使 用 , 怎 麼 建 立DLLs 的 方 法卻 只 大 略 一 題 , 因 為 製 造DLLs 的 方 法 有 很 多 , 也 有 很 多 的技 巧 , 若 把 這 些 通 通 都 給 寫 出 來 , 可 能 這 個 主 題 就 要 變成 連 載 小 說 般 分 成 好 幾 期 來 刊 載 了 , 一 期 為 時 一 個 月 ,討 論 這 個 主 題 的 時 間 會 拖 得 太 久 , 且 相 差 時 間 會 過 久 ,造 成 讀 者 的 無 法 連 貫 , 筆 者 只 好 忍 痛 割 捨 , 不 過 筆 者 在此 還 是 列 出 數 本 對 於 製 作DLLs 一 題 著 墨 相 當 詳 細 的 書 籍 :

1 、Adcanced Windows 3/E, Microsoft Press, Jeffrey Richter Chapter 12

2 、Programming Windows 95, Microsoft Press, Charles Petzold Chapter 19

3 、Multithreading Application in Win32, Addison Wesley, JimBeveridge, etc . Chapter 14

4 、Borland C++Builder 3 Unleashed, Sams Publishing, Charlie Calvert, etc. Chapter 35

註1 、PE 格 式 的 執 行 檔 :PE ,Portable Executable , 是Microsoft 設 計 用 於 其 所 有Win32 作 業 系 統 (Win32s 、Windows NT 及Windows 95/98 ) 的 可 執 行 檔 格 式 。

註2 、 在C++Builder 下 觀 看 編 譯 器 產 生 的 真 正 名 稱 方 法 :


選 單 上 『Project/Options 』 裡 的 『Linker 』 一 頁 中 的Map File 選 項 中 勾 選derailed :

註3 、 在Visual C++ 下 觀 看 編 譯 器 產 生 的 真 正 名 稱 方 法 :


選 單 上 『Project/Settings … 』 中 『Link 』 一 頁 且Category 為General 時 將Generate mapfile 選 項 勾 選 。

參 考 資 料 、

  1. Windows 95 System Programmung SECRETS, IDG BOOKS, Matt Pietrek
  2. Adcanced Windows 3/E, Microsoft Press, Jeffrey Richter
  3. Programming Windows 95, Microsoft Press, Charles Petzold
  4. Borland C++Builder 3 Unleashed, Sams Publishing, Charlie Calvert
  5. High Preformance Borland C++Builder, Coriolis Group Books, Matt Telles