内核文件解读
为了解决 initcall_t
到底是什么变量类型则必须提及 C 语言中一个比较少见的内容——可执行文件格 式,只有了解了这种文件格式才能具体知道 initcall_t
的意义。
ELF 文件格式
ELF 是*nix 系统上可执行文件的标准格式,它取代了 out 格式的可执行文件,原因在于它的可扩展 性。 ELF 格式的可执行文件可有多个 section。DWARF(Debugging With Attribute Record Format)是经常 碰到的名词,它在 ELF 格式的可执行文件中。
ELF文件有三种不同的形式:
1. Relocatable:由编译器和汇编器生成,由 linker 处理它。
2. Executable:所有的重定位和符号解析都完成了,也许共享库的符号要在运行时刻解析。
3. hared Object:包含 linker 需要的符号信息和运行时刻所需的代码。
ELF 文件有双重性质:一方面,编译器、汇编器、连接器都把它看作是逻辑段(sections)的集合, 另一方面 loader 把它看作段(segments)的集合。Section 是给 linker 做进一步处理的,而 segments 是被 映射到内存中去的。 (中文里面 section 可以叫做节也可以叫段,而 segment 亦然,为避免歧义,这里坚持 用英文表示)一个 segment 可以由几个 sections 组成。为了定位不同 segment/section,可执行文件用一个 table 来记录各个 segment/section 的位置和描述。Relocatable 有 section table,Executable 有 program header table。而 Shared Object 两者都有。
上图演示了从不同的角度来解释 segment 和 section 的不同。
当 as 生成一个目标文件时,它假设程序段是从地址 0 开始,ld 则把最后的地址赋给这个程序段.以至于 不同的程序段不会相互复盖。ld 把程序移动到各自的运行时地址,指定 section 的运行时地址叫重定位 (relocation)。
as 输出的目标文件至上有三个 section,任何一个都有可能为空,它们是 text,data,bss 段。你可以不写诸 如.text 或.data 段,但目标文件中还是存在这些段,只不过是空的,在目标文件中段是如下排列
为了让 ld 能正确重定位各段,as 生成一些重定位所需的信息。
实际上 as 用的每一个地址都是以这样的形式:(section)+(offset into section)
表示,ld 把所有相同的 section 放到连续的地址里。你还可以用 subsections 把一个大的 section 分成多个小的 section。可以用标号 来区分,这里就不细说了。
普通情况下 ld 处理四种段: named section
:
text section
data section //这两个段放着你的程序,它们是分开的但是却是相等的段。只是在运行的时刻,text 段不 能被改变。
bss section
absolute section //这个段的 0 地址总是被重定位到运行地址0
undefined section //用来放置不在前面几个段里的数据。
对于一个在 text、data、bss 中的符号而言,它的值就是从段首到它的偏移,于是,当 ld 在连接各段 时就改变了 label 的值。
对于没有定义(undefined)的值,ld 尽量从外部其他文件引入并确定其值。
不同的 sections 的含义(大家都知道的我就不说了): .dynamic
:该 section 保存着动态连接的信息。 .dtnstr
:保存动态连接时需要的字符串。 .dynsym
:保存动态符号表如“symbol table”的描述。 .interp
:保存程序的解释程序(interperter)的路径 .line
:包含编辑字符的行数信息,它描述源代码与机器代码之间的对应关系。 .rel<name>
和.rela<name>
:保存重定位的信息。 .rodata
和.rodata1
:保存只读数据,在进程映像中构造不可写的段。
前缀是点(.)的 section 名是系统保留的
Link Scripts 知识
我们通常有一个疑问,为什么我们编出来的代码肯定是在用户地址空间运行,而内核编出来的代码 却一定是运行在内核空间?如果我们把目光仅仅盯在 C 文件或 h 文件甚至 Makefile,估计想破脑袋也不 知道为什么会这样。其实我们经常忽视了链接器的作用。不能简单地认为链接器仅仅完成将各 obj 文件 拼在一起的任务,而且它还指定每个段被装入内存的真正地址,没错,是装入!
链接器(Linker)其实有自己的一套语言规范,其目的是描述输入文件中的 sections 是如何映射到输 出文件中,并控制输出文件的内存排列。如果你从来没有看到过 ld script
,那么请用 ld -verbose
查看输出 结果,那就是 ld script
。只是它是内置在链接器中,而且 ld 就是使用这个缺省的 script 去生成输出我们平 时应用程序 obj,所以如果是用缺省的 ld script 生成内核,那么它肯定也只能跑在用户空间。
我们已经知道每一个目标文件有一个 sections 的列表,在输入文件中,是 input section,在输出文件 中叫 output section。每个 section 有名字和大小。大多数 section 有相关的数据块,就是 section contents。 一个 section 可以是标记为 loadable,意味着输出文件在运行时可以把这一 section 装入内存。没有内容的 section 可以叫 allocatable,表示这块区域放在内存的某个地方,但没有什么特殊的东西放在里面(一般都 是被初始化为 0),一个既不是 loadable 也不是 allocatable 的 section 一般是包含一堆调试信息。
每一个 loadable 和 allocatable 的 section 有两个地址。第一个是 VMA,即虚存地址。这是输出文件运 行时的地址。第二个是 LMA,即装入内存地址。这是 section 被装入的地址。在多数情况下,这两个地 址是相同的。他们不同的例子是:当数据 section 被装入 ROM,当程序开始执行时被复制到 RAM(这个 技术通常用来初始化基于 ROM 系统的全局量)。
你可以用 dumpobj -h
去查看目标文件的 section 信息。每个目标文件有符号表。每个符号有一个名字, 且每个有定义的符号有一个地址及其他信息。你可以用 nm
查看符号表信息,也可以用 objdump -t
命令查 看。
最简单的 script 只有一个 SECTIONS 命令。它描述输出文件的内存排列。
例子:
SECTIONS {
.=0x10000; /*代码被装入到此地址*/
.text SIZEOF_HEADERS:{
*(.init)
*(.text)
*(.fini)
}
.=0x8000000; /*数据被装入到此地址*/
.data:{*(.data)}
.bss:{*(.bss)} }
在上例中,第 3 行的’.’是一个特殊符号,用来做定位计数器。它根据输出段的大小增长。在 SECTIONS 开始时它等于 0。
‘*
‘是一个通配符,匹配所有的文件,表达式”*(.text)
“表示所有输入文件的”.text
“段。输出文件的.text
段包含所有输入文件的.init
和.text
及.fini
。
在 linker 放置”.data
“后,定位计数器的值等于0x8000000
加上”.data
“的大小。然后 linker 会把.bss 段
放在.data
之后。注意:linker 可能会在.data
和.bss
段之间划出一个 gap。
程序中执行的第一条指令叫 entry point,可以用 ENTRY 指定入口点。如ENTRY(symbol)
。linker 有 几种方法设置入口点:
1。在命令行中输入`-e entry`
2。在 linker script 文件中指定 `ENTRY(symbol) `
3。如果定义了 start,则 start 的值就是入口点
4。.text 的第一个字节
5。地址 0
在一些目标文件里,公共符号不属于某个特别的段,linker 认为它们属于一个叫 COMMON 的段,大 多数情况,输入文件里的公共符号被放在输出文件的.bss 段中,如 .bss {*(.bss) *(COMMON)}
输出段属性
linker 一般在 input section 的基础上设置 output section 的属性。 可以使用 AT 命令改变地址值。 覆盖命令提供了一个简单的方法把不同的 section 装入单一内存镜像中,但在执行的时候是从同一的 地址开始执行。
PROVIDE 输出符号以便让 linker 能在解析过程中用到。用法是 PROVIDE(symbol = express)
。
例如
SECTIONS {
.text : {
*(.text)
_etext = .;
PROVIDE(etext = .);
}
}
如果程序里定义了_etext
,则 linker 会报错:多个_etext
的定义。但如果程序定义了 etext
,则编译器 默认使用程序里的etext
定义;如果程序没有定义etext
但却用到了etext
,则 linker 使用 link script 中的定 义。
Linux 内核镜像研究
下面我们就拿 Linux 内核源代码作为复习以上的例子。先回顾 include/linux 目录下这么一个 init.h 文 件,它不仅定义了 initcall_t 类型变量,还定义了一些常规 C 语言编程中未见过得类型:
#ifndef _LINUX_INIT_H
#define _LINUX_INIT_H
#include <linux/compiler.h>
#include <linux/types.h>
/* These macros are used to mark some functions or
* initialized data (doesn't apply to uninitialized data)
* as `initialization' functions. The kernel can take this
* as hint that the function is used only during the initialization
* phase and free up used memory resources after
*
* Usage:
* For functions:
*
* You should add __init immediately before the function name, like:
* 对于函数,应该在函数名之前加一个__init,如下:
* static void __init initme(int x, int y)
* {
* extern int z; z = x * y;
* }
*
* If the function has a prototype somewhere, you can also add
* __init between closing brace of the prototype and semicolon:
* 如果函数在其他地方有原型,那么你可以在括号和分号之间加__init,如下
*
* extern int initialize_foobar_device(int, int, int) __init;
*
* For initialized data:
* You should insert __initdata or __initconst between the variable name
* and equal sign followed by value, e.g.:
*
* static int init_variable __initdata = 0;
* static const char linux_logo[] __initconst = { 0x32, 0x36, ... };
*
* 记住不要在文件范围以外初始化数据,比如在函数中,否则 gcc 会把这些数据放在 bss 段中而
* 不是 init 段中。而且要注意:这些数据不能是 const 类型
* Don't forget to initialize data not at file scope, i.e. within a function,
* as gcc otherwise puts the data into the bss section and not into the init
* section.
*/
/* These are for everybody (although not all archs will actually
discard it in modules)
* 这里要碰到一个__attribute__关键字,这是告诉编译器要做一些特殊的操作。
* 在这里,它和__section__ 共同指示凡是被它们修饰的函数或变量应该放在特殊的 section 中,
* 不能任由编译器自己决定这些函数放在哪
*/
#define __init __section(.init.text) __cold notrace
#define __initdata __section(.init.data)
#define __initconst __constsection(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
/*
* Some architecture have tool chains which do not handle rodata attributes
* correctly. For those disable special sections for const, so that other
* architectures can annotate correctly.
*/
#ifdef CONFIG_BROKEN_RODATA
#define __constsection(x)
#else
#define __constsection(x) __section(x)
#endif
/*
* modpost check for section mismatches during the kernel build.
* A section mismatch happens when there are references from a
* code or data section to an init section (both code or data).
* The init sections are (for most archs) discarded by the kernel
* when early init has completed so all such references are potential bugs.
* For exit sections the same issue exists.
*
* The following markers are used for the cases where the reference to
* the *init / *exit section (code or data) is valid and will teach
* modpost not to issue a warning. Intended semantics is that a code or
* data tagged __ref* can reference code or data from init section without
* producing a warning (of course, no warning does not mean code is
* correct, so optimally document why the __ref is needed and why it's OK).
*
* The markers follow same syntax rules as __init / __initdata.
*/
#define __ref __section(.ref.text) noinline
#define __refdata __section(.ref.data)
#define __refconst __constsection(.ref.rodata)
/* compatibility defines */
#define __init_refok __ref
#define __initdata_refok __refdata
#define __exit_refok __ref
#ifdef MODULE
#define __exitused
#else
#define __exitused __used
#endif
#define __exit __section(.exit.text) __exitused __cold notrace
/* Used for MEMORY_HOTPLUG */
#define __meminit __section(.meminit.text) __cold notrace
#define __meminitdata __section(.meminit.data)
#define __meminitconst __constsection(.meminit.rodata)
#define __memexit __section(.memexit.text) __exitused __cold notrace
#define __memexitdata __section(.memexit.data)
#define __memexitconst __constsection(.memexit.rodata)
/* For assembly routines */
#define __HEAD .section ".head.text","ax"
#define __INIT .section ".init.text","ax"
#define __FINIT .previous
#define __INITDATA .section ".init.data","aw",%progbits
#define __INITRODATA .section ".init.rodata","a",%progbits
#define __FINITDATA .previous
#define __MEMINIT .section ".meminit.text", "ax"
#define __MEMINITDATA .section ".meminit.data", "aw"
#define __MEMINITRODATA .section ".meminit.rodata", "a"
/* silence warnings when references are OK */
#define __REF .section ".ref.text", "ax"
#define __REFDATA .section ".ref.data", "aw"
#define __REFCONST .section ".ref.rodata", "a"
/*系统中没有定义__ASSEMBLY__宏,所以会编译下面的代码*/
#ifndef __ASSEMBLY__
/*
* Used for initialization calls..
* 这就是前面我们提到的函数类型定义,它是一个参数为空,返回值为 int 类型的函数,
* 还有 exitcall_t 类 型函数类型定义,参数为空,返回值也为空。
*/
typedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);
extern initcall_t __con_initcall_start[], __con_initcall_end[];
extern initcall_t __security_initcall_start[], __security_initcall_end[];
/* Used for contructor calls. */
typedef void (*ctor_fn_t)(void);
/* Defined in init/main.c */
extern int do_one_initcall(initcall_t fn);
extern char __initdata boot_command_line[];
extern char *saved_command_line;
extern unsigned int reset_devices;
/* used by init/main.c */
void setup_arch(char **);
void prepare_namespace(void);
void __init load_default_modules(void);
int __init init_rootfs(void);
#ifdef CONFIG_DEBUG_RODATA
void mark_rodata_ro(void);
#endif
extern void (*late_time_init)(void);
extern bool initcall_debug;
#endif
#ifndef MODULE
#ifndef __ASSEMBLY__
#ifdef CONFIG_LTO
/* Work around a LTO gcc problem: when there is no reference to a variable
* in a module it will be moved to the end of the program. This causes
* reordering of initcalls which the kernel does not like.
* Add a dummy reference function to avoid this. The function is
* deleted by the linker.
*/
#define LTO_REFERENCE_INITCALL(x) \
; /* yes this is needed */ \
static __used __exit void *reference_##x(void) \
{ \
return &x; \
}
#else
#define LTO_REFERENCE_INITCALL(x)
#endif
/* 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; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
/*
* Early initcalls run before initializing SMP.
*
* Only for built-in code, not modules.
*/
#define early_initcall(fn) __define_initcall(fn, early)
/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
/*我们常见的__initcall 宏实际指的是 device_initcall*/
#define __initcall(fn) device_initcall(fn)
#define __exitcall(fn) \
static exitcall_t __exitcall_##fn __exit_call = fn
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__used __section(.con_initcall.init) = fn
#define security_initcall(fn) \
static initcall_t __initcall_##fn \
__used __section(.security_initcall.init) = fn
struct obs_kernel_param {
const char *str;
int (*setup_func)(char *);
int early;
};
/*
* Only for really core code. See moduleparam.h for the normal way.
*
* Force the alignment so the compiler doesn't space elements of the
* obs_kernel_param "array" too far apart in .init.setup.
*/
#define __setup_param(str, unique_id, fn, early) \
static const char __setup_str_##unique_id[] __initconst \
__aligned(1) = str; \
static struct obs_kernel_param __setup_##unique_id \
__used __section(.init.setup) \
__attribute__((aligned((sizeof(long))))) \
= { __setup_str_##unique_id, fn, early }
#define __setup(str, fn) \
__setup_param(str, fn, fn, 0)
/*
* NOTE: fn is as per module_param, not __setup!
* Emits warning if fn returns non-zero.
*/
#define early_param(str, fn) \
__setup_param(str, fn, fn, 1)
#define early_param_on_off(str_on, str_off, var, config) \
\
int var = IS_ENABLED(config); \
\
static int __init parse_##var##_on(char *arg) \
{ \
var = 1; \
return 0; \
} \
__setup_param(str_on, parse_##var##_on, parse_##var##_on, 1); \
\
static int __init parse_##var##_off(char *arg) \
{ \
var = 0; \
return 0; \
} \
__setup_param(str_off, parse_##var##_off, parse_##var##_off, 1)
/* Relies on boot_command_line being set */
void __init parse_early_param(void);
void __init parse_early_options(char *cmdline);
#endif /* __ASSEMBLY__ */
#else /* MODULE */
#define __setup_param(str, unique_id, fn) /* nothing */
#define __setup(str, func) /* nothing */
#endif
/* Data marked not to be saved by software suspend */
#define __nosavedata __section(.data..nosave)
#ifdef MODULE
#define __exit_p(x) x
#else
#define __exit_p(x) NULL
#endif
#endif /* _LINUX_INIT_H */
这个文件包含了一些关于 section 的定义,所以本质上initcall_t
及其他类似的类型 变量实际是宏,通过对这个文件的宏替换可以大概了解这些宏的含义。例如,代码中如果含有__init XXX ()
这么一个函数定义,你就知道那个 XXX 函数属于初始化的时候就被调用的,它被放在.init.text
节中。
前面说到 ELF 文件格式时曾说了 Link Script 会把特定类型的段放在特定位置让 loader 装入到特定的 内存地址,那么对于被 init 等宏修饰的函数及全局变量肯定被放在了特定位置,但如何让 basic_init
函数 去调用它,我们还得再研究 arch/i386/kernel 目录下的vmlinux.lds.S
文件,它就是使内核成为内核的 ld script。在这个文件中便定义了__initcall_start
和__initcall_end
。记住了,不止是 C 文件和 H 文件可以定义 变量。而且也不是象 C 语言那样定义一个变量还要指定类型,编译器有足够的智商把这个文件中的变量 定义为需要的类型,一般情况下会定义为整型变量。
当编译器编译整个源代码的时候,它会把所有定义为__init
的函数放在以__initcall_start
开始、以__initcall_end
结束的节中,在 basic_init
中会逐个的调用该节里所有的函数。让我们编译 完内核,我们在源文件根目录使用 objdump –t vmlinux |grep _initcall
输出信息,就看到我们想看到的东西
(下面是经过处理的符号信息):
1. c12946d4 l O 4 __initcall_cpufreq_tsc 第一个被初始化的函数,它的地址正好是 __initcall_start 的地址
2. ......
3. c12946e4 l O 4 __initcall_ksysfs_init
4. ......
5. c12946fc l O 4 __initcall_sock_init 网络基础设施比如内存系统的初始化
6. c1294700 l O 4 __initcall_netlink_proto_init
7. ......
8. c1294770 l O 4 __initcall_proto_init
9. c1294774 l O 4 __initcall_net_dev_init 网络设备的初始化
10. c1294778 l O 4 __initcall_wireless_nlevent_init
11. c129477c l O 4 __initcall_pktsched_init
12. .......
13. c129478c l O 4 __initcall_init_pipe_fs 管道系统的初始化
14. c1294790 l O 4 __initcall_chr_dev_init 字符设备的初始化
15. .......
16. c12947a4 l O 4 __initcall_inet_init 网络系统的初始化
17. c12947a8 l O 4 __initcall_time_init_device
18. ......
19. c12947b0 l O 4 __initcall_i8259A_init_sysfs 中断控制器的初始化
20. ......
21. c12947e8 l O 4 __initcall_init_posix_timers
22. c12947ec l O 4 __initcall_init_posix_cpu_timers
23. c12947f0 l O 4 __initcall_init_clocksource_sysfs
24. c12947f4 l O 4 __initcall_init_jiffies_clocksource
25. ......
26. c1294848 l O 4 __initcall_eventpoll_init
27. ......
28. c1294864 l O 4 __initcall_init_ext2_fs EXT2 文件系统的初始化
29. ......
30. c1294888 l O 4 __initcall_init 这不是 init/main.c 文件中的初始化函数
31. ......
32. c12948a8 l O 4 __initcall_pci_init PCI 系统总线的初始化
33. c12948ac l O 4 __initcall_pci_sysfs_init
34. c12948b0 l O 4 __initcall_pci_proc_init
35. ......
36. c1294918 l O 4 __initcall_topology_sysfs_init
37. c129491c l O 4 __initcall_rd_init
38. c1294920 l O 4 __initcall_net_olddevs_init 老实的网络设备驱动程序的初始化
39. ......
40. c1294974 l O 4 __initcall_generic_ide_init IDE 硬盘设备的初始化
41. ......
42. c12949b4 l O 4 __initcall_mousedev_init 鼠标设备的初始化
43. ......
44. c12949cc lO4__initcall_flow_cache_init
45. c12949d0lO4__initcall_blackhole_module_init
46. c12949d4lO4__initcall_init_syncookies
47. ....
48. c12949e0lO4__initcall_af_unix_init
49. c12949e4lO4__initcall_packet_init
50. ....
51. c1294a30 l O 4 __initcall_net_random_reseed 最后一个被调用的初始化函数,它的地址 正好是__initcall_end 的前面 4 个字节
52.
53. c12946d4 g "*ABS* 00000000 __initcall_start"
54. c1294a34 g "*ABS* 00000000 __initcall_end"
55. ...... 下面是一些处于初始化 section 的函数
56. c03ce000 l d ".data.init_task 00000000"
57. c03d0000 l d ".init.text 00000000"
58. c03ece80 l d ".init.data 00000000"
59. c03f57e0 l d ".init.setup 00000000"
60. c03f5c78 l d ".initcall.init 00000000"
61. c03f5f84 l d ".con_initcall.init 00000000"
62. c03f5f90 l d ".security_initcall.init 00000000"
63. ......
64. c03d0510 l F ".init.text 00000025 init_setup"
65. c03f5840 l O ".init.setup 0000000c __setup_init_setup"
66. c0100220 l F ".text 0000002a rest_init"
67. c0100290 l F ".text 00000151 init"
68. c03d0570 l F ".init.text 00000091 do_early_param"
69. c03d0840 l F ".init.text 000000bc do_initcalls"
70. c03d0900 l F ".init.text 0000001f do_basic_setup"
71. c0100260 l F ".text 00000029 run_init_process"
72. c010b090 l F ".text 00000127 init_intel"
73. c01c8340 g F ".text 00000034 kobject_init"
74. c0248e90 g F ".text 00000089 device_initialize"
75. c02f09c0 g F ".text 0000011c tcp_select_initial_window"
76. c03e93f0 g F ".init.text 00000053 loopback_init"
77. c01254d0 g F ".text 00000015 init_timer"
78. c03d7aa0 g F ".init.text 00000062 init_IRQ"
79. c03eb650 g F ".init.text 0000006d skb_init"
80. c03df270 g F ".init.text 000003eb kmem_cache_init"
81. c03ecb00 g F ".init.text 00000090 ip_misc_proc_init"
82. c031f150 g F ".text 00000033 klist_init"
83. a5808bbf g "*ABS* 00000000 tasklet_init"
84. c03dcde0 g F ".init.text 000000fd proc_caches_init"
85. c0307050 g F ".text 000000c9 ip_mc_init_dev"
86. c02e57c0 g F ".text 0000007a inet_csk_init_xmit_timers"
87. c039ba80 g O ".data 00000048 tcp_init_congestion_ops"
88. c03dd590 g F ".init.text 00000027 init_timers"
89. 89. c012d460 g F ".text 0000001f init_workqueues"
90. .....
91. .....
要对这份输出信息做一个说明:第一列是所有符号的装入地址;第二列表示符号的全局性和可读写性,l
表示 local,凡是用 static 修饰的都是这种属性,g
表示 global,内核源代码中全局变量属于这一类,还有用 EXPORT_SYMBOL
(这个宏在 linux/module.h 中定义)修饰的变量和函数也都是属于 global。w
表示 writable;第三列表示此符号的本质是什么,O
表示是变量(一般是全局变量),F
表示函数,d
表示可执行文件中的 section;第四列和第五列比较复杂,如果符号属于 d
,那么第五列表示该 section 缺省值,一般都为 0,如果属于 O
或F
,那么第四列表示符号所属的 section,第五列则表示该符号所占用的内存大小;还有一类最特殊,就是用 ABS 修饰的符号,*ABS*
表示绝对 (absolute),这意味着不能将该值更改为其他的连接,我们关注的__initcall_start
和__initcall_end
就属于这一类。所以 do_initcalls
就是用来调用所有使用__initcall
标记过的函数的。
系统初始化各模块有两种方式,我们一般都可以在编译内核的时候控制,这两种方式分别是:一种是嵌入内核中;另一种是以模块加载方式。前者将设备驱动模块嵌入整个 Linux 内核(vmlinux)中,系统启动的时候会从.init
代码段执行它们的初始化函数,所以我们可以在上面那个 vmlinux 文件中找到__initcall_xxx_drv_init
函数。以上就是关于这种方式的分析。后一种就是将设备驱动模块编译成独立的可执行文件,以.ko 为后缀名放在/lib/modules/2.6.xxx/kernel/目录下。当系统启动时,内核启动代码执行/etc/rc.d/rc.sysinit 教本,其中的代码会执行sys_init_module
内核函数把它们加载到内核中。
不管是什么方式加载,每个驱动程序执行的第一行代码是以module_init 定义
的函数。比如module_init(e100_init_module)
,这里第一行被系统执行的函数是e100_init_module
。
从很多的资料上得出一个信息:凡是用__init 修饰过的函数在被调用一次后,其占用的内存区会被清除掉,以便让其它代码可以使用