linux:核心如何定位並呼叫裝置驅動初始化函式
寫過linux驅動程式的人都知道需要將驅動的初始化函式通過module_init註冊,然後在通過menuconfig配置的時候選擇隨核心一起編譯(非模組),系統在啟動的時候就能夠自動呼叫驅動初始化函數了。真是一件神奇的事情!
#include <linux/kernel.h>
#include <linux/module.h>
MODULE_LICENSE ("GPL");//開源協議GPL 或者Dual BSD
MODULE_AUTHOR ("TOM");//作者
MODULE_DESCRIPTION ("MY_TEST");//描述此驅動
//EXPORT_NO_SYMBOLS;//不匯出函式 可以不寫
//EXPORT_SYMBOL(hello_data);//匯出hello_data
int test_init(void)
{
printk(KERN_INFO "hello world\n");
return 0;
}
void test_exit(void)
{
printk(KERN_INFO "goodbye world\n");
}
module_init(test_init); //註冊DriverEntry
module_exit(test_exit); //註冊DriverUnload
核心是如何知道存在這個驅動?又是在何時呼叫了驅動初始化函式?看來祕密應該就在這個module_init上。我們就從module_init入手,在linux kernel的原始碼中找尋這個問題的答案。下面是對相關程式碼的摘錄,省略了不相關的部分。(檔案位於include/linux/init.h)
/*
* Used for initialization calls..
*/
typedef int (*initcall_t)(void);
/* initcalls are now grouped by functionality into separate
* subsections. Ordering inside the subsections is determined
* by link order.
* For backwards compatibility, initcall() puts the call in
* the device init subsection.
*
* The `id' arg to __define_initcall() is needed so that multiple initcalls
* can point at the same handler without causing duplicate-symbol build errors.
*/
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
#define device_initcall(fn) __define_initcall(fn, 6)
#define __initcall(fn) device_initcall(fn)
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
從程式碼我們可以看到module_init是一個巢狀巨集定義,巢狀層次為:module_init->__initcall->device_initcall->__define_initcall 經過這麼多層巢狀,模板中的module_init(test_init)到底最後變成了什麼模樣?我們做個實驗,首先對模板程式進行適當的調整,並命名為driver.c。
// driver.c
typedef int (*initcall_t)(void);
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
#define device_initcall(fn) __define_initcall(fn, 6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x);
int test_init(void)
{
return 0;
}
module_init(test_init);
接下在我們使用gcc對driver.c進行預處理
$ gcc -E driver.c
# 1 "driver.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 1 "<command-line>" 2
# 1 "driver.c"
typedef int (*initcall_t)(void);
# 12 "driver.c"
int test_init(void)
{
return 0;
}
static initcall_t __initcall_test_init6 __used __attribute__((__section__(".initcall" "6" ".init"))) = test_init;;
module_init(test_init);經過幾次巨集擴充套件變成了static initcall_t __initcall_test_init6 __used attribute((section(".initcall" “6” “.init”))) = test_init;看上去是變成了一個變數賦值語句,祕密一定就在這條語句中,我們逐個來分析一下它的組成部分
序號 | 組成部分 | 說明 |
---|---|---|
1 | static | 關鍵字,說明是檔案內部變數 |
2 | initcall_t | 自定義函式指標,見driver.c開頭部分 |
3 | __initcall_test_init6 | 變數名,看著和test_init函式還是有些兒像的哦 |
4 | __used | 變數的編譯器屬性,這個和本次實驗關係不大,有興趣的可以去核心原始碼中搜索一下 |
5 | attribute((section(".initcall" “6” “.init”))) | 變數的編譯器屬性,這個是祕密的重點!!! |
6 | test_init | 初始化函式名 |
我們著重看一下第5項,對於編譯器而言這一項相當於__attribute__((section(".initcall6.init"))),可這又是什麼意思呢?就是告訴編譯器要把這個變數放到段名是".initcall6.init"的段裡。對於什麼叫做段(section),大家可以自行補習一下編譯原理和ELF檔案結構。簡單著理解,想象一下程式是由許多段組成的,有些兒段裡放的是程式,有些兒段裡放的是資料。這句話就是將變數__initcall_test_init6放到.initcall6.init資料段裡。 這樣就行了嗎?就放到.initcall6.init段裡核心就知道需要呼叫啦?事實確實是這樣子的,那核心又是怎麼呼叫的呢?其實這才是核心設計的巧妙所在,我們來看一下核心的啟動檔案/init/main.c。關鍵部分摘錄如下。
extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
"early",
"core",
"postcore",
"arch",
"subsys",
"fs",
"device",
"late",
};
static void __init do_initcall_level(int level)
{
extern const struct kernel_param __start___param[], __stop___param[];
initcall_t *fn;
strcpy(static_command_line, saved_command_line);
parse_args(initcall_level_names[level],
static_command_line, __start___param,
__stop___param - __start___param,
level, level,
&repair_env_string);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
核心對初始化呼叫分為了8個等級"early"、“core”、“postcore”、“arch”、“subsys”、“fs”、“device”、“late”,每個等級又對應著一個型別為initcall_t *的變數,依次為__initcall0_start、__initcall1_start、__initcall2_start、 __initcall3_start、__initcall4_start、__initcall5_start、__initcall6_start、__initcall7_start。 這裡我們就看到__initcall6_start和之前的段.initcall6.init應該有些聯絡。可是我們搜遍核心程式碼都找不到__initcall6_start變數的定義。那__initcall6_start究竟在哪裡? 我們來看/arch/XXX/kernel/vmlinux.lds檔案(其中XXX是平臺名字,如果是PC的話就是x86,我用的是mips平臺),這個檔案是自動生成的,需要配置和編譯核心原始碼。那這個lds是什麼檔案呢?lds就是連結指令碼檔案,我們知道程式是需要通過編譯然後連結,最後才能生成完整的可執行程式,核心也不例外。只是我們平時用gcc編譯應用程式的時候,都是使用的內建預設lds檔案,但是核心是提供了自身的連結指令碼檔案的。對連結指令碼檔案語法有興趣的參考https://sourceware.org/binutils/docs/ld/index.html#SEC_Contents 摘錄關鍵部分如下
/* will be freed after init */
. = ALIGN(4096); /* Init code and data */
__init_begin = .;
. = ALIGN(4096);
.init.text : AT(ADDR(.init.text) - 0)
{
_sinittext = .;
*(.init.text) *(.cpuinit.text) *(.meminit.text) _einittext = .;
}
.init.data : AT(ADDR(.init.data) - 0)
{
*(.init.data) *(.cpuinit.data) *(.meminit.data) *(.init.rodata) *(.cpuinit.rodata) *(.meminit.rodata) . = ALIGN(32);
__dtb_start = .;
*(.dtb.init.rodata) __dtb_end = .;
. = ALIGN(16);
__setup_start = .;
*(.init.setup) __setup_end = .;
__initcall_start = .;
*(.initcallearly.init)
__initcall0_start = .;
*(.initcall0.init) *(.initcall0s.init)
__initcall1_start = .;
*(.initcall1.init) *(.initcall1s.init)
__initcall2_start = .;
*(.initcall2.init) *(.initcall2s.init)
__initcall3_start = .;
*(.initcall3.init) *(.initcall3s.init)
__initcall4_start = .;
*(.initcall4.init) *(.initcall4s.init)
__initcall5_start = .;
*(.initcall5.init) *(.initcall5s.init)
__initcallrootfs_start = .;
*(.initcallrootfs.init) *(.initcallrootfss.init)
__initcall6_start = .;
*(.initcall6.init) *(.initcall6s.init)
__initcall7_start = .;
*(.initcall7.init) *(.initcall7s.init)
__initcall_end = .;
__con_initcall_start = .;
*(.con_initcall.init)
__con_initcall_end = .;
__security_initcall_start = .;
*(.security_initcall.init)
__security_initcall_end = .; }
總算是找到 __initcall6_start了,原來它藏在vmlinux.lds裡,可這是什麼意思呢?
語句 | 釋義 |
---|---|
__initcall6_start = .; | 這句是將當前的連結地址賦值給__initcall6_start |
*(.initcall6.init) | 為.initcall6.init申請接下來的連結地址空間 |
也就是說__initcall6_start擁有.initcall6.init的起始地址,所有指定分配到.initcall6.init段的變數都將在這個地址之後順序排列。結合do_initcall_level的程式碼來看一下。fn是指向initcall_t型別的指標,我們看那個for迴圈,當level=6的時候,fn賦值為initcall_levels[6],也就是__initcall6_start,也就是分配到.initcall6.init段裡的第一個initcall_t型別變數,假如只有模板中的那一個驅動的話,也就是指向了__initcall_test_init6,通過*fn就自然取到了test_init。
static void __init do_initcall_level(int level)
{
extern const struct kernel_param __start___param[], __stop___param[];
initcall_t *fn;
strcpy(static_command_line, saved_command_line);
parse_args(initcall_level_names[level],
static_command_line, __start___param,
__stop___param - __start___param,
level, level,
&repair_env_string);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
寫到這裡,相信大家就明白了,原來核心是通過指定段儲存函式指標結合連結檔案來實現的定位並呼叫裝置驅動初始化函式。
下面使用一個例子,在應用層來演示一下這個技術實現。我們需要編寫c和lds檔案
/*
* Copyright (C) 2018 Jafon.Tian
*/
#include <stdio.h>
typedef int (*initcall_t)(void);
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id \
__attribute__((__section__(".initcall" #id ".init"))) = fn
#define device_initcall(fn) __define_initcall(fn, 6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x);
extern initcall_t __initcall6_start[];
extern initcall_t __initcall6_end[];
int test_init_1(void)
{
fprintf(stdout, "In test_init_1 ...\n");
return 0;
}
int test_init_2(void)
{
fprintf(stdout, "In test_init_2 ...\n");
return 0;
}
module_init(test_init_1);
module_init(test_init_2);
void do_initcall_level6(void)
{
initcall_t *fn = __initcall6_start;
for(fn = __initcall6_start; fn < __initcall6_end; fn++)
(*fn)();
return;
}
int main(int argc, char* argv[])
{
fprintf(stdout, "start testing\n");
do_initcall_level6();
fprintf(stdout, "end testing\n");
return 0;
}
lds檔案如下(可以通過ld --verbose命令得到內建的連結指令碼,在此基礎上進行修改)
/* linker2.lds */
/* Script for -z combreloc: combine and sort reloc sections */
/* Copyright (C) 2014-2015 Free Software Foundation, Inc.
Copying and distribution of this script, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu"); SEARCH_DIR("=/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/lib/x86_64-linux-gnu"); SEARCH_DIR("=/usr/local/lib64"); SEARCH_DIR("=/lib64"); SEARCH_DIR("=/usr/lib64"); SEARCH_DIR("=/usr/local/lib"); SEARCH_DIR("=/lib"); SEARCH_DIR("=/usr/lib"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64"); SEARCH_DIR("=/usr/x86_64-linux-gnu/lib");
SECTIONS
{
/* Read-only sections, merged into text segment: */
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
.interp : { *(.interp) }
.note.gnu.build-id : { *(.note.gnu.build-id) }
.hash : { *(.hash) }
.gnu.hash : { *(.gnu.hash) }
.dynsym : { *(.dynsym) }
.dynstr : { *(.dynstr) }
.gnu.version : { *(.gnu.version) }
.gnu.version_d : { *(.gnu.version_d) }
.gnu.version_r : { *(.gnu.version_r) }
.rela.dyn :
{
*(.rela.init)
*(.rela.text .rela.text.* .rela.gnu.linkonce.t.*)
*(.rela.fini)
*(.rela.rodata .rela.rodata.* .rela.gnu.linkonce.r.*)
*(.rela.data .rela.data.* .rela.gnu.linkonce.d.*)
*(.rela.tdata .rela.tdata.* .rela.gnu.linkonce.td.*)
*(.rela.tbss .rela.tbss.* .rela.gnu.linkonce.tb.*)
*(.rela.ctors)
*(.rela.dtors)
*(.rela.got)
*(.rela.bss .rela.bss.* .rela.gnu.linkonce.b.*)
*(.rela.ldata .rela.ldata.* .rela.gnu.linkonce.l.*)
*(.rela.lbss .rela.lbss.* .rela.gnu.linkonce.lb.*)
*(.rela.lrodata .rela.lrodata.* .rela.gnu.linkonce.lr.*)
*(.rela.ifunc)
}
.rela.plt :
{
*(.rela.plt)
PROVIDE_HIDDEN (__rela_iplt_start = .);
*(.rela.iplt)
PROVIDE_HIDDEN (__rela_iplt_end = .);
}
.init :
{
KEEP (*(SORT_NONE(.init)))
}
.plt : { *(.plt) *(.iplt) }
.plt.got : { *(.plt.got) }
.plt.bnd : { *(.plt.bnd) }
.text :
{
*(.text.unlikely .text.*_unlikely .text.unlikely.*)
*(.text.exit .text.exit.*)
*(.text.startup .text.startup.*)
*(.text.hot .text.hot.*)
*(.text .stub .text.* .gnu.linkonce.t.*)
/* .gnu.warning sections are handled specially by elf32.em. */
*(.gnu.warning)
}
.fini :
{
KEEP (*(SORT_NONE(.fini)))
}
PROVIDE (__etext = .);
PROVIDE (_etext = .);
PROVIDE (etext = .);
.rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) }
.rodata1 : { *(.rodata1) }
.eh_frame_hdr : { *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) }
.eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) }
.gcc_except_table : ONLY_IF_RO { *(.gcc_except_table
.gcc_except_table.*) }
.gnu_extab : ONLY_IF_RO { *(.gnu_extab*) }
/* These sections are generated by the Sun/Oracle C++ compiler. */
.exception_ranges : ONLY_IF_RO { *(.exception_ranges
.exception_ranges*) }
/* Adjust the address for the data segment. We want to adjust up to
the same address within the page on the next page up. */
. = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
/* Exception handling */
.eh_frame : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) }
.gnu_extab : ONLY_IF_RW { *(.gnu_extab) }
.gcc_except_table : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) }
.exception_ranges : ONLY_IF_RW { *(.exception_ranges .exception_ranges*) }
/* Thread Local Storage sections */
.tdata : { *(.tdata .tdata.* .gnu.linkonce.td.*) }
.tbss : { *(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon) }
.preinit_array :
{
PROVIDE_HIDDEN (__preinit_array_start = .);
KEEP (*(.preinit_array))
PROVIDE_HIDDEN (__preinit_array_end = .);
}
.init_array :
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)))
KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors))
PROVIDE_HIDDEN (__init_array_end = .);
}
.fini_array :
{
PROVIDE_HIDDEN (__fini_array_start = .);
KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)))
KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors))
PROVIDE_HIDDEN (__fini_array_end = .);
}
.ctors :
{
/* gcc uses crtbegin.o to find the start of
the constructors, so we make sure it is
first. Because this is a wildcard, it
doesn't matter if the user does not
actually link against crtbegin.o; the
linker won't look for a file to match a
wildcard. The wildcard also means that it
doesn't matter which directory crtbegin.o
is in. */
KEEP (*crtbegin.o(.ctors))
KEEP (*crtbegin?.o(.ctors))
/* We don't want to include the .ctor section from
the crtend.o file until after the sorted ctors.
The .ctor section from the crtend file contains the
end of ctors marker and it must be last */
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors))
KEEP (*(SORT(.ctors.*)))
KEEP (*(.ctors))
}
.dtors :
{
KEEP (*crtbegin.o(.dtors))
KEEP (*crtbegin?.o(.dtors))
KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))
KEEP (*(SORT(.dtors.*)))
KEEP (*(.dtors))
}
.jcr : { KEEP (*(.jcr)) }
.data.rel.ro : { *(.data.rel.ro.local* .gnu.linkonce.d.rel.ro.local.*) *(.data.rel.ro .data.rel.ro.* .gnu.linkonce.d.rel.ro.*) }
.dynamic : { *(.dynamic) }
.got : { *(.got) *(.igot) }
. = DATA_SEGMENT_RELRO_END (SIZEOF (.got.plt) >= 24 ? 24 : 0, .);
.got.plt : { *(.got.plt) *(.igot.plt) }
.data :
{
*(.data .data.* .gnu.linkonce.d.*)
SORT(CONSTRUCTORS)
}
.data1 : { *(.data1) }
_edata = .; PROVIDE (edata = .);
. = .;
__bss_start = .;
.bss :
{
*(.dynbss)
*(.bss .bss.* .gnu.linkonce.b.*)
*(COMMON)
/* Align here to ensure that the .bss section occupies space up to
_end. Align after .bss to ensure correct alignment even if the
.bss section disappears because there are no input sections.
FIXME: Why do we need it? When there is no .bss section, we don't
pad the .data section. */
. = ALIGN(. != 0 ? 64 / 8 : 1);
}
.lbss :
{
*(.dynlbss)
*(.lbss .lbss.* .gnu.linkonce.lb.*)
*(LARGE_COMMON)
}
. = ALIGN(64 / 8);
. = SEGMENT_START("ldata-segment", .);
.lrodata ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
{
*(.lrodata .lrodata.* .gnu.linkonce.lr.*)
}
.ldata ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)) :
{
*(.ldata .ldata.* .gnu.linkonce.l.*)
. = ALIGN(. != 0 ? 64 / 8 : 1);
}
. = ALIGN(64 / 8);
_end = .; PROVIDE (end = .);
. = DATA_SEGMENT_END (.);
. = SEGMENT_START(".initcall6.init", .);
__initcall6_start = ADDR(.initcall6.init);
.initcall6.init : { *(.initcall6.init) }
__initcall6_end = .;
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
.comment 0 : { *(.comment) }
/* DWARF debug sections.
Symbols in the DWARF debugging sections are relative to the beginning
of the section so we begin them at 0. */
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info .gnu.linkonce.wi.*) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line .debug_line.* .debug_line_end ) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
/* SGI/MIPS DWARF 2 extensions */
.debug_weaknames 0 : { *(.debug_weaknames) }
.debug_funcnames 0 : { *(.debug_funcnames) }
.debug_typenames 0 : { *(.debug_typenames) }
.debug_varnames 0 : { *(.debug_varnames) }
/* DWARF 3 */
.debug_pubtypes 0 : { *(.debug_pubtypes) }
.debug_ranges 0 : { *(.debug_ranges) }
/* DWARF Extension. */
.debug_macro 0 : { *(.debug_macro) }
.gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}
進行編譯
$ gcc -Tlinker2.lds l2.c -o l2
測試
$ ./l2
start testing
In test_init_1 ...
In test_init_2 ...
end testing