前往
大廳
主題 達人專欄

DLL 檔案到底是什麼?淺談動態連結函式庫

解凍豬腳 | 2020-12-28 19:25:01 | 巴幣 9728 | 人氣 31436

 
  電腦裡總是散亂著各種 DLL 檔案,無論是遊戲、工具軟體,甚至是 Windows 系統的資料夾裡,我們通常都可以見到 DLL 的蹤影。那麼,DLL 檔案到底是什麼呢?

  今天就來聊聊動態連結函式庫吧。



。DLL 檔案是什麼?

  動態連結函式庫(Dynamic-Link Library),簡稱 DLL。在我小屋看過 C 語言系列文的巴友們只要看到「函式」兩個字,應該可以很快地聯想到它可以做什麼。

  事實上,如果不去細究差別的話,你可以很乾脆地說:「dll 檔案就是 exe 執行檔。」當然這麼說有點誇張了,但無論是 *.exe 的執行檔,還是 *.dll 的動態函式庫,它們的檔案內容都遵循 Windows 系統的 PE 格式(Portable Executable,可移植的可執行檔),本質上還是很相似的。

  差別在於,exe 執行檔通常都是已經幫你設計好整個程式的流程、知道打開執行檔以後該先從哪裡開始執行,而 dll 執行檔卻沒有。

  你可以把它理解成是程式的片段,就是一個已經編譯好的工具箱,隨時等著你去取用,這就是 dll 檔案的作用。

  當然,有動態函式庫,就會有與之相對的靜態函式庫。靜態函式庫又是什麼呢?一般來說靜態函式庫和動態函式庫最顯見的差別就是靜態函式庫 *.lib 會一起被包在 exe 可執行檔裡面,而且一般來說靜態函式庫不能再另外接著用其他的函式庫,但是動態函式庫卻可以引用其他的函式庫,活用性比起來就已經有很大的差別了(先前敘述有誤,感謝巴友 happylin 指正



。把程式的片段包在 DLL 檔案有什麼好處?

  之前在《跟著豬腳 C 起來:程式語言的函數,不只是算數》一文裡就有說過遵循「程式模組化」原則設計的好處。如果把常常重複使用的程式碼包到函式裡面,那麼你的程式就會變得乾淨、環保、方便。

  這樣的觀念對於平常有在寫程式的人來說,應該已經是再也熟悉不過。實際上,我們除了把程式碼包在函式裡,讓它能在程式裡的其他地方重複使用以外,我們還可以把這樣的觀念擴展到讓它能在「不同的程式」之間重複使用,這就涉及了 DLL 的編寫。

  比如說,你今天寫好了一個函數 f(n),用來計算費氏數列 {1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, ...} 第 N 項的值,那麼如果你想把這樣的功能拿到另一個程式上面執行呢?把它編譯成 DLL 就對了。

  編譯成 DLL 檔案之後,當你有任何一個程式想要計算費氏數列,你只需要把這個 DLL 檔案丟到你的程式旁邊,讓程式直接去讀取它,你就可以使用這個函式了。



。不只複用,它還可以在特定情況下用來提昇效率

  如果你已經學過好幾種程式語言,那麼你應該會發現諸如 JavaScript、PHP、Python、Ruby 這種直譯語言在執行大量運算的時候效率非常糟糕,而像 C、C++、Golang、Java 這種能編譯成執行檔的編譯語言則會快上許多,這是我們在 C 語言系列的第一篇曾說過的,就不再贅述了。

  然而,有時候情況沒有這麼簡單。我們都知道每個程式語言都有各自擅長的項目,比如你想利用 Python 寫個腳本讀取網頁資料(即網路爬蟲),又需要用到大量的運算——要是全用 C 語言完成的話,寫起來一定很麻煩;要是全用 Python 語言完成的話,一遇到大量運算的部分就會慢得要命,非常尷尬。

  遇上這種問題,只要這個「具有大量運算的函式」本身具有可以獨立完成作業的性質,那麼我們可以選擇用 C 語言把這一部分的過程編譯成 DLL,然後再讓 Python 去引用它,那就可以同時兼具 Python 和 C 語言的優點了。

  對於一個已經學過多種程式語言的人來說,這是個很棒的必學技巧。事實上,坊間也有不少軟體的執行檔主要只負責 GUI 圖形使用者介面(也就是純粹外觀、按鈕的功能),實際的軟體核心功能都寫在 DLL 檔案裡面,如此將外觀和功能分開來,也便於工程師維護。



。如何用 C 寫一個 DLL 檔案,供 Python 使用?

  不同的程式語言都有各自的方法。如果是 C 語言的話(以 Dev-C++ 環境為例),我們只需要在創建專案的時候選擇 DLL,它就會自動幫你產生一個 dllMain.c 和一個 dll.h:


  一般來說我們主要的 code 都寫在 dllMain.c 裡面,然後 dll.h 用來放原型宣告(忘了原型宣告是什麼嗎?快去複習剛剛上面附的函數文吧!):



  至於這個專案預先幫你產生好的 DllMain 函式內容對於一般的使用者來說不用太在意,畢竟這段涉及底層的操作,就不細講。

  我們只需要在函式前面加上 DLLIMPORT(註),讓 C 語言的編譯器知道你編譯成 DLL 的時候想開放這個函式供其他程式呼叫。這裡每一個開放的函式都被放在 DLL 檔案裡面特定的位置上,就像是一個入口一樣,所以我們可以說它是一個 entry point(進入點)。

  坊間也有工具可以直接看 DLL 的 entry point,例如 Dependency Walker:


  註:DLLIMPORT 只是由 Dev-C++ 設的巨集而已,你用不同的工具寫 C 語言可能預設會是不一樣的用法,你高興也可以從 dll.h 裡把它修改成 ENTRY 或是任何你看了覺得順眼的單字。仔細看看 dll.h 前半段的定義內容,可以看到預設是依照情況把「__declspec(dllexport)」和「__declspec(dllimport)」兩種符號定義為 DLLIMPORT,看這裡就能知道你的開發環境預設用什麼詞來把函式 export。

  總之把 DLL 的 code 寫好,編譯它,你就得到了一個新鮮的 DLL 檔案。

  再來我們用 Python 寫主程式,這步驟也不難:

from ctypes import cdll
import time

dll = cdll.LoadLibrary(".\\Fibonacci.dll")
startTime = time.time()
print("Fibonacci(40) =", dll.Fibonacci(40))
print("Execution time:", time.time() - startTime, "second(s)")

  把這個寫好的 py 檔案和剛才編譯好的 DLL 放在一起。

  只要引用 ctypes 裡頭的 cdll 函式庫,然後用 cdll.LoadLibrary(fileName) 載入剛才用 C 語言寫好的 Fibonacci.dll,再呼叫剛才寫在 DLL 裡面的函數 Fibonacci(n),就能直接使用了。你用 Python 寫的程式在執行 dll.Fibonacci(n) 的時候,就相當於你的電腦用 C 語言來執行剛才寫的函式內容。

  同時利用 time 函式庫,我們可以藉此得知花了多久時間來計算:


  我的電腦用了大約 3.32 秒的時間計算出 Fibonacci(45) = 1134903170 的結果。

  相較之下,我們把同樣的演算法拿到 Python 上面直接寫:


  同一台電腦用了大約 150.79 秒的時間計算出同樣的結果。

  當然若要單憑這點來認定 Python 和 C 語言之間的優劣是不公平的,就像剛才所說的,每個程式語言都有它擅長的地方,你總不能要求一個游泳選手在鉛球項目拿第一名給你看。

  上面示例的演算法很慢,不過這也只是為了讓大家感受到直譯語言和編譯語言在大量呼叫函式的效率差別才這麼做,不然其實還有更好的算法。以我的操作環境而言,如果改用迴圈搭配陣列的算法,那無論是直接在 Python 上面執行,或者是用 C 寫到 DLL 來取用,都可以只花費大約 0.0005 秒(0.5 毫秒)的時間把 Fibonacci(45) 算出來。

  你也可以利用跨語言的特點做一些神奇的操作,例如用 C 語言寫一個可以彈出視窗的 DLL 檔案,讓你能夠在執行 Python 程式的時候彈出視窗:


  另外講個跟開發程式不太相關的,其實 Windows 內建的 rundll32.exe 也是用來執行 DLL 函式的工具喔:


  所以,當你在網路上下載東西的時候,千萬不要覺得「只要不是 .exe 檔案就一定安全」。有心的駭客是可以把木馬或病毒程式寫在 DLL 檔案裡面,然後騙你用系統內建的 rundll32.exe 去啟動它(慢著,不要因為我這麼說而把 rundll32 砍掉啊,這東西很重要)。



。撰寫 DLL 檔案的注意事項

  因為上面範例使用的是動態型別的 Python 語言,當它和 DLL 溝通的時候,對於資料型態這方面就比較不會那麼嚴格,自然就比較輕鬆。

  不過坦白講,把 code 寫進 DLL 很容易,但在不少情況下,實際上使用起來還真不是那麼簡單。我們撰寫和引用 DLL 的時候時常需要多參考網路上的資料(或慘案),確保自己沒有忽略掉該注意的事項。

  比如說,同樣名字的資料型態在不同的程式語言可能會有不同的規範,不同語言的 string 不見得可以通用(以下是用 Golang 撰寫 DLL 的例子):

import "C"
import (
    "fmt"
)

//export Hello
func Hello(name *C.Char) {
    nameStr := C.GoString(name)
    fmt.Printf("Hello! My name is %s\n", nameStr)
}

  需要用 Golang 的 *C.Char 來接收從 C# 傳入的 []byte,然後再用 C.GoString 轉成 Golang 的 String 才能在 Golang 裡面使用。

  又或者,往 DLL 傳入、傳出資料的時候需要特別注意記憶體的管理,像是在 Golang 裡面使用 C.CString 轉換型態而存入的變數空間是不會被系統自動回收的,之後當你不再使用該變數的時候就需要使用 C.free() 來釋放記憶體空間,否則一旦使用量多了就會造成大量的記憶體浪費(用 Golang 寫成的 DLL 不免要常常跟 C、C++、C# 等語言打交道,所以很可能會用到):

package main

/*
#include <stdio.h>
#include <stdlib.h>
*/
import "C"
import (
    "unsafe"
)

func main() {
    cstr := C.CString("Hello, world")
    C.free(unsafe.Pointer(cstr))
}

  另外也必須注意你編譯的 DLL 是 64 位元(x64)還是 32 位元(x86),因為這個涉及了記憶體位址的轉換,如果版本不同的話就無法順利執行。若你是把主程式編譯成 32 位元版本,那你的 DLL 也應該要編譯成 32 位元版,只要編譯的時候選用不同的編譯器,就可以分別編譯出 64 位元和 32 位元的成品。

  像 Dev-C++ 就可以直接從選項看到所選的編譯器版本:





。Managed DLL

  到目前為止,我們討論的都是傳統意義上的 unmanaged DLL(或稱 native DLL,原生的動態連結函式庫)。如果你平常是 VB、C# 等基於 .NET framework 的平台上執行的程式語言的開發者,你會發現你的 Visual Studio 也有專門用來編譯 DLL 檔的專案選項:


  然而,從這個途徑編譯出來的 DLL 檔並不是傳統意義上的 DLL。

  說到這個就不得不講講 .NET framework。我們在 Windows 上面時常聽到的 .NET framework(以下簡稱 .NET)是 Microsoft 設計出來的開發平台,為了方便管理 VB、C#、ASP 等程式語言而生。

  當我們用 Visual Studio 編譯程式的時候,這些程式碼會首先被編譯成一種 .NET 平台專用的中繼語言,而這個 .NET 就是負責提供 CLR(common language runtime,通用語言執行平台),把中繼語言再次翻譯成電腦真正可以執行、理解的機器碼來執行。

  這麼設計有好處嗎?當然有。Microsoft 的人只要讓他們做的程式語言開發環境遵循 .NET 的規範來設計,那麼以後如果想要新增或優化某些功能(例如記憶體管理、多執行緒)的時候,只需要讓 Windows 使用者更新 .NET 的元件就可以了,而不需要讓全天下的程式設計師把個別的程式重新編譯、發布。

  因此,這些 code 都是受到 CLR 的管理,在 .NET 平台創建出來的虛擬環境上執行。既然如此,在 .NET 平台編譯出來的 DLL 也就稱為 managed(受管理的)DLL 了,當然就不能被其他非 .NET 平台的語言直接使用。

  其實要想用 C# 輸出 unmanaged DLL 也不是不行啦,只是你得另外安裝一些第三方工具來達到要求就是了,例如 DllExport

  DLL 能做出什麼「只有做成 DLL 才能實現的功能」,就看你如何去運用了。以我自己的開發經驗,我還試過先在 GUI 端監聽一個 port,然後把 port 編號送到 DLL 裡面,再讓 DLL 端另外監聽一個 port,同時把 port 編號用 HTTP request 回傳到 GUI 端的 port 上面,藉此達到 GUI 和 DLL 兩端能夠用 HTTP request 互相通訊的效果。

  那麼,關於 DLL 就說到這裡。如果你是個通曉多種程式語言的開發者,下次當你想起你會的其他語言可以替你正在開發的程式補足缺點,不妨試試把這些功能寫成 DLL,讓它變得更強大吧。
 
送禮物贊助創作者 !
50
留言

創作回應

屠屠
其實dll還有一個好處 當exe寫到很大時可以將一些函數獨立出dll 當程式要更新時 如果只是函數要更新那就只要更新dll 不需要整個exe都更新
2020-12-30 18:59:45
派大星教授死掉了咩噗
dll就是給你偷偷修改hook內容,黑人家程式用的地方XD
2021-01-08 17:34:00
勳章向創作者進行贊助 ✦
2021-11-08 15:00:32
解凍豬腳
感謝贊助 [e38]
2021-11-09 02:20:16
馴龍高手尹志平
受用 感謝
2021-11-25 10:07:31
xxxar
我之前寫VC++的DLL讓某些套裝軟體(也是用VC++寫的)可擴充功能去存取資料庫,也碰過會長提到的string字符不相容!明明都是同一個字,但是在C++裡就要轉來轉去,此外語法讓我超級抓狂。若非因為那個套裝軟體的限制(存取資料庫的功能要自己寫dll)以及追求那一秒之間的操作,我是不會考慮dll的。
2021-12-14 15:37:57
追蹤 創作集

作者相關創作

更多創作