移植 Linux 内核

更新记录

version status description date author
V1.0 C Create Document 2019.1.7 John Wan

status:
C―― Create,
A—— Add,
M—— Modify,
D—— Delete。

注:内核版本 3.0.15,硬件迅为 iTop4412精英板

1、Linux 版本及特点

2、打补丁、编译、烧写、启动内核

3、内核源码文件结构

目录名 描述
arch 体系结构相关的代码,对于每个架构的CPU,arch目录下有一个对应的子目录,比如arch/arm、arch/i386
block 块设备的通用函数
crypto 常用的加密和散列算法(如AES、SHA等),还有一些压缩和CRC校验算法
drivers 所有的设备驱动程序,里面每一个子目录对一个一类驱动程序,比如drivers/block为块设备驱动程序,drivers/char为字符串设备驱动程序,drivers/mtd为NorFlash、NandFlash等存储设备的驱动程序
fs Linux支持的文件系统的代码,每个子目录对应一种文件系统,比如fs/jffs2、fs/ext2、fs/ext3
include 内核头文件,有基本头文件(存放在include/linux/目录下)、各种驱动或功能部件的头文件(比如include/media/、/include/mtd、include/net)、各种体系相关的头文件(比如include/asm-arm、include/asm-i386/)。当配置内核后,include/asm/是某个include/asm-xxx/(比如asm-arm)的链接
init 内核的初始化代码(不是系统的引导代码),其中的main.c文件中的start_kernel函数时内核引导后的第一个函数
ipc 进程间通信的代码
kernel 内核管理的核心代码,与处理器相关的代码位于arch/*/kernel/目录下
lib 内核用到的一些库函数代码,比如crc32.c、string.c,与处理器相关的库函数代码位于arch/*/lib目录下
mm 内核管理代码,与处理器相关的内存管理代码位于arch/*/mm
net 网络支持代码,每个子目录对应于网络的一个方面
security 安全、密钥相关的代码
sound 音频设备的驱动程序
usr 用来制作一个压缩的cpio归档文件:initrd的镜像,它可以作为内核启动后挂接的第一个文件系统
Documentation 内核文档
scripts 用于配制、编译内核的脚本文件

4、内核架构分析

4.1 内核配置

  Linux 系统具有庞大的文件,其中绝大部分文件都是驱动文件,这是为了适应市面上的设备;作为应用广泛的系统,Linux 系统为适应不同芯片的体系架构,也有很多文件。那么为了将 Linux 系统移植到指定的 CPU 上,就需要进行内核配置(内核裁剪)。

4.1.1 内核的配置方式

  内核配置有三种方式:

1) 配置项从头到尾全都配一遍。

  在解压后的源码文件中执行 "make menuconfig",然后根据菜单进行配置。

2) 在某个默认配置文件的基础上进行修改。

  根据开发板的型号,在/arch/arm/configs 目录下找到一个名为 exynos4_defconfig 的文件,这就是开发板对应的默认配置文件。

  执行 make exynos4_defconfig 命令加载。

  执行 make menuconfig 命令修改。

3) 直接使用厂商提供的配置文件。

  使用 “cp 配置文件名 .config” 命令,加载指定的配置文件。

  执行 make menuconfig 命令修改。

  前面的加载配置文件的过程,都是将配置项写入 .config 文件中,而 make menuconfig 就是去读出 .config 文件中内容,并以菜单的形式显示。菜单的具体操作,再进入菜单之后,有提示。

  至于菜单的各选项功能,可参考《嵌入式linux应用开发完全手册》的16.2.4章节。

4.1.2 分析 .config 配置文件

  配置的结果:生成 .config 文件。

  而 .config 文件中的内容,就是各配置项的设置,主要分为三类:

  1)配置项为 "y",表示编译进内核;

  2)配置项为 "m",表示作为模块可加载;

  3)配置项为 "not set" ,表示不配置。

  以 CONFIG_CAN 为例,在编译好的源码(生成内核镜像)中搜索 grep "CONFIG_CAN" * -nwR 可以查看到在以下的文件中包含有 CONFIG_CAN

  1)C源码中

  2)/drivers/net/Makefile

  3)/include/generate/autoconf.h

  4)/include/configs/auto.conf

  在执行编译命令时 make zImage/uImage等 ,会自动生成 auto.conf,该文件的内容时将配置项的宏定义进行设置。那么该宏定义肯定是在 C源码中 被包含,进行调用。

  在执行编译命令时,也会自动生成 autoconf.h ,该文件的内容同 Makefile 的内容相似,决定了配置项编译成何种形式(y:编译进内核,m:可加载的模块),那么它肯定是被子目录中的 Makefile 文件进行调用,也就是 /drivers/net/Makefile

  前面了解了内核的配置过程,那么内核的配置原理及框架是如何实现的那?那么就需要分析 Makefile 及 Kconfig 文件。

4.2 Makefile架构分析

  Makefile 的目标:第一个文件、链接脚本。

  Makefile 的功能:

    1)决定编译哪些文件?

    2)怎样编译这些文件?

    3)怎样连接这些文件,它们的顺序是怎样的?

4.2.1 Makefile 的分类

  Linux 内核源码中含有很多个 Makefile 文件,这些 Makefile 文件又包含了其他一些文件(如:配置信息、通用的规则等),这就构成了 Linux 的 Makefile 体系。

名称 描述
顶层 Makefile 它是所有 Makefile 文件的核心,从总体上控制着内核的编译、连接
.config 配置文件,在配置内核时生成。所有 Makefile 文件(包括顶层目录及各级子目录)都是根据 .config 来决定使用哪些文件
arch/$(ARCH)/Makefile 对应体系结构的 Makefile,它用来决定哪些体系结构相关的文件参与内核的生成,并提供一些规则来生成特定格式的内核映像
scripts/Makefile.* Makefile 共用的通用规则、脚本等
kbuild Makefile 各级子目录下的 Makefile,它们相对简单,被上一层 Makefile 调用来编译当前目录下的文件

4.2.2 Makefile 语法

  编进内核、编成模块、编程库文件、引用、包含、添加、替换等。

  Makefile 的语法请参考另外的文章及Documentation/kbuild/makefile.txt文档

4.2.3 决定编译哪些文件?

  Linux 内核的编译过程从顶层 Makefile 开始,然后递归地进入各级子目录调用它们的 Makefile,分为 3 个步骤。

  1)顶层 Makefile 决定内核根目录下哪些子目录将被编进内核。

  2)arch/$(ARCH)/Makefile 决定 arch/$(ARCH) 目录下哪些文件、哪些目录将被编进内核。

  3)各级子目录下的 Makefile 决定所在目录下哪些文件将被编进内核,哪些文件将被编程模块(即驱动程序),进入哪些子目录继续调用它们的 Makefile。

4.2.3.1 分析顶层 Makefile
  1. 包含的根目录文件
# Objects we will link into vmlinux / subdirs we need to visit
init-y      := init/
drivers-y   := drivers/ sound/ firmware/
net-y       := net/
libs-y      := lib/
core-y      := usr/
#......
core-y      += kernel/ mm/ fs/ ipc/ security/ crypto/ block/

  顶层 Makefile 将 14 个子目录分为 5 类:init-y、drivers-y、net-y、libs-y、core-y

  1. 包含 /arch/$(ARCH)/Makefile
include $(srctree)/arch/$(SRCARCH)/Makefile

  对于 SRCARCH 变量:引用的是 ARCH 变量,需要在编译时指定。且对于非x86平台,还需指定交叉编译工具。两种方式:

  1)在 make 时添加参数;

make menuconfig ARCH=arm CROSS_COMPILE=/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-

  2)修改 Makefile 文件。

ARCH        ?= arm
CROSS_COMPILE   ?= /usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-
4.2.3.2 分析 arch/$(ARCH)/Makefile
  1. 新增 head-y
head-y       := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o  

  除去顶层 Makefile 的 5 类子目录外,在这新增一类:head-y ,不过它直接以文件名出现。MMUEXTarch/arm/Makefile 前面定义,

  对于没有 MMU 的处理器,MMUEXT 的值为 -nommu,使用文件 head-nommu.S

  对于有 MMU 的处理器,MMUEXT 的值为空,使用文件head.S

  1. 扩展 core-y

    machine-$(CONFIG_ARCH_EXYNOS)        := exynos
    ......
    plat-$(CONFIG_ARCH_S3C64XX)  := samsung
    ......
    plat-$(CONFIG_PLAT_S3C24XX)  := s3c24xx samsung
    plat-$(CONFIG_PLAT_S5P)      := s5p samsung
    ......
    machdirs := $(patsubst %,arch/arm/mach-%/,$(machine-y))
    platdirs := $(patsubst %,arch/arm/plat-%/,$(plat-y))
    ......
    
    # If we have a machine-specific directory, then include it in the build.
    core-y               += arch/arm/kernel/ arch/arm/mm/ arch/arm/common/
    core-y               += $(machdirs) $(platdirs)

  这里进一步扩展 core-y 的内容,这些都是体系结构相关的目录。

  1)添加 arch/arm 目录下的 kernel、mm、common子目录。

  2)添加 ARCHarch/arm/mach-exynosCONFIG_ARCH_EXYNOS 配置项在配置内核时进行了定义。

  3)添加 PLATarch/arm/plat-plat-y 是什么?

​ 搜索内核配置文件,平台相关的配置有两项:

CONFIG_PLAT_SAMSUNG=y

CONFIG_PLAT_S5P=y

  然后在源码中分别搜索两项 grep "CONFIG_PLAT_SAMSUNG" * -nwR,发现 CONFIG_PLAT_SAMSUNG 只存在于配置文件中,没有任何 Makefile 包含。而 CONFIG_PLAT_S5P 除了配置文件包含,还存在与 arch/arm/Makefile、arch/arm/plat-samsung/pm-gpio.c 中,那么上面的平台文件就有了解释,定义的是 CONFIG_PLAT_S5P也就是 plat-ys5p samsung

  那么这里还有两个问题 :

  CONFIG_PLAT_SAMSUNG 配置它干嘛?Makefile 中根本就没有该选项。

  根据 arch/arm/Makefile 中来看,取名太奇怪了,定义CONFIG_ARCH_S3C64XX才是包含 palt-samsung目录。

  1. 扩展 libs-y
libs-y              := arch/arm/lib/ $(libs-y)

  这里进一步扩展 libs-y 的内容,关于体系架构相关的目录。

  1. 扩展 drivers-y
drivers-$(CONFIG_OPROFILE)      += arch/arm/oprofile/

  CONFIG_OPROFILE 配置项在内核配置时未定义,该功能是 linux 中的性能分析机制。

4.2.3.3 各级子目录的 Makefile

  在配置内核时,生成配置文件 .config。内核顶层 Makefile 使用如下语句间接包含 .config 文件。之所以是"间接",因为 include/config/auto.conf 是根据 .config 文件生成的,内容差别不大。

# Read in config
-include include/config/auto.conf

  在配置文件中定义各种配置项,然后被顶层 Makefile 包含,最后传入各级子目录的 Makefile。各级子目录的 Makefile 使用这些配置项来决定哪些文件被编进内核中,哪些文件被编程模块(即驱动程序),哪些要进入下一级子目录继续编译。

4.2.4 怎样编译这些文件?

  即编译选项、连接选项是什么。这些选项分为 3 类:

  1)全局的,适用于整个内核代码树;

  2)局部的,仅适用于某个 Makefile 中的所有文件;

  3)个体的,仅适用于某个文件。

4.2.4.1 全局选项

  全局选项在顶层 Makefile 和 arch/$(ARCH)/Makefile 中定义,这些选项的名称及含义:

CFLAGS:编译 C 文件的选项
AFLAGS:编译 汇编 文件的选项
LDFAGS:连接文件的选项
ARFLAGS:制作库文件的选项
4.2.4.2 局部选项

  局部选项在各级子目录中定义,这些选项的名称及含义:

EXTRA_CFLAGS:编译 C 文件的选项
EXTRA_AFLAGS:编译 汇编 文件的选项
EXTRA_LDFAGS:连接文件的选项
EXTRA_ARFLAGS:制作库文件的选项

  功能与前面相同,只是适用范围缩小,只针对当前 Makefile 中的所有文件。

4.2.4.3 个体选项

  针对某个文件定义它的编译选项,可使用:

CFLAGS_$@:编译某个 C 文件
AFLAGS_$@:编译某个汇编文件

$@ :表示某个目标文件名

  例如以下代码编译 aha152x.c 时,选项中要额外加上 “-DAHA152X_STAT -DAUTOCONF”。

CFLAGS_aha152x.o =   -DAHA152X_STAT -DAUTOCONF

  注意:这 3 类选项是一起使用的,在 scripts/Makefile.lib 中能看到.

4.2.5 怎样连接这些文件,它们的顺序是怎样的?

  前面分析有哪些文件要编进内核时,顶层 Makefile 和 arch/$(ARCH)/Makefile 定义了 6 类目录(或文件):head-y、init-y、drivers-y、net-y、libs-y、core-y。除了 head-y 以外,其余的都是目录名。

4.2.5.1 内核映像文件 vmlinux

  在顶层 Makefile 中,这些目录名的后面直接加上 bult-in.olib.a,表示要连接进内核的文件,如下所示:

init-y      := $(patsubst %/, %/built-in.o, $(init-y))
core-y      := $(patsubst %/, %/built-in.o, $(core-y))
drivers-y   := $(patsubst %/, %/built-in.o, $(drivers-y))
net-y       := $(patsubst %/, %/built-in.o, $(net-y))
libs-y1     := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2     := $(patsubst %/, %/built-in.o, $(libs-y))
libs-y      := $(libs-y1) $(libs-y2)

  vmlinux相关:

vmlinux-init := $(head-y) $(init-y)
vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)
vmlinux-all  := $(vmlinux-init) $(vmlinux-main)
vmlinux-lds  := arch/$(SRCARCH)/kernel/vmlinux.lds

   vmlinux-all :表示所有构成内核映像文件 vmlinux 的目标文件,文件的顺序遵循从左到右的顺序,依次为:arch/arm/kernel/head.o(或者head-nommu.o)、arch/arm/kernel/init_task.o、init/built-in.o、usr/built-in.o等。由此可知,内核运行的第一个文件是 head.o(由 head.S生成)

   vmlinux-lds:表示连接脚本为 arch/arm/kernel/vmlinux.lds。它由 arch/arm/kernel/vmlinux.lds.S 文件生成,规则在 scripts/Makefile.build中,如下:

$(obj)/%.lst: $(src)/%.c FORCE
    $(call if_changed_dep,cc_lst_c)
4.2.5.2 连接脚本 vmlinux.lds

  暂不深入。

4.2.6 Makefile 小结

  1)配置文件 .config 中定义了一系列的变量, Makefile 将结合它们来决定哪些文件被编进内核、哪些文件被编成模块、涉及哪些子目录。

  2)顶层 Makefile 和 arch/$(ARCH)/Makefile 决定根目录下哪些子目录、arch/$(ARCH) 目录下哪些文件和目录将被编进内核。

  3)最后,各级子目录下的 Makefile 决定所在目录下哪些文件将被编进内核、哪些文件被编成模块、进入下一级的哪些子目录继续调用它们的 Makefile。

  4)顶层 Makefile 和 arch/$(ARCH)/Makefile 设置了可以影响所有文件的编译、连接选项:CFLAGS、AFLAGS、LDFAGS、ARFLAGS

  5)各级子目录下的 Makefile 中可以设置能够影响当前目录下所有文件的编译、连接选项:EXTRA_CFLAGS、EXTRA_AFLAGS、EXTRA_LDFAGS、EXTRA_ARFLAGS;还有设置可以影响某个文件的编译选项:CFLAGS_$@、AFLAGS_$@

  6)顶层 Makefile 按照一定的顺序组织文件,根据连接脚本 arch/$(ARCH)/vmlinux.lds 生成内核映像文件 vmlinux

4.3 Kconfig 架构文件

  前面了解了在内核目录下执行 "make menuconfig" 能够进入内核配置界面。除了以菜单形式显示的配置界面,还有其他形式的配置界面。如 "make config" 命令启动字符配置界面,对于每个选项都会依次出现一行提示信息,配置需要进行逐个回答;"make xconfig" 命令启动 X-windows 图形配置界面。

  所有配置工具都是通过读取 arch/$(ARCH)/Kconfig 文件来生成配置界面,这个文件是所有配置文件的总入口,它会包含其他目录的 Kconfig 文件。

  内核源码每个子目录中,都有个 Makefile 文件和 Kconfig 文件。Kconfig 用于配置内核,它就是各种配置界面的源文件。内核的配置工具读取各个 Kconfig 文件,生成配置界面供开发人员配置内核,最后生成配置文件 .config

  内核的配置界面以树状的菜单形式组织,主菜单下有子菜单或配置选项。每个子菜单或选项都可以有依赖关系,这些依赖关系用来确定它们是否显示(即只有被依赖项的父项被选中的情况下,子项才会显示出来)。

   Kconfig 文件的语法可以参考 Documentation/kbuild/kconfig-language.txt文件。

5、内核启动过程

5.1 内核启动过程概述

  内核的启动过程分为两部分:

  1)架构/开发板相关的引导过程;

  2)后续的通用启动过程。

  分析的是 ARM 架构处理器上 Linux 内核 vmlinux 的启动过程,之所以强调 vmlinux,参考vmlinux、uImage和zImage的区别

  引导阶段通常使用汇编语言编写,整个过程可分三个步骤:

  ① 检查内核是否支持当前架构的处理器、开发板;

  ② 因连接内核时使用的是虚拟地址,所以要设置页表、使能 MMU;

  ③ 调用 C 函数 start_kernel 及调用前的常规工作,复制数据段、清除 BSS 段等。

  通用启动阶段主要使用 C ,分为以下两部分:

  ① 进行内核初始化的全部工作;

  ② 最后调用 rest_init() 函数启动 init 过程,创建系统的第一个进程:init 进程。

01_ARM处理器的Linux启动过程

5.2 引导阶段代码分析

  由前面对 Makefile 架构分析,可知 arch/arm/kernel/head.S 是内核执行的第一个文件。分析其源码:

ENTRY(stext)
    setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
                        @ and irqs disabled
    mrc p15, 0, r9, c0, c0      @ get processor id
    bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    movs    r10, r5             @ invalid processor (r5=0)?
 THUMB( it  eq )        @ force fixup-able long branch encoding
    beq __error_p           @ yes, error 'p'


/*
* 获取RAM的起始物理地址,并保存于 r8 = phys_offset
* XIP内核与普通在RAM中运行的内核不同
* 1)CONFIG_XIP_KERNEL
*  通过运行时计算????
* 2)正常RAM中运行的内核
*  通过编译时确定(PLAT_PHYS_OFFSET 一般在arch/arm/mach-xxx/include/mach/memory.h定义)
*/
#ifndef CONFIG_XIP_KERNEL
    adr r3, 2f
    ldmia   r3, {r4, r8}
    sub r4, r3, r4          @ (PHYS_OFFSET - PAGE_OFFSET)
    add r8, r8, r4          @ PHYS_OFFSET
#else
    ldr r8, =PLAT_PHYS_OFFSET
#endif

    /*
     * r1 = machine no, r2 = atags or dtb,
     * r8 = phys_offset, r9 = cpuid, r10 = procinfo
     */
    bl  __vet_atags         @ 判断 r2 (内核启动参数) 指针的有效性
#ifdef CONFIG_SMP_ON_UP
    bl  __fixup_smp         @ 如果运行SMP内核在单处理器系统中启动,做适当调整
#endif
#ifdef CONFIG_ARM_PATCH_PHYS_VIRT
    bl  __fixup_pv_table    @ 根据内核在内存中的位置修正物理地址与虚拟地址的转换机制
#endif
    bl  __create_page_tables    @ 初始化页表

    /*
     * The following calls CPU specific code in a position independent
     * manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of
     * xxx_proc_info structure selected by __lookup_processor_type
     * above.  On return, the CPU will be ready for the MMU to be
     * turned on, and r0 will hold the CPU control register value.
     */
    ldr r13, =__mmap_switched       @ address to jump to after
                        @ mmu has been enabled
    adr lr, BSYM(1f)            @ return (PIC) address
    mov r8, r4              @ 将swapper_pg_dir的物理地址放入 r8
                            @ 以备_enable_mmu中,将其放入 TTBR1
 ARM(   add pc, r10, #PROCINFO_INITFUNC )   @ 跳入构架相关的初始化处理器函数
 THUMB( add r12, r10, #PROCINFO_INITFUNC    )   @ 主要目的只配置CP15(包括缓存配置)
 THUMB( mov pc, r12             )
1:  b   __enable_mmu
ENDPROC(stext)
    .ltorg
#ifndef CONFIG_XIP_KERNEL
2:  .long   .
    .long   PAGE_OFFSET
#endif  

  第二行:通过设置 CPSR 寄存器让处理器进入管理模式(SVC),并禁止中断。

  第三行:读取协处理器 CP15 的寄存器 C0 获得 CPU ID,并存入 r9 寄存器。

  第四行:调用 __lookup_processor_type 函数,检查是否支持当前 CPU。如果支持,r5 寄存器返回一个用来描述这个 CPU 的结构体的地址。否则,r5 返回 0。

  第五行:将 r5 赋值给 r10 并判断 r5 的值是否为 0。

  第六行:如果 r5 为 0,打印错误。

5.2.1 __lookup_processor_type 函数

  源码处于 arch/arm/kernel/head-common.S 中。

    __CPUINIT
__lookup_processor_type:
    adr r3, __lookup_processor_type_data @ r3 = 该函数存放的物理地址
    ldmia   r3, {r4 - r6}
    sub r3, r3, r4          @ r3 = r3 - r4,物理地址与虚拟地址的偏移量
    add r5, r5, r3          @ r5 = __proc_info_begin 对应的物理地址
    add r6, r6, r3          @ r6 = __proc_info_end 对应的物理地址
1:  ldmia   r5, {r3, r4}    @ proc_info_list结构中的cpu_value, cpu_mask
    and r4, r4, r9          @ r4 = r4 & r9 = cpu_mask & 传入的CPU ID
    teq r3, r4              @ 比较
    beq 2f              @ 如果相等,表示找到匹配的proc_info_list结构。
                        @ 并跳转到 2:mov pc, lr
    add r5, r5, #PROC_INFO_SZ   @ r5 指向下一个proc_info_list结构
                        @ #PROC_INFO_SZ = sizeof(proc_info_list)
    cmp r5, r6              @ 是否比较玩所有的proc_info_list结构
    blo 1b                  @ 没有,则跳转到 1: 继续比较
    mov r5, #0              @ 比较完所有的,无匹配项,r5 = 0
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
5.2.1.1 地址转换

   在没有启动 mmu 之前,使用的都是物理地址,而内核却是以虚拟地址连接的,所以在访问内核的数据时,需要先将其转换为物理地址。(改函数上面注释所说明的情况是什么?不能对 __proc_info_list 使用绝对地址?)

  adr 指令基于 pc 寄存器计算地址值,由于调用该函数时,还未使能 MMU,因此 pc 寄存器中使用的还是物理地址,该语句是获取该函数存放的物理地址。

5.2.1.2 __lookup_processor_type_data函数

  该函数内的数据都是在连接内核时确定,定义在连接脚本文件 arch/arm/kernel/vmlinux.S 中:

   __proc_info_begin = .; 
   *(.proc.info.init) 
   __proc_info_end = .;

  __proc_info_begin:表示某个东西开始地址。

  __proc_info_end:表示某个东西结束地址。

   . :表示当前的代码的虚拟地址。

  那么整体的含义就是 连接所有的 .proc.info.init 段,将其组织起来放在地址__proc_info_begin 到 地址__proc_info_end 之间。

  那么 .proc.info.init 段究竟是什么呢?

  在内核源码中搜索 grep ".proc.info.init" * -nwR ,发现除了在连接脚本汇总出现,还在arch/arm/mm/中的某个CPU 硬件架构的文件中出现,处理器 exynos 4412 是 Cortex-A9 的核,ARMv7的架构,在配置文件可找到 CONFIG_CPU_V7=y,在arch/arm/mm/Makefile 中被编译。

  打开 arch/arm/mm/proc-v7.S

    .section ".proc.info.init", #alloc, #execinstr
    
    /*
     * Standard v7 proc info content
     */
.macro __v7_proc initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0
    ALT_SMP(.long   PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
            PMD_FLAGS_SMP | \mm_mmuflags)
    ALT_UP(.long    PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
            PMD_FLAGS_UP | \mm_mmuflags)
    .long   PMD_TYPE_SECT | PMD_SECT_XN | PMD_SECT_AP_WRITE | \
        PMD_SECT_AP_READ | \io_mmuflags
    W(b)    \initfunc
    .long   cpu_arch_name
    .long   cpu_elf_name
    .long   HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \
        HWCAP_EDSP | HWCAP_TLS | \hwcaps
    .long   cpu_v7_name
    .long   v7_processor_functions
    .long   v7wbi_tlb_fns
    .long   v6_user_fns
    .long   v7_cache_fns
.endm
    
    /*
     * ARM Ltd. Cortex A9 processor.
     */
    .type   __v7_ca9mp_proc_info, #object
__v7_ca9mp_proc_info:
    .long   0x410fc090
    .long   0xff0ffff0
    __v7_proc __v7_ca9mp_setup
    .size   __v7_ca9mp_proc_info, . - __v7_ca9mp_proc_info
......
    /*
     * Match any ARMv7 processor core.
     */
    .type   __v7_proc_info, #object
__v7_proc_info:
    .long   0x000f0000      @ Required ID value
    .long   0x000f0000      @ Mask for ID
    __v7_proc __v7_setup
    .size   __v7_proc_info, . - __v7_proc_info  

  从上面可了解到,通过使用 __v7_proc 宏定义初始化了一些 processer 的proc info,Cortex-A 的 CPU ID,即0x410fc090

  在 arch/arm/include/asm/procinfo.h 中有关于 struct proc_info_list 的定义,用来描述 CPU。从注释信息来看,与汇编代码相关,除了上面的文件之外还与 head.S 相关,具体的该结构与 ".proc.info.init" 段 是如何联系起来的?还是没搞懂。

  小结:不同的 proc_info_list 结构被用来支持不同的 CPU,它们都定义在 .proc.info.init 段中。在执行连接脚本arch/arm/kernel/vmlinux.lds 连接内核时,这些结构体被组织在一起,开始地址为 __proc_info_begin,结束地址为 __proc_info_end

5.2.1.3 比较 CPU ID

  依次从开始地址 __proc_info_begin 读取 每个proc_info_list结构的前面两个成员(cpu_val、cpu_mask),将它们与 解码后的 r9 中传入的 CPU ID 进行比较。相等,则表示支持该CPU,直接返回该结构的地址(存入 r5中);否则,继续读取地址间的下一个proc_info_list 。所有的都比较完,还未找到匹配的,表示不支持该CPU,返回 0(存入r5 中)。

5.2.2 __mmap_switched

  ① 跳转之后所做的事。

  ② 调用start_kernel 的过程。

5.2.3 __enable_mmu

5.3 start_kernel 函数部分代码分析

  start_kernel() 是内核启动时进入的第一个 C 函数,它要进行内核的全部初始化工作,最后创建并启动 init 进程。

  内核启动之前是由 U-boot 引导的,它传入的参数有两类:

  1)预先存在某个地址的 tag 列表;

  2)调用内核时在 r1 寄存器中指定的机器类型 ID。

  后者在内核的引导阶段已经用到,而 tag 列表将在这个阶段处理。主要分析 step_arch()函数、console_init()函数,以 tag 列表的处理(内存 tag、命令行 tag)、串口控制台的初始化为主线。

5.3.1 setup_arch()函数分析

  源码处于 arch/arm/kernel/setup.c 中:

void __init setup_arch(char **cmdline_p)
{
    struct machine_desc *mdesc;
......
    setup_processor();
    mdesc = setup_machine_fdt(__atags_pointer);
    if (!mdesc)
        mdesc = setup_machine_tags(machine_arch_type);
......
    parse_early_param();
    
    paging_init(mdesc);
......
}

  详细请参考 Linux 3.0 内核源码分析- setup_arch函数

5.3.1.1 setup_processor() 函数

  进行处理器相关的一些设置。它会调用引导阶段的 lookup_processor_type h函数,再次检测处理器类型,并初始化处理器相关的底层变量。内核启动时的处理器信息(包括cache)就是通过这个函数打印的,例如:

[    0.000000] CPU: ARMv7 Processor [413fc090] revision 0 (ARMv7), cr=10c5387d
[    0.000000] CPU: VIPT nonaliasing data cache, VIPT aliasing instruction cache
5.3.1.2 setup_machine_fdt()函数

  在此函数通过 bootloader 传递过来的设备ID来匹配一个 struct machine_desc 结构体。整个的原理同前面引导阶段的 __lookup_processor_type 相似,如下:

  不同的 machie_desc 结构被用来支持不同的设备,它们都定义在 .arch.info.init 段中。在执行连接脚本arch/arm/kernel/vmlinux.lds 连接内核时,这些结构体被组织在一起,开始地址为 __arch_info_begin,结束地址为 __arch_info_end

  在连接的 .arch.info.init 断中,与 bootloader 传入的设备ID进行比较。如果没有匹配上就死循环。
如果匹配上了就打印机器名 ,并处理bootloader传递过来的 tagged_list,将所有的 tag 信息保存到相应的全局变量或结构体中,并返回匹配的 machine_desc

  内核启动时的机器信息就是这里打印的,例如:

[    0.000000] Machine: SMDK4X12

  exynos 4412开发板(迅为iTop4412)的 machine_desc 结构,在文件 arch/arm/mach-exynos/mach-itop4412.c 中定义:

#ifdef CONFIG_TC4_ICS

MACHINE_START(SMDK4412, "SMDK4X12")
    .boot_params    = S5P_PA_SDRAM + 0x100,
    .init_irq   = exynos4_init_irq,
    .map_io     = smdk4x12_map_io,
    .init_machine   = smdk4x12_machine_init,
    .timer      = &exynos4_timer,

    #if defined(CONFIG_KERNEL_PANIC_DUMP)       //mj for panic-dump
    .reserve        = reserve_panic_dump_area,
    #endif

#ifdef CONFIG_EXYNOS_C2C
    .reserve    = &exynos_c2c_reserve,
#endif
MACHINE_END
#endif

在文件 arch/arm/include/asm/mach/arch.h 中定义 MACHINE_START、MACHINE_END

#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             \
};
5.3.2 parse_early_param()函数分析

  参考《嵌入式Linux应用开发手册》P323。没弄懂,后续再深入了解。

5.3.2 paging_init() 函数分析

  有mmu 的情况下,在arch/arm/mm/mmu.c中:

void __init paging_init(struct machine_desc *mdesc)
{
    void *zero_page;

    memblock_set_current_limit(lowmem_limit);

    build_mem_type_table();
    prepare_page_table();
    map_lowmem();
    devicemaps_init(mdesc);
    kmap_init();

    top_pmd = pmd_off_k(0xffff0000);

    /* allocate the zero page. */
    zero_page = early_alloc(PAGE_SIZE);

    bootmem_init();

    empty_zero_page = virt_to_page(zero_page);
    __flush_dcache_page(NULL, empty_zero_page);
}

该函数的主要目的:

  1)设置内核的参考页表。

  2)此页表不仅用于物理内存映射,还用于管理vmalloc区。

  3)此函数中非常重要的一点就是初始化了bootmem分配器。

  而根据移植的目的(内核在开发板上运行),只需关注:

devicemaps_init(mdesc)中的:
    mdesc->map_io();

  对于迅为 iTop4412 开发板,mdesc->map_io(); 调用的是 smdk4x12_map_io() 函数,用来设定相关参数,如晶振、串口等。在文件 arch/arm/mach-exynos/mach-itop4412.c 中 :

static void __init smdk4x12_map_io(void)
{
    clk_xusbxti.rate = 24000000;
    s5p_init_io(NULL, 0, S5P_VA_CHIPID);
    s3c24xx_init_clocks(24000000);
    s3c24xx_init_uarts(smdk4x12_uartcfgs, ARRAY_SIZE(smdk4x12_uartcfgs));

#if defined(CONFIG_S5P_MEM_CMA)
    exynos4_reserve_mem();
#endif
}
5.3.3 console_init() 函数分析

drivers/tty/tty_io.c

void __init console_init(void)
{
    initcall_t *call;

    /* Setup the default TTY line discipline. */
    tty_ldisc_begin();

    /*
     * set up the console device so that later boot sequences can
     * inform about problems etc..
     */
    call = __con_initcall_start;
    while (call < __con_initcall_end) {
        (*call)();
        call++;
    }
}

  调用地址范围 __con_initcall_start__con_initcall_end 之间定义的每个函数,这些函数使用 console_initcall 宏来指定,在文件include/linux/init.h 中:

#define console_initcall(fn) \
    static initcall_t __initcall_##fn \
    __used __section(.con_initcall.init) = fn

  还是通过连接脚本,将 .con_initcall.init 段组织在一起。

  通过 register_console() 函数在内核中注册控制台,就是将指定的 struct console 结构链入一个全局链表 console_drivers 中。并且使用结构成员 .name、.index 名字与序号与前面 "console = " 指定的控制台相比较(make menuconfig 中 boot options 参数中的 console?),如果相符,则以后的 printk 信息从该控制台输出。

  这里注意一个问题:在调用 setup_arch() 函数之前已经调用 printk(KERN_NOTICE "%s", linux_banner); 了,但是这个时候 printk 函数只是将打印信息放在缓冲区中,并没有打印到控制台上(如串口,LCD屏),因为这时候控制台还未初始化。

6、内核移植

写到这里,暂时不写了,太深入。待后续了解更多再完善。

6.1 修改内核

  参考前面。

6.2 修改 MTD 分区

6.3 移植 YAFFS 文件系统

6.4 编译、烧写、启动内核

参考

  1. 嵌入式Linux应用开发完全手册 - 韦东山,20章
  2. 韦东山第一期视频,第十二课
  3. 迅为iTop4412资料
  4. vmlinux、uImage和zImage的区别
  5. Linux 3.0 内核源码分析 - 启动时汇编部分
  6. Linux 3.0 内核源码分析- setup_arch函数
  7. __proc_info_begin->__proc_info_end
  8. arm linux kernel 从入口到start_kernel 的代码分析

猜你喜欢

转载自www.cnblogs.com/wanjianjun777/p/10483993.html