arm-linux内核start_kernel之前启动分析(1)-接过bootloader的衣钵

   2023-02-09 学习力0
核心提示:前段时间移植uboot细致研究过uboot启动过程,近期耐不住寂寞。想对kernel下手。Uboot启动过程分析博文连接例如以下:http://blog.csdn.net/skyflying2012/article/details/25804209移植内核时kernel启动过程须要我们改动的地方比較少。研究这个对于编写driver
前段时间移植uboot细致研究过uboot启动过程,近期耐不住寂寞。想对kernel下手。


Uboot启动过程分析博文连接例如以下:


http://blog.csdn.net/skyflying2012/article/details/25804209


移植内核时kernel启动过程须要我们改动的地方比較少。研究这个对于编写driver也没有多大帮助,但对了解整个linux架构,各种机制还是非常实用。


仅仅有知道kernel怎样启动,我们才干真正的去理解kernel

作为一个嵌入式工作者,我想不能仅仅局限于某个module driver。而应深入到kernel的汪洋大海中去傲游!


学习启动过程,我本着打破沙锅问究竟的原则,希望能研究的明明确白,但也鉴于水平有限。还是有非常多纰漏之处

共享博文。希望大家多多交流指正,辛苦整理。如需转载,还请注明出处。

对于arm linux,start_kernel之前都是汇编代码,区区上百行汇编,可是却蕴含着非常多精髓。

这部分代码分3篇来分析,另外两篇链接地址例如以下:

http://blog.csdn.net/skyflying2012/article/details/41447843

http://blog.csdn.net/skyflying2012/article/details/48054417


今天先来学习前几十行!


Kernel版本:3.4.55

在arch/arm/kernel/head.S中。例如以下:

.arm

    __HEAD
ENTRY(stext)

 THUMB( adr r9, BSYM(1f)    )   @ Kernel is always entered in ARM.
 THUMB( bx  r9      )   @ If this is a Thumb-2 kernel,
 THUMB( .thumb          )   @ switch to Thumb now.
 THUMB(1:           )

    //处理器进入svc模式。关闭中断
    setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @ ensure svc mode
                        @ and irqs disabled
    //获取处理器ID
    mrc p15, 0, r9, c0, c0      @ get processor id
    bl  __lookup_processor_type     @ r5=procinfo r9=cpuid
    //将proc_type_list pointer存在r10中。假设为NULL,则error_p
    movs    r10, r5             @ invalid processor (r5=0)?
 THUMB( it  eq )        @ force fixup-able long branch encoding
    beq __error_p           @ yes, error 'p'

    //CONFIG_ARM_LPAE不太明确含义。我使用处理器配置文件没有选择该项,感兴趣朋友能够研究下
#ifdef CONFIG_ARM_LPAE
    mrc p15, 0, r3, c0, c1, 4       @ read ID_MMFR0
    and r3, r3, #0xf            @ extract VMSA support
    cmp r3, #5              @ long-descriptor translation table format?

THUMB( it lo ) @ force fixup-able long branch encoding blo __error_p @ only classic page table format #endif #ifndef CONFIG_XIP_KERNEL //获取物理地址与虚拟地址的offset。存在r8中 adr r3, 2f ldmia r3, {r4, r8} sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET) add r8, r8, r4 @ PHYS_OFFSET #else //定义CONFIG_XIP_KERNEL,offset为PHYS_OFFSET ldr r8, =PHYS_OFFSET @ always constant in this case #endif /* * r1 = machine no, r2 = atags or dtb, * r8 = phys_offset, r9 = cpuid, r10 = procinfo */ //对bootloader传来的tags參数进行检查 bl __vet_atags

Kernel的入口函数是哪个,入口地址在哪,须要依据连接脚本来确定。
在arch/arm/kernel/vmlinux.lds.S,例如以下:

OUTPUT_ARCH(arm)
ENTRY(stext)

#ifndef __ARMEB__
jiffies = jiffies_64;
#else
jiffies = jiffies_64 + 4;
#endif

SECTIONS
{
........
#ifdef CONFIG_XIP_KERNEL
    . = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);
#else
    . = PAGE_OFFSET + TEXT_OFFSET;
#endif
}
入口函数是head.S中的stext。不採用XIP技术,入口地址是PAGE_OFFSET+TEXT_OFFSET。


./arch/arm/include/asm/memory.h中:

#define PAGE_OFFSET     UL(CONFIG_PAGE_OFFSET)
Menuconfig中CONFIG_PAGE_OFFSET = 0xc0000000
./arch/arm/Makefile中:
textofs-y   := 0x00008000
textofs-$(CONFIG_ARCH_CLPS711X) := 0x00028000
# We don't want the htc bootloader to corrupt kernel during resume
textofs-$(CONFIG_PM_H1940)      := 0x00108000
# SA1111 DMA bug: we don't want the kernel to live in precious DMA-able memory
ifeq ($(CONFIG_ARCH_SA1100),y)
textofs-$(CONFIG_SA1111) := 0x00208000
endif
textofs-$(CONFIG_ARCH_MSM7X30) := 0x00208000
textofs-$(CONFIG_ARCH_MSM8X60) := 0x00208000
textofs-$(CONFIG_ARCH_MSM8960) := 0x00208000
......
# The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)
入口地址是0xc0008000.
可是实际操作中,kernel是载入到0x80008000地址执行的。
(我使用处理器sdram物理起始地址是0x80000000)


为什么链接地址和执行地址不一致?

学习完start_kernel之前的汇编,就会明确原因了。


在stext中。首先调用到__lookup_processor_type,Kernel代码将全部CPU信息的定义都放到.proc.info.init段中。因此能够觉得.proc.info.init段就是一个数组,每一个元素都定义了一个或一种CPU的信息。

眼下__lookup_processor_type使用该元素的前两个字段cpuid和mask来匹配当前CPUID。假设满足CPUID & mask == cpuid,则找到当前cpu的定义并返回。

 

代码例如以下:

   __CPUINIT
__lookup_processor_type:
    //3行汇编,计算出物理地址与虚拟地址之间的offset。存在r3中
    adr r3, __lookup_processor_type_data
    ldmia   r3, {r4 - r6}
    sub r3, r3, r4          @ get offset between virt&phys
    //获取__proc_info_begin的物理地址
    add r5, r5, r3          @ convert virt addresses to
    //获取__proc_info_end的物理地址
    add r6, r6, r3          @ physical address space
    //mask cp15读出的cpuid,与proc_type_list中value对照
1:  ldmia   r5, {r3, r4}            @ value, mask
    and r4, r4, r9          @ mask wanted bits
    teq r3, r4
    //一致则返回,不一致则跳到下一个proc_type_list,继续对照
    beq 2f
    add r5, r5, #PROC_INFO_SZ       @ sizeof(proc_info_list)
    cmp r5, r6
    blo 1b
    //匹配成功。r5存该proc_type_list指针。匹配失败,r5置0
    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</span>


由于kernel要开启MMU,所以kernel编译链接地址是虚拟地址(物理地址经过MMU转换后CPU看到的地址)。并非物理地址。

 链接确定了变量的绝对地址(虚拟地址),但在现阶段。没开启MMU,CPU看到的sdram地址就是其物理地址(0x80000000起始)。
 假设直接执行,对于变量的寻址则会出现故障(函数寻址没问题,由于arm函数寻址使用相对跳转指令b bl)
 比方。kernel image中全局变量i链接地址在0xc0009000,但现阶段i物理地址是在0x80009000。对于CPU来说,仅仅能在0x80009000上才干找到i。
 去0xc0009000寻址,程序执行就出错了。
 这就是为什么我们所理解的,链接地址 载入地址 执行地址必须一致的原因。



 kernel现阶段给出的解决方法,就是lookup_processor_type前3行汇编:

adr r3, __lookup_processor_type_data 载入__lookup_processor_type_data地址(实际执行地址,这里就是物理地址)到r3

ldmia r3。 {r4 - r6} 获取以r3 r3+4 r3+8为地址的变量到r4,r5。r6.
地址变量值是在链接时确定的,所以r4中存的是__lookup_processor_type_data的链接地址(虚拟地址)。



sub r3 ,r3 。r4     r3中存储的是物理地址与虚拟地址的偏移。


这是多么genius的操作啊!

_proc_info_begin _proc_info_end在链接脚本中定义,是.proc.info.init段的首尾。
该段中是proc_info_list struct,表示处理器相关信息,定义例如以下:

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;
};


该段是在arch/arm/mm/proc-xxx.S中填充,定义了相应arm指令集的处理器特性和初始化函数。在第三篇文章中我们还会具体来理解proc info的作用,这里先按下不表。

lookup_processor_type_data返回stext中。


接下来相同用上面的方法获取phy&virt offset,存在r8.


依据我之前分析uboot传參kernel的博文(链接例如以下:http://blog.csdn.net/skyflying2012/article/details/35787971)

r1存储machine id,r2存储atags。


stext中__vet_atags会对atags做一个主要的检查,代码例如以下:


__vet_atags:
    tst r2, #0x3            @ aligned?
    bne 1f

    ldr r5, [r2, #0]
    //推断是否是dtb类型
#ifdef CONFIG_OF_FLATTREE
    ldr r6, =OF_DT_MAGIC        @ is it a DTB?
    cmp r5, r6
    beq 2f
#endif
    cmp r5, #ATAG_CORE_SIZE     @ is first tag ATAG_CORE?
    cmpne   r5, #ATAG_CORE_SIZE_EMPTY
    bne 1f
    ldr r5, [r2, #4]
    ldr r6, =ATAG_CORE
    cmp r5, r6
    bne 1f

    //正确tags,返回
2:  mov pc, lr              @ atag/dtb pointer is ok

    //错误tags,清空r2。返回
1:  mov r2, #0
    mov pc, lr
ENDPROC(__vet_atags)


检查tag头4 byte(tag_core的size)和第二个4 byte(tag_core的type)是否正确。


对于stext中前几十行汇编,已经分析完毕,总结下做了哪些工作:
(1)设置CPU模式
(2)检查CPUID是否匹配
(3)获取phy&virt offset
(4)检查atags參数


这段代码就分析到这,只是引起了我对于链接地址 执行地址的思考。
教科书上是这样教的。我也一直这么觉得,链接地址 执行地址(载入地址)必须是一致。可是却没有真正去思考过为什么。

程序的链接地址与执行地址为什么要一致?

我的理解,链接确定程序执行绝对地址。也确定了当中变量及函数的绝对地址,载入执行地址不是其链接地址,变量实际存储的地址就变了。
这时假设对变量进行寻址,就会有不可知的结果,这是我能想到的原因。
平时我们编译链接都是一些C语言编敲代码,难免会定义一些全局变量。假设链接和执行地址不一致。就不能正常寻址。



假设想执行和链接地址不一致。我能想到的办法,仅仅能是汇编中尽量不去涉及一些绝对地址,使用PIC位置无关代码。

联想之前分析的uboot relocation原理(博文链接:http://blog.csdn.net/skyflying2012/article/details/37660265),

uboot在relocation之后,kernel在开启MMU之前,都实现了链接地址和执行地址不一致,看看它们用的什么方法?


(1)uboot在relocation时改动rel.dyn段(存储全部变量地址)。实现将全部变量地址重定位到新执行地址


(2)kernel在开启MMU之前,计算执行地址(物理地址)与链接地址(虚拟地址)的偏移,对变量寻址时都进行地址转换。从而正常找到变量。开启MMU之后。利用硬件机制。来实现链接和执行地址的统一


所以说,链接地址一定要等于执行地址吗?不一定。嵌入式最著名的uboot  kernel就是样例!



 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 【强转】QEMU+GDB调试linux内核全过程
    【强转】QEMU+GDB调试linux内核全过程
    昨天更新了一篇名为《QEMU+GDB调试linux内核全过程》[link][https://blog.csdn.net/weixin_37867857/article/details/88138432]的博客,发现排版比较混乱,而且思维也比较混乱。咋一看下来简直是惨不忍睹,而且会给读者在安装过程中一种云里雾里的感觉,加上
    03-08
  • Linux下Bochs,NASM安装和使用 linux bom
    Linux下Bochs,NASM安装和使用 linux bom
    以Ubuntu为例,先更新一下:sudo apt-get updatesudo apt-get upgrade然后安装Bochs环境:sudo apt-get install build-essential xorg-dev libgtk2.0-dev安装NASMNASM官网下载,这以nasm-2.14.02.tar.gz为例:用tar zxvf nasm-2.14.02.tar.gz解压后编译安装cd
    03-08
  • 把玩Alpine linux(一):安装
    把玩Alpine linux(一):安装
    导读Alpine Linux是一个面向安全应用的轻量级Linux发行版。它采用了musl libc和busybox以减小系统的体积和运行时资源消耗,同时还提供了自己的包管理工具apk。Alpine 的内核都打了grsecurity/PaX补丁,并且所有的程序都编译为Position Independent Executabl
    03-08
  • 日志审计与分析实验三(rsyslog服务器端和客户端配置)(Linux日志收集)
    日志审计与分析实验三(rsyslog服务器端和客户
     Linux日志收集一、实验目的:1、掌握rsyslog配置方法2、配置rsyslog服务收集其他Linux服务器日志:C/S架构:客户端将其日志上传到服务器端,通过对服务器端日志的查询,来实现对其他客户端的日志进行集中管理;下面实现就是通过两套机器来实现,(server:19
    03-08
  • Linux学习系列--如何在Linux中进行文件的管理
    Linux学习系列--如何在Linux中进行文件的管理
    文件在常见的Linux的文件系统中,经常使用能了解到的文件管理系统是分为多个文件夹进行管理的。如何查看文件路径 pwd ,在文件目录中,会有一个点(.)代表的是当前目录,两个点(..)代表的是当前目录的上层目录在Linux下,所有以点开始的文件都是“隐藏文件
    03-08
  • [JetBrains] 我想在 Linux 上使用 macOS 键绑定!
    [JetBrains] 我想在 Linux 上使用 macOS 键绑定
    很高兴认识你,我的名字是kitakkun。我最近开始实习,是工程界的新手。顺便说一句,这是我的第一篇文章。你最喜欢的操作系统是什么?视窗?苹果系统?还是Linux?我将它们全部用于不同的目的,但感觉就像 macOS ≒ LinuxWindows。一两个月前,我最喜欢 Linux
    03-08
  • linux 配置Socks51
    linux 配置Socks51
    ***大家耳熟能详,但是socks用到的人比较少,那什么是socks呢?请看第二段或者百度百科,socks分别有4和5两个版本,现在5为主流。工作中经常用***访问国外,但是同时国内的速度又慢了,让人很纠结,实际上这个时候可以考虑使用socks。指定某一个程序使用国外s
    02-10
  • linux下如何单独编译设备树? linux设备树是什
    答: make vendor/device_name.dtb  如: make freescale/fsl-1043a-rdb.dtb
    02-10
  • linux下mysql开启远程访问权限及防火墙开放3306端口
    linux下mysql开启远程访问权限及防火墙开放3306
    开启mysql的远程访问权限默认mysql的用户是没有远程访问的权限的,因此当程序跟数据库不在同一台服务器上时,我们需要开启mysql的远程访问权限。主流的有两种方法,改表法和授权法。相对而言,改表法比较容易一点,个人也是比较倾向于使用这种方法,因此,这
    02-10
  • 移植linux3.7到nuc900系列开发板遇到的问题
    通过移植学习linux新版本内核,大概了解一下内核变化。记录一下移植过程中遇到的问题或值得注意的地方。1,添加一款arm9芯片的支持首先修改\arch\arm\tools\mach-types文件添加一行w90p950evbMACH_W90P950EVBW90P950EVB同目录下的脚本文件在编译内核时会根据
    02-10
点击排行