Linux設備驅動程序學習----3.模塊的編譯和裝載


模塊的編譯和裝載

更多內容請參考Linux設備驅動程序學習----目錄

1. 設置測試系統

第1步,要先從kernel.org的鏡像網站上獲取一個主線內核,並安裝到自己的系統中,因為學習驅動程序的編寫,最好使用標准內核。

第2步,必須在自己的系統中配置並構造好內核樹,這樣可以得到一個更加健壯的模塊裝載器,可以使內核的模塊要和內核源碼樹中的目標文件連接。同時也需要這些目標文件存在於內核目錄樹中。這樣,准備一個內核源代碼樹,構造一個新內核,並安裝到自己的系統中,有利於開發工作的進行。

第3步,要決定在什么地方完成模塊的開發、調試,內核代碼中的錯誤可能導致用戶進程甚至整個系統崩潰,這些錯誤通常不會制造更加嚴重的問題,但建議開發者應該在一個不包含任何敏感數據或者不執行重要服務的系統上完成內核的調試實驗。

2. Hello World模塊

  幾乎所有編程學習都是以“Hello world”示例程序開始的,在這里的模塊中也可以使用這個經典歷程。如下代碼段:

#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void)
{
    printk(KERN_ALERT "Hello, world\n");
    
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");

module_init() 宏表示模塊被裝載到內核時調用hello_init函數
module_exit() 宏表示模塊被移除內核時調用hello_exit函數
MODULE_LICENSE() 宏用來告訴內核,該模塊采用自由許可證;如果沒有該聲明,內核在裝載該模塊時會產生抱怨。

  printk() 函數在Linux內核中定義,功能類似標准C庫中的printf() 函數。是內核獨有的打印輸出函數,因為內核載運行的時不能依賴C庫。模塊中能夠調用printk()函數,是因為在insmod插入模塊之后,模塊連接到內核,可以訪問內核的公共符號。KERN_ALERT定義了消息的優先級,需要在模塊中顯式地指定高優先級的原因是:具有默認優先級的消息可能不會輸出在控制台上。

模塊編譯過程如下:

# make
make -C /lib/modules/4.15.0-55-generic/build M=/home/mcy/code/ldd3-demo/1_module modules
make[1]: Entering directory '/usr/src/linux-headers-4.15.0-55-generic'
Makefile:976: "Cannot use CONFIG_STACK_VALIDATION=y, please install libelf-dev, libelf-devel or elfutils-libelf-devel"
  CC [M]  /home/mcy/code/ldd3-demo/1_module/hello.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/mcy/code/ldd3-demo/1_module/hello.mod.o
  LD [M]  /home/mcy/code/ldd3-demo/1_module/hello.ko
make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-55-generic'

  可以通過insmod和rmmod命令來加載和卸載模塊,注意:只有超級用戶才有權加載和卸載模塊。

# insmod hello.ko
Hello, world

# lsmod 
Module                  Size  Used by
hello                  16384  0

# rmmod hello
Goodbye, cruel world

  能夠編譯模塊的前提條件是,必須在模塊的Makefile能夠找到的路徑,正確配置和構造了內核樹。模塊的編譯參考下文中編譯和裝載部分。

  編寫設備驅動程序並不困難,真正的困難在於理解設備並最大化其性能。

3. 編譯和裝載

  本節將詳細介紹如何將源代碼編譯成能夠裝載到內核中的可執行模塊。

3.1 編譯模塊

在構造內核模塊之前,需要先滿足一下條件:

  1. 應確保具備了正確版本的編譯器、模塊工具和其他必要的工具;使用太舊、太新的工具都有可能出現未知問題;
  2. 應該先配置並構造內核;最好運行和模塊對應的內核;

之后,為模塊創建Makefile很簡單,只需要添加就可以了:

obj-m := hello.o

  由內核構造系統處理其余問題,上面的賦值語句說明有一個模塊要從目標文件hello.o構造,而由該目標文件構造的模塊名稱為hello.ko。

  如果要構造的模塊名稱為module.ko,該模塊由兩個源文件file1.c和file2.c生成,則Makefile應該如下書寫:

obj-m := module.o
module-objs := file1.o file2.o

  為了上述類型的Makefile文件正常工作,必須在大的內核構造系統中調用它,即在包含模塊源代碼和Makefile文件的目錄中,用下面的命令:

make -C kernel_path M=`pwd` modules

  kernel_path為已經構造好的內核源代碼的路徑。該命令先改變目錄到-C選項指定的目錄(內核源代碼目錄),其中保存有內核的頂層Makefile文件。
M=選項讓該Makefile在構造modules目標之前返回到模塊源代碼目錄。然后,modules目標指向obj-m變量中設定的模塊,本例中為module.o。

  一般情況下,編譯模塊的文件為module.ko,在加載該模塊文件時,會出現:

# insmod my_module.ko
module: module is already loaded

  這是因為module.ko這個模塊名字和系統中的模塊名稱沖突造成的,可以在Makefile中修改目標文件,避免使用module作為模塊的命名。

  為了使內核樹之外的模塊構造更加方便,可以使用一下Makefile方法:

// Makefile

# 如果定義了KERNELRELEASE,則說明是從內核構造系統調用的
ifneq ($(KERNELRELEASE), )
    obj-m := hello.o

# 否則,是直接從命令行調用的,這時需要調用內核構造系統
else
    KERNELDIR ?= ......
    PWD := $(shell pwd)
default:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
    $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
endif

  在一個典型的構造過程中,Makefile被讀取兩次。當Makefile從命令行調用時,KERNELRELEASE變量還未設置,已安裝的模塊目錄中存在一個符號鏈接,指向內核的構造樹,以找到內核的源代碼目錄。在找到內核源代碼樹之后,該Makefile會調用default目標,這個目錄使用之前描述過的方法,第二次運行make命令($MAKE),以運行內核構造系統。在第二次讀取該Makefile文件時,設置了obj-m,而內核的Makefile負責真正構造該模塊。

  如上述Makefile所示,所有的Makefile都應該包含通常用來清除無用文件的目標、安裝模塊的目標等。

編譯清理過程如下所示:

# make clean
make -C /lib/modules/4.15.0-55-generic/build M=/home/mcy/code/ldd3-demo/1_module clean
make[1]: Entering directory '/usr/src/linux-headers-4.15.0-55-generic'
  CLEAN   /home/mcy/code/ldd3-demo/1_module/.tmp_versions
  CLEAN   /home/mcy/code/ldd3-demo/1_module/Module.symvers
make[1]: Leaving directory '/usr/src/linux-headers-4.15.0-55-generic'

3.2 裝載和卸載模塊

  模塊構造完成之后,下一步就是將模塊裝入內核,insmod和ld類似,insmod將模塊的代碼和數據裝入內核,然后使用內核的符號表解析模塊中任何未解析的符號。內核不會修改模塊在磁盤的文件,而是修改內存中的副本。insmod可以接受一些命令行選項,並且可以在模塊鏈接到內核之前給模塊中的變量賦值(即模塊傳參,參考模塊參數),模塊可以在裝載時進行配置,比編譯時的配置更加靈活。

  insmod程序加載模塊的過程解析,insmod依賴於定義在kernel/module.c中的系統調用,sys_init_module() 函數給模塊分配內核內存以便裝載模塊,然后將模塊正文復制到內存區域,並通過內核符號表解析模塊中的內核引用,最后調用模塊的初始化函數。sys_前綴的函數用於系統調用,其他函數不能使用,方便使用grep搜索系統調用。

  modprobe工具和insmod類似,也用來將模塊裝載到內核中。modprobe和insmod的區別在於,modprobe會考慮要裝載的模塊是否引用了一些當前內核不存在的符號。如果有,modprobe會在當前模塊搜索路徑中查找定義了這些符號的其他模塊。如果找到了這些模塊,會同時將這些模塊裝載到內核。此時如果使用insmod將會失敗,並在系統日志文件中記錄“unresolved symbols”(未解析的符號)消息。

  rmmod工具從內核中移除模塊。如果內核認為該模塊正在使用狀態,或者內核配置為禁止卸載模塊,則無法移除該模塊。

  lsmod工具列出當前裝載到內核中的所有模塊,還有其他模塊是否在使用某個模塊等信息。lsmod工具通過讀取/proc/modules文件來獲取這些信息。當前已裝載模塊信息也可以在/sys/module目錄下找到。

# insmod hello.ko
Hello, world

# lsmod 
Module                  Size  Used by
hello                  16384  0

# rmmod hello
Goodbye, cruel world

3.3 版本依賴

  在缺少modversions的情況下,模塊代碼必須針對要鏈接的每個版本的內核重新編譯。模塊和特定內核版本定義的數據結構和函數原型緊密關聯。

  在構造模塊過程中,可以將模塊和當前內核樹中的vermagic.o文件鏈接,該目標文件包含了大量有關內核的信息,包括目標內核版本、編譯器版本以及一些重要配置變量的設置。在試圖裝載模塊時,這些信息用來檢查模塊和正在運行的內核的兼容性。如果有任何不匹配,就不會裝載該模塊,同時會有如下信息:

# insmod hello.ko
Error inserting './hello.ko': -1 Invalid module format

查看系統日志文件/var/log/messages,將看到導致模塊裝載失敗的具體原因。

  如果要為某個特定的內核版本編譯模塊,則需要該特定版本對應的構造系統和源代碼樹。同時需要修改Makefile中的KERNELDIR變量來實現。如果打算編寫一個能夠和多個內核版本一起工作的模塊,則必須使用宏以及#ifdef條件編譯來構造並編譯模塊代碼。

可以參考<linux/version.h>中的相關定義,該頭文件已包含於<linux/module.h>頭文件中,參考如下宏定義:

UTS_RELEASE
&emsp;&emsp;擴展為一個描述內核版本的字符串,如:2.6.10
LINUX_VERSION_CODE
&emsp;&emsp;擴展為內核版本的二進制表示,版本發行號中的每一部分對應一個字節
KERNEL_VERSION(major, minor, release)
&emsp;&emsp;該宏已組成版本號的三部分為參數,創建整數的版本號

  通過檢查KERNEL_VERSION和LINUX_VERSION_CODE宏而使用預處理條件,能夠解決大部分基於內核版本的依賴性問題。最好的處理方法是,將所有相關的預處理條件語句幾種存放在一個特定的頭文件里。一般而言,依賴於特定版本或平台的代碼應該隱藏在低層宏或者函數之后,高層函數可直接調用這些函數,而無需關注低層細節。這樣的代碼便於閱讀,更為健壯。

更多內容請參考Linux設備驅動程序學習----目錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM