linux内核链接脚本vmlinux.lds分析续篇之---* (.proc.info.init)、* (.arch.info.init)、*(.taglist.init)段的分析(十二)
序言
为什么这里又单独写一篇关于内核连接脚本中各种段的分析。因为在内核启动中有如下三个比较重要的步骤:
- 校验处理器ID,检验内核是否支持该处理器;若不支持,则停止启动内核。 - - -> *(.proc.info.init)段
- 校验机器码,检验内核是否支持该机器;若不支持,则停止启动内核 - - -> *(.arch.info.init)段
- 解析uboot传入的tag - - -> *(.taglist.init)段
一. 基础知识
r0,r1,r2三个寄存器的设置
uboot启动内核时,会设置r0,r1,r2三个寄存器:
- r0一般设置为0;
- r1一般设置为machine id (在使用设备树时该参数没有被使用);
- r2一般设置ATAGS或DTB的开始地址;
这里的machine id,是让内核知道是哪个CPU,从而调用对应的初始化函数。
(1) 在没有使用设备树时,需要uboot传一个machine id给内核(现在使用设备树的话,这个参数就不需要设置)。
(2) r2要么是以前的ATAGS开始地址,要么是现在使用设备树后的DTB文件开始地址。
附注:由于我手里目前只有一块mini6410的开发板(s3c6410是ARMv6架构的),所以本节还是以没有使用设备树的情景为例进行讲解。当然当前主流的开发基本上都是基于设备树的,因为它的优点很多(会放在设备树专题进行讲解)。(6月将会入手一块基于ARMV8架构的cortex-A53,选型中!!!*,到时候再补上基于设备树的启动方式)
二. * (.proc.info.init)段
# arch/arm/kernel
...
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
1. 如何把CPU对应的proc_info_list结构放入 * (.proc.info.init)段
在内核源码中,定义了若干个proc_info_list结构,以此来表示它支持的CPU。其中proc_info_list 结构体定义如下:
# arch/arm/include/asm/procinfo.h
/*
* Note! struct processor is always defined if we're
* using MULTI_CPU, otherwise this entry is unused,
* but still exists.
*
* NOTE! The following structure is defined by assembly
* language, NOT C code. For more information, check:
* arch/arm/mm/proc-*.S and arch/arm/kernel/head.S
*/
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags; /* used by head.S */
unsigned long __cpu_io_mmu_flags; /* used by head.S */
unsigned long __cpu_flush; /* used by head.S */
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};
对于ARM架构的CPU,表示支持的CPU的源码定义在在arch/arm/mm/目录下。比如arch/arm/mm/proc-v6.S中有如下代码,它表示所有ARMv6架构CPU的proc_info_list结构。
.section ".proc.info.init", #alloc, #execinstr
/*
* Match any ARMv6 processor core.
*/
.type __v6_proc_info, #object
__v6_proc_info:
.long 0x0007b000 /*cpu_val*/
.long 0x0007f000 /*cpu_mask*/
...
不同的proc_info_list结构被用来支持不同的CPU,它们都是定义在“.proc.info.init”段中,在链接内核镜像时,这些结构被组织在一起。开始地址为__proc_info_begin,结束地址为__proc_info_end。在arm/kernel/vmlinux.lds.S中可以看到这样的代码:
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
2. 如何获取CPUDID
通过读协处理器CP15的寄存器C0获取CPUID,并放在r9中。以确定内核是否支持当前CPU。如果支持,r5寄存器返回一个用来描述处理器的结构体的地址,否则r5为0。CPU ID格式如图所示:
3. 如何判断内核是否支持当前的CPU
__lookup_processor_type函数就是根据前面从协处理器CP15的寄存器C0中读取到CPUID(存入r9寄存器),并尝试从这些proc_info_list结构中找出匹配目标,以确定内核是否支持当前的CPU。代码如下:
# arch/arm/kernel/head-common.S
/*
* Read processor ID register (CP#15, CR0), and look up in the linker-built
* supported processor list. Note that we can't use the absolute addresses
* for the __proc_info lists since we aren't running with the MMU on
* (and therefore, we are not in the correct address space). We have to
* calculate the offset.
*
* r9 = cpuid
* Returns:
* r3, r4, r6 corrupted
* r5 = proc_info pointer in physical address space
* r9 = cpuid (preserved)
*/
__lookup_processor_type:
/* adr指令是采用相对地址,因此这里r3装的实际上是标号 __lookup_processor_type_data 对应的物理地址 */
adr r3, __lookup_processor_type_data
/* LDM是多寄存器存取的意思,IA表示数据传输后地址增加(increase after);(IB:increase before, DA: decrease after, DB: decrease before)
意思是r3指示的内存数据依次加载到寄存器r4,r5,r6中去
@r4 = . 的地址(虚拟地址)
@r5= __proc_info_begin (虚拟地址)
@r6 = __proc_info_end (虚拟地址)
*/
ldmia r3, {r4 - r6}
sub r3, r3, r4 @ get offset between virt&phys// 得到虚拟地址和物理地址的差值
add r5, r5, r3 @ convert virt addresses to // 得到 __proc_info_begin 对应的物理地址
add r6, r6, r3 @ physical address space // 得到 __proc_info_end 对应的物理地址
1: ldmia r5, {r3, r4} @ value, mask //将proc_info_list结构中cpu_val、cpu_mask分别存放在r3, r4中
and r4, r4, r9 @ mask wanted bits //r4 = cpu_mask&CPU_ID
teq r3, r4 //比较
beq 2f //如果相等,找到匹配的proc_info_list结构,跳转到标号2处,并返回
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) /如果不相等,指向下一个proc_info_list结构
cmp r5, r6 //判断是否到达__proc_info_end
blo 1b //没有则跳转到标号1,继续比较
mov r5, #0 @ unknown processor
2: mov pc, lr
ENDPROC(__lookup_processor_type)
/*
* Look in <asm/procinfo.h> for information about the __proc_info structure.
*/
.align 2
.type __lookup_processor_type_data, %object
__lookup_processor_type_data:
.long .
.long __proc_info_begin
.long __proc_info_end
.size __lookup_processor_type_data, . - __lookup_processor_type_data
注意:
__proc_info_begin、 __proc_info_end和“.”这三个数据都是在链接内核时确定的,它们都是虚拟地址,前两个表示proc_info_list结构的开始地址和结束地址,“.”表示当前行代码在编译链接后的虚拟地址。因为MMU没有开启,所以我们此时还不能直接使用这些地址。所以在访问proc_info_list结构前,需要先将它的虚拟地址转化为物理地址。
4. 小节
- __lookup_processor_type函数首先将标号__lookup_processor_type_data的物理地址加载到r3,
- 然后将标号__lookup_processor_type_data的虚拟地址载入到r4,编译时生成的 __proc_info_begin虚拟地址载入到r5,__proc_info_end虚拟地址载入到r6。由于r3和r4分别存储的是同一位置标号__lookup_processor_type_data的物理地址和虚拟地址,所以两者相减即得到虚拟地址和物理地址之间的offset。
- 利用此offset,将r5和r6中保存的虚拟地址转变为物理地址,然后从proc_info中读出内核编译时写入的processor ID和之前从cpsr中读到的processor ID对比,查看代码和CPU硬件是否匹配。如果编译了多种处理器支持则会循环每种type依次检验,如果硬件读出的ID在内核中找不到匹配,则r5置0并返回。
注意,在arch/arm/mm/Makefile中有如下行,需要配置CONFIG_CPU_V6 = y(在配置菜单中, Processor Type->)
obj-$(CONFIG_CPU_V6) += proc-v6.o
三. * (.arch.info.init)段
# arch/arm/kernel
...
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
1.如何把描述机器(开发板)的machine_desc 结构放入 *(.arch.info.init)段
内核中对每种支持的开发板都会使用宏MACHINE_START、MACHINE_END来定义一个machine_desc结构,它定义了开发板相关的一些属性和函数,如机器的类型ID、起始I/O物理地址、Bootloader传入的参数地址、中断初始化函数、I/O映射函数等。对于mini6410开发板在arch/arm/mach-s3c64xx/ mach-mini6410.c有如下代码:
MACHINE_START(MINI6410, "MINI6410")
/* Maintainer: Ben Dooks <ben-linux@fluff.org> */
.boot_params = S3C64XX_PA_SDRAM + 0x100,
.init_irq = s3c6410_init_irq,
.map_io = mini6410_map_io,
.init_machine = mini6410_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END
其中:宏MACHINE_START、MACHINE_END在arch/arm/include/asm/mach/arch.h文件中定义:
/*
* Set of macros to define architecture features. This is built into
* a table by the linker.
*/
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
将宏展开:
static const struct machine_desc __mach_desc_MINI6410 \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_MINI6410, \
.name =MINI6410,
.boot_params = S3C64XX_PA_SDRAM + 0x100,
.init_irq = s3c6410_init_irq,
.map_io = mini6410_map_io,
.init_machine = mini6410_machine_init,
.timer = &s3c24xx_timer,
};
不同的machine_desc结构被用来支持不同的开发板,它们都是定义在“.arch.info.init”段中,在链接接内核时,这些结构被组织在一起。开始地址为__arch_info_begin,结束地址为__arch_info_end =。在arm/kernel/vmlinux.lds.S中可以看到这样的段定义:
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
2. 如何判断内核是否支持当前的机器(开发板)
uboot 启动内核准备阶段时,会在r1寄存器中给出开发板的标记即机器类型ID。__lookup_machine_type函数将这个值与.arch.info.init段中定义的若干个machine_desc结构中的nr值进行逐一比较,如果两者相等则表示找到匹配的machine_desc结构,并把该machine_desc结构中的nr值保存到r5中。如果__arch_info_begin、__arch_info_end之间所有machine_desc结构的nr值都不等于r1寄存器中的值,则返回0,即r5中值为0。分析方法与* (.proc.info.init)段相同,这里只给出源代码:
# arch/arm/kernel/head-common.S
/*
* Lookup machine architecture in the linker-build list of architectures.
* Note that we can't use the absolute addresses for the __arch_info
* lists since we aren't running with the MMU on (and therefore, we are
* not in the correct address space). We have to calculate the offset.
*
* r1 = machine architecture number
* Returns:
* r3, r4, r6 corrupted
* r5 = mach_info pointer in physical address space
*/
__lookup_machine_type:
adr r3, __lookup_machine_type_data
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
teq r3, r1 @ matches loader number?
beq 2f @ found
add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
cmp r5, r6
blo 1b
mov r5, #0 @ unknown machine
2: mov pc, lr
ENDPROC(__lookup_machine_type)
/*
* Look in arch/arm/kernel/arch.[ch] for information about the
* __arch_info structures.
*/
.align 2
.type __lookup_machine_type_data, %object
__lookup_machine_type_data:
.long .
.long __arch_info_begin
.long __arch_info_end
.size __lookup_machine_type_data, . - __lookup_machine_type_data
注意:
对于mini6410开发板uboot传入的机器ID为2520,对应machine_desc结构在arch/arm/mach-s3c64xx/ mach-mini6410.c文件中定义,所以这个文件要编进内核中,因此需要配置CONFIG_MACH_MINI6410=y:
四. * (taglist.init)段
*(.taglist.init) 段存放的是 uboot 传递到内核的 tag 的处理函数。在 uboot 中,定义了一个 tag 结构体,里面存放要传递给内核的信息,uboot 将 tag 依次排放在和内核约定的地址,如 mini6410是 0x50000100 处,排放顺序是有要求的,必须以 ATAG_CORE 标记的 tag 开头,以 ATAG_NONE 为标记的 tag 结尾。
1. 如何把tag结构放入 * (taglist.init)段
在内核中,使用 __tagtable 来将处理 tag 的函数放到 *(.taglist.init) 段。如下:
# arch/arm/kernel/setup.c
__tagtable(ATAG_CORE, parse_tag_core);
__tagtable(ATAG_MEM, parse_tag_mem32);
__tagtable(ATAG_RAMDISK, parse_tag_ramdisk);
__tagtable(ATAG_SERIAL, parse_tag_serialnr);
__tagtable(ATAG_REVISION, parse_tag_revision);
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);
宏定义在arch/arm/include/asm/setup.h
#define __tag __used __attribute__((__section__(".taglist.init")))
#define __tagtable(tag, fn) \
static struct tagtable __tagtable_##fn __tag = { tag, fn }
以 __tagtable(ATAG_CORE, parse_tag_core)为例,把宏展开:
static struct tagtable __tagtable_parse_tag_core __used __attribute__((__section__(".taglist.init"))) = {
ATAG_CORE,
parse_tag_core
}
其中struct tag结构体定义在arch/arm/include/asm/setup.h中:
# arch/arm/include/asm/setup.h
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
* Acorn specific
*/
struct tag_acorn acorn;
/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};
2. uboot中传入这些tag的方法
setup_start_tag (bd); /*设置ATAG_CORE标志*/
setup_memory_tags (bd); /*设置内存标记*/
setup_commandline_tag (bd, commandline); /*设置命令行标记*/
...
setup_end_tag (bd); /*设置ATAG_NONE标志 */
关于uboot如何传入这些tag,我放在uboot启动系列篇中讲解,这里只给出调用的函数。
(3)内核如何解析uboot传入的tag
调用链如下:
# init/main.c
|--- start_kernel
|--- setup_arch(&command_line)
|--- mdesc = setup_machine(machine_arch_type)//获取machine_desc,见上一小节
|--- tags = phys_to_virt(mdesc->boot_params) //获取定义的boot_params内存地址,并赋值给tags
|--- parse_tags(tags) //解析这些tags
|--- parse_tag(t) //遍历__tagtable_begin ~ __tagtable_end中的各种类型tag,并调用该类型tag真正的解析函数
|--- t->parse(tag) //调用该类型tag真正的解析函数
在内核启动过程中,会使用 parse_tags 来处理 tag ,它最终会调用到 parse_tag ,取出 __tagtable_begin 和 __tagtable_end 之间的每一个 tagtable ,比较它们的类型,如果相同则调用 tagtable 里的处理函数来处理这个tag 。对于例子中的tag就是要调用parse_tag_core来处理这个 tag。
总结
内核通过链接脚本链接成内核映像文件时,指定了很多段,这些段的摆放顺序都是有一定的要求的,特别是涉及到内核启动准备阶段的段。关于这些段的解析和使用方法都是大同小异。参考前面的就能弄明白了。以后再涉及到段的使用,我就不再一步步的讲解了。
本文来自博客园,作者:BSP-路人甲,转载请注明原文链接:https://www.cnblogs.com/jianhua1992/p/16852794.html,并保留此段声明,否则保留追究法律责任的权利。