ARM架构内核启动分析
首先需要明确的是,内核镜像在被解压之后执行,是执行哪段代码,这是个重要的问题,平时在编译生成应用程序或内核模块时,我们无需考虑链接的具体细节,如代码和数据放在哪里、代码执行入口在哪等等,但在编译生成内核镜像时就不能不考虑这些了,对于arm架构,下面是它的编译内核时的arm交叉链接器命令:
arm-linux-ld -EL -p --no-undefined -X --build-id -ovmlinux -T arch/arm/kernel/vmlinux.lds
可以通过man –ld命令查看,-T的意思是:为链接器ld指定一个链接脚本linker script,就是让链接器根据这个链接脚本的内容来生成最终二进制镜像。
以我手中的arm芯片(marvell 88F6500)为例,它对应的linux源码的arch/arm/kernel目录下都会有对应的vmlinux.lds文件,这个文件是由同一目录下的vmlinux.lds.S文件生成的,所以这个vmlinux.lds.S文件需要重点分析;
对于任何一种可执行程序,不论是ELF/EXE、so/dll、ko,应该都听说过代码段text、数据段data、未初始化数据段bss等等的说法,其实这些都是一个个的段(section);每个编译好的.o文件,都有该文件的text/data/bss段;事实上每个.o文件不仅仅这三个段,还有一些别的段,但不是分析重点;
链接脚本最终要把这一大堆.o文件生成最终的二进制可执行文件,也就是把每一个.o文件整合到一个大文件中,这个大文件有一个总的text/data/bss段,分别囊括每个.o文件的text/data/bss段;那具体是怎么使用的呢,举例如下:
SECTIONS
{
. = 0x10000;
.text : { *(.text) }
. = 0x8000000;
.data : { *(.data) }
.bss : { *(.bss) }
}
第一行指示,链接地址为0x100000;即指定了后面的text段的链接地址
第二行指示:输出文件的text段内容由所有输入文件(*,理解为所有的.o文件,*.o)的text段组成;
第三行指示:链接地址变了,变为0x8000000;即重新指定了后面的data段的链接地址;
第四行指示:输出文件的data端由所有输入文件的data段组成;
第五行指示:输出文件的bss端由所有输入文件的bss段组成;
个人认为理解到此就差不多可以了,下面分析vmlinux.lds.S文件:
OUTPUT_ARCH(arm):表示输出文件基于ARM架构;
ENTRY(stext):ENTRY用来设置入口点。这里表示入口点是stext。这就是内核代码的入口!入口点的意思就是程序运行的第一条指令,内核本身也是一个程序,同样需要设置入口;
jiffies = jiffies_64:
/*下面开始正式定义输出文件的段!!!*/
SECTIONS
{
/*1、设置链接地址(也即虚拟地址)为0xc0008000,这就是内核代码的虚拟地址为0xc0008000的由来值的依据就是PAGE_OFFSET和TEXT_OFFSET*/
#ifdef CONFIG_XIP_KERNEL
. = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);
#else
. = PAGE_OFFSET + TEXT_OFFSET;
#endif
/*2、定义一个.text.head段,由输入文件中所有.text.head段组成,并定义了两个地址变量下面描述链接脚本中变量的意思: 如
start_of_ROM = .ROM;
end_of_ROM = .ROM + sizeof (.ROM) - 1;
start_of_FLASH = .FLASH;
这三个变量分别指向ROM段的开始和结尾、FLASH段的开始,那么在C代码中是这样使用这些变量:
extern char start_of_ROM, end_of_ROM, start_of_FLASH;
注意,C代码这些变量指示的都是指针变量,比如希望在C代码中把ROM段的内容拷贝到FLASH段中:
memcpy (&start_of_FLASH, &start_of_ROM, &end_of_ROM - &start_of_ROM);
可见,注意其中的取地址符号&,C代码中只能通过这种方式来使用LS中定义的变量。start_of_ROM这个值本身是没有意义的,只有它的地址才有意义
说白了,链接脚本中定义的变量其实就是地址,即_stext=0x100就是C代码中的一个地址:int *_stext=0x100 */
.text.head : {
_stext = .; @@定义变量_stext存储当前地址(PAGE_OFFSET + TEXT_OFFSET)
_sinittext = .; @@定义变量_sinittext存储当前地址(PAGE_OFFSET + TEXT_OFFSET)
*(.text.head) @@所有输入文件中所有.text.head段在此
}
/*3、定义一个.init段*/
.init : { /* Init code and data */
/*INIT_TEXT在include/asm-generic/vmlinux.lds.h文件中定义,意为所有的.init.text/.cpuinit.text/.meminit.text在此/
INIT_TEXT
/*定义变量_einittext,它其实是INIT_TEXT的结尾标识*/
_einittext = .;
/*这是一种典型的用法,后面大量使用该方法,前面已知可以在链接脚本中定义变量存储地址,然后由C代码使用,这里就用这种方式,定义两个变量,两个变量之间是输出文件.proc.info.init,用这两个变量就把中间的内容牢牢卡住,供C代码使用*/
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
__tagtable_begin = .;
*(.taglist.init)
__tagtable_end = .;
. = ALIGN(16);
__setup_start = .;
*(.init.setup)
__setup_end = .;
__early_begin = .;
*(.early_param.init)
__early_end = .;
__initcall_start = .;
INITCALLS
__initcall_end = .;
__con_initcall_start = .;
*(.con_initcall.init)
__con_initcall_end = .;
__security_initcall_start = .;
*(.security_initcall.init)
__security_initcall_end = .;
/*
#ifdef CONFIG_BLK_DEV_INITRD
. = ALIGN(32);
__initramfs_start = .;
usr/built-in.o(.init.ramfs)
__initramfs_end = .;
#endif
*/
. = ALIGN(PAGE_SIZE);
__per_cpu_load = .;
__per_cpu_start = .;
*(.data.percpu.page_aligned)
*(.data.percpu)
*(.data.percpu.shared_aligned)
__per_cpu_end = .;
#ifndef CONFIG_XIP_KERNEL
__init_begin = _stext;
INIT_DATA
. = ALIGN(PAGE_SIZE);
__init_end = .;
#endif
}
/*4、DISACARD是一种特殊的段,表示符合这个条件的输入段都不会写到输出段中,也就是输出文件中不包含下列段,不是分析重点*/
/DISCARD/ : { /* Exit code and data */
EXIT_TEXT
EXIT_DATA
*(.exitcall.exit)
*(.discard)
*(.ARM.exidx.exit.text)
*(.ARM.extab.exit.text)
#ifndef CONFIG_HOTPLUG_CPU
*(.ARM.exidx.cpuexit.text)
*(.ARM.extab.cpuexit.text)
#endif
#ifndef CONFIG_HOTPLUG
*(.ARM.exidx.devexit.text)
*(.ARM.extab.devexit.text)
#endif
#ifndef CONFIG_MMU
*(.fixup)
*(__ex_table)
#endif
}
/*5、这是text段*/
.text : { /* Real text segment */
_text = .; /* Text and read-only data */
__exception_text_start = .;
*(.exception.text)
__exception_text_end = .;
/* Then all the functions that are "hot" in profiles, to group them onto the same hugetlb entry */
#include "functionlist"
/* Then the rest */
TEXT_TEXT
SCHED_TEXT
LOCK_TEXT
KPROBES_TEXT
#ifdef CONFIG_MMU
*(.fixup)
#endif
*(.gnu.warning)
*(.rodata)
*(.rodata.*)
*(.glue_7)
*(.glue_7t)
*(.got) /* Global offset table */
}
O_DATA(PAGE_SIZE)
_etext = .; /* End of text and rodata section */
. = ALIGN(THREAD_SIZE);
__data_loc = .;
/*6、这是data段*/
.data : AT(__data_loc) {
_data = .; /* address in memory */
_sdata = .;
/*
* first, the init task union, aligned
* to an 8192 byte boundary.
*/
*(.data.init_task)
. = ALIGN(PAGE_SIZE);
__nosave_begin = .;
*(.data.nosave)
. = ALIGN(PAGE_SIZE);
__nosave_end = .;
/*
* then the cacheline aligned data
*/
. = ALIGN(32);
*(.data.cacheline_aligned)
/*
* The exception fixup table (might need resorting at runtime)
*/
. = ALIGN(32);
__start___ex_table = .;
*(__ex_table)
__stop___ex_table = .;
/*
* and the usual data section
*/
DATA_DATA
CONSTRUCTORS
_edata = .;
}
/*7、这是bss段*/
.bss : {
__bss_start = .; /* BSS */
*(.bss)
*(COMMON)
__bss_stop = .;
_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) }
}
上面罗列也描述了很多内容,重点把握以下几点:
1、 内核代码入口为stext;并且它应该是属于输入文件的.text.head段;并且stext的实现在arch/arm/kernel/head.S文件中,可找到.section ".text.head", "ax"声明;
2、 内核代码的链接地址从0xc0008000开始,不断增加,这体现了PAGE_OFFSET 、TEXT_OFFSET两个宏的作用何在;
3、 注意init段中的类似用__proc_info_begin、__proc_info_end来卡住中间内容的方式的原理,这在后面频繁使用;
其余的关于链接脚本的内容,可暂时不用分析的很细,后面会逐渐的理解。