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

Microsoft Visual Basic 之 Variant 變數應用

Microsoft® Visual BasicVariant 變數應用


鄭子璉 http://feitsui.hyd.ncku.edu.tw/TLCheng/Basic/ 現任:成大資工 BBS Basic 版版主



-、Variant 概述

  在 Microsoft® Visual Basic (以下簡稱 VB) 中, Variant 為預設變數型態,基於執行速度的考量,在 VB 線上手冊及相關文件均不建議採用 Variant ,因為 Variant 會拖慢程式執行效能。唯 Variant 實為一多變特性之變數型態,透過 Variant 可解決特定目標之問題,筆者建議認為您應深入了解 Variant 特性,並善加利用 Variant 變數。在 VB6 線上手冊中指出, Variant 是一種特殊的資料型態,除了固定長度 String的資料及使用者自訂型態外,也可以包含任何種類的資料, Variant 亦可以包含 Empty、Error、Nothing 及 Null特殊值。您可以用 VarType 函數或 TypeName 函數來決定如何處理 Variant 中的資料。



二、Variant 內容

  在 VB 中有一函數 VarType 可傳回 Variant 目前之型態,筆者用一簡單之函數來觀察,關於 CopyMemory 的宣告,在後續的程式碼將不再重複。

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" (hpvDest As Any, hpvSource As Any, ByVal cbCopy As Long)

Public Function VariantMemory(ByVal Expression)

Dim tBytes(0 To 15) As Byte
CopyMemory tBytes(0), Expression, 16
VariantMemory = tBytes

End Function

  如表 1 ,表中略去本文不討論之 VbDataObject 、 VbUserDefinedType 之兩種型態,表中以各範例展現各變數型態在記憶體中的內容。

表 1 不同型態變數與記憶體的關係

常數

範例

16 進位記憶體內容 說明
VbEmpty 0 Empty 0000000000000000
0000000000000000

Empty (未初始化)

VbNull 1 Null 0100000000000000
0000000000000000

Null (無有效資料)

VbInteger 2 1% 0200000000000000
0100000000000000
整數
VbLong 3 1& 0300000000000000
0100000000000000
長整數
VbSingle 4 1! 0400000000000000
0000803F00000000
單倍精確浮點數
VbDouble 5 1# 0500000000000000
000000000000F03F
雙倍精確浮點數
VbCurrency 6 1@ 0600000000000000
1027000000000000
貨幣
VbDate 7 #2001/4/1# 0700000000000000
00000000C00EE240
日期
VbString 8 "培基語言" 0800000000000000
3CA7060C00000000
字串
VbObject 9 tFont As New StdFont 0900000000000000
F8A40A0C00000000
物件
VbError 10 CVErr(1) 0A00000000000000
01000A8000000000
錯誤
VbBoolean 11 True 0B00000000000000
FFFF000000000000
布林值
VbVariant 12 Array(1, 2) 0C20000000000000
3C75080C00000000
Variant(只適用於 Variants 中的陣列)
VbDecimal 14 CDec(1) 0E00000000000000
0100000000000000
十進位值
VbByte 17 CByte(1) 1100000000000000
0100000000000000
位元值
VbArray 8192 tBytes() 0C20000000000000
0000000000000000
陣列
  多數人都知道, Variant 變數為 16 位元組,是 VB 內建變數中記憶體使用量最大者,那麼在 Variant 中又是如何運用這 16 位元組呢?

  如圖 1 ,由上表可推知變數型態以前兩位元組存放,變數內容依所需要之位元組數,分別存放於 Variant 變數內。

 

圖 1 Variant 變數位元使用狀態



三、取得 Variant 物件變數型態
  在獲得如上的資訊後,首先先來檢討 VB6 中的函數 VarType ,讀者可測試以下的程式碼  

Dim tFont As New StdFont, tPic As New StdPicture

Debug.Print VarType(tFont), VarType(tPic)

  在由 VB6 執行測試前,直覺上程式碼應傳回 9 (vbObject),但經實際測試後得到卻是 8 (vbString) 及 3 (vbLong),筆者推測 VarType 傳回的可能是該物件預設屬性之型態。

  例如 StdFont 預設屬性為 Name As String ,StdPicture 預設屬性為 Handle As OLE_HANDLE (Long) ,但對程式設計人員來說,通常需要得到的是 vbObject ,所以筆者設計了一個小函數   Public Function myVarType(ByVal Expression As Variant) As VbVarType

CopyMemory myVarType, Expression, 2

End Function

  這個函數利用 1 中第 0 與 1 個位元組代表變數型態,在直接使用上與 VarType 函數相同,並可支援傳回 vbObject 代碼。



四、 Variant 變數型態各位元組的位置

  其次欲了解 Variant 變數其它各位元組的意義,大部分 Variant 變數均由第 8 個位元值開始,變數型態與所佔之位元組整理,請參考表 2

表 2 不同型態變數Variant 內所佔之位元組

所需記憶體位元組

位置

變數型態

1

8

位元值
2

8 ~ 9

整數、錯誤、布林值
4 8 ~ B 長整數、單倍精確浮點數、字串指標、物件指標、 Variant 指標、陣列指標
8 8 ~ F 雙倍精確浮點數、貨幣、日期
14 3 ~ F 十進位值



五、 Variant 位元值排列方式
  在 Win32 (95/98/Me/NT/2k) 下,變數在記憶體中的位置是採用低位元排列 (little endian byte order) ,即位元組由左至右代表低位元值至高位元值,對於 Unix 系統高位元排列來說是相反的,甚至部分軟體在二進位檔內會高位元、低位元排列混用。

  例如:某著名地理資訊系統軟體之檔案在前 4 位元組以高位元排列之長整數 9,994 為檢查碼,若要讓 VB 存取必須自行撰寫轉換函數(如右列)

  由於位元組排列方向剛好相反,故上述函數可做正逆雙向轉換,目前僅支援整數、長整數、單倍精確浮點數與雙倍精確浮點數,由於筆者目前碰到需要轉換的變數只有這四種,尚無法對其它變數測試。

  Public Function EndianBigToLittle(ByVal hEndian As Variant)

Len = 16
Offset = 8

Select Case VarType(hEndian)
Case Is = vbInteger
 nBytes = 2
Case Is = vbLong, vbSingle
 nBytes = 4
Case Is = vbDouble
 nBytes = 8
Case Else
 nBytes = 0
End Select

Select Case nBytes
Case Is > 1
 ReDim tBytes(1 To nLen) As Byte

 CopyMemory tBytes(1), hEndian, nLen

 For i = 1 To nBytes \ 2
  MySwap tBytes(Offset + i), tBytes(Offset + nBytes - i + 1)
 Next

 CopyMemory hEndian, tBytes(1), nLen
End Select

EndianBigToLittle = hEndian

End Function


六、 Variant 變數與 16 進位值轉換

  對於經常沉醉在 RPG 的玩家或是面對不同二進位檔的程式設計人員,比較關心的是各變數在的 16 進位值, VB 內建的 Hex 函數會先將引數轉為整數或長整數才求得 16 進位值,但這不符合大部分的需求,您可利用本文一開始提及之 VariantMemory 函數將各位元組轉換為 16 進位值,但一般閱讀習慣上仍喜好以高位元排列為主,以下程式,描述將陣列字串納入考量之函數及反轉換函數

Public Function VariantToHex(ByVal Expression)

tBytes = VariantToBytes(Expression)
VariantToHex = ByteToHex(tBytes)

End Function

Public Function ByteToHex(ByVal hBytes) As String

If VarType(hBytes) > vbArray Then
 lb = LBound(hBytes)
 ub = UBound(hBytes)
 
 For i = lb To ub
  tHex = tHex & ByteToHex(hBytes(i))
 Next I
Else
 tHex = Hex(hBytes)
 If Len(tHex) = 1 Then
  tHex = "0" & tHex
 End If
End If

ByteToHex = tHex

End Function

Public Function HexToByte(ByVal HexString As String) As Variant

If (Len(HexString) Mod 2) > 0 Then
 HexString = "0" & HexString
End If

nByte = Len(HexString) / 2

ReDim tByte(1 To nByte) As Byte

For i = 1 To nByte
 tByte(i) = Val("&H" + Mid(HexString, (i - 1) * 2 + 1, 2))
Next i

HexToByte = tByte

End Function
  Public Function HexToVariant(ByVal HexString As String, Optional ByVal wFlags As VbVarType = vbLong) As Variant

nLen = 16
Offset = 8

Select Case wFlags
Case vbByte
 nByte = 1
Case vbInteger, vbBoolean, vbError
 nByte = 2
Case vbLong, vbSingle
 nByte = 4
Case vbDouble, vbDate, vbCurrency
 nByte = 8
Case vbString
 nByte = Len(HexString) \ 2
Case vbDecimal
 nByte = 14
 Offset = 2
Case Else
 nByte = 0
End Select

If nByte = 0 Then
 HexToVariant = HexString
Else
 tByte = HexToByte(HexString)
 
 If wFlags = vbString Then
  HexToVariant = CStr(tByte)
 Else
  ReDim vByte(1 To nLen) As Byte
  
  lb = LBound(tByte)
  ub = UBound(tByte)
  nb = ub - lb + 1
 
  If nb < sloc =" nByte" sloc =" 1" i =" sLoc" wflags =" vbError">

  VariantToHex 函數目前可支援整數、長整數、單倍精確浮點數、雙倍精確浮點數、貨幣、日期、字串、物件、錯誤、布林、十進位值、位元、陣列,若為陣列變數,則利用 ByteToHex 函數遞迴呼叫,可將 16 進位值依序傳回,若為物件變數,則僅傳回物件指標。其中比較特別的是若為字串變數,則利用 VB 內建功能將字串轉換為位元值陣列。

  但字串傳回的是 UniCode 的 16 進位值,若需傳回存在檔案中 ASCII 的 16 進位值,則改為   Debug.Print VariantToHex(StrConv("培基語言",vbFromUnicode))

' 傳回 B0F6B0F2BB79A8A5

  HexToVariant 函數目前可支援整數、長整數、單倍精確浮點數、雙倍精確浮點數、貨幣、日期、字串、錯誤、布林、十進位值、位元。

  其中若為字串變數,則利用 VB 內建功能將 16 進位值將位元值陣列轉換為字串,但字串引數使用的是 UniCode 的 16 進位值,若需使用存在檔案中 ASCII 的 16 進位值,則改為   StrConv(HexToVariant("B0F6B0F2BB79A8A5",vbString),vbUnicode)

' 傳回 "培基語言"

  欲轉換為 2 進位值或其它進制,筆者建議您可藉位元值陣列進行各項變換,在此不再贅述,您可參考筆者所建置網頁上的範例。



七、 Variant 變數與記憶體指標
  在 VB 中有 3 個隱藏保留函數可取得變數在記憶體中的位置,由前面敘述可知,物件及字串在 Variant 變數存放為記憶體指標,然 VB.Net 將不再支援這 3 個保留函數,因此筆者分別以右列兩個自訂函數取得記憶體指標

  myObjPtr 為模擬 VB 內建之 ObjPtr 函數,為取得物件記憶體指標, myStrPtr 為模擬 VB 內建之 StrPtr 函數,為取得字串記憶體指標,目前僅支援不定長度字串及 Variant 字串,關於字串在記憶體轉移間的變化及不支援定長度字串的原因,筆者擬另闢專文向您報告,不在本文討論。

  Public Function myObjPtr(ByVal hObject As Variant) As Long

Select Case myVarType(hObject)
Case Is = vbObject
 ReDim tBytes(1 To 16) As Byte
 Dim tLong As Long
 
 CopyMemory tBytes(1), hObject, 16
 CopyMemory tLong, tBytes(9), 4
 myObjPtr = tLong
End Select

End Function

Public Function myStrPtr(lpString As Variant) As Long

ReDim tBytes(1 To 16) As Byte
Dim tLong As Long

CopyMemory tBytes(1), lpString, 16
CopyMemory tLong, tBytes(9), 4

If tBytes(2) > 0 Then
 CopyMemory myStrPtr, ByVal tLong, 4
Else
 myStrPtr = tLong
End If

End Function


八、支援超長整數

  在 Win32 中另有一變數型態為超長整數 (Long Long) ,在 VB 中並不直接支援,但可利用十進位值 (Decimal) 配合運用。十進位值在Variant 變數中,第 2 ~ 3 位元值為正負符號及浮點位置,第 4 ~ 7 位元值為超高位元組,第 8 ~ F 恰可當超長整數使用,故十進位值總共使用 14 個位元組,一般讀者多有看到記載為 12 個位元組,筆者以為疑是漏列正負符號及浮點位置的部分。

  因此若配合超長整數型態時,應避免使用第 2 ~ 7 位元值,文中為避免贅述,僅介紹讀取及設定兩種應用

Private Declare Function GetDiskFreeSpaceEx Lib "kernel32" Alias _
"GetDiskFreeSpaceExA" (ByVal lpRootPathName As String, _
lpFreeBytesAvailableToCaller As typLong64, _
lpTotalNumberOfBytes As typLong64, _
lpTotalNumberOfFreeBytes As typLong64) As Long
Private Declare Function SetEndOfFile Lib "kernel32" _
(ByVal hFile As Long) As Long

Private Type typLong64
 Low As Long
 High As Long
End Type

Public Enum enuDiskFreeSpace
 dfs_FreeBytes = 0
 dfs_TotalBytes = 1
 dfs_TotalFreeBytes = 2
End Enum


Private Function CLong64(hLong64 As typLong64)

ReDim tBytes(1 To 16) As Byte
Dim tVar As Variant

CopyMemory tBytes(9), hLong64, Len(hLong64)
tBytes(1) = vbDecimal
CopyMemory tVar, tBytes(1), 16

CLong64 = tVar

End Function

Public Function myGetDiskFreeSpace(ByVal lpRootPathName As String, _
Optional ByVal dwFlags As enuDiskFreeSpace = dfs_FreeBytes)

Dim lpFreeBytesAvailableToCaller As typLong64
Dim lpTotalNumberOfBytes As typLong64
Dim lpTotalNumberOfFreeBytes As typLong64
summy = GetDiskFreeSpaceEx(lpRootPathName, lpFreeBytesAvailableToCaller,_
lpTotalNumberOfBytes, lpTotalNumberOfFreeBytes)

Select Case dwFlags
Case Is = dfs_FreeBytes
 tdfs = CLong64(lpFreeBytesAvailableToCaller)
Case Is = dfs_TotalBytes
 tdfs = CLong64(lpTotalNumberOfBytes)
Case Is = dfs_TotalFreeBytes
 tdfs = CLong64(lpTotalNumberOfFreeBytes)
End Select

myGetDiskFreeSpace = tdfs

End Function
  Private Function CTypeLong64(ByVal hDecimal As Variant) As typLong64

ReDim tBytes(1 To 16) As Byte

Select Case VarType(hDecimal)
Case vbDecimal
 hDecimal = CDec(CStr(hDecimal))
Case Else
 hDecimal = CDec(CStr(Int(hDecimal)))
End Select

CopyMemory tBytes(1), hDecimal, 16
CopyMemory CTypeLong64, tBytes(9), 8

End Function


Public Function mySetFilePointer (Byval hFile As Long, ByVal FilePos)

Dim tFileLen As typLong64

FilePos = FilePos - 1
tFileLen = CTypeLong64(FilePos)
tFileLen.Low = SetFilePointer(hFile, tFileLen.Low, tFileLen.High, FILE_BEGIN)
mySetFilePointer = CLong64(tFileLen) + 1

End Function

  函數 myGetDiskFreeSpace 可取得輸入引數磁碟機總空間,由於 VB 內建長整數僅能支援到 2 GB ,而目前市面多可購買到超過 2 GB 之硬碟。

  因此以CLong64 函數將超長整數轉換為十進位值變數,如此可在 VB 中支援到 18,446,744,073,709,551,615 Byte (18 EB 或 18,446,744,073 GB) ,可解決 VB 無法顯示或展示超長整數之變數型態。

  函數 mySetFilePointer 為設定讀取或寫入檔案動作位置指標,若存取檔案遇上超過 2 GB 以上,原 VB 內建函數或陳述式將不敷使用,透過 Windows API 存取時,需注意到 Windows API 檔案指標是以 0 為基底, VB 是以 1 為基底,所以處理前後均應經過轉換,轉換為十進位值前,為避免十進位值第 3 ~ 7 位元值不為 0 ,故程式碼中透過字串來轉換。



九、 Variant 陣列

  前面敘述到 Variant 陣列變數在第 8 ~ B 位元值為陣列記憶體指標,因此 Variant 陣列適當靈活運用下,可以達到多重多維度陣列、陣列中可再指定陣列、次維度陣列可不等長等特性,很多在 C 語言中才有的特性及陣列運用, VB 可利用 Variant 陣列來完成,若有機會,筆者將另文探討,本文仍著重於 Variant 的討論。

   Variant 的陣列記憶體指標實際上並不是指向實體陣列(陣列第一個元素的位置),實際上是指向陣列描述器的位置。 VB 的 Variant 陣列型態詳細定義於 oleaut32.dll 中,其型態為 SafeArray ,藉由 SafeArray 的描述,可以得到 Variant 陣列的維度、單一元素的記憶體大小、實體陣列記憶體指標、各維度宣告個數及下標,因此可以補充及增進 VB 內建函數所沒有之功能:

Private Declare Function SafeArrayGetDim Lib "oleaut32" _
(ByVal lpSafeArray As Long) As Long
Private Declare Function SafeArrayRedim Lib "oleaut32" _
(ByVal lpSafeArray As Long, lpSafeArrayBound As typSafeArrayBound) As Long

Public Type typArrayVariant
 uVarType As Integer
 unUsed1 As Integer
 unUsed2 As Long
 Pointer As Long
 unUsed3 As Long
End Type

Public Type typSafeArrayBound
 cElements As Long
 LowerBound As Long
End Type

Public Type typSafeArray
 nDimension As Integer
 fFeatures As Integer
 cbElements As Long
 cLocks As Long
 Pointer As Long
End Type

Private Enum enuSafeArrayMessage
 S_OK = &H0
End Enum

Private Const Lenth_Variant = 16
Private Const Offset_Variant = 8


Public Function GetArrayStructPtr(hVariant As Variant) As Long

Dim ArrayStructPtr As typArrayVariant
CopyMemory ArrayStructPtr, hVariant, Len(ArrayStructPtr)
GetArrayStructPtr = ArrayStructPtr.Pointer

End Function

Public Function mySafeArrayGetDim(ByVal hVariant As Variant) As Long

mySafeArrayGetDim = SafeArrayGetDim(GetArrayStructPtr(hVariant))

End Function


Public Function IsArrayInit(ByVal SourceArray As Variant) As Boolean

IsArrayInit = False

Select Case VarType(SourceArray)
Case vbVariant, Is >= vbArray
 nDim = mySafeArrayGetDim(SourceArray)

 If nDim > 0 Then
  IsArrayInit = True
 End If
End Select

End Function
  Public Function mySafeArrayRedim(ByVal hVariant As Variant, _
Optional ByVal ArrayElements, Optional ByVal ArrayLowerBound)

Dim lpArrayBound As typSafeArrayBound

nDim = mySafeArrayGetDim(hVariant)

If IsMissing(ArrayLowerBound) Then
 ArrayLowerBound = LBound(hVariant, nDim)
End If

If IsMissing(ArrayElements) Then
 ArrayElements = UBound(hVariant, nDim) - LBound(hVariant, nDim) + 1
End If

lpArrayBound.cElements = ArrayElements
lpArrayBound.LowerBound = ArrayLowerBound

summy = SafeArrayRedim(GetArrayStructPtr(hVariant), lpArrayBound)

If summy = S_OK Then
 mySafeArrayRedim = hVariant
End If

End Function


Public Function TransformArrayToOneDimension _
(ByVal SourceArray As Variant) As Variant

Dim TargetArray As Variant
Dim tPointer As Long
Dim tSA As typSafeArray

TargetArray = SourceArray
tPointer = GetArrayStructPtr(TargetArray)
CopyMemory tSA, ByVal tPointer, Len(tSA)

With tSA
 ReDim BoundItem(1 To .nDimension) As typSafeArrayBound
 
 tPointer = tPointer + Len(tSA)
 CopyMemory BoundItem(1), ByVal tPointer, Len(BoundItem(1)) * .nDimension

 nCount = BoundItem(1).cElements
 For i = 2 To .nDimension
  nCount = nCount * BoundItem(i).cElements
 Next i
 
 BoundItem(1).cElements = nCount
 BoundItem(1).LowerBound = 1
 
 CopyMemory ByVal tPointer, BoundItem(1), Len(BoundItem(1))
 
 .nDimension = 1
 
End With

CopyMemory ByVal tPointer - Len(tSA), tSA, Len(tSA)

TransformArrayToOneDimension = TargetArray

End Function

  函數 GetArrayStructPtr 為取得 Variant 陣列描述器記憶體指標,函數mySafeArrayGetDim 為取得Variant 陣列宣告的維度,目前在VB 中並沒有任一函數可取得陣列宣告之維度,透過本函數可增進 VB 的功能,原則上亦可透過 SafeArray 之型態取得陣列維度,筆者在這僅介紹透過 Windows API 直接取得的方式,您若欲了解透過 SafeArray 之型態取得陣列維度,可至筆者架設之網頁瀏覽。

  函數 IsArrayInit 為判斷陣列是否初始化,陣列在宣告大小前及刪除後,其陣列維度為 0 ,利用此特性可判斷陣列是否已宣告大小,如此可避免程式中在未宣告陣列大小時,使用陣列中元素所產生的錯誤。 VB 中若要不影響現有資料而調整陣列之個數,可用

ReDim Preserve varname(lb to ub)

  但運用此陳述式變更陣列個數僅能更改上標,且 Variant 變數陣列不能變更,例如 Array 傳回值即為 Variant 變數陣列,利用函數 mySafeArrayRedim 可同時或部分變更上標及下標,如下例

項目

程式碼

下標

上標
原始宣告

ReDim vArray(0 To 4) 或 vArray = Array(1, 2, 3, 4, 5)

0

4

變更上標 vArray = mySafeArrayRedim(vArray, 6) 0

5

變更下標 vArray = mySafeArrayRedim(vArray, , 1) 1 5
其它變更 vArray = mySafeArrayRedim(vArray, 6, 1) 1 6

  函數 TransformArrayToOneDimension 可將任意維度陣列轉換為一維,本函數主要僅修改陣列描述器,可以最短的執行速度將多維陣列變更為一維陣列,算是較高效率的方法,但沒把握時請您勿任意修改程式碼,避免記憶體錯誤導致當機,如下例

ReDim tArray(1 To 3, 2 To 4, 11 To 13)

tArray = TransformArrayToOneDimension(tArray) ' 傳回一維陣列

  若需從多維陣列中找尋最大值或最小值,轉換為一維陣列後再處理亦是不錯的方法。



十、總結

  本文完整簡單介紹 VB 內建的 Variant 型態變數,在 VB 、 VBA 及 VBScript 預設的變數型態均為 Variant 變數,在掌握 Variant 變數特性後,您可依個人需求開發出更多的應用,若您目前尚不能理解本文內容,可於筆者經常參與之公開討論區發表討論,但本文並不是使用 VB 系列的必備知識,您不必勉強一定要了解,對於所需參照之程式碼仍可直接引用而不受影響,您會隨著使用 VB 的經驗,慢慢體會 Variant 變數型態的強大,並逐漸了解本文的意義,筆者期望能藉本文之介紹,引出更多 Variant 變數應用,筆者所架設網站尚有部分 Variant 變數相關應用,讀者若有興趣,可自行上網瀏覽。



備註
  1. 筆者採用 Microsoft Visual Basic 6 SP4 中文專業版,及 Windows 98 SE 為作業系統。
  2. 筆者自行架設與 VB 相關網頁為「培基語言」網頁,網址為 http://feitsui.hyd.ncku.edu.tw/TLCheng/Basic/ ,筆者現為成大資工 BBS basic 版版主,網址為 telnet://140.116.247.7/
  3. 筆者經常於「VBQA - 討論中心」網頁參與VB 相關問題討論,若有需要討論,建議請於該網站發表討論,網址為 http://www.vbqa.com/
  4. 本文所介紹之內容可於 VBA 上應用,唯 Office 97 系列之 VBA 目前不支援列舉 (Enum) 及選擇性參數 (Optional) ,故於 VBA 上應用需將列舉改為常數,選擇性參數改為固定參數方可使用。
  5. 本文主要參考資料為 VB5 線上手冊、 MSDN for Visual Studio (VB6 線上手冊) 及 MSDN Subscriptions Library April 2001 。


瀏覽原稿及引用

  本篇經「微軟之友」編輯刪修,原稿可參閱 old_Variant.htm ,我是覺得「微軟之友」選色很漂亮,所以原稿亦沿用相關色系,祇是文字多了一些些而已。不過寫科學論文習慣了,通常均會避免採用你、我、他,說實在的,被編輯將「讀者」改成「您」,我自己是覺得有點肉麻。

  本文原文刊載在「微軟之友 季訊」,若要引用,請註明原文出處,不要以網址為參考文獻出處,網頁僅供不易閱讀到「微軟之友 季訊」網友所設置,畢竟拿了稿費,所以要特別強調資料來源及原始出處,才不會感到愧疚。參考文獻列示建議如下所列:

鄭子璉,「Microsoft® Visual Basic Variant 變數應用」,微軟之友季訊,夏季 6 月號,第 42 ~ 49 頁,民國 90 年 6 月。